mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-28 01:10:54 +01:00
feat: add everyone group (#1322)
* feat: add everyone group through seed * feat: add reserved group name check in group router actions * feat: improve user interface for everyone group * fix: reserved group alert is a server component * feat: add all users to everyone group * chore: update lockfile * fix: format issues * fix: lint issues * fix: lint format issues * test: add unit tests for everyone group * refactor: add codegen for documentation urls by sitemap * refactor: change group query to count * chore: remove migrations temporarily * chore: add migrations again * chore: add lint rule to prevent usage of raw documentation links * fix: format issues
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
@@ -124,7 +125,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
{
|
||||
label: t("items.help.items.documentation"),
|
||||
icon: IconBook2,
|
||||
href: "https://homarr.dev/docs/getting-started/",
|
||||
href: createDocumentationLink("/docs/getting-started"),
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,9 +15,10 @@ interface RenameGroupFormProps {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
||||
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
|
||||
const t = useI18n();
|
||||
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
||||
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
||||
@@ -28,6 +29,9 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: FormType) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
mutate(
|
||||
{
|
||||
...values,
|
||||
@@ -60,13 +64,15 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} />
|
||||
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} disabled={disabled} />
|
||||
|
||||
<Group justify="end">
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
{!disabled && (
|
||||
<Group justify="end">
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Link from "next/link";
|
||||
import { Alert, Anchor } from "@mantine/core";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
export const ReservedGroupAlert = async () => {
|
||||
const t = await getI18n();
|
||||
|
||||
return (
|
||||
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
||||
{t("group.reservedNotice.message", {
|
||||
checkoutDocs: (
|
||||
<Anchor
|
||||
size="sm"
|
||||
component={Link}
|
||||
href={createDocumentationLink("/docs/management/users", "#special-groups")}
|
||||
target="_blank"
|
||||
>
|
||||
{t("common.action.checkoutDocs")}
|
||||
</Anchor>
|
||||
),
|
||||
})}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -6,9 +6,11 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { env } from "@homarr/auth/env.mjs";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { ReservedGroupAlert } from "../_reserved-group-alert";
|
||||
import { AddGroupMember } from "./_add-group-member";
|
||||
import { RemoveGroupMember } from "./_remove-group-member";
|
||||
|
||||
@@ -25,6 +27,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
||||
const t = await getI18n();
|
||||
const tMembers = await getScopedI18n("management.page.group.setting.members");
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
const isReserved = group.name === everyoneGroup;
|
||||
|
||||
const filteredMembers = searchParams.search
|
||||
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -41,15 +44,19 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
||||
<Stack>
|
||||
<Title>{tMembers("title")}</Title>
|
||||
|
||||
{providerTypes !== "credentials" && (
|
||||
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
||||
{t(`group.memberNotice.${providerTypes}`)}
|
||||
</Alert>
|
||||
{isReserved ? (
|
||||
<ReservedGroupAlert />
|
||||
) : (
|
||||
providerTypes !== "credentials" && (
|
||||
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
||||
{t(`group.memberNotice.${providerTypes}`)}
|
||||
</Alert>
|
||||
)
|
||||
)}
|
||||
|
||||
<Group justify="space-between">
|
||||
<SearchInput placeholder={`${tMembers("search")}...`} defaultValue={searchParams.search} />
|
||||
{isProviderEnabled("credentials") && (
|
||||
{isProviderEnabled("credentials") && !isReserved && (
|
||||
<AddGroupMember groupId={group.id} presentUserIds={group.members.map((member) => member.id)} />
|
||||
)}
|
||||
</Group>
|
||||
@@ -63,7 +70,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
||||
<Table striped highlightOnHover>
|
||||
<TableTbody>
|
||||
{filteredMembers.map((member) => (
|
||||
<Row key={group.id} member={member} groupId={group.id} />
|
||||
<Row key={group.id} member={member} groupId={group.id} disabled={isReserved} />
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
@@ -74,9 +81,10 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
||||
interface RowProps {
|
||||
member: RouterOutputs["group"]["getById"]["members"][number];
|
||||
groupId: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Row = ({ member, groupId }: RowProps) => {
|
||||
const Row = ({ member, groupId, disabled }: RowProps) => {
|
||||
return (
|
||||
<TableTr>
|
||||
<TableTd>
|
||||
@@ -88,7 +96,7 @@ const Row = ({ member, groupId }: RowProps) => {
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd w={100}>
|
||||
{member.provider === "credentials" && <RemoveGroupMember user={member} groupId={groupId} />}
|
||||
{member.provider === "credentials" && !disabled && <RemoveGroupMember user={member} groupId={groupId} />}
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
|
||||
import { DeleteGroup } from "./_delete-group";
|
||||
import { RenameGroupForm } from "./_rename-group-form";
|
||||
import { ReservedGroupAlert } from "./_reserved-group-alert";
|
||||
import { TransferGroupOwnership } from "./_transfer-group-ownership";
|
||||
|
||||
interface GroupsDetailPageProps {
|
||||
@@ -18,26 +20,31 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
const tGeneral = await getScopedI18n("management.page.group.setting.general");
|
||||
const tGroupAction = await getScopedI18n("group.action");
|
||||
const isReserved = group.name === everyoneGroup;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title>{tGeneral("title")}</Title>
|
||||
|
||||
<RenameGroupForm group={group} />
|
||||
{isReserved && <ReservedGroupAlert />}
|
||||
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={tGroupAction("transfer.label")}
|
||||
description={tGroupAction("transfer.description")}
|
||||
action={<TransferGroupOwnership group={group} />}
|
||||
/>
|
||||
<RenameGroupForm group={group} disabled={isReserved} />
|
||||
|
||||
<DangerZoneItem
|
||||
label={tGroupAction("delete.label")}
|
||||
description={tGroupAction("delete.description")}
|
||||
action={<DeleteGroup group={group} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
{!isReserved && (
|
||||
<DangerZoneRoot>
|
||||
<DangerZoneItem
|
||||
label={tGroupAction("transfer.label")}
|
||||
description={tGroupAction("transfer.description")}
|
||||
action={<TransferGroupOwnership group={group} />}
|
||||
/>
|
||||
|
||||
<DangerZoneItem
|
||||
label={tGroupAction("delete.label")}
|
||||
description={tGroupAction("delete.description")}
|
||||
action={<DeleteGroup group={group} />}
|
||||
/>
|
||||
</DangerZoneRoot>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user