feat: add board access settings (#249)

* wip: add board access settings

* wip: add user access control

* wip: add user access control

* feat: add user access control

* refactor: move away from mantine-modal-manager

* fix: ci issues and failing tests

* fix: lint issue

* fix: format issue

* fix: deepsource issues

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-03-20 20:30:58 +01:00
committed by GitHub
parent 4753bc7162
commit 361700b239
59 changed files with 1763 additions and 1338 deletions

View File

@@ -22,6 +22,7 @@
"@homarr/form": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.0.0", "@homarr/gridstack": "^1.0.0",
"@homarr/log": "workspace:^", "@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0", "@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
@@ -46,7 +47,6 @@
"chroma-js": "^2.4.2", "chroma-js": "^2.4.2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"jotai": "^2.7.1", "jotai": "^2.7.1",
"mantine-modal-manager": "^7.6.2",
"next": "^14.1.3", "next": "^14.1.3",
"postcss-preset-mantine": "^1.13.0", "postcss-preset-mantine": "^1.13.0",
"react": "18.2.0", "react": "18.2.0",

View File

@@ -4,6 +4,7 @@ import { useCallback } from "react";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import { import {
showErrorNotification, showErrorNotification,
showSuccessNotification, showSuccessNotification,
@@ -12,7 +13,6 @@ import { useScopedI18n } from "@homarr/translation/client";
import { ActionIcon, IconTrash } from "@homarr/ui"; import { ActionIcon, IconTrash } from "@homarr/ui";
import { revalidatePathAction } from "../../../revalidatePathAction"; import { revalidatePathAction } from "../../../revalidatePathAction";
import { modalEvents } from "../../modals";
interface AppDeleteButtonProps { interface AppDeleteButtonProps {
app: RouterOutputs["app"]["all"][number]; app: RouterOutputs["app"]["all"][number];
@@ -20,10 +20,11 @@ interface AppDeleteButtonProps {
export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => { export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
const t = useScopedI18n("app.page.delete"); const t = useScopedI18n("app.page.delete");
const { openConfirmModal } = useConfirmModal();
const { mutate, isPending } = clientApi.app.delete.useMutation(); const { mutate, isPending } = clientApi.app.delete.useMutation();
const onClick = useCallback(() => { const onClick = useCallback(() => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t("title"), title: t("title"),
children: t("message", app), children: t("message", app),
onConfirm: () => { onConfirm: () => {
@@ -47,7 +48,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
); );
}, },
}); });
}, [app, mutate, t]); }, [app, mutate, t, openConfirmModal]);
return ( return (
<ActionIcon <ActionIcon

View File

@@ -3,6 +3,7 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useConfirmModal } from "@homarr/modals";
import { import {
showErrorNotification, showErrorNotification,
showSuccessNotification, showSuccessNotification,
@@ -11,7 +12,6 @@ import { useScopedI18n } from "@homarr/translation/client";
import { ActionIcon, IconTrash } from "@homarr/ui"; import { ActionIcon, IconTrash } from "@homarr/ui";
import { revalidatePathAction } from "../../../revalidatePathAction"; import { revalidatePathAction } from "../../../revalidatePathAction";
import { modalEvents } from "../../modals";
interface DeleteIntegrationActionButtonProps { interface DeleteIntegrationActionButtonProps {
count: number; count: number;
@@ -24,6 +24,7 @@ export const DeleteIntegrationActionButton = ({
}: DeleteIntegrationActionButtonProps) => { }: DeleteIntegrationActionButtonProps) => {
const t = useScopedI18n("integration.page.delete"); const t = useScopedI18n("integration.page.delete");
const router = useRouter(); const router = useRouter();
const { openConfirmModal } = useConfirmModal();
const { mutateAsync, isPending } = clientApi.integration.delete.useMutation(); const { mutateAsync, isPending } = clientApi.integration.delete.useMutation();
return ( return (
@@ -32,7 +33,7 @@ export const DeleteIntegrationActionButton = ({
variant="subtle" variant="subtle"
color="red" color="red"
onClick={() => { onClick={() => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t("title"), title: t("title"),
children: t("message", integration), children: t("message", integration),
onConfirm: () => { onConfirm: () => {

View File

@@ -10,6 +10,7 @@ import {
getDefaultSecretKinds, getDefaultSecretKinds,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { useForm, zodResolver } from "@homarr/form"; import { useForm, zodResolver } from "@homarr/form";
import { useConfirmModal } from "@homarr/modals";
import { import {
showErrorNotification, showErrorNotification,
showSuccessNotification, showSuccessNotification,
@@ -19,7 +20,6 @@ import { Button, Fieldset, Group, Stack, TextInput } from "@homarr/ui";
import type { z } from "@homarr/validation"; import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation"; import { validation } from "@homarr/validation";
import { modalEvents } from "~/app/[locale]/modals";
import { SecretCard } from "../../_integration-secret-card"; import { SecretCard } from "../../_integration-secret-card";
import { IntegrationSecretInput } from "../../_integration-secret-inputs"; import { IntegrationSecretInput } from "../../_integration-secret-inputs";
import { import {
@@ -35,9 +35,10 @@ interface EditIntegrationForm {
export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const secretsKinds = const secretsKinds =
getAllSecretKindOptions(integration.kind).find((x) => getAllSecretKindOptions(integration.kind).find((secretKinds) =>
integration.secrets.every((y) => x.includes(y.kind)), integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
) ?? getDefaultSecretKinds(integration.kind); ) ?? getDefaultSecretKinds(integration.kind);
const initialFormValues = { const initialFormValues = {
name: integration.name, name: integration.name,
@@ -99,7 +100,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
}; };
return ( return (
<form onSubmit={form.onSubmit((v) => void handleSubmit(v))}> <form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack> <Stack>
<TestConnectionNoticeAlert /> <TestConnectionNoticeAlert />
@@ -128,7 +129,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
) { ) {
return res(true); return res(true);
} }
modalEvents.openConfirmModal({ openConfirmModal({
title: t("integration.secrets.reset.title"), title: t("integration.secrets.reset.title"),
children: t("integration.secrets.reset.message"), children: t("integration.secrets.reset.message"),
onCancel: () => res(false), onCancel: () => res(false),

View File

@@ -33,7 +33,7 @@ export const IntegrationCreateDropdownContent = () => {
leftSection={<IconSearch stroke={1.5} size={20} />} leftSection={<IconSearch stroke={1.5} size={20} />}
placeholder={t("integration.page.list.search")} placeholder={t("integration.page.list.search")}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(event) => setSearch(event.target.value)}
/> />
{filteredKinds.length > 0 ? ( {filteredKinds.length > 0 ? (

View File

@@ -1,29 +0,0 @@
"use client";
import type { PropsWithChildren } from "react";
import { useScopedI18n } from "@homarr/translation/client";
import { ModalsManager } from "../modals";
export const ModalsProvider = ({ children }: PropsWithChildren) => {
const t = useScopedI18n("common.action");
return (
<ModalsManager
labels={{
cancel: t("cancel"),
confirm: t("confirm"),
}}
modalProps={{
styles: {
title: {
fontSize: "1.25rem",
fontWeight: 500,
},
},
}}
>
{children}
</ModalsManager>
);
};

View File

@@ -54,7 +54,7 @@ export const LoginForm = () => {
return ( return (
<Stack gap="xl"> <Stack gap="xl">
<form onSubmit={form.onSubmit((v) => void handleSubmit(v))}> <form onSubmit={form.onSubmit((values) => void handleSubmit(values))}>
<Stack gap="lg"> <Stack gap="lg">
<TextInput <TextInput
label={t("field.username.label")} label={t("field.username.label")}

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import { useCallback } from "react";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { import {
showErrorNotification, showErrorNotification,
showSuccessNotification, showSuccessNotification,
@@ -21,10 +23,11 @@ import {
Menu, Menu,
} from "@homarr/ui"; } from "@homarr/ui";
import { modalEvents } from "~/app/[locale]/modals";
import { revalidatePathAction } from "~/app/revalidatePathAction"; import { revalidatePathAction } from "~/app/revalidatePathAction";
import { editModeAtom } from "~/components/board/editMode"; import { editModeAtom } from "~/components/board/editMode";
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
import { useCategoryActions } from "~/components/board/sections/category/category-actions"; import { useCategoryActions } from "~/components/board/sections/category/category-actions";
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { HeaderButton } from "~/components/layout/header/button"; import { HeaderButton } from "~/components/layout/header/button";
import { useRequiredBoard } from "../../_context"; import { useRequiredBoard } from "../../_context";
@@ -46,9 +49,36 @@ export default function BoardViewHeaderActions() {
} }
const AddMenu = () => { const AddMenu = () => {
const { openModal: openCategoryEditModal } =
useModalAction(CategoryEditModal);
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
const { addCategoryToEnd } = useCategoryActions(); const { addCategoryToEnd } = useCategoryActions();
const t = useI18n(); const t = useI18n();
const handleAddCategory = useCallback(
() =>
openCategoryEditModal(
{
category: {
id: "new",
name: "",
},
onSuccess({ name }) {
addCategoryToEnd({ name });
},
submitLabel: t("section.category.create.submit"),
},
{
title: (t) => t("section.category.create.title"),
},
),
[addCategoryToEnd, openCategoryEditModal, t],
);
const handleSelectItem = useCallback(() => {
openItemSelectModal();
}, [openItemSelectModal]);
return ( return (
<Menu position="bottom-end" withArrow> <Menu position="bottom-end" withArrow>
<Menu.Target> <Menu.Target>
@@ -62,14 +92,7 @@ const AddMenu = () => {
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}> <Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
<Menu.Item <Menu.Item
leftSection={<IconBox size={20} />} leftSection={<IconBox size={20} />}
onClick={() => onClick={handleSelectItem}
modalEvents.openManagedModal({
title: t("item.create.title"),
size: "xl",
modal: "itemSelectModal",
innerProps: {},
})
}
> >
{t("item.action.create")} {t("item.action.create")}
</Menu.Item> </Menu.Item>
@@ -81,22 +104,7 @@ const AddMenu = () => {
<Menu.Item <Menu.Item
leftSection={<IconBoxAlignTop size={20} />} leftSection={<IconBoxAlignTop size={20} />}
onClick={() => onClick={handleAddCategory}
modalEvents.openManagedModal({
title: t("section.category.create.title"),
modal: "categoryEditModal",
innerProps: {
submitLabel: t("section.category.create.submit"),
category: {
id: "new",
name: "",
},
onSuccess({ name }) {
addCategoryToEnd({ name });
},
},
})
}
> >
{t("section.category.action.create")} {t("section.category.action.create")}
</Menu.Item> </Menu.Item>

View File

@@ -0,0 +1,293 @@
"use client";
import { useCallback } from "react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { BoardPermission } from "@homarr/definitions";
import { boardPermissions } from "@homarr/definitions";
import { useForm } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import type { SelectProps, TablerIcon } from "@homarr/ui";
import {
Button,
Flex,
Group,
IconCheck,
IconEye,
IconPencil,
IconPlus,
IconSettings,
Select,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
} from "@homarr/ui";
import type { Board } from "../../_types";
interface Props {
board: Board;
initialPermissions: RouterOutputs["board"]["permissions"];
}
export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
const { data: permissions } = clientApi.board.permissions.useQuery(
{
id: board.id,
},
{
initialData: initialPermissions,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
const t = useI18n();
const form = useForm<FormType>({
initialValues: {
permissions: permissions.sort((permissionA, permissionB) => {
if (permissionA.user.id === board.creatorId) return -1;
if (permissionB.user.id === board.creatorId) return 1;
return permissionA.user.name.localeCompare(permissionB.user.name);
}),
},
});
const { mutate, isPending } = clientApi.board.savePermissions.useMutation();
const utils = clientApi.useUtils();
const { openModal } = useModalAction(UserSelectModal);
const handleSubmit = useCallback(
(v: FormType) => {
mutate(
{
id: board.id,
permissions: v.permissions,
},
{
onSuccess: () => {
void utils.board.permissions.invalidate();
},
},
);
},
[board.id, mutate, utils.board.permissions],
);
const handleAddUser = useCallback(() => {
const presentUserIds = form.values.permissions.map(
(permission) => permission.user.id,
);
openModal({
presentUserIds: board.creatorId
? presentUserIds.concat(board.creatorId)
: presentUserIds,
onSelect: (user) => {
form.setFieldValue("permissions", [
...form.values.permissions,
{
user,
permission: "board-view",
},
]);
},
});
}, [form, openModal, board.creatorId]);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Table>
<TableThead>
<TableTr>
<TableTh>
{t("board.setting.section.access.permission.field.user.label")}
</TableTh>
<TableTh>
{t(
"board.setting.section.access.permission.field.permission.label",
)}
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{board.creator && <CreatorRow user={board.creator} />}
{form.values.permissions.map((row, index) => {
const Icon = icons[row.permission];
return (
<TableTr key={row.user.id}>
<TableTd>{row.user.name}</TableTd>
<TableTd>
<Group wrap="nowrap">
<Select
flex="1"
leftSection={<Icon size="1rem" />}
renderOption={RenderOption}
variant="unstyled"
data={boardPermissions.map((permission) => ({
value: permission,
label: t(
`board.setting.section.access.permission.item.${permission}.label`,
),
}))}
{...form.getInputProps(
`permissions.${index}.permission`,
)}
/>
<Button
size="xs"
variant="subtle"
onClick={() => {
form.setFieldValue(
"permissions",
form.values.permissions.filter(
(_, i) => i !== index,
),
);
}}
>
{t("common.action.remove")}
</Button>
</Group>
</TableTd>
</TableTr>
);
})}
</TableTbody>
</Table>
<Group justify="space-between">
<Button
rightSection={<IconPlus size="1rem" />}
variant="light"
onClick={handleAddUser}
>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
</form>
);
};
interface CreatorRowProps {
user: Exclude<Board["creator"], null>;
}
const CreatorRow = ({ user }: CreatorRowProps) => {
const t = useI18n();
return (
<TableTr>
<TableTd>{user.name}</TableTd>
<TableTd>
<Group gap={0}>
<Flex w={34} h={34} align="center" justify="center">
<IconSettings
size="1rem"
color="var(--input-section-color, var(--mantine-color-dimmed))"
/>
</Flex>
<Text size="sm">
{t("board.setting.section.access.permission.item.board-full.label")}
</Text>
</Group>
</TableTd>
</TableTr>
);
};
const icons = {
"board-change": IconPencil,
"board-view": IconEye,
} satisfies Record<BoardPermission, TablerIcon>;
const iconProps = {
stroke: 1.5,
color: "currentColor",
opacity: 0.6,
size: "1rem",
};
const RenderOption: SelectProps["renderOption"] = ({ option, checked }) => {
const Icon = icons[option.value as BoardPermission];
return (
<Group flex="1" gap="xs">
<Icon {...iconProps} />
{option.label}
{checked && (
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
)}
</Group>
);
};
interface FormType {
permissions: RouterOutputs["board"]["permissions"];
}
interface InnerProps {
presentUserIds: string[];
onSelect: (props: { id: string; name: string }) => void;
}
interface UserSelectFormType {
userId: string;
}
export const UserSelectModal = createModal<InnerProps>(
({ actions, innerProps }) => {
const t = useI18n();
const { data: users } = clientApi.user.selectable.useQuery();
const form = useForm<UserSelectFormType>();
const handleSubmit = (v: UserSelectFormType) => {
const currentUser = users?.find((user) => user.id === v.userId);
if (!currentUser) return;
innerProps.onSelect({
id: currentUser.id,
name: currentUser.name ?? "",
});
actions.closeModal();
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Select
{...form.getInputProps("userId")}
label={t(
"board.setting.section.access.permission.userSelect.label",
)}
searchable
nothingFoundMessage={t(
"board.setting.section.access.permission.userSelect.notFound",
)}
limit={5}
data={users
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
/>
<Group justify="end">
<Button onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit">{t("common.action.add")}</Button>
</Group>
</Stack>
</form>
);
},
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.access.permission.userSelect.title"),
});

View File

@@ -4,10 +4,11 @@ import { useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { Button, Divider, Group, Stack, Text } from "@homarr/ui"; import { Button, Divider, Group, Stack, Text } from "@homarr/ui";
import { modalEvents } from "~/app/[locale]/modals"; import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { useRequiredBoard } from "../../_context"; import { useRequiredBoard } from "../../_context";
import classes from "./danger.module.css"; import classes from "./danger.module.css";
@@ -15,6 +16,8 @@ export const DangerZoneSettingsContent = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
const t = useScopedI18n("board.setting"); const t = useScopedI18n("board.setting");
const router = useRouter(); const router = useRouter();
const { openConfirmModal } = useConfirmModal();
const { openModal } = useModalAction(BoardRenameModal);
const { mutate: changeVisibility, isPending: isChangeVisibilityPending } = const { mutate: changeVisibility, isPending: isChangeVisibilityPending } =
clientApi.board.changeVisibility.useMutation(); clientApi.board.changeVisibility.useMutation();
const { mutate: deleteBoard, isPending: isDeletePending } = const { mutate: deleteBoard, isPending: isDeletePending } =
@@ -24,31 +27,22 @@ export const DangerZoneSettingsContent = () => {
const onRenameClick = useCallback( const onRenameClick = useCallback(
() => () =>
modalEvents.openManagedModal({ openModal({
modal: "boardRenameModal", id: board.id,
title: t("section.dangerZone.action.rename.modal.title"), previousName: board.name,
innerProps: { onSuccess: (name) => router.push(`/boards/${name}/settings`),
id: board.id,
previousName: board.name,
onSuccess: (name) => {
router.push(`/boards/${name}/settings`);
},
},
}), }),
[board.id, board.name, router, t], [board.id, board.name, router, openModal],
); );
const onVisibilityClick = useCallback(() => { const onVisibilityClick = useCallback(() => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t( title: t(
`section.dangerZone.action.visibility.confirm.${visibility}.title`, `section.dangerZone.action.visibility.confirm.${visibility}.title`,
), ),
children: t( children: t(
`section.dangerZone.action.visibility.confirm.${visibility}.description`, `section.dangerZone.action.visibility.confirm.${visibility}.description`,
), ),
confirmProps: {
color: "red.9",
},
onConfirm: () => { onConfirm: () => {
changeVisibility( changeVisibility(
{ {
@@ -72,15 +66,13 @@ export const DangerZoneSettingsContent = () => {
utils.board.byName, utils.board.byName,
utils.board.default, utils.board.default,
visibility, visibility,
openConfirmModal,
]); ]);
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t("section.dangerZone.action.delete.confirm.title"), title: t("section.dangerZone.action.delete.confirm.title"),
children: t("section.dangerZone.action.delete.confirm.description"), children: t("section.dangerZone.action.delete.confirm.description"),
confirmProps: {
color: "red.9",
},
onConfirm: () => { onConfirm: () => {
deleteBoard( deleteBoard(
{ id: board.id }, { id: board.id },
@@ -92,7 +84,7 @@ export const DangerZoneSettingsContent = () => {
); );
}, },
}); });
}, [board.id, deleteBoard, router, t]); }, [board.id, deleteBoard, router, t, openConfirmModal]);
return ( return (
<Stack gap="sm"> <Stack gap="sm">

View File

@@ -16,12 +16,14 @@ import {
IconLayout, IconLayout,
IconPhoto, IconPhoto,
IconSettings, IconSettings,
IconUser,
Stack, Stack,
Text, Text,
Title, Title,
} from "@homarr/ui"; } from "@homarr/ui";
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion"; import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
import { AccessSettingsContent } from "./_access";
import { BackgroundSettingsContent } from "./_background"; import { BackgroundSettingsContent } from "./_background";
import { ColorSettingsContent } from "./_colors"; import { ColorSettingsContent } from "./_colors";
import { CustomCssSettingsContent } from "./_customCss"; import { CustomCssSettingsContent } from "./_customCss";
@@ -43,6 +45,7 @@ export default async function BoardSettingsPage({
searchParams, searchParams,
}: Props) { }: Props) {
const board = await api.board.byName({ name: params.name }); const board = await api.board.byName({ name: params.name });
const permissions = await api.board.permissions({ id: board.id });
const t = await getScopedI18n("board.setting"); const t = await getScopedI18n("board.setting");
return ( return (
@@ -68,6 +71,12 @@ export default async function BoardSettingsPage({
<AccordionItemFor value="customCss" icon={IconFileTypeCss}> <AccordionItemFor value="customCss" icon={IconFileTypeCss}>
<CustomCssSettingsContent /> <CustomCssSettingsContent />
</AccordionItemFor> </AccordionItemFor>
<AccordionItemFor value="access" icon={IconUser}>
<AccessSettingsContent
board={board}
initialPermissions={permissions}
/>
</AccordionItemFor>
<AccordionItemFor <AccordionItemFor
value="dangerZone" value="dangerZone"
icon={IconAlertTriangle} icon={IconAlertTriangle}

View File

@@ -45,7 +45,9 @@ export const ClientBoard = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
const isReady = useIsBoardReady(); const isReady = useIsBoardReady();
const sortedSections = board.sections.sort((a, b) => a.position - b.position); const sortedSections = board.sections.sort(
(sectionA, sectionB) => sectionA.position - sectionB.position,
);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);

View File

@@ -13,6 +13,10 @@ import type { Board } from "./_types";
// This is placed here because it's used in the layout and the page and because it's here it's not needed to load it everywhere // This is placed here because it's used in the layout and the page and because it's here it's not needed to load it everywhere
import "../../../styles/gridstack.scss"; import "../../../styles/gridstack.scss";
import { notFound } from "next/navigation";
import { auth } from "@homarr/auth";
import { and, db, eq, schema } from "@homarr/db";
import { GlobalItemServerDataRunner } from "@homarr/widgets"; import { GlobalItemServerDataRunner } from "@homarr/widgets";
import { BoardMantineProvider } from "./_theme"; import { BoardMantineProvider } from "./_theme";
@@ -51,10 +55,14 @@ export const createBoardPage = <TParams extends Record<string, unknown>>({
</GlobalItemServerDataRunner> </GlobalItemServerDataRunner>
); );
}, },
page: () => { page: async ({ params }: { params: TParams }) => {
// TODO: Add check if board is private and user is not logged in const board = await getInitialBoard(params);
return <ClientBoard />; if (await canAccessBoardAsync(board)) {
return <ClientBoard />;
}
return notFound();
}, },
generateMetadata: async ({ generateMetadata: async ({
params, params,
@@ -63,6 +71,10 @@ export const createBoardPage = <TParams extends Record<string, unknown>>({
}): Promise<Metadata> => { }): Promise<Metadata> => {
const board = await getInitialBoard(params); const board = await getInitialBoard(params);
if (!(await canAccessBoardAsync(board))) {
return {};
}
return { return {
title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`, title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`,
icons: { icons: {
@@ -72,3 +84,30 @@ export const createBoardPage = <TParams extends Record<string, unknown>>({
}, },
}; };
}; };
const canAccessBoardAsync = async (board: Board) => {
const session = await auth();
if (board.isPublic) {
return true; // Public boards can be accessed by anyone
}
if (!session) {
return false; // Not logged in users can't access private boards
}
if (board.creatorId === session?.user.id) {
return true; // Creators can access their own private boards
}
const permissions = await db.query.boardPermissions.findMany({
where: and(
eq(schema.boardPermissions.userId, session.user.id),
eq(schema.boardPermissions.boardId, board.id),
),
});
return ["board-view", "board-change"].some((key) =>
permissions.some(({ permission }) => key === permission),
); // Allow access for all with any board permission
};

View File

@@ -51,7 +51,7 @@ export const InitUserForm = () => {
<Stack gap="xl"> <Stack gap="xl">
<form <form
onSubmit={form.onSubmit( onSubmit={form.onSubmit(
(v) => void handleSubmit(v), (values) => void handleSubmit(values),
(err) => console.log(err), (err) => console.log(err),
)} )}
> >

View File

@@ -5,11 +5,11 @@ import "@homarr/notifications/styles.css";
import "@homarr/spotlight/styles.css"; import "@homarr/spotlight/styles.css";
import "@homarr/ui/styles.css"; import "@homarr/ui/styles.css";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications"; import { Notifications } from "@homarr/notifications";
import { ColorSchemeScript, createTheme, MantineProvider } from "@homarr/ui"; import { ColorSchemeScript, createTheme, MantineProvider } from "@homarr/ui";
import { JotaiProvider } from "./_client-providers/jotai"; import { JotaiProvider } from "./_client-providers/jotai";
import { ModalsProvider } from "./_client-providers/modals";
import { NextInternationalProvider } from "./_client-providers/next-international"; import { NextInternationalProvider } from "./_client-providers/next-international";
import { TRPCReactProvider } from "./_client-providers/trpc"; import { TRPCReactProvider } from "./_client-providers/trpc";
import { composeWrappers } from "./compose"; import { composeWrappers } from "./compose";
@@ -67,7 +67,7 @@ export default function Layout(props: {
})} })}
/> />
), ),
(innerProps) => <ModalsProvider {...innerProps} />, (innerProps) => <ModalProvider {...innerProps} />,
]); ]);
return ( return (

View File

@@ -1,13 +1,14 @@
"use client"; "use client";
import React from "react"; import { useCallback } from "react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, IconCategoryPlus } from "@homarr/ui"; import { Button, IconCategoryPlus } from "@homarr/ui";
import { modalEvents } from "~/app/[locale]/modals";
import { revalidatePathAction } from "~/app/revalidatePathAction"; import { revalidatePathAction } from "~/app/revalidatePathAction";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
interface CreateBoardButtonProps { interface CreateBoardButtonProps {
boardNames: string[]; boardNames: string[];
@@ -15,6 +16,7 @@ interface CreateBoardButtonProps {
export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => { export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
const t = useI18n(); const t = useI18n();
const { openModal } = useModalAction(AddBoardModal);
const { mutateAsync, isPending } = clientApi.board.create.useMutation({ const { mutateAsync, isPending } = clientApi.board.create.useMutation({
onSettled: async () => { onSettled: async () => {
@@ -22,20 +24,16 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
}, },
}); });
const onClick = React.useCallback(() => { const onClick = useCallback(() => {
modalEvents.openManagedModal({ openModal({
modal: "addBoardModal", onSuccess: async (values) => {
title: t("management.page.board.button.create"), await mutateAsync({
innerProps: { name: values.name,
onSuccess: async (values) => { });
await mutateAsync({
name: values.name,
});
},
boardNames,
}, },
boardNames,
}); });
}, [mutateAsync, t, boardNames]); }, [mutateAsync, boardNames, openModal]);
return ( return (
<Button <Button

View File

@@ -1,20 +0,0 @@
"use client";
import { createModalManager } from "mantine-modal-manager";
import { WidgetEditModal } from "@homarr/widgets";
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
import { PreviewDimensionsModal } from "./widgets/[kind]/_dimension-modal";
export const [ModalsManager, modalEvents] = createModalManager({
categoryEditModal: CategoryEditModal,
widgetEditModal: WidgetEditModal,
itemSelectModal: ItemSelectModal,
addBoardModal: AddBoardModal,
boardRenameModal: BoardRenameModal,
dimensionsModal: PreviewDimensionsModal,
});

View File

@@ -3,6 +3,7 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications"; import { showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { import {
@@ -17,10 +18,11 @@ import {
import { import {
loadWidgetDynamic, loadWidgetDynamic,
reduceWidgetOptionsWithDefaultValues, reduceWidgetOptionsWithDefaultValues,
WidgetEditModal,
widgetImports, widgetImports,
} from "@homarr/widgets"; } from "@homarr/widgets";
import { modalEvents } from "../../modals"; import { PreviewDimensionsModal } from "./_dimension-modal";
import type { Dimensions } from "./_dimension-modal"; import type { Dimensions } from "./_dimension-modal";
interface WidgetPreviewPageContentProps { interface WidgetPreviewPageContentProps {
@@ -38,6 +40,10 @@ export const WidgetPreviewPageContent = ({
integrationData, integrationData,
}: WidgetPreviewPageContentProps) => { }: WidgetPreviewPageContentProps) => {
const t = useScopedI18n("widgetPreview"); const t = useScopedI18n("widgetPreview");
const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal);
const { openModal: openPreviewDimensionsModal } = useModalAction(
PreviewDimensionsModal,
);
const currentDefinition = useMemo( const currentDefinition = useMemo(
() => widgetImports[kind].definition, () => widgetImports[kind].definition,
[kind], [kind],
@@ -55,28 +61,25 @@ export const WidgetPreviewPageContent = ({
integrations: [], integrations: [],
}); });
const Comp = loadWidgetDynamic(kind); const handleOpenEditWidgetModal = useCallback(() => {
openWidgetEditModal({
const openWitgetEditModal = useCallback(() => { kind,
return modalEvents.openManagedModal({ value: state,
modal: "widgetEditModal", onSuccessfulEdit: (value) => {
innerProps: { setState(value);
kind,
value: state,
onSuccessfulEdit: (value) => {
setState(value);
},
integrationData: integrationData.filter(
(integration) =>
"supportedIntegrations" in currentDefinition &&
(currentDefinition.supportedIntegrations as string[]).some(
(kind) => kind === integration.kind,
),
),
integrationSupport: "supportedIntegrations" in currentDefinition,
}, },
integrationData: integrationData.filter(
(integration) =>
"supportedIntegrations" in currentDefinition &&
(currentDefinition.supportedIntegrations as string[]).some(
(kind) => kind === integration.kind,
),
),
integrationSupport: "supportedIntegrations" in currentDefinition,
}); });
}, [kind, state, integrationData, currentDefinition]); }, [currentDefinition, integrationData, kind, openWidgetEditModal, state]);
const Comp = loadWidgetDynamic(kind);
const toggleEditMode = useCallback(() => { const toggleEditMode = useCallback(() => {
setEditMode((editMode) => !editMode); setEditMode((editMode) => !editMode);
@@ -86,15 +89,11 @@ export const WidgetPreviewPageContent = ({
}, [editMode, t]); }, [editMode, t]);
const openDimensionsModal = useCallback(() => { const openDimensionsModal = useCallback(() => {
modalEvents.openManagedModal({ openPreviewDimensionsModal({
modal: "dimensionsModal", dimensions,
title: t("dimensions.title"), setDimensions,
innerProps: {
dimensions,
setDimensions,
},
}); });
}, [dimensions, t]); }, [dimensions, openPreviewDimensionsModal]);
return ( return (
<> <>
@@ -107,7 +106,8 @@ export const WidgetPreviewPageContent = ({
<Comp <Comp
options={state.options as never} options={state.options as never}
integrations={state.integrations.map( integrations={state.integrations.map(
(id) => integrationData.find((x) => x.id === id)!, (id) =>
integrationData.find((integration) => integration.id === id)!,
)} )}
width={dimensions.width} width={dimensions.width}
height={dimensions.height} height={dimensions.height}
@@ -119,7 +119,7 @@ export const WidgetPreviewPageContent = ({
size={48} size={48}
variant="default" variant="default"
radius="xl" radius="xl"
onClick={openWitgetEditModal} onClick={handleOpenEditWidgetModal}
> >
<IconPencil size={24} /> <IconPencil size={24} />
</ActionIcon> </ActionIcon>

View File

@@ -1,8 +1,7 @@
"use client"; "use client";
import type { ManagedModal } from "mantine-modal-manager";
import { useForm } from "@homarr/form"; import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Group, InputWrapper, Slider, Stack } from "@homarr/ui"; import { Button, Group, InputWrapper, Slider, Stack } from "@homarr/ui";
@@ -11,49 +10,50 @@ interface InnerProps {
setDimensions: (dimensions: Dimensions) => void; setDimensions: (dimensions: Dimensions) => void;
} }
export const PreviewDimensionsModal: ManagedModal<InnerProps> = ({ export const PreviewDimensionsModal = createModal<InnerProps>(
actions, ({ actions, innerProps }) => {
innerProps, const t = useI18n();
}) => { const form = useForm({
const t = useI18n(); initialValues: innerProps.dimensions,
const form = useForm({ });
initialValues: innerProps.dimensions,
});
const handleSubmit = (values: Dimensions) => { const handleSubmit = (values: Dimensions) => {
innerProps.setDimensions(values); innerProps.setDimensions(values);
actions.closeModal(); actions.closeModal();
}; };
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack> <Stack>
<InputWrapper label={t("item.move.field.width.label")}> <InputWrapper label={t("item.move.field.width.label")}>
<Slider <Slider
min={64} min={64}
max={1024} max={1024}
step={64} step={64}
{...form.getInputProps("width")} {...form.getInputProps("width")}
/> />
</InputWrapper> </InputWrapper>
<InputWrapper label={t("item.move.field.height.label")}> <InputWrapper label={t("item.move.field.height.label")}>
<Slider <Slider
min={64} min={64}
max={1024} max={1024}
step={64} step={64}
{...form.getInputProps("height")} {...form.getInputProps("height")}
/> />
</InputWrapper> </InputWrapper>
<Group justify="end"> <Group justify="end">
<Button variant="subtle" color="gray" onClick={actions.closeModal}> <Button variant="subtle" color="gray" onClick={actions.closeModal}>
{t("common.action.cancel")} {t("common.action.cancel")}
</Button> </Button>
<Button type="submit">{t("common.action.confirm")}</Button> <Button type="submit">{t("common.action.confirm")}</Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
); );
}; },
).withOptions({
defaultTitle: (t) => t("widgetPreview.dimensions.title"),
});
export interface Dimensions { export interface Dimensions {
width: number; width: number;

View File

@@ -41,8 +41,12 @@ export const useItemActions = () => {
({ kind }: CreateItem) => { ({ kind }: CreateItem) => {
updateBoard((previous) => { updateBoard((previous) => {
const lastSection = previous.sections const lastSection = previous.sections
.filter((s): s is EmptySection => s.kind === "empty") .filter(
.sort((a, b) => b.position - a.position)[0]; (section): section is EmptySection => section.kind === "empty",
)
.sort(
(sectionA, sectionB) => sectionB.position - sectionA.position,
)[0];
if (!lastSection) return previous; if (!lastSection) return previous;

View File

@@ -1,6 +1,5 @@
import type { ManagedModal } from "mantine-modal-manager";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Card, Center, Grid, Stack, Text } from "@homarr/ui"; import { Button, Card, Center, Grid, Stack, Text } from "@homarr/ui";
@@ -9,9 +8,7 @@ import { widgetImports } from "../../../../../../packages/widgets/src";
import type { WidgetDefinition } from "../../../../../../packages/widgets/src/definition"; import type { WidgetDefinition } from "../../../../../../packages/widgets/src/definition";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
export const ItemSelectModal: ManagedModal<Record<string, never>> = ({ export const ItemSelectModal = createModal<void>(({ actions }) => {
actions,
}) => {
return ( return (
<Grid> <Grid>
{objectEntries(widgetImports).map(([key, value]) => { {objectEntries(widgetImports).map(([key, value]) => {
@@ -26,7 +23,10 @@ export const ItemSelectModal: ManagedModal<Record<string, never>> = ({
})} })}
</Grid> </Grid>
); );
}; }).withOptions({
defaultTitle: (t) => t("item.create.title"),
size: "xl",
});
const WidgetItem = ({ const WidgetItem = ({
kind, kind,

View File

@@ -1,9 +1,8 @@
"use client"; "use client";
import type { ManagedModal } from "mantine-modal-manager";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form"; import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Group, Stack, TextInput } from "@homarr/ui"; import { Button, Group, Stack, TextInput } from "@homarr/ui";
import type { validation, z } from "@homarr/validation"; import type { validation, z } from "@homarr/validation";
@@ -14,58 +13,60 @@ interface InnerProps {
onSuccess?: (name: string) => void; onSuccess?: (name: string) => void;
} }
export const BoardRenameModal: ManagedModal<InnerProps> = ({ export const BoardRenameModal = createModal<InnerProps>(
actions, ({ actions, innerProps }) => {
innerProps, const utils = clientApi.useUtils();
}) => { const t = useI18n();
const utils = clientApi.useUtils(); const { mutate, isPending } = clientApi.board.rename.useMutation({
const t = useI18n(); onSettled() {
const { mutate, isPending } = clientApi.board.rename.useMutation({ void utils.board.byName.invalidate({ name: innerProps.previousName });
onSettled() { void utils.board.default.invalidate();
void utils.board.byName.invalidate({ name: innerProps.previousName });
void utils.board.default.invalidate();
},
});
const form = useForm<FormType>({
initialValues: {
name: innerProps.previousName,
},
});
const handleSubmit = (values: FormType) => {
mutate(
{
id: innerProps.id,
name: values.name,
}, },
{ });
onSuccess: () => { const form = useForm<FormType>({
actions.closeModal(); initialValues: {
innerProps.onSuccess?.(values.name); name: innerProps.previousName,
},
});
const handleSubmit = (values: FormType) => {
mutate(
{
id: innerProps.id,
name: values.name,
}, },
}, {
); onSuccess: () => {
}; actions.closeModal();
innerProps.onSuccess?.(values.name);
},
},
);
};
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack> <Stack>
<TextInput <TextInput
label={t("board.field.name.label")} label={t("board.field.name.label")}
{...form.getInputProps("name")} {...form.getInputProps("name")}
data-autofocus data-autofocus
/> />
<Group justify="end"> <Group justify="end">
<Button variant="subtle" color="gray" onClick={actions.closeModal}> <Button variant="subtle" color="gray" onClick={actions.closeModal}>
{t("common.action.cancel")} {t("common.action.cancel")}
</Button> </Button>
<Button type="submit" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.confirm")} {t("common.action.confirm")}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
); );
}; },
).withOptions({
defaultTitle: (t) =>
t("board.setting.section.dangerZone.action.rename.modal.title"),
});
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">; type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">;

View File

@@ -80,10 +80,10 @@ export const useCategoryActions = () => {
updateBoard((previous) => { updateBoard((previous) => {
const lastSection = previous.sections const lastSection = previous.sections
.filter( .filter(
(x): x is CategorySection | EmptySection => (section): section is CategorySection | EmptySection =>
x.kind === "empty" || x.kind === "category", section.kind === "empty" || section.kind === "category",
) )
.sort((a, b) => b.position - a.position) .sort((sectionA, sectionB) => sectionB.position - sectionA.position)
.at(0); .at(0);
if (!lastSection) return previous; if (!lastSection) return previous;

View File

@@ -1,6 +1,5 @@
import type { ManagedModal } from "mantine-modal-manager";
import { useForm } from "@homarr/form"; import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Group, Stack, TextInput } from "@homarr/ui"; import { Button, Group, Stack, TextInput } from "@homarr/ui";
@@ -15,42 +14,41 @@ interface InnerProps {
onSuccess: (category: Category) => void; onSuccess: (category: Category) => void;
} }
export const CategoryEditModal: ManagedModal<InnerProps> = ({ export const CategoryEditModal = createModal<InnerProps>(
actions, ({ actions, innerProps }) => {
innerProps, const t = useI18n();
}) => { const form = useForm({
const t = useI18n(); initialValues: {
const form = useForm({ name: innerProps.category.name,
initialValues: { },
name: innerProps.category.name, });
},
});
return ( return (
<form <form
onSubmit={form.onSubmit((v) => { onSubmit={form.onSubmit((values) => {
void innerProps.onSuccess({ void innerProps.onSuccess({
...innerProps.category, ...innerProps.category,
name: v.name, name: values.name,
}); });
actions.closeModal(); actions.closeModal();
})} })}
> >
<Stack> <Stack>
<TextInput <TextInput
label={t("section.category.field.name.label")} label={t("section.category.field.name.label")}
data-autofocus data-autofocus
{...form.getInputProps("name")} {...form.getInputProps("name")}
/> />
<Group justify="right"> <Group justify="right">
<Button onClick={actions.closeModal} variant="subtle" color="gray"> <Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")} {t("common.action.cancel")}
</Button> </Button>
<Button type="submit" color="teal"> <Button type="submit" color="teal">
{innerProps.submitLabel} {innerProps.submitLabel}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
); );
}; },
).withOptions({});

View File

@@ -1,23 +1,24 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import type { CategorySection } from "~/app/[locale]/boards/_types"; import type { CategorySection } from "~/app/[locale]/boards/_types";
import { modalEvents } from "~/app/[locale]/modals";
import { useCategoryActions } from "./category-actions"; import { useCategoryActions } from "./category-actions";
import { CategoryEditModal } from "./category-edit-modal";
export const useCategoryMenuActions = (category: CategorySection) => { export const useCategoryMenuActions = (category: CategorySection) => {
const { openModal } = useModalAction(CategoryEditModal);
const { openConfirmModal } = useConfirmModal();
const { addCategory, moveCategory, removeCategory, renameCategory } = const { addCategory, moveCategory, removeCategory, renameCategory } =
useCategoryActions(); useCategoryActions();
const t = useI18n(); const t = useI18n();
const createCategoryAtPosition = useCallback( const createCategoryAtPosition = useCallback(
(position: number) => { (position: number) => {
modalEvents.openManagedModal({ openModal(
title: t("section.category.create.title"), {
modal: "categoryEditModal",
innerProps: {
category: { category: {
id: createId(), id: createId(),
name: t("section.category.create.title"), name: t("section.category.create.title"),
@@ -30,9 +31,12 @@ export const useCategoryMenuActions = (category: CategorySection) => {
}, },
submitLabel: t("section.category.create.submit"), submitLabel: t("section.category.create.submit"),
}, },
}); {
title: (t) => t("section.category.create.title"),
},
);
}, },
[addCategory, t], [addCategory, t, openModal],
); );
// creates a new category above the current // creates a new category above the current
@@ -63,7 +67,7 @@ export const useCategoryMenuActions = (category: CategorySection) => {
// Removes the current category // Removes the current category
const remove = useCallback(() => { const remove = useCallback(() => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t("section.category.remove.title"), title: t("section.category.remove.title"),
children: t("section.category.remove.message", { children: t("section.category.remove.message", {
name: category.name, name: category.name,
@@ -73,17 +77,12 @@ export const useCategoryMenuActions = (category: CategorySection) => {
id: category.id, id: category.id,
}); });
}, },
confirmProps: {
color: "red",
},
}); });
}, [category.id, category.name, removeCategory, t]); }, [category.id, category.name, removeCategory, t, openConfirmModal]);
const edit = () => { const edit = useCallback(() => {
modalEvents.openManagedModal({ openModal(
modal: "categoryEditModal", {
title: t("section.category.edit.title"),
innerProps: {
category, category,
submitLabel: t("section.category.edit.submit"), submitLabel: t("section.category.edit.submit"),
onSuccess: (category) => { onSuccess: (category) => {
@@ -93,8 +92,11 @@ export const useCategoryMenuActions = (category: CategorySection) => {
}); });
}, },
}, },
}); {
}; title: (t) => t("section.category.edit.title"),
},
);
}, [category, openModal, renameCategory, t]);
return { return {
addCategoryAbove, addCategoryAbove,

View File

@@ -6,6 +6,7 @@ import { useElementSize } from "@mantine/hooks";
import cx from "clsx"; import cx from "clsx";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { import {
ActionIcon, ActionIcon,
@@ -20,11 +21,11 @@ import {
loadWidgetDynamic, loadWidgetDynamic,
reduceWidgetOptionsWithDefaultValues, reduceWidgetOptionsWithDefaultValues,
useServerDataFor, useServerDataFor,
WidgetEditModal,
} from "@homarr/widgets"; } from "@homarr/widgets";
import { useRequiredBoard } from "~/app/[locale]/boards/_context"; import { useRequiredBoard } from "~/app/[locale]/boards/_context";
import type { Item } from "~/app/[locale]/boards/_types"; import type { Item } from "~/app/[locale]/boards/_types";
import { modalEvents } from "~/app/[locale]/modals";
import { editModeAtom } from "../editMode"; import { editModeAtom } from "../editMode";
import { useItemActions } from "../items/item-actions"; import { useItemActions } from "../items/item-actions";
import type { UseGridstackRefs } from "./gridstack/use-gridstack"; import type { UseGridstackRefs } from "./gridstack/use-gridstack";
@@ -108,43 +109,38 @@ const BoardItem = ({ item, ...dimensions }: ItemProps) => {
const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => { const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
const t = useScopedI18n("item"); const t = useScopedI18n("item");
const { openModal } = useModalAction(WidgetEditModal);
const { openConfirmModal } = useConfirmModal();
const isEditMode = useAtomValue(editModeAtom); const isEditMode = useAtomValue(editModeAtom);
const { updateItemOptions, removeItem } = useItemActions(); const { updateItemOptions, removeItem } = useItemActions();
if (!isEditMode) return null; if (!isEditMode) return null;
const openEditModal = () => { const openEditModal = () => {
modalEvents.openManagedModal({ openModal({
title: t("edit.title"), kind: item.kind,
modal: "widgetEditModal", value: {
innerProps: { options: item.options,
kind: item.kind, integrations: item.integrations.map(({ id }) => id),
value: {
options: item.options,
integrations: item.integrations.map(({ id }) => id),
},
onSuccessfulEdit: ({ options, integrations: _ }) => {
updateItemOptions({
itemId: item.id,
newOptions: options,
});
},
integrationData: [],
integrationSupport: false,
}, },
onSuccessfulEdit: ({ options, integrations: _ }) => {
updateItemOptions({
itemId: item.id,
newOptions: options,
});
},
integrationData: [],
integrationSupport: false,
}); });
}; };
const openRemoveModal = () => { const openRemoveModal = () => {
modalEvents.openConfirmModal({ openConfirmModal({
title: t("remove.title"), title: t("remove.title"),
children: t("remove.message"), children: t("remove.message"),
onConfirm: () => { onConfirm: () => {
removeItem({ itemId: item.id }); removeItem({ itemId: item.id });
}, },
confirmProps: {
color: "red",
},
}); });
}; };

View File

@@ -10,7 +10,10 @@ export const navigationCollapsedAtom = atom(true);
export const ClientBurger = () => { export const ClientBurger = () => {
const [collapsed, setCollapsed] = useAtom(navigationCollapsedAtom); const [collapsed, setCollapsed] = useAtom(navigationCollapsedAtom);
const toggle = useCallback(() => setCollapsed((c) => !c), [setCollapsed]); const toggle = useCallback(
() => setCollapsed((collapsed) => !collapsed),
[setCollapsed],
);
return ( return (
<Burger opened={!collapsed} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger opened={!collapsed} onClick={toggle} hiddenFrom="sm" size="sm" />

View File

@@ -1,7 +1,7 @@
import type { ManagedModal } from "mantine-modal-manager";
import { boardSchemas } from "node_modules/@homarr/validation/src/board"; import { boardSchemas } from "node_modules/@homarr/validation/src/board";
import { useForm, zodResolver } from "@homarr/form"; import { useForm, zodResolver } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Group, Stack, TextInput } from "@homarr/ui"; import { Button, Group, Stack, TextInput } from "@homarr/ui";
import { z } from "@homarr/validation"; import { z } from "@homarr/validation";
@@ -11,48 +11,51 @@ interface InnerProps {
onSuccess: ({ name }: { name: string }) => Promise<void>; onSuccess: ({ name }: { name: string }) => Promise<void>;
} }
export const AddBoardModal: ManagedModal<InnerProps> = ({ export const AddBoardModal = createModal<InnerProps>(
actions, ({ actions, innerProps }) => {
innerProps, const t = useI18n();
}) => { const form = useForm({
const t = useI18n(); initialValues: {
const form = useForm({ name: "",
initialValues: { },
name: "", validate: zodResolver(
}, z.object({
validate: zodResolver( name: boardSchemas.byName.shape.name.refine(
z.object({ (value) => !innerProps.boardNames.includes(value),
name: boardSchemas.byName.shape.name.refine( ),
(value) => !innerProps.boardNames.includes(value), }),
), ),
}), validateInputOnBlur: true,
), validateInputOnChange: true,
validateInputOnBlur: true, });
validateInputOnChange: true,
});
return ( return (
<form <form
onSubmit={form.onSubmit((values) => { onSubmit={form.onSubmit((values) => {
void innerProps.onSuccess(values); void innerProps.onSuccess(values);
actions.closeModal(); actions.closeModal();
})} })}
> >
<Stack> <Stack>
<TextInput <TextInput
label={t("management.page.board.modal.createBoard.field.name.label")} label={t(
data-autofocus "management.page.board.modal.createBoard.field.name.label",
{...form.getInputProps("name")} )}
/> data-autofocus
<Group justify="right"> {...form.getInputProps("name")}
<Button onClick={actions.closeModal} variant="subtle" color="gray"> />
{t("common.action.cancel")} <Group justify="right">
</Button> <Button onClick={actions.closeModal} variant="subtle" color="gray">
<Button disabled={!form.isValid()} type="submit" color="teal"> {t("common.action.cancel")}
{t("common.action.create")} </Button>
</Button> <Button disabled={!form.isValid()} type="submit" color="teal">
</Group> {t("common.action.create")}
</Stack> </Button>
</form> </Group>
); </Stack>
}; </form>
);
},
).withOptions({
defaultTitle: (t) => t("management.page.board.button.create"),
});

View File

@@ -10,7 +10,7 @@ export const env = createEnv({
VERCEL_URL: z VERCEL_URL: z
.string() .string()
.optional() .optional()
.transform((v) => (v ? `https://${v}` : undefined)), .transform((url) => (url ? `https://${url}` : undefined)),
PORT: z.coerce.number().default(3000), PORT: z.coerce.number().default(3000),
}, },
/** /**

View File

@@ -4,6 +4,7 @@ import superjson from "superjson";
import type { Database, SQL } from "@homarr/db"; import type { Database, SQL } from "@homarr/db";
import { and, createId, eq, inArray } from "@homarr/db"; import { and, createId, eq, inArray } from "@homarr/db";
import { import {
boardPermissions,
boards, boards,
integrationItems, integrationItems,
items, items,
@@ -309,6 +310,52 @@ export const boardRouter = createTRPCRouter({
} }
}); });
}), }),
permissions: publicProcedure
.input(validation.board.permissions)
.query(async ({ input, ctx }) => {
const permissions = await ctx.db.query.boardPermissions.findMany({
where: eq(boardPermissions.boardId, input.id),
with: {
user: {
columns: {
id: true,
name: true,
},
},
},
});
return permissions
.map((permission) => ({
user: {
id: permission.userId,
name: permission.user.name ?? "",
},
permission: permission.permission,
}))
.sort((permissionA, permissionB) => {
return permissionA.user.name.localeCompare(permissionB.user.name);
});
}),
savePermissions: publicProcedure
.input(validation.board.savePermissions)
.mutation(async ({ input, ctx }) => {
await ctx.db.transaction(async (tx) => {
await tx
.delete(boardPermissions)
.where(eq(boardPermissions.boardId, input.id));
if (input.permissions.length === 0) {
return;
}
await tx.insert(boardPermissions).values(
input.permissions.map((permission) => ({
userId: permission.user.id,
permission: permission.permission,
boardId: input.id,
})),
);
});
}),
}); });
const noBoardWithSimilarName = async ( const noBoardWithSimilarName = async (
@@ -341,6 +388,12 @@ const getFullBoardWithWhere = async (db: Database, where: SQL<unknown>) => {
const board = await db.query.boards.findFirst({ const board = await db.query.boards.findFirst({
where, where,
with: { with: {
creator: {
columns: {
id: true,
name: true,
},
},
sections: { sections: {
with: { with: {
items: { items: {

View File

@@ -44,6 +44,14 @@ export const userRouter = createTRPCRouter({
}, },
}); });
}), }),
selectable: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.users.findMany({
columns: {
id: true,
name: true,
},
});
}),
getById: publicProcedure getById: publicProcedure
.input(z.object({ userId: z.string() })) .input(z.object({ userId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {

View File

@@ -54,12 +54,12 @@ const createAdapter = () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports // eslint-disable-next-line @typescript-eslint/consistent-type-imports
type SessionExport = typeof import("../session"); type SessionExport = typeof import("../session");
const mockSessionToken = "e9ef3010-6981-4a81-b9d6-8495d09cf3b5" as const; const mockSessionToken = "e9ef3010-6981-4a81-b9d6-8495d09cf3b5";
const mockSessionExpiry = new Date("2023-07-01"); const mockSessionExpiry = new Date("2023-07-01");
vi.mock("../session", async (importOriginal) => { vi.mock("../session", async (importOriginal) => {
const mod = await importOriginal<SessionExport>(); const mod = await importOriginal<SessionExport>();
const generateSessionToken = () => mockSessionToken; const generateSessionToken = (): typeof mockSessionToken => mockSessionToken;
const expireDateAfter = (_seconds: number) => mockSessionExpiry; const expireDateAfter = (_seconds: number) => mockSessionExpiry;
return { return {

View File

@@ -14,10 +14,28 @@ CREATE TABLE `account` (
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `app` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`description` text,
`icon_url` text NOT NULL,
`href` text
);
--> statement-breakpoint
CREATE TABLE `boardPermission` (
`board_id` text NOT NULL,
`user_id` text NOT NULL,
`permission` text NOT NULL,
PRIMARY KEY(`board_id`, `permission`, `user_id`),
FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `board` ( CREATE TABLE `board` (
`id` text PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`is_public` integer DEFAULT false NOT NULL, `is_public` integer DEFAULT false NOT NULL,
`creator_id` text,
`page_title` text, `page_title` text,
`meta_title` text, `meta_title` text,
`logo_image_url` text, `logo_image_url` text,
@@ -30,7 +48,8 @@ CREATE TABLE `board` (
`secondary_color` text DEFAULT '#fd7e14' NOT NULL, `secondary_color` text DEFAULT '#fd7e14' NOT NULL,
`opacity` integer DEFAULT 100 NOT NULL, `opacity` integer DEFAULT 100 NOT NULL,
`custom_css` text, `custom_css` text,
`column_count` integer DEFAULT 10 NOT NULL `column_count` integer DEFAULT 10 NOT NULL,
FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `integration_item` ( CREATE TABLE `integration_item` (

View File

@@ -1,7 +0,0 @@
CREATE TABLE `app` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`description` text,
`icon_url` text NOT NULL,
`href` text
);

View File

@@ -1,7 +1,7 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "7ba12cbf-d8eb-4469-b7c5-7f31acb7717d", "id": "7c2291ee-febd-4b90-994c-85e6ef27102d",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@@ -111,6 +111,104 @@
}, },
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"app": {
"name": "app",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"icon_url": {
"name": "icon_url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"href": {
"name": "href",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"boardPermission": {
"name": "boardPermission",
"columns": {
"board_id": {
"name": "board_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"boardPermission_board_id_board_id_fk": {
"name": "boardPermission_board_id_board_id_fk",
"tableFrom": "boardPermission",
"tableTo": "board",
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"boardPermission_user_id_user_id_fk": {
"name": "boardPermission_user_id_user_id_fk",
"tableFrom": "boardPermission",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardPermission_board_id_user_id_permission_pk": {
"columns": ["board_id", "permission", "user_id"],
"name": "boardPermission_board_id_user_id_permission_pk"
}
},
"uniqueConstraints": {}
},
"board": { "board": {
"name": "board", "name": "board",
"columns": { "columns": {
@@ -136,6 +234,13 @@
"autoincrement": false, "autoincrement": false,
"default": false "default": false
}, },
"creator_id": {
"name": "creator_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"page_title": { "page_title": {
"name": "page_title", "name": "page_title",
"type": "text", "type": "text",
@@ -242,7 +347,17 @@
"isUnique": true "isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {
"board_creator_id_user_id_fk": {
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },

View File

@@ -1,722 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "f7263224-116a-42ba-8fb1-4574cb637880",
"prevId": "7ba12cbf-d8eb-4469-b7c5-7f31acb7717d",
"tables": {
"account": {
"name": "account",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_providerAccountId_pk": {
"columns": ["provider", "providerAccountId"],
"name": "account_provider_providerAccountId_pk"
}
},
"uniqueConstraints": {}
},
"app": {
"name": "app",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"icon_url": {
"name": "icon_url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"href": {
"name": "href",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"board": {
"name": "board",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_public": {
"name": "is_public",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"page_title": {
"name": "page_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"meta_title": {
"name": "meta_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"logo_image_url": {
"name": "logo_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"favicon_image_url": {
"name": "favicon_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"background_image_url": {
"name": "background_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"background_image_attachment": {
"name": "background_image_attachment",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'fixed'"
},
"background_image_repeat": {
"name": "background_image_repeat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'no-repeat'"
},
"background_image_size": {
"name": "background_image_size",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'cover'"
},
"primary_color": {
"name": "primary_color",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#fa5252'"
},
"secondary_color": {
"name": "secondary_color",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#fd7e14'"
},
"opacity": {
"name": "opacity",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 100
},
"custom_css": {
"name": "custom_css",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"column_count": {
"name": "column_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 10
}
},
"indexes": {
"board_name_unique": {
"name": "board_name_unique",
"columns": ["name"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"integration_item": {
"name": "integration_item",
"columns": {
"item_id": {
"name": "item_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"integration_id": {
"name": "integration_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"integration_item_item_id_item_id_fk": {
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"integration_item_integration_id_integration_id_fk": {
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"columns": ["integration_id", "item_id"],
"name": "integration_item_item_id_integration_id_pk"
}
},
"uniqueConstraints": {}
},
"integrationSecret": {
"name": "integrationSecret",
"columns": {
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"integration_id": {
"name": "integration_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": ["updated_at"],
"isUnique": false
}
},
"foreignKeys": {
"integrationSecret_integration_id_integration_id_fk": {
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"columns": ["integration_id", "kind"],
"name": "integrationSecret_integration_id_kind_pk"
}
},
"uniqueConstraints": {}
},
"integration": {
"name": "integration",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": ["kind"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"item": {
"name": "item",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"section_id": {
"name": "section_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"x_offset": {
"name": "x_offset",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"y_offset": {
"name": "y_offset",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"width": {
"name": "width",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"height": {
"name": "height",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"options": {
"name": "options",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{\"json\": {}}'"
}
},
"indexes": {},
"foreignKeys": {
"item_section_id_section_id_fk": {
"name": "item_section_id_section_id_fk",
"tableFrom": "item",
"tableTo": "section",
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"section": {
"name": "section",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"board_id": {
"name": "board_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"section_board_id_board_id_fk": {
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"emailVerified": {
"name": "emailVerified",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"salt": {
"name": "salt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"verificationToken": {
"name": "verificationToken",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@@ -5,15 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1709409142712, "when": 1710878250235,
"tag": "0000_sloppy_bloodstorm", "tag": "0000_productive_changeling",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1709585624230,
"tag": "0001_slim_swarm",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -15,6 +15,7 @@ import type {
BackgroundImageAttachment, BackgroundImageAttachment,
BackgroundImageRepeat, BackgroundImageRepeat,
BackgroundImageSize, BackgroundImageSize,
BoardPermission,
IntegrationKind, IntegrationKind,
IntegrationSecretKind, IntegrationSecretKind,
SectionKind, SectionKind,
@@ -97,8 +98,8 @@ export const integrations = mysqlTable(
url: text("url").notNull(), url: text("url").notNull(),
kind: varchar("kind", { length: 128 }).$type<IntegrationKind>().notNull(), kind: varchar("kind", { length: 128 }).$type<IntegrationKind>().notNull(),
}, },
(i) => ({ (integrations) => ({
kindIdx: index("integration__kind_idx").on(i.kind), kindIdx: index("integration__kind_idx").on(integrations.kind),
}), }),
); );
@@ -127,6 +128,9 @@ export const boards = mysqlTable("board", {
id: varchar("id", { length: 256 }).notNull().primaryKey(), id: varchar("id", { length: 256 }).notNull().primaryKey(),
name: varchar("name", { length: 256 }).unique().notNull(), name: varchar("name", { length: 256 }).unique().notNull(),
isPublic: boolean("is_public").default(false).notNull(), isPublic: boolean("is_public").default(false).notNull(),
creatorId: text("creator_id").references(() => users.id, {
onDelete: "set null",
}),
pageTitle: text("page_title"), pageTitle: text("page_title"),
metaTitle: text("meta_title"), metaTitle: text("meta_title"),
logoImageUrl: text("logo_image_url"), logoImageUrl: text("logo_image_url"),
@@ -151,6 +155,24 @@ export const boards = mysqlTable("board", {
columnCount: int("column_count").default(10).notNull(), columnCount: int("column_count").default(10).notNull(),
}); });
export const boardPermissions = mysqlTable(
"boardPermission",
{
boardId: text("board_id")
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
permission: text("permission").$type<BoardPermission>().notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.boardId, table.userId, table.permission],
}),
}),
);
export const sections = mysqlTable("section", { export const sections = mysqlTable("section", {
id: varchar("id", { length: 256 }).notNull().primaryKey(), id: varchar("id", { length: 256 }).notNull().primaryKey(),
boardId: varchar("board_id", { length: 256 }) boardId: varchar("board_id", { length: 256 })
@@ -208,8 +230,25 @@ export const accountRelations = relations(accounts, ({ one }) => ({
export const userRelations = relations(users, ({ many }) => ({ export const userRelations = relations(users, ({ many }) => ({
accounts: many(accounts), accounts: many(accounts),
boards: many(boards),
boardPermissions: many(boardPermissions),
})); }));
export const boardPermissionRelations = relations(
boardPermissions,
({ one }) => ({
user: one(users, {
fields: [boardPermissions.userId],
references: [users.id],
}),
board: one(boards, {
fields: [boardPermissions.boardId],
references: [boards.id],
}),
}),
);
export const integrationRelations = relations(integrations, ({ many }) => ({ export const integrationRelations = relations(integrations, ({ many }) => ({
secrets: many(integrationSecrets), secrets: many(integrationSecrets),
items: many(integrationItems), items: many(integrationItems),
@@ -225,8 +264,13 @@ export const integrationSecretRelations = relations(
}), }),
); );
export const boardRelations = relations(boards, ({ many }) => ({ export const boardRelations = relations(boards, ({ many, one }) => ({
sections: many(sections), sections: many(sections),
creator: one(users, {
fields: [boards.creatorId],
references: [users.id],
}),
permissions: many(boardPermissions),
})); }));
export const sectionRelations = relations(sections, ({ many, one }) => ({ export const sectionRelations = relations(sections, ({ many, one }) => ({

View File

@@ -19,6 +19,7 @@ import type {
BackgroundImageAttachment, BackgroundImageAttachment,
BackgroundImageRepeat, BackgroundImageRepeat,
BackgroundImageSize, BackgroundImageSize,
BoardPermission,
IntegrationKind, IntegrationKind,
IntegrationSecretKind, IntegrationSecretKind,
SectionKind, SectionKind,
@@ -94,8 +95,8 @@ export const integrations = sqliteTable(
url: text("url").notNull(), url: text("url").notNull(),
kind: text("kind").$type<IntegrationKind>().notNull(), kind: text("kind").$type<IntegrationKind>().notNull(),
}, },
(i) => ({ (integrations) => ({
kindIdx: index("integration__kind_idx").on(i.kind), kindIdx: index("integration__kind_idx").on(integrations.kind),
}), }),
); );
@@ -122,6 +123,9 @@ export const boards = sqliteTable("board", {
id: text("id").notNull().primaryKey(), id: text("id").notNull().primaryKey(),
name: text("name").unique().notNull(), name: text("name").unique().notNull(),
isPublic: int("is_public", { mode: "boolean" }).default(false).notNull(), isPublic: int("is_public", { mode: "boolean" }).default(false).notNull(),
creatorId: text("creator_id").references(() => users.id, {
onDelete: "set null",
}),
pageTitle: text("page_title"), pageTitle: text("page_title"),
metaTitle: text("meta_title"), metaTitle: text("meta_title"),
logoImageUrl: text("logo_image_url"), logoImageUrl: text("logo_image_url"),
@@ -146,6 +150,24 @@ export const boards = sqliteTable("board", {
columnCount: int("column_count").default(10).notNull(), columnCount: int("column_count").default(10).notNull(),
}); });
export const boardPermissions = sqliteTable(
"boardPermission",
{
boardId: text("board_id")
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
permission: text("permission").$type<BoardPermission>().notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.boardId, table.userId, table.permission],
}),
}),
);
export const sections = sqliteTable("section", { export const sections = sqliteTable("section", {
id: text("id").notNull().primaryKey(), id: text("id").notNull().primaryKey(),
boardId: text("board_id") boardId: text("board_id")
@@ -203,8 +225,24 @@ export const accountRelations = relations(accounts, ({ one }) => ({
export const userRelations = relations(users, ({ many }) => ({ export const userRelations = relations(users, ({ many }) => ({
accounts: many(accounts), accounts: many(accounts),
boards: many(boards),
boardPermissions: many(boardPermissions),
})); }));
export const boardPermissionRelations = relations(
boardPermissions,
({ one }) => ({
user: one(users, {
fields: [boardPermissions.userId],
references: [users.id],
}),
board: one(boards, {
fields: [boardPermissions.boardId],
references: [boards.id],
}),
}),
);
export const integrationRelations = relations(integrations, ({ many }) => ({ export const integrationRelations = relations(integrations, ({ many }) => ({
secrets: many(integrationSecrets), secrets: many(integrationSecrets),
items: many(integrationItems), items: many(integrationItems),
@@ -220,8 +258,13 @@ export const integrationSecretRelations = relations(
}), }),
); );
export const boardRelations = relations(boards, ({ many }) => ({ export const boardRelations = relations(boards, ({ many, one }) => ({
sections: many(sections), sections: many(sections),
creator: one(users, {
fields: [boards.creatorId],
references: [users.id],
}),
permissions: many(boardPermissions),
})); }));
export const sectionRelations = relations(sections, ({ many, one }) => ({ export const sectionRelations = relations(sections, ({ many, one }) => ({

View File

@@ -51,7 +51,7 @@ test("schemas should match", () => {
}, },
); );
const mysqlTable = mysqlSchema[tableName as keyof typeof mysqlSchema]; const mysqlTable = mysqlSchema[tableName];
const sqliteForeignKeys = sqliteTable[ const sqliteForeignKeys = sqliteTable[
Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable
] as SqliteForeignKey[] | undefined; ] as SqliteForeignKey[] | undefined;
@@ -97,7 +97,9 @@ test("schemas should match", () => {
sqliteForeignKey.reference().foreignColumns.forEach((column) => { sqliteForeignKey.reference().foreignColumns.forEach((column) => {
expect( expect(
mysqlForeignKey!.reference().foreignColumns.map((x) => x.name), mysqlForeignKey!
.reference()
.foreignColumns.map((column) => column.name),
`expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`, `expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`,
).toContainEqual(column.name); ).toContainEqual(column.name);
}); });

View File

@@ -2,3 +2,4 @@ export * from "./board";
export * from "./integration"; export * from "./integration";
export * from "./section"; export * from "./section";
export * from "./widget"; export * from "./widget";
export * from "./permissions";

View File

@@ -0,0 +1,3 @@
export const boardPermissions = ["board-view", "board-change"] as const;
export type BoardPermission = (typeof boardPermissions)[number];

2
packages/modals/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { ModalProvider, useModalAction, useConfirmModal } from "./src";
export { createModal } from "./src/creator";

View File

@@ -0,0 +1,39 @@
{
"name": "@homarr/modals",
"private": true,
"version": "0.1.0",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^8.57.0",
"typescript": "^5.4.2"
},
"eslintConfig": {
"extends": [
"@homarr/eslint-config/base"
]
},
"prettier": "@homarr/prettier-config"
}

View File

@@ -0,0 +1,92 @@
import { useCallback } from "react";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import type {
stringOrTranslation,
TranslationFunction,
} from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { ButtonProps, GroupProps } from "@homarr/ui";
import { Box, Button, Group } from "@homarr/ui";
import { createModal } from "./creator";
type MaybePromise<T> = T | Promise<T>;
export interface ConfirmModalProps {
title: string;
children: ReactNode;
onConfirm?: () => MaybePromise<void>;
onCancel?: () => MaybePromise<void>;
closeOnConfirm?: boolean;
closeOnCancel?: boolean;
cancelProps?: ButtonProps & ComponentPropsWithoutRef<"button">;
confirmProps?: ButtonProps & ComponentPropsWithoutRef<"button">;
groupProps?: GroupProps;
labels?: {
confirm?: stringOrTranslation;
cancel?: stringOrTranslation;
};
}
export const ConfirmModal = createModal<Omit<ConfirmModalProps, "title">>(
({ actions, innerProps }) => {
const t = useI18n();
const {
children,
onConfirm,
onCancel,
cancelProps,
confirmProps,
groupProps,
labels,
} = innerProps;
const closeOnConfirm = innerProps.closeOnConfirm ?? true;
const closeOnCancel = innerProps.closeOnCancel ?? true;
const cancelLabel =
labels?.cancel ?? ((t: TranslationFunction) => t("common.action.cancel"));
const confirmLabel =
labels?.confirm ??
((t: TranslationFunction) => t("common.action.confirm"));
const handleCancel = useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
typeof cancelProps?.onClick === "function" &&
cancelProps?.onClick(event);
typeof onCancel === "function" && (await onCancel());
closeOnCancel && actions.closeModal();
},
[cancelProps?.onClick, onCancel, actions.closeModal],
);
const handleConfirm = useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
typeof confirmProps?.onClick === "function" &&
confirmProps?.onClick(event);
typeof onConfirm === "function" && (await onConfirm());
closeOnConfirm && actions.closeModal();
},
[confirmProps?.onClick, onConfirm, actions.closeModal],
);
return (
<>
{children && <Box mb="md">{children}</Box>}
<Group justify="flex-end" {...groupProps}>
<Button variant="default" {...cancelProps} onClick={handleCancel}>
{cancelProps?.children || translateIfNecessary(t, cancelLabel)}
</Button>
<Button {...confirmProps} onClick={handleConfirm} color="red.9">
{confirmProps?.children || translateIfNecessary(t, confirmLabel)}
</Button>
</Group>
</>
);
},
).withOptions({});

View File

@@ -0,0 +1,14 @@
import type { CreateModalOptions, ModalComponent } from "./type";
export const createModal = <TInnerProps>(
component: ModalComponent<TInnerProps>,
) => {
return {
withOptions: (options: Partial<CreateModalOptions>) => {
return {
component,
options,
};
},
};
};

View File

@@ -0,0 +1,141 @@
"use client";
import type { PropsWithChildren } from "react";
import {
createContext,
useCallback,
useContext,
useReducer,
useRef,
} from "react";
import { randomId } from "@mantine/hooks";
import type { stringOrTranslation } from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { getDefaultZIndex, Modal } from "@homarr/ui";
import type { ConfirmModalProps } from "./confirm-modal";
import { ConfirmModal } from "./confirm-modal";
import { modalReducer } from "./reducer";
import type { inferInnerProps, ModalDefinition } from "./type";
interface ModalContextProps {
openModalInner: <TModal extends ModalDefinition>(props: {
modal: TModal;
innerProps: inferInnerProps<TModal>;
options: OpenModalOptions;
}) => void;
closeModal: (id: string) => void;
}
export const ModalContext = createContext<ModalContextProps | null>(null);
export const ModalProvider = ({ children }: PropsWithChildren) => {
const t = useI18n();
const [state, dispatch] = useReducer(modalReducer, {
modals: [],
current: null,
});
const stateRef = useRef(state);
stateRef.current = state;
const closeModal = useCallback(
(id: string, canceled?: boolean) => {
dispatch({ type: "CLOSE", modalId: id, canceled });
},
[stateRef, dispatch],
);
const openModalInner: ModalContextProps["openModalInner"] = useCallback(
({ modal, innerProps, options }) => {
const id = randomId();
const { title, ...rest } = options;
dispatch({
type: "OPEN",
modal: {
id,
modal,
props: {
...modal.options,
...rest,
defaultTitle: title ?? modal.options.defaultTitle,
innerProps,
},
},
});
return id;
},
[dispatch],
);
const handleCloseModal = useCallback(
() => state.current && closeModal(state.current.id),
[closeModal, state.current?.id],
);
const activeModals = state.modals.filter(
(modal) => modal.id === state.current?.id || modal.props.keepMounted,
);
return (
<ModalContext.Provider value={{ openModalInner, closeModal }}>
{activeModals.map((modal) => (
<Modal
key={modal.id}
zIndex={getDefaultZIndex("modal") + 1}
display={modal.id === state.current?.id ? undefined : "none"}
style={{
userSelect: modal.id === state.current?.id ? undefined : "none",
}}
styles={{
title: {
fontSize: "1.25rem",
fontWeight: 500,
},
}}
trapFocus={modal.id === state.current?.id}
{...modal.reference.modalProps}
title={translateIfNecessary(t, modal.props.defaultTitle)}
opened={state.modals.length > 0}
onClose={handleCloseModal}
>
{modal.reference.content}
</Modal>
))}
{children}
</ModalContext.Provider>
);
};
interface OpenModalOptions {
keepMounted?: boolean;
title?: stringOrTranslation;
}
export const useModalAction = <TModal extends ModalDefinition>(
modal: TModal,
) => {
const context = useContext(ModalContext);
if (!context) throw new Error("ModalContext is not provided");
return {
openModal: (
innerProps: inferInnerProps<TModal>,
options: OpenModalOptions | void,
) => {
context.openModalInner({ modal, innerProps, options: options ?? {} });
},
};
};
export const useConfirmModal = () => {
const { openModal } = useModalAction(ConfirmModal);
return {
openConfirmModal: (props: ConfirmModalProps) =>
openModal(props, { title: props.title }),
};
};

View File

@@ -0,0 +1,125 @@
"use client";
import { useContext } from "react";
import { ModalContext } from ".";
import type { ModalDefinition, ModalState } from "./type";
type ModalStateWithReference = ModalState & {
/**
* Reference to modal component instance
* Used so the modal can be persisted between navigating in newer modals
*/
reference: ReturnType<typeof getModal>;
};
interface ModalsState {
modals: ModalStateWithReference[];
/**
* Modal that is currently open or was the last open one.
* Keeping the last one is necessary for providing a clean exit transition.
*/
current: ModalStateWithReference | null;
}
interface OpenAction {
type: "OPEN";
modal: ModalState;
}
interface CloseAction {
type: "CLOSE";
modalId: string;
canceled?: boolean;
}
interface CloseAllAction {
type: "CLOSE_ALL";
canceled?: boolean;
}
export const modalReducer = (
state: ModalsState,
action: OpenAction | CloseAction | CloseAllAction,
): ModalsState => {
switch (action.type) {
case "OPEN": {
const newModal = {
...action.modal,
reference: getModal(action.modal),
};
return {
current: newModal,
modals: [...state.modals, newModal],
};
}
case "CLOSE": {
const modal = state.modals.find((modal) => modal.id === action.modalId);
if (!modal) {
return state;
}
modal.props.onClose?.();
const remainingModals = state.modals.filter(
(modal) => modal.id !== action.modalId,
);
return {
current: remainingModals[remainingModals.length - 1] || state.current,
modals: remainingModals,
};
}
case "CLOSE_ALL": {
if (!state.modals.length) {
return state;
}
// Resolve modal stack from top to bottom
state.modals
.concat()
.reverse()
.forEach((modal) => {
modal.props.onClose?.();
});
return {
current: state.current,
modals: [],
};
}
default: {
return state;
}
}
};
const getModal = <TModal extends ModalDefinition>(
modal: ModalState<TModal>,
) => {
const ModalContent = modal.modal.component;
const { innerProps, ...rest } = modal.props;
const FullModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error("Modal component used outside of modal context");
}
return (
<ModalContent
innerProps={innerProps}
actions={{
closeModal: () => context.closeModal(modal.id),
}}
/>
);
};
return {
modalProps: rest,
content: <FullModal />,
};
};

View File

@@ -0,0 +1,43 @@
import type { ReactNode } from "react";
import type { stringOrTranslation } from "@homarr/translation";
import type { ModalProps } from "@homarr/ui";
export type ModalComponent<TInnerProps> = (props: {
actions: { closeModal: () => void };
innerProps: TInnerProps;
}) => ReactNode;
export type CreateModalOptions = Pick<
ModalOptions<unknown>,
| "size"
| "fullScreen"
| "centered"
| "keepMounted"
| "withCloseButton"
| "zIndex"
| "scrollAreaComponent"
| "yOffset"
> & {
defaultTitle: stringOrTranslation;
};
export interface ModalDefinition {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: ModalComponent<any>;
options: Partial<CreateModalOptions>;
}
type ModalOptions<TInnerProps> = Partial<Omit<ModalProps, "opened">> & {
innerProps: TInnerProps;
defaultTitle?: stringOrTranslation;
};
export interface ModalState<TModal extends ModalDefinition = ModalDefinition> {
id: string;
modal: TModal;
props: ModalOptions<inferInnerProps<TModal>>;
}
export type inferInnerProps<TModal extends ModalDefinition> =
TModal["component"] extends ModalComponent<infer P> ? P : never;

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "*.tsx", "src"],
"exclude": ["node_modules"]
}

View File

@@ -1,3 +1,5 @@
import type { stringOrTranslation, TranslationFunction } from "./type";
export * from "./type"; export * from "./type";
export const supportedLanguages = ["en", "de"] as const; export const supportedLanguages = ["en", "de"] as const;
@@ -5,3 +7,13 @@ export type SupportedLanguage = (typeof supportedLanguages)[number];
export const defaultLocale = "en"; export const defaultLocale = "en";
export { languageMapping } from "./lang"; export { languageMapping } from "./lang";
export const translateIfNecessary = (
t: TranslationFunction,
value: stringOrTranslation | undefined,
) => {
if (typeof value === "function") {
return value(t);
}
return value;
};

View File

@@ -191,9 +191,11 @@ export default {
}, },
common: { common: {
action: { action: {
add: "Add",
backToOverview: "Back to overview", backToOverview: "Back to overview",
create: "Create", create: "Create",
edit: "Edit", edit: "Edit",
remove: "Remove",
save: "Save", save: "Save",
saveChanges: "Save changes", saveChanges: "Save changes",
cancel: "Cancel", cancel: "Cancel",
@@ -512,6 +514,35 @@ export default {
customCss: { customCss: {
title: "Custom css", title: "Custom css",
}, },
access: {
title: "Access control",
permission: {
userSelect: {
title: "Add user permission",
label: "Select user",
notFound: "No user found",
},
field: {
user: {
label: "User",
},
permission: {
label: "Permission",
},
},
item: {
"board-view": {
label: "View board",
},
"board-change": {
label: "Change board",
},
"board-full": {
label: "Full access",
},
},
},
},
dangerZone: { dangerZone: {
title: "Danger Zone", title: "Danger Zone",
action: { action: {

View File

@@ -3,3 +3,4 @@ import type enTranslation from "./lang/en";
export type TranslationFunction = ReturnType<typeof useI18n>; export type TranslationFunction = ReturnType<typeof useI18n>;
export type TranslationObject = typeof enTranslation; export type TranslationObject = typeof enTranslation;
export type stringOrTranslation = string | ((t: TranslationFunction) => string);

View File

@@ -4,8 +4,10 @@ import {
backgroundImageAttachments, backgroundImageAttachments,
backgroundImageRepeats, backgroundImageRepeats,
backgroundImageSizes, backgroundImageSizes,
boardPermissions,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { zodEnumFromArray } from "./enums";
import { commonItemSchema, createSectionSchema } from "./shared"; import { commonItemSchema, createSectionSchema } from "./shared";
const hexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/); const hexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
@@ -65,6 +67,23 @@ const saveSchema = z.object({
const createSchema = z.object({ name: boardNameSchema }); const createSchema = z.object({ name: boardNameSchema });
const permissionsSchema = z.object({
id: z.string(),
});
const savePermissionsSchema = z.object({
id: z.string(),
permissions: z.array(
z.object({
user: z.object({
id: z.string(),
name: z.string(),
}),
permission: zodEnumFromArray(boardPermissions),
}),
),
});
export const boardSchemas = { export const boardSchemas = {
byName: byNameSchema, byName: byNameSchema,
savePartialSettings: savePartialSettingsSchema, savePartialSettings: savePartialSettingsSchema,
@@ -72,4 +91,6 @@ export const boardSchemas = {
create: createSchema, create: createSchema,
rename: renameSchema, rename: renameSchema,
changeVisibility: changeVisibilitySchema, changeVisibility: changeVisibilitySchema,
permissions: permissionsSchema,
savePermissions: savePermissionsSchema,
}; };

View File

@@ -37,6 +37,7 @@
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0",
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",

View File

@@ -1,8 +1,7 @@
"use client"; "use client";
import type { ManagedModal } from "mantine-modal-manager";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { Button, Group, Stack } from "@homarr/ui"; import { Button, Group, Stack } from "@homarr/ui";
@@ -26,61 +25,67 @@ interface ModalProps<TSort extends WidgetKind> {
integrationSupport: boolean; integrationSupport: boolean;
} }
export const WidgetEditModal: ManagedModal<ModalProps<WidgetKind>> = ({ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(
actions, ({ actions, innerProps }) => {
innerProps, const t = useI18n();
}) => { const form = useForm({
const t = useI18n(); initialValues: innerProps.value,
const form = useForm({ });
initialValues: innerProps.value,
});
const { definition } = widgetImports[innerProps.kind]; const { definition } = widgetImports[innerProps.kind];
return ( return (
<form <form
onSubmit={form.onSubmit((v) => { onSubmit={form.onSubmit((values) => {
innerProps.onSuccessfulEdit(v); innerProps.onSuccessfulEdit(values);
actions.closeModal(); actions.closeModal();
})} })}
> >
<FormProvider form={form}> <FormProvider form={form}>
<Stack> <Stack>
{innerProps.integrationSupport && ( {innerProps.integrationSupport && (
<WidgetIntegrationSelect <WidgetIntegrationSelect
label={t("item.edit.field.integrations.label")} label={t("item.edit.field.integrations.label")}
data={innerProps.integrationData} data={innerProps.integrationData}
{...form.getInputProps("integrations")} {...form.getInputProps("integrations")}
/> />
)} )}
{Object.entries(definition.options).map( {Object.entries(definition.options).map(
([key, value]: [string, OptionsBuilderResult[string]]) => { ([key, value]: [string, OptionsBuilderResult[string]]) => {
const Input = getInputForType(value.type); const Input = getInputForType(value.type);
if (!Input || value.shouldHide?.(form.values.options as never)) { if (
return null; !Input ||
} value.shouldHide?.(form.values.options as never)
) {
return null;
}
return ( return (
<Input <Input
key={key} key={key}
kind={innerProps.kind} kind={innerProps.kind}
property={key} property={key}
options={value as never} options={value as never}
/> />
); );
}, },
)} )}
<Group justify="right"> <Group justify="right">
<Button onClick={actions.closeModal} variant="subtle" color="gray"> <Button
{t("common.action.cancel")} onClick={actions.closeModal}
</Button> variant="subtle"
<Button type="submit" color="teal"> color="gray"
{t("common.action.saveChanges")} >
</Button> {t("common.action.cancel")}
</Group> </Button>
</Stack> <Button type="submit" color="teal">
</FormProvider> {t("common.action.saveChanges")}
</form> </Button>
); </Group>
}; </Stack>
</FormProvider>
</form>
);
},
).withOptions({});

View File

@@ -48,17 +48,17 @@ export const WidgetIntegrationSelect = ({
const handleValueSelect = (selectedValue: string) => const handleValueSelect = (selectedValue: string) =>
onChange( onChange(
multiSelectValues.includes(selectedValue) multiSelectValues.includes(selectedValue)
? multiSelectValues.filter((v) => v !== selectedValue) ? multiSelectValues.filter((value) => value !== selectedValue)
: [...multiSelectValues, selectedValue], : [...multiSelectValues, selectedValue],
); );
const handleValueRemove = (val: string) => const handleValueRemove = (valueToRemove: string) =>
onChange(multiSelectValues.filter((v) => v !== val)); onChange(multiSelectValues.filter((value) => value !== valueToRemove));
const values = multiSelectValues.map((item) => ( const values = multiSelectValues.map((item) => (
<IntegrationPill <IntegrationPill
key={item} key={item}
option={data.find((i) => i.id === item)!} option={data.find((integration) => integration.id === item)!}
onRemove={() => handleValueRemove(item)} onRemove={() => handleValueRemove(item)}
/> />
)); ));

300
pnpm-lock.yaml generated
View File

@@ -162,6 +162,9 @@ importers:
'@homarr/log': '@homarr/log':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/log version: link:../../packages/log
'@homarr/modals':
specifier: workspace:^0.1.0
version: link:../../packages/modals
'@homarr/notifications': '@homarr/notifications':
specifier: workspace:^0.1.0 specifier: workspace:^0.1.0
version: link:../../packages/notifications version: link:../../packages/notifications
@@ -236,10 +239,7 @@ importers:
version: 16.4.5 version: 16.4.5
jotai: jotai:
specifier: ^2.7.1 specifier: ^2.7.1
version: 2.7.1(@types/react@18.2.66)(react@18.2.0) version: 2.7.1(@types/react@18.2.67)(react@18.2.0)
mantine-modal-manager:
specifier: ^7.6.2
version: 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(react-dom@18.2.0)(react@18.2.0)
next: next:
specifier: ^14.1.3 specifier: ^14.1.3
version: 14.1.3(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.72.0) version: 14.1.3(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.72.0)
@@ -279,7 +279,7 @@ importers:
version: 20.11.30 version: 20.11.30
'@types/react': '@types/react':
specifier: ^18.2.66 specifier: ^18.2.66
version: 18.2.66 version: 18.2.67
'@types/react-dom': '@types/react-dom':
specifier: ^18.2.22 specifier: ^18.2.22
version: 18.2.22 version: 18.2.22
@@ -452,7 +452,7 @@ importers:
version: 0.20.14 version: 0.20.14
drizzle-orm: drizzle-orm:
specifier: ^0.30.2 specifier: ^0.30.2
version: 0.30.2(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2) version: 0.30.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2)
mysql2: mysql2:
specifier: ^3.9.2 specifier: ^3.9.2
version: 3.9.2 version: 3.9.2
@@ -548,6 +548,31 @@ importers:
specifier: ^5.4.2 specifier: ^5.4.2
version: 5.4.2 version: 5.4.2
packages/modals:
dependencies:
'@homarr/translation':
specifier: workspace:^0.1.0
version: link:../translation
'@homarr/ui':
specifier: workspace:^0.1.0
version: link:../ui
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
version: link:../../tooling/eslint
'@homarr/prettier-config':
specifier: workspace:^0.1.0
version: link:../../tooling/prettier
'@homarr/tsconfig':
specifier: workspace:^0.1.0
version: link:../../tooling/typescript
eslint:
specifier: ^8.57.0
version: 8.57.0
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/notifications: packages/notifications:
dependencies: dependencies:
'@homarr/ui': '@homarr/ui':
@@ -627,7 +652,7 @@ importers:
dependencies: dependencies:
'@mantine/core': '@mantine/core':
specifier: ^7.6.2 specifier: ^7.6.2
version: 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) version: 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/dates': '@mantine/dates':
specifier: ^7.6.2 specifier: ^7.6.2
version: 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) version: 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0)
@@ -696,6 +721,9 @@ importers:
'@homarr/form': '@homarr/form':
specifier: workspace:^0.1.0 specifier: workspace:^0.1.0
version: link:../form version: link:../form
'@homarr/modals':
specifier: workspace:^0.1.0
version: link:../modals
'@homarr/notifications': '@homarr/notifications':
specifier: workspace:^0.1.0 specifier: workspace:^0.1.0
version: link:../notifications version: link:../notifications
@@ -732,10 +760,10 @@ importers:
version: 14.1.3 version: 14.1.3
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) version: 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) version: 7.3.1(eslint@8.57.0)(typescript@5.4.2)
eslint-config-prettier: eslint-config-prettier:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@8.57.0) version: 9.1.0(eslint@8.57.0)
@@ -744,7 +772,7 @@ importers:
version: 1.12.5(eslint@8.57.0) version: 1.12.5(eslint@8.57.0)
eslint-plugin-import: eslint-plugin-import:
specifier: ^2.29.1 specifier: ^2.29.1
version: 2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0) version: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: eslint-plugin-jsx-a11y:
specifier: ^6.8.0 specifier: ^6.8.0
version: 6.8.0(eslint@8.57.0) version: 6.8.0(eslint@8.57.0)
@@ -803,8 +831,8 @@ packages:
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.22
/@angular-devkit/core@17.1.2(chokidar@3.6.0): /@angular-devkit/core@17.1.2(chokidar@3.6.0):
resolution: {integrity: sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==} resolution: {integrity: sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==}
@@ -945,9 +973,9 @@ packages:
resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.22
jsesc: 2.5.2 jsesc: 2.5.2
/@babel/helper-compilation-targets@7.23.6: /@babel/helper-compilation-targets@7.23.6:
@@ -969,19 +997,19 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/template': 7.23.9 '@babel/template': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/helper-hoist-variables@7.22.5: /@babel/helper-hoist-variables@7.22.5:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/helper-module-imports@7.22.15: /@babel/helper-module-imports@7.22.15:
resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9):
resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
@@ -1019,13 +1047,13 @@ packages:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/helper-split-export-declaration@7.22.6: /@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/helper-string-parser@7.23.4: /@babel/helper-string-parser@7.23.4:
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
@@ -1045,7 +1073,7 @@ packages:
dependencies: dependencies:
'@babel/template': 7.23.9 '@babel/template': 7.23.9
'@babel/traverse': 7.23.9 '@babel/traverse': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -1073,7 +1101,7 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/parser@7.24.0: /@babel/parser@7.24.0:
resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==}
@@ -1081,6 +1109,7 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.24.0
dev: false
/@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.9): /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.9):
resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==} resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==}
@@ -1121,8 +1150,8 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/code-frame': 7.23.5 '@babel/code-frame': 7.23.5
'@babel/parser': 7.24.0 '@babel/parser': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
/@babel/template@7.24.0: /@babel/template@7.24.0:
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
@@ -1143,8 +1172,8 @@ packages:
'@babel/helper-function-name': 7.23.0 '@babel/helper-function-name': 7.23.0
'@babel/helper-hoist-variables': 7.22.5 '@babel/helper-hoist-variables': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.24.0 '@babel/parser': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
debug: 4.3.4 debug: 4.3.4
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -1183,6 +1212,7 @@ packages:
'@babel/helper-string-parser': 7.23.4 '@babel/helper-string-parser': 7.23.4
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: false
/@bcoe/v8-coverage@0.2.3: /@bcoe/v8-coverage@0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
@@ -1951,6 +1981,14 @@ packages:
'@sinclair/typebox': 0.27.8 '@sinclair/typebox': 0.27.8
dev: true dev: true
/@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.25
/@jridgewell/gen-mapping@0.3.5: /@jridgewell/gen-mapping@0.3.5:
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -1958,14 +1996,20 @@ packages:
'@jridgewell/set-array': 1.2.1 '@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
dev: true
/@jridgewell/resolve-uri@3.1.2: /@jridgewell/resolve-uri@3.1.2:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
/@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
/@jridgewell/set-array@1.2.1: /@jridgewell/set-array@1.2.1:
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/source-map@0.3.5: /@jridgewell/source-map@0.3.5:
resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
@@ -1977,6 +2021,12 @@ packages:
/@jridgewell/sourcemap-codec@1.4.15: /@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
/@jridgewell/trace-mapping@0.3.22:
resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==}
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
/@jridgewell/trace-mapping@0.3.25: /@jridgewell/trace-mapping@0.3.25:
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
dependencies: dependencies:
@@ -2009,7 +2059,7 @@ packages:
chroma-js: 2.4.2 chroma-js: 2.4.2
dev: false dev: false
/@mantine/core@7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): /@mantine/core@7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-qmZhmQVc7ZZ8EKKhPkGuZbfBnLXR0xE45ikxfx+1E6/8hLY5Ypr4nWqh5Pk6p3b+K71yYnBqlbNXbtHLQH0h3g==} resolution: {integrity: sha512-qmZhmQVc7ZZ8EKKhPkGuZbfBnLXR0xE45ikxfx+1E6/8hLY5Ypr4nWqh5Pk6p3b+K71yYnBqlbNXbtHLQH0h3g==}
peerDependencies: peerDependencies:
'@mantine/hooks': 7.6.2 '@mantine/hooks': 7.6.2
@@ -2022,8 +2072,8 @@ packages:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-number-format: 5.3.1(react-dom@18.2.0)(react@18.2.0) react-number-format: 5.3.1(react-dom@18.2.0)(react@18.2.0)
react-remove-scroll: 2.5.7(@types/react@18.2.66)(react@18.2.0) react-remove-scroll: 2.5.7(@types/react@18.2.67)(react@18.2.0)
react-textarea-autosize: 8.5.3(@types/react@18.2.66)(react@18.2.0) react-textarea-autosize: 8.5.3(@types/react@18.2.67)(react@18.2.0)
type-fest: 4.12.0 type-fest: 4.12.0
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
@@ -2038,7 +2088,7 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
clsx: 2.1.0 clsx: 2.1.0
dayjs: 1.11.10 dayjs: 1.11.10
@@ -2072,7 +2122,7 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
@@ -2086,7 +2136,7 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
'@mantine/store': 7.6.2(react@18.2.0) '@mantine/store': 7.6.2(react@18.2.0)
react: 18.2.0 react: 18.2.0
@@ -2102,7 +2152,7 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
'@mantine/store': 7.6.2(react@18.2.0) '@mantine/store': 7.6.2(react@18.2.0)
react: 18.2.0 react: 18.2.0
@@ -2127,7 +2177,7 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
'@tiptap/extension-link': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) '@tiptap/extension-link': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)
'@tiptap/react': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(react-dom@18.2.0)(react@18.2.0) '@tiptap/react': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(react-dom@18.2.0)(react@18.2.0)
@@ -3241,20 +3291,20 @@ packages:
/@types/babel__generator@7.6.8: /@types/babel__generator@7.6.8:
resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
dev: true dev: true
/@types/babel__template@7.4.4: /@types/babel__template@7.4.4:
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
dependencies: dependencies:
'@babel/parser': 7.24.0 '@babel/parser': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
dev: true dev: true
/@types/babel__traverse@7.20.5: /@types/babel__traverse@7.20.5:
resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==}
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.23.9
dev: true dev: true
/@types/bcrypt@5.0.2: /@types/bcrypt@5.0.2:
@@ -3417,11 +3467,11 @@ packages:
/@types/react-dom@18.2.22: /@types/react-dom@18.2.22:
resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==}
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
dev: true dev: true
/@types/react@18.2.66: /@types/react@18.2.67:
resolution: {integrity: sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==} resolution: {integrity: sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==}
dependencies: dependencies:
'@types/prop-types': 15.7.11 '@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8 '@types/scheduler': 0.16.8
@@ -3488,9 +3538,9 @@ packages:
'@types/node': 20.11.30 '@types/node': 20.11.30
dev: true dev: true
/@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2): /@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^7.0.0 '@typescript-eslint/parser': ^7.0.0
eslint: ^8.56.0 eslint: ^8.56.0
@@ -3500,11 +3550,11 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@eslint-community/regexpp': 4.10.0 '@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/scope-manager': 7.3.1
'@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/type-utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/visitor-keys': 7.2.0 '@typescript-eslint/visitor-keys': 7.3.1
debug: 4.3.4 debug: 4.3.4
eslint: 8.57.0 eslint: 8.57.0
graphemer: 1.4.0 graphemer: 1.4.0
@@ -3517,9 +3567,9 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2): /@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} resolution: {integrity: sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
typescript: '*' typescript: '*'
@@ -3527,10 +3577,10 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/scope-manager': 7.3.1
'@typescript-eslint/types': 7.2.0 '@typescript-eslint/types': 7.3.1
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2)
'@typescript-eslint/visitor-keys': 7.2.0 '@typescript-eslint/visitor-keys': 7.3.1
debug: 4.3.4 debug: 4.3.4
eslint: 8.57.0 eslint: 8.57.0
typescript: 5.4.2 typescript: 5.4.2
@@ -3538,17 +3588,17 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/scope-manager@7.2.0: /@typescript-eslint/scope-manager@7.3.1:
resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} resolution: {integrity: sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
dependencies: dependencies:
'@typescript-eslint/types': 7.2.0 '@typescript-eslint/types': 7.3.1
'@typescript-eslint/visitor-keys': 7.2.0 '@typescript-eslint/visitor-keys': 7.3.1
dev: false dev: false
/@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): /@typescript-eslint/type-utils@7.3.1(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} resolution: {integrity: sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
typescript: '*' typescript: '*'
@@ -3556,8 +3606,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2)
'@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
debug: 4.3.4 debug: 4.3.4
eslint: 8.57.0 eslint: 8.57.0
ts-api-utils: 1.2.1(typescript@5.4.2) ts-api-utils: 1.2.1(typescript@5.4.2)
@@ -3566,22 +3616,22 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/types@7.2.0: /@typescript-eslint/types@7.3.1:
resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} resolution: {integrity: sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
dev: false dev: false
/@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.2): /@typescript-eslint/typescript-estree@7.3.1(typescript@5.4.2):
resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} resolution: {integrity: sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/types': 7.2.0 '@typescript-eslint/types': 7.3.1
'@typescript-eslint/visitor-keys': 7.2.0 '@typescript-eslint/visitor-keys': 7.3.1
debug: 4.3.4 debug: 4.3.4
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
@@ -3593,18 +3643,18 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): /@typescript-eslint/utils@7.3.1(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} resolution: {integrity: sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@types/semver': 7.5.7 '@types/semver': 7.5.7
'@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/scope-manager': 7.3.1
'@typescript-eslint/types': 7.2.0 '@typescript-eslint/types': 7.3.1
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2)
eslint: 8.57.0 eslint: 8.57.0
semver: 7.6.0 semver: 7.6.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -3612,11 +3662,11 @@ packages:
- typescript - typescript
dev: false dev: false
/@typescript-eslint/visitor-keys@7.2.0: /@typescript-eslint/visitor-keys@7.3.1:
resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} resolution: {integrity: sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^18.18.0 || >=20.0.0}
dependencies: dependencies:
'@typescript-eslint/types': 7.2.0 '@typescript-eslint/types': 7.3.1
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
dev: false dev: false
@@ -5066,8 +5116,8 @@ packages:
- supports-color - supports-color
dev: false dev: false
/drizzle-orm@0.30.2(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2): /drizzle-orm@0.30.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2):
resolution: {integrity: sha512-DNd3djg03o+WxZX3pGD8YD+qrWT8gbrbhaZ2W0PVb6yH4rtM/VTB92cTGvumcRh7SSd2KfV0NWYDB70BHIXQTg==} resolution: {integrity: sha512-tmIUPy71Ca7BUD5M7Tn9bvXESDWoj66d6lTdKCdf30V26xDFFjbx7DMamhOiWU+H1fflBk5rCdtGyt2SiFPgRg==}
peerDependencies: peerDependencies:
'@aws-sdk/client-rds-data': '>=3' '@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=3' '@cloudflare/workers-types': '>=3'
@@ -5473,7 +5523,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
/eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): /eslint-module-utils@2.8.0(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
engines: {node: '>=4'} engines: {node: '>=4'}
peerDependencies: peerDependencies:
@@ -5494,7 +5544,7 @@ packages:
eslint-import-resolver-webpack: eslint-import-resolver-webpack:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
debug: 3.2.7 debug: 3.2.7
eslint: 8.57.0 eslint: 8.57.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
@@ -5502,7 +5552,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0): /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'} engines: {node: '>=4'}
peerDependencies: peerDependencies:
@@ -5512,7 +5562,7 @@ packages:
'@typescript-eslint/parser': '@typescript-eslint/parser':
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
array-includes: 3.1.7 array-includes: 3.1.7
array.prototype.findlastindex: 1.2.4 array.prototype.findlastindex: 1.2.4
array.prototype.flat: 1.3.2 array.prototype.flat: 1.3.2
@@ -5521,7 +5571,7 @@ packages:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.0 eslint: 8.57.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.2.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
hasown: 2.0.1 hasown: 2.0.1
is-core-module: 2.13.1 is-core-module: 2.13.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -6881,7 +6931,7 @@ packages:
resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==}
dev: false dev: false
/jotai@2.7.1(@types/react@18.2.66)(react@18.2.0): /jotai@2.7.1(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-bsaTPn02nFgWNP6cBtg/htZhCu4s0wxqoklRHePp6l/vlsypR9eLn7diRliwXYWMXDpPvW/LLA2afI8vwgFFaw==} resolution: {integrity: sha512-bsaTPn02nFgWNP6cBtg/htZhCu4s0wxqoklRHePp6l/vlsypR9eLn7diRliwXYWMXDpPvW/LLA2afI8vwgFFaw==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
peerDependencies: peerDependencies:
@@ -6893,7 +6943,7 @@ packages:
react: react:
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
dev: false dev: false
@@ -7206,8 +7256,8 @@ packages:
/magicast@0.3.3: /magicast@0.3.3:
resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==}
dependencies: dependencies:
'@babel/parser': 7.24.0 '@babel/parser': 7.23.9
'@babel/types': 7.24.0 '@babel/types': 7.23.9
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
@@ -7228,20 +7278,6 @@ packages:
/make-error@1.3.6: /make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
/mantine-modal-manager@7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-tU6nHe8ImEdpBY9WaY4xtVXYc23BTKwJ+0bJ7m3/KkbDw7SSWOXOMevIrcnunlmvZfb5jAwm3CGGmdcS5gYx5w==}
peerDependencies:
'@mantine/core': 7.6.2
'@mantine/hooks': 7.6.2
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/mantine-react-table@2.0.0-beta.0(@mantine/core@7.6.2)(@mantine/dates@7.6.2)(@mantine/hooks@7.6.2)(@tabler/icons-react@3.1.0)(clsx@2.1.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): /mantine-react-table@2.0.0-beta.0(@mantine/core@7.6.2)(@mantine/dates@7.6.2)(@mantine/hooks@7.6.2)(@tabler/icons-react@3.1.0)(clsx@2.1.0)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-KZOr7nzoSt4aF0hyWKJJBK9/dxVWB3tdg2fFSNnqns9cbPFlLTGXDKguLSoNE8WkzjWE0ThpYJknIAoraL/7ug==} resolution: {integrity: sha512-KZOr7nzoSt4aF0hyWKJJBK9/dxVWB3tdg2fFSNnqns9cbPFlLTGXDKguLSoNE8WkzjWE0ThpYJknIAoraL/7ug==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -7255,7 +7291,7 @@ packages:
react: '>=18.0' react: '>=18.0'
react-dom: '>=18.0' react-dom: '>=18.0'
dependencies: dependencies:
'@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mantine/core': 7.6.2(@mantine/hooks@7.6.2)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0)
'@mantine/dates': 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) '@mantine/dates': 7.6.2(@mantine/core@7.6.2)(@mantine/hooks@7.6.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.6.2(react@18.2.0) '@mantine/hooks': 7.6.2(react@18.2.0)
'@tabler/icons-react': 3.1.0(react@18.2.0) '@tabler/icons-react': 3.1.0(react@18.2.0)
@@ -8472,7 +8508,7 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/react-remove-scroll-bar@2.3.4(@types/react@18.2.66)(react@18.2.0): /react-remove-scroll-bar@2.3.4(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -8482,13 +8518,13 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
react-style-singleton: 2.2.1(@types/react@18.2.66)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.67)(react@18.2.0)
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/react-remove-scroll@2.5.7(@types/react@18.2.66)(react@18.2.0): /react-remove-scroll@2.5.7(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -8498,16 +8534,16 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
react-remove-scroll-bar: 2.3.4(@types/react@18.2.66)(react@18.2.0) react-remove-scroll-bar: 2.3.4(@types/react@18.2.67)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.66)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.67)(react@18.2.0)
tslib: 2.6.2 tslib: 2.6.2
use-callback-ref: 1.3.1(@types/react@18.2.66)(react@18.2.0) use-callback-ref: 1.3.1(@types/react@18.2.67)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.66)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.67)(react@18.2.0)
dev: false dev: false
/react-style-singleton@2.2.1(@types/react@18.2.66)(react@18.2.0): /react-style-singleton@2.2.1(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -8517,14 +8553,14 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
get-nonce: 1.0.1 get-nonce: 1.0.1
invariant: 2.2.4 invariant: 2.2.4
react: 18.2.0 react: 18.2.0
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/react-textarea-autosize@8.5.3(@types/react@18.2.66)(react@18.2.0): /react-textarea-autosize@8.5.3(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -8533,7 +8569,7 @@ packages:
'@babel/runtime': 7.23.9 '@babel/runtime': 7.23.9
react: 18.2.0 react: 18.2.0
use-composed-ref: 1.3.0(react@18.2.0) use-composed-ref: 1.3.0(react@18.2.0)
use-latest: 1.2.1(@types/react@18.2.66)(react@18.2.0) use-latest: 1.2.1(@types/react@18.2.67)(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
dev: false dev: false
@@ -9889,7 +9925,7 @@ packages:
requires-port: 1.0.0 requires-port: 1.0.0
dev: true dev: true
/use-callback-ref@1.3.1(@types/react@18.2.66)(react@18.2.0): /use-callback-ref@1.3.1(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -9899,7 +9935,7 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
@@ -9923,7 +9959,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.2.66)(react@18.2.0): /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
@@ -9932,11 +9968,11 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
dev: false dev: false
/use-latest@1.2.1(@types/react@18.2.66)(react@18.2.0): /use-latest@1.2.1(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
@@ -9945,12 +9981,12 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
react: 18.2.0 react: 18.2.0
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.66)(react@18.2.0) use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.67)(react@18.2.0)
dev: false dev: false
/use-sidecar@1.1.2(@types/react@18.2.66)(react@18.2.0): /use-sidecar@1.1.2(@types/react@18.2.67)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -9960,7 +9996,7 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.66 '@types/react': 18.2.67
detect-node-es: 1.1.0 detect-node-es: 1.1.0
react: 18.2.0 react: 18.2.0
tslib: 2.6.2 tslib: 2.6.2

View File

@@ -4,6 +4,6 @@
"format": "prettier --check . --ignore-path ../../.gitignore", "typecheck": "tsc "format": "prettier --check . --ignore-path ../../.gitignore", "typecheck": "tsc
--noEmit" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", --noEmit" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig":
"workspace:^0.1.0", "eslint": "^8.56.0", "typescript": "^5.3.3" }, "workspace:^0.1.0", "eslint": "^8.57.0", "typescript": "^5.4.2" },
"eslintConfig": { "extends": [ "@homarr/eslint-config/base" ] }, "prettier": "eslintConfig": { "extends": [ "@homarr/eslint-config/base" ] }, "prettier":
"@homarr/prettier-config" } "@homarr/prettier-config" }