chore: update prettier configuration for print width (#519)

* feat: update prettier configuration for print width

* chore: apply code formatting to entire repository

* fix: remove build files

* fix: format issue

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Thomas Camlong
2024-05-19 22:38:39 +02:00
committed by GitHub
parent 919161798e
commit f1b1ec59ec
234 changed files with 2444 additions and 5375 deletions

View File

@@ -18,9 +18,7 @@ export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (
prev: RouterOutputs["board"]["getHomeBoard"],
) => RouterOutputs["board"]["getHomeBoard"];
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
@@ -46,9 +44,7 @@ export const ClientBoard = () => {
const board = useRequiredBoard();
const isReady = useIsBoardReady();
const sortedSections = board.sections.sort(
(sectionA, sectionB) => sectionA.position - sectionB.position,
);
const sortedSections = board.sections.sort((sectionA, sectionB) => sectionA.position - sectionB.position);
const ref = useRef<HTMLDivElement>(null);
@@ -61,24 +57,12 @@ export const ClientBoard = () => {
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack
ref={ref}
h="100%"
style={{ visibility: isReady ? "visible" : "hidden" }}
>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
{sortedSections.map((section) =>
section.kind === "empty" ? (
<BoardEmptySection
key={section.id}
section={section}
mainRef={ref}
/>
<BoardEmptySection key={section.id} section={section} mainRef={ref} />
) : (
<BoardCategorySection
key={section.id}
section={section}
mainRef={ref}
/>
<BoardCategorySection key={section.id} section={section} mainRef={ref} />
),
)}
</Stack>

View File

@@ -1,13 +1,7 @@
"use client";
import type { PropsWithChildren } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
@@ -52,18 +46,12 @@ export const BoardProvider = ({
}, [pathname, utils, initialBoard.name]);
useEffect(() => {
setReadySections((previous) =>
previous.filter((id) =>
data.sections.some((section) => section.id === id),
),
);
setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) =>
previous.includes(id) ? previous : [...previous, id],
);
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
return (

View File

@@ -18,9 +18,7 @@ interface Props<TParams extends Params> {
getInitialBoardAsync: (params: TParams) => Promise<Board>;
}
export const createBoardContentPage = <
TParams extends Record<string, unknown>,
>({
export const createBoardContentPage = <TParams extends Record<string, unknown>>({
getInitialBoardAsync: getInitialBoard,
}: Props<TParams>) => {
return {
@@ -32,21 +30,13 @@ export const createBoardContentPage = <
page: () => {
return <ClientBoard />;
},
generateMetadataAsync: async ({
params,
}: {
params: TParams;
}): Promise<Metadata> => {
generateMetadataAsync: async ({ params }: { params: TParams }): Promise<Metadata> => {
try {
const board = await getInitialBoard(params);
const t = await getI18n();
return {
title:
board.metaTitle ??
createMetaTitle(
t("board.content.metaTitle", { boardName: board.name }),
),
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
icons: {
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
},

View File

@@ -16,10 +16,7 @@ import { useAtom, useAtomValue } from "jotai";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import {
showErrorNotification,
showSuccessNotification,
} from "@homarr/notifications";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
@@ -54,8 +51,7 @@ export const BoardContentHeaderActions = () => {
};
const AddMenu = () => {
const { openModal: openCategoryEditModal } =
useModalAction(CategoryEditModal);
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
const { addCategoryToEnd } = useCategoryActions();
const t = useI18n();
@@ -95,22 +91,14 @@ const AddMenu = () => {
</HeaderButton>
</Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
<Menu.Item
leftSection={<IconBox size={20} />}
onClick={handleSelectItem}
>
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectItem}>
{t("item.action.create")}
</Menu.Item>
<Menu.Item leftSection={<IconPackageImport size={20} />}>
{t("item.action.import")}
</Menu.Item>
<Menu.Item leftSection={<IconPackageImport size={20} />}>{t("item.action.import")}</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconBoxAlignTop size={20} />}
onClick={handleAddCategory}
>
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
{t("section.category.action.create")}
</Menu.Item>
</Menu.Dropdown>
@@ -123,24 +111,23 @@ const EditModeMenu = () => {
const board = useRequiredBoard();
const utils = clientApi.useUtils();
const t = useScopedI18n("board.action.edit");
const { mutate: saveBoard, isPending } =
clientApi.board.saveBoard.useMutation({
onSuccess() {
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
void utils.board.getBoardByName.invalidate({ name: board.name });
void revalidatePathActionAsync(`/boards/${board.name}`);
setEditMode(false);
},
onError() {
showErrorNotification({
title: t("notification.error.title"),
message: t("notification.error.message"),
});
},
});
const { mutate: saveBoard, isPending } = clientApi.board.saveBoard.useMutation({
onSuccess() {
showSuccessNotification({
title: t("notification.success.title"),
message: t("notification.success.message"),
});
void utils.board.getBoardByName.invalidate({ name: board.name });
void revalidatePathActionAsync(`/boards/${board.name}`);
setEditMode(false);
},
onError() {
showErrorNotification({
title: t("notification.error.title"),
message: t("notification.error.message"),
});
},
});
const toggle = useCallback(() => {
if (isEditMode) return saveBoard(board);
@@ -149,11 +136,7 @@ const EditModeMenu = () => {
return (
<HeaderButton onClick={toggle} loading={isPending}>
{isEditMode ? (
<IconPencilOff stroke={1.5} />
) : (
<IconPencil stroke={1.5} />
)}
{isEditMode ? <IconPencilOff stroke={1.5} /> : <IconPencil stroke={1.5} />}
</HeaderButton>
);
};

View File

@@ -22,9 +22,7 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
};
export const generateColors = (hex: string) => {
const lightnessForColors = [
-0.25, -0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2,
] as const;
const lightnessForColors = [-0.25, -0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2] as const;
const rgbaColors = lightnessForColors.map((lightness) => {
if (lightness < 0) {
return lighten(hex, -lightness);

View File

@@ -44,11 +44,7 @@ export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
<Tabs.List grow>
<TabItem value="user" count={counts.user} icon={IconUser} />
<TabItem value="group" count={counts.group} icon={IconUsersGroup} />
<TabItem
value="inherited"
count={initialPermissions.inherited.length}
icon={IconUserDown}
/>
<TabItem value="inherited" count={initialPermissions.inherited.length} icon={IconUserDown} />
</Tabs.List>
<Tabs.Panel value="user">

View File

@@ -1,21 +1,8 @@
import { useCallback } from "react";
import type { ReactNode } from "react";
import type { SelectProps } from "@mantine/core";
import {
Button,
Flex,
Group,
Select,
TableTd,
TableTr,
Text,
} from "@mantine/core";
import {
IconCheck,
IconEye,
IconPencil,
IconSettings,
} from "@tabler/icons-react";
import { Button, Flex, Group, Select, TableTd, TableTr, Text } from "@mantine/core";
import { IconCheck, IconEye, IconPencil, IconSettings } from "@tabler/icons-react";
import type { BoardPermission } from "@homarr/definitions";
import { boardPermissions } from "@homarr/definitions";
@@ -38,12 +25,7 @@ interface BoardAccessSelectRowProps {
onCountChange: OnCountChange;
}
export const BoardAccessSelectRow = ({
itemContent,
permission,
index,
onCountChange,
}: BoardAccessSelectRowProps) => {
export const BoardAccessSelectRow = ({ itemContent, permission, index, onCountChange }: BoardAccessSelectRowProps) => {
const tRoot = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useFormContext();
@@ -61,11 +43,7 @@ export const BoardAccessSelectRow = ({
<TableTr>
<TableTd w={{ sm: 128, lg: 256 }}>{itemContent}</TableTd>
<TableTd>
<Flex
direction={{ base: "column", xs: "row" }}
align={{ base: "end", xs: "center" }}
wrap="nowrap"
>
<Flex direction={{ base: "column", xs: "row" }} align={{ base: "end", xs: "center" }} wrap="nowrap">
<Select
allowDeselect={false}
flex="1"
@@ -93,10 +71,7 @@ interface BoardAccessDisplayRowProps {
permission: BoardPermission | "board-full";
}
export const BoardAccessDisplayRow = ({
itemContent,
permission,
}: BoardAccessDisplayRowProps) => {
export const BoardAccessDisplayRow = ({ itemContent, permission }: BoardAccessDisplayRowProps) => {
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const Icon = icons[permission];
@@ -106,10 +81,7 @@ export const BoardAccessDisplayRow = ({
<TableTd>
<Group gap={0}>
<Flex w={34} h={34} align="center" justify="center">
<Icon
size="1rem"
color="var(--input-section-color, var(--mantine-color-dimmed))"
/>
<Icon size="1rem" color="var(--input-section-color, var(--mantine-color-dimmed))" />
</Flex>
<Text size="sm">{tPermissions(`item.${permission}.label`)}</Text>
</Group>
@@ -131,9 +103,7 @@ const RenderOption: SelectProps["renderOption"] = ({ option, checked }) => {
<Group flex="1" gap="xs" wrap="nowrap">
<Icon {...iconProps} />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
</Group>
);
};

View File

@@ -8,7 +8,6 @@ export interface BoardAccessFormType {
}[];
}
export const [FormProvider, useFormContext, useForm] =
createFormContext<BoardAccessFormType>();
export const [FormProvider, useFormContext, useForm] = createFormContext<BoardAccessFormType>();
export type OnCountChange = (callback: (prev: number) => number) => void;

View File

@@ -1,16 +1,6 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Anchor,
Button,
Group,
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { Anchor, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -24,30 +14,21 @@ import { FormProvider, useForm } from "./form";
import { GroupSelectModal } from "./group-select-modal";
import type { FormProps } from "./user-access";
export const GroupsForm = ({
board,
initialPermissions,
onCountChange,
}: FormProps) => {
const { mutate, isPending } =
clientApi.board.saveGroupBoardPermissions.useMutation();
export const GroupsForm = ({ board, initialPermissions, onCountChange }: FormProps) => {
const { mutate, isPending } = clientApi.board.saveGroupBoardPermissions.useMutation();
const utils = clientApi.useUtils();
const [groups, setGroups] = useState<Map<string, Group>>(
new Map(
initialPermissions.groupPermissions.map(({ group }) => [group.id, group]),
),
new Map(initialPermissions.groupPermissions.map(({ group }) => [group.id, group])),
);
const { openModal } = useModalAction(GroupSelectModal);
const t = useI18n();
const tPermissions = useScopedI18n("board.setting.section.access.permission");
const form = useForm({
initialValues: {
items: initialPermissions.groupPermissions.map(
({ group, permission }) => ({
itemId: group.id,
permission,
}),
),
items: initialPermissions.groupPermissions.map(({ group, permission }) => ({
itemId: group.id,
permission,
})),
},
});
@@ -92,9 +73,7 @@ export const GroupsForm = ({
<Table>
<TableThead>
<TableTr>
<TableTh style={{ whiteSpace: "nowrap" }}>
{tPermissions("field.group.label")}
</TableTh>
<TableTh style={{ whiteSpace: "nowrap" }}>{tPermissions("field.group.label")}</TableTh>
<TableTh>{tPermissions("field.permission.label")}</TableTh>
</TableTr>
</TableThead>
@@ -102,9 +81,7 @@ export const GroupsForm = ({
{form.values.items.map((row, index) => (
<BoardAccessSelectRow
key={row.itemId}
itemContent={
<GroupItemContent group={groups.get(row.itemId)!} />
}
itemContent={<GroupItemContent group={groups.get(row.itemId)!} />}
permission={row.permission}
index={index}
onCountChange={onCountChange}
@@ -114,11 +91,7 @@ export const GroupsForm = ({
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
@@ -133,16 +106,10 @@ export const GroupsForm = ({
export const GroupItemContent = ({ group }: { group: Group }) => {
return (
<Anchor
component={Link}
href={`/manage/users/groups/${group.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
<Anchor component={Link} href={`/manage/users/groups/${group.id}`} size="sm" style={{ whiteSpace: "nowrap" }}>
{group.name}
</Anchor>
);
};
type Group =
RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];
type Group = RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];

View File

@@ -16,59 +16,52 @@ interface GroupSelectFormType {
groupId: string;
}
export const GroupSelectModal = createModal<InnerProps>(
({ actions, innerProps }) => {
const t = useI18n();
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<GroupSelectFormType>();
const handleSubmitAsync = async (values: GroupSelectFormType) => {
const currentGroup = groups?.find((group) => group.id === values.groupId);
if (!currentGroup) return;
setLoading(true);
await innerProps.onSelect({
id: currentGroup.id,
name: currentGroup.name,
});
export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n();
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<GroupSelectFormType>();
const handleSubmitAsync = async (values: GroupSelectFormType) => {
const currentGroup = groups?.find((group) => group.id === values.groupId);
if (!currentGroup) return;
setLoading(true);
await innerProps.onSelect({
id: currentGroup.id,
name: currentGroup.name,
});
setLoading(false);
actions.closeModal();
};
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
return (
<form
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
>
<Stack>
<Select
{...form.getInputProps("groupId")}
label={t("group.action.select.label")}
clearable
searchable
leftSection={isPending ? <Loader size="xs" /> : undefined}
nothingFoundMessage={t("group.action.select.notFound")}
limit={5}
data={groups
?.filter(
(group) => !innerProps.presentGroupIds.includes(group.id),
)
.map((group) => ({ value: group.id, label: group.name }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
},
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.access.permission.groupSelect.title"),
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack>
<Select
{...form.getInputProps("groupId")}
label={t("group.action.select.label")}
clearable
searchable
leftSection={isPending ? <Loader size="xs" /> : undefined}
nothingFoundMessage={t("group.action.select.notFound")}
limit={5}
data={groups
?.filter((group) => !innerProps.presentGroupIds.includes(group.id))
.map((group) => ({ value: group.id, label: group.name }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
}).withOptions({
defaultTitle: (t) => t("board.setting.section.access.permission.groupSelect.title"),
});

View File

@@ -1,11 +1,4 @@
import {
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { getPermissionsWithChildren } from "@homarr/definitions";
@@ -41,9 +34,7 @@ export const InheritTable = ({ initialPermissions }: InheritTableProps) => {
const boardPermission =
permission in mapPermissions
? mapPermissions[permission as keyof typeof mapPermissions]
: getPermissionsWithChildren([permission]).includes(
"board-full-access",
)
: getPermissionsWithChildren([permission]).includes("board-full-access")
? "board-full"
: null;

View File

@@ -1,17 +1,6 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Anchor,
Box,
Button,
Group,
Stack,
Table,
TableTbody,
TableTh,
TableThead,
TableTr,
} from "@mantine/core";
import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -21,10 +10,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui";
import type { Board } from "../../../_types";
import {
BoardAccessDisplayRow,
BoardAccessSelectRow,
} from "./board-access-table-rows";
import { BoardAccessDisplayRow, BoardAccessSelectRow } from "./board-access-table-rows";
import type { BoardAccessFormType, OnCountChange } from "./form";
import { FormProvider, useForm } from "./form";
import { UserSelectModal } from "./user-select-modal";
@@ -35,18 +21,11 @@ export interface FormProps {
onCountChange: OnCountChange;
}
export const UsersForm = ({
board,
initialPermissions,
onCountChange,
}: FormProps) => {
const { mutate, isPending } =
clientApi.board.saveUserBoardPermissions.useMutation();
export const UsersForm = ({ board, initialPermissions, onCountChange }: FormProps) => {
const { mutate, isPending } = clientApi.board.saveUserBoardPermissions.useMutation();
const utils = clientApi.useUtils();
const [users, setUsers] = useState<Map<string, User>>(
new Map(
initialPermissions.userPermissions.map(({ user }) => [user.id, user]),
),
new Map(initialPermissions.userPermissions.map(({ user }) => [user.id, user])),
);
const { openModal } = useModalAction(UserSelectModal);
const t = useI18n();
@@ -81,9 +60,7 @@ export const UsersForm = ({
const presentUserIds = form.values.items.map(({ itemId: id }) => id);
openModal({
presentUserIds: board.creatorId
? presentUserIds.concat(board.creatorId)
: presentUserIds,
presentUserIds: board.creatorId ? presentUserIds.concat(board.creatorId) : presentUserIds,
onSelect: (user) => {
setUsers((prev) => new Map(prev).set(user.id, user));
form.setFieldValue("items", [
@@ -111,17 +88,12 @@ export const UsersForm = ({
</TableThead>
<TableTbody>
{board.creator && (
<BoardAccessDisplayRow
itemContent={<UserItemContent user={board.creator} />}
permission="board-full"
/>
<BoardAccessDisplayRow itemContent={<UserItemContent user={board.creator} />} permission="board-full" />
)}
{form.values.items.map((row, index) => (
<BoardAccessSelectRow
key={row.itemId}
itemContent={
<UserItemContent user={users.get(row.itemId)!} />
}
itemContent={<UserItemContent user={users.get(row.itemId)!} />}
permission={row.permission}
index={index}
onCountChange={onCountChange}
@@ -131,11 +103,7 @@ export const UsersForm = ({
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
@@ -154,12 +122,7 @@ const UserItemContent = ({ user }: { user: User }) => {
<Box visibleFrom="xs">
<UserAvatar user={user} size="sm" />
</Box>
<Anchor
component={Link}
href={`/manage/users/${user.id}`}
size="sm"
style={{ whiteSpace: "nowrap" }}
>
<Anchor component={Link} href={`/manage/users/${user.id}`} size="sm" style={{ whiteSpace: "nowrap" }}>
{user.name}
</Anchor>
</Group>

View File

@@ -12,11 +12,7 @@ import { UserAvatar } from "@homarr/ui";
interface InnerProps {
presentUserIds: string[];
onSelect: (props: {
id: string;
name: string;
image: string;
}) => void | Promise<void>;
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
confirmLabel?: string;
}
@@ -24,68 +20,59 @@ interface UserSelectFormType {
userId: string;
}
export const UserSelectModal = createModal<InnerProps>(
({ actions, innerProps }) => {
const t = useI18n();
const { data: users, isPending } = clientApi.user.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<UserSelectFormType>();
const handleSubmitAsync = async (values: UserSelectFormType) => {
const currentUser = users?.find((user) => user.id === values.userId);
if (!currentUser) return;
setLoading(true);
await innerProps.onSelect({
id: currentUser.id,
name: currentUser.name ?? "",
image: currentUser.image ?? "",
});
export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n();
const { data: users, isPending } = clientApi.user.selectable.useQuery();
const [loading, setLoading] = useState(false);
const form = useForm<UserSelectFormType>();
const handleSubmitAsync = async (values: UserSelectFormType) => {
const currentUser = users?.find((user) => user.id === values.userId);
if (!currentUser) return;
setLoading(true);
await innerProps.onSelect({
id: currentUser.id,
name: currentUser.name ?? "",
image: currentUser.image ?? "",
});
setLoading(false);
actions.closeModal();
};
setLoading(false);
actions.closeModal();
};
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
const currentUser = users?.find((user) => user.id === form.values.userId);
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
const currentUser = users?.find((user) => user.id === form.values.userId);
return (
<form
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
>
<Stack>
<Select
{...form.getInputProps("userId")}
label={t("user.action.select.label")}
searchable
clearable
leftSection={
isPending ? (
<Loader size="xs" />
) : currentUser ? (
<UserAvatar user={currentUser} size="xs" />
) : undefined
}
nothingFoundMessage={t("user.action.select.notFound")}
renderOption={createRenderOption(users ?? [])}
limit={5}
data={users
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
},
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.access.permission.userSelect.title"),
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack>
<Select
{...form.getInputProps("userId")}
label={t("user.action.select.label")}
searchable
clearable
leftSection={
isPending ? <Loader size="xs" /> : currentUser ? <UserAvatar user={currentUser} size="xs" /> : undefined
}
nothingFoundMessage={t("user.action.select.notFound")}
renderOption={createRenderOption(users ?? [])}
limit={5}
data={users
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
/>
<Group justify="end">
<Button variant="default" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={loading}>
{confirmLabel}
</Button>
</Group>
</Stack>
</form>
);
}).withOptions({
defaultTitle: (t) => t("board.setting.section.access.permission.userSelect.title"),
});
const iconProps = {
@@ -95,9 +82,7 @@ const iconProps = {
size: "1rem",
};
const createRenderOption = (
users: RouterOutputs["user"]["selectable"],
): SelectProps["renderOption"] =>
const createRenderOption = (users: RouterOutputs["user"]["selectable"]): SelectProps["renderOption"] =>
function InnerRenderRoot({ option, checked }) {
const user = users.find((user) => user.id === option.value);
if (!user) return null;
@@ -106,9 +91,7 @@ const createRenderOption = (
<Group flex="1" gap="xs">
<UserAvatar user={user} size="xs" />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
</Group>
);
};

View File

@@ -2,11 +2,7 @@
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
} from "@homarr/definitions";
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
@@ -22,8 +18,7 @@ interface Props {
}
export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings, {
initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "",
@@ -37,14 +32,8 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
"backgroundImageAttachment",
backgroundImageAttachments,
);
const backgroundImageSizeData = useBackgroundOptionData(
"backgroundImageSize",
backgroundImageSizes,
);
const backgroundImageRepeatData = useBackgroundOptionData(
"backgroundImageRepeat",
backgroundImageRepeats,
);
const backgroundImageSizeData = useBackgroundOptionData("backgroundImageSize", backgroundImageSizes);
const backgroundImageRepeatData = useBackgroundOptionData("backgroundImageRepeat", backgroundImageRepeats);
return (
<form
@@ -96,13 +85,9 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
);
};
type BackgroundImageKey =
| "backgroundImageAttachment"
| "backgroundImageSize"
| "backgroundImageRepeat";
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
type inferOptions<TKey extends BackgroundImageKey> =
TranslationObject["board"]["field"][TKey]["option"];
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
const useBackgroundOptionData = <
TKey extends BackgroundImageKey,
@@ -120,9 +105,7 @@ const useBackgroundOptionData = <
(value) =>
({
label: t(`board.field.${key}.option.${value as string}.label` as never),
description: t(
`board.field.${key}.option.${value as string}.description` as never,
),
description: t(`board.field.${key}.option.${value as string}.description` as never),
value: value as string,
badge:
data.defaultValue === value

View File

@@ -44,8 +44,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
const [showPreview, { toggle }] = useDisclosure(false);
const t = useI18n();
const theme = useMantineTheme();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
return (
<form
@@ -77,11 +76,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
/>
</Grid.Col>
<Grid.Col span={12}>
<Anchor onClick={toggle}>
{showPreview
? t("common.preview.hide")
: t("common.preview.show")}
</Anchor>
<Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor>
</Grid.Col>
<Grid.Col span={12}>
<Collapse in={showPreview}>
@@ -121,10 +116,7 @@ interface ColorsPreviewProps {
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
const theme = useMantineTheme();
const colors =
previewColor && hexRegex.test(previewColor)
? generateColors(previewColor)
: generateColors("#000000");
const colors = previewColor && hexRegex.test(previewColor) ? generateColors(previewColor) : generateColors("#000000");
return (
<Group gap={0} wrap="nowrap">

View File

@@ -20,8 +20,7 @@ export const DangerZoneSettingsContent = () => {
const { openModal } = useModalAction(BoardRenameModal);
const { mutate: changeVisibility, isPending: isChangeVisibilityPending } =
clientApi.board.changeBoardVisibility.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } =
clientApi.board.deleteBoard.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } = clientApi.board.deleteBoard.useMutation();
const utils = clientApi.useUtils();
const visibility = board.isPublic ? "public" : "private";
@@ -37,12 +36,8 @@ export const DangerZoneSettingsContent = () => {
const onVisibilityClick = useCallback(() => {
openConfirmModal({
title: t(
`section.dangerZone.action.visibility.confirm.${visibility}.title`,
),
children: t(
`section.dangerZone.action.visibility.confirm.${visibility}.description`,
),
title: t(`section.dangerZone.action.visibility.confirm.${visibility}.title`),
children: t(`section.dangerZone.action.visibility.confirm.${visibility}.description`),
onConfirm: () => {
changeVisibility(
{
@@ -98,12 +93,8 @@ export const DangerZoneSettingsContent = () => {
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(
`section.dangerZone.action.visibility.description.${visibility}`,
)}
buttonText={t(
`section.dangerZone.action.visibility.button.${visibility}`,
)}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
@@ -127,13 +118,7 @@ interface DangerZoneRowProps {
onClick: () => void;
}
const DangerZoneRow = ({
label,
description,
buttonText,
onClick,
isPending,
}: DangerZoneRowProps) => {
const DangerZoneRow = ({ label, description, buttonText, onClick, isPending }: DangerZoneRowProps) => {
return (
<Group justify="space-between" px="md" className={classes.dangerZoneGroup}>
<Stack gap={0}>
@@ -143,12 +128,7 @@ const DangerZoneRow = ({
<Text size="sm">{description}</Text>
</Stack>
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
<Button
variant="subtle"
color="red"
loading={isPending}
onClick={onClick}
>
<Button variant="subtle" color="red" loading={isPending} onClick={onClick}>
{buttonText}
</Button>
</Group>

View File

@@ -1,20 +1,8 @@
"use client";
import { useEffect, useRef } from "react";
import {
Button,
Grid,
Group,
Loader,
Stack,
TextInput,
Tooltip,
} from "@mantine/core";
import {
useDebouncedValue,
useDocumentTitle,
useFavicon,
} from "@mantine/hooks";
import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine/core";
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useZodForm } from "@homarr/form";
@@ -38,8 +26,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
});
const { updateBoard } = useUpdateBoard();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(
validation.board.savePartialSettings
.pick({
@@ -106,9 +93,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
<Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput
label={t("board.field.metaTitle.label")}
placeholder={createMetaTitle(
t("board.content.metaTitle", { boardName: board.name }),
)}
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
{...form.getInputProps("metaTitle")}
/>
@@ -140,22 +125,12 @@ export const GeneralSettingsContent = ({ board }: Props) => {
);
};
const PendingOrInvalidIndicator = ({
isPending,
isInvalid,
}: {
isPending: boolean;
isInvalid?: boolean;
}) => {
const PendingOrInvalidIndicator = ({ isPending, isInvalid }: { isPending: boolean; isInvalid?: boolean }) => {
const t = useI18n();
if (isInvalid) {
return (
<Tooltip
multiline
w={220}
label={t("board.setting.section.general.unrecognizedLink")}
>
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
<IconAlertTriangle size="1rem" color="red" />
</Tooltip>
);
@@ -197,8 +172,7 @@ const useMetaTitlePreview = (title: string | null) => {
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
const isValidUrl = (url: string) =>
url.includes("/") &&
validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
const useFaviconPreview = (url: string | null) => {
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);

View File

@@ -14,16 +14,12 @@ interface Props {
}
export const LayoutSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { mutate: savePartialSettings, isPending } =
useSavePartialSettingsMutation(board);
const form = useZodForm(
validation.board.savePartialSettings.pick({ columnCount: true }).required(),
{
initialValues: {
columnCount: board.columnCount,
},
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings.pick({ columnCount: true }).required(), {
initialValues: {
columnCount: board.columnCount,
},
);
});
return (
<form
@@ -38,13 +34,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
<Grid>
<Grid.Col span={{ sm: 12, md: 6 }}>
<Input.Wrapper label={t("board.field.columnCount.label")}>
<Slider
mt="xs"
min={1}
max={24}
step={1}
{...form.getInputProps("columnCount")}
/>
<Slider mt="xs" min={1} max={24} step={1} {...form.getInputProps("columnCount")} />
</Input.Wrapper>
</Grid.Col>
</Grid>

View File

@@ -1,14 +1,6 @@
import type { PropsWithChildren } from "react";
import { notFound } from "next/navigation";
import {
AccordionControl,
AccordionItem,
AccordionPanel,
Container,
Stack,
Text,
Title,
} from "@mantine/core";
import { AccordionControl, AccordionItem, AccordionPanel, Container, Stack, Text, Title } from "@mantine/core";
import {
IconAlertTriangle,
IconBrush,
@@ -69,10 +61,7 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
}
};
export default async function BoardSettingsPage({
params,
searchParams,
}: Props) {
export default async function BoardSettingsPage({ params, searchParams }: Props) {
const { board, permissions } = await getBoardAndPermissionsAsync(params);
const { hasFullAccess } = await getBoardPermissionsAsync(board);
const t = await getScopedI18n("board.setting");
@@ -81,10 +70,7 @@ export default async function BoardSettingsPage({
<Container>
<Stack>
<Title>{t("title", { boardName: capitalize(board.name) })}</Title>
<ActiveTabAccordion
variant="separated"
defaultValue={searchParams.tab ?? "general"}
>
<ActiveTabAccordion variant="separated" defaultValue={searchParams.tab ?? "general"}>
<AccordionItemFor value="general" icon={IconSettings}>
<GeneralSettingsContent board={board} />
</AccordionItemFor>
@@ -103,17 +89,9 @@ export default async function BoardSettingsPage({
{hasFullAccess && (
<>
<AccordionItemFor value="access" icon={IconUser}>
<AccessSettingsContent
board={board}
initialPermissions={permissions}
/>
<AccessSettingsContent board={board} initialPermissions={permissions} />
</AccordionItemFor>
<AccordionItemFor
value="dangerZone"
icon={IconAlertTriangle}
danger
noPadding
>
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
<DangerZoneSettingsContent />
</AccordionItemFor>
</>
@@ -131,13 +109,7 @@ type AccordionItemForProps = PropsWithChildren<{
noPadding?: boolean;
}>;
const AccordionItemFor = async ({
value,
children,
icon: Icon,
danger,
noPadding,
}: AccordionItemForProps) => {
const AccordionItemFor = async ({ value, children, icon: Icon, danger, noPadding }: AccordionItemForProps) => {
const t = await getScopedI18n("board.setting.section");
return (
<AccordionItem
@@ -158,13 +130,7 @@ const AccordionItemFor = async ({
{t(`${value}.title`)}
</Text>
</AccordionControl>
<AccordionPanel
styles={
noPadding
? { content: { paddingRight: 0, paddingLeft: 0 } }
: undefined
}
>
<AccordionPanel styles={noPadding ? { content: { paddingRight: 0, paddingLeft: 0 } } : undefined}>
{children}
</AccordionPanel>
</AccordionItem>

View File

@@ -41,10 +41,7 @@ export const createBoardLayout = <TParams extends Params>({
});
return (
<GlobalItemServerDataRunner
board={initialBoard}
shouldRun={isBoardContentPage}
>
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<ClientShell hasNavigation={false}>

View File

@@ -8,7 +8,4 @@ export type Item = Section["items"][number];
export type CategorySection = Extract<Section, { kind: "category" }>;
export type EmptySection = Extract<Section, { kind: "empty" }>;
export type ItemOfKind<TKind extends WidgetKind> = Extract<
Item,
{ kind: TKind }
>;
export type ItemOfKind<TKind extends WidgetKind> = Extract<Item, { kind: TKind }>;