refactor: move modals to seperate package (#1135)
* refactor: move modals to seperate package * fix: format issue * fix: lint issues * fix: format issue * fix: only used as type
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/old-schema": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Te
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
import { signIn } from "@homarr/auth/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { useForm } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -14,8 +15,6 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface LoginFormProps {
|
||||
providers: string[];
|
||||
oidcClientName: string;
|
||||
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
|
||||
|
||||
@@ -6,12 +6,11 @@ import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||
|
||||
interface AppDeleteButtonProps {
|
||||
app: RouterOutputs["app"]["all"][number];
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { AppForm } from "../../_form";
|
||||
|
||||
interface AppEditFormProps {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { AppForm } from "../_form";
|
||||
|
||||
export const AppNewForm = () => {
|
||||
|
||||
@@ -7,10 +7,10 @@ import { IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
|
||||
const iconProps = {
|
||||
|
||||
@@ -4,15 +4,12 @@ import { useCallback } from "react";
|
||||
import { Affix, Button, Group, Menu } from "@mantine/core";
|
||||
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { BetaBadge } from "@homarr/ui";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
||||
import { ImportBoardModal } from "~/components/manage/boards/import-board-modal";
|
||||
|
||||
interface CreateBoardButtonProps {
|
||||
boardNames: string[];
|
||||
}
|
||||
@@ -22,24 +19,13 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
||||
const { openModal: openAddModal } = useModalAction(AddBoardModal);
|
||||
const { openModal: openImportModal } = useModalAction(ImportBoardModal);
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
});
|
||||
|
||||
const onCreateClick = useCallback(() => {
|
||||
openAddModal({
|
||||
onSuccess: async (values) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
columnCount: values.columnCount,
|
||||
isPublic: values.isPublic,
|
||||
});
|
||||
onSettled: async () => {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
boardNames,
|
||||
});
|
||||
}, [mutateAsync, boardNames, openAddModal]);
|
||||
}, [openAddModal]);
|
||||
|
||||
const onImportClick = useCallback(() => {
|
||||
openImportModal({ boardNames });
|
||||
@@ -47,7 +33,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
||||
|
||||
const buttonGroupContent = (
|
||||
<>
|
||||
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={onCreateClick} loading={isPending}>
|
||||
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={onCreateClick}>
|
||||
{t("management.page.board.action.new.label")}
|
||||
</Button>
|
||||
<Menu position="bottom-end">
|
||||
|
||||
@@ -5,12 +5,11 @@ import { ActionIcon } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||
|
||||
interface DeleteIntegrationActionButtonProps {
|
||||
count: number;
|
||||
integration: { id: string; name: string };
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||
@@ -15,7 +16,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInpu
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
@@ -18,7 +19,6 @@ import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
||||
import { revalidatePathActionAsync } from "../../../../revalidatePathAction";
|
||||
|
||||
interface NewIntegrationFormProps {
|
||||
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
|
||||
|
||||
@@ -4,12 +4,12 @@ import React from "react";
|
||||
import { Card, LoadingOverlay, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useForm } from "@homarr/form";
|
||||
import type { defaultServerSettings } from "@homarr/server-settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { SwitchSetting } from "~/app/[locale]/manage/settings/_components/setting-switch";
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface AnalyticsSettingsProps {
|
||||
initialData: typeof defaultServerSettings.analytics;
|
||||
|
||||
@@ -4,12 +4,12 @@ import React from "react";
|
||||
import { Card, LoadingOverlay, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useForm } from "@homarr/form";
|
||||
import type { defaultServerSettings } from "@homarr/server-settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { SwitchSetting } from "~/app/[locale]/manage/settings/_components/setting-switch";
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface CrawlingAndIndexingSettingsProps {
|
||||
initialData: typeof defaultServerSettings.crawlingAndIndexing;
|
||||
|
||||
@@ -4,14 +4,13 @@ import { Button, Group, Select, Stack } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface ChangeHomeBoardFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
boardsData: { value: string; label: string }[];
|
||||
|
||||
@@ -6,11 +6,10 @@ import { Button } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface DeleteUserButtonProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface UserProfileAvatarForm {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface UserProfileFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import { Button, Fieldset, Group, PasswordInput, Stack } from "@mantine/core";
|
||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { CustomPasswordInput } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface ChangePasswordFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import { useRouter } from "next/navigation";
|
||||
import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface DeleteGroupProps {
|
||||
group: {
|
||||
id: string;
|
||||
|
||||
@@ -4,13 +4,12 @@ import { useCallback } from "react";
|
||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface RenameGroupFormProps {
|
||||
group: {
|
||||
id: string;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { UserSelectModal } from "~/components/access/user-select-modal";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ import { useCallback } from "react";
|
||||
import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface RemoveGroupMemberProps {
|
||||
groupId: string;
|
||||
user: { id: string; name: string | null };
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useCallback } from "react";
|
||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
|
||||
export const AddGroup = () => {
|
||||
|
||||
@@ -11,11 +11,10 @@ import { MantineReactTable } from "mantine-react-table";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { InviteCreateModal } from "@homarr/modals-collection";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
import { InviteCreateModal } from "./invite-create-modal";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface InviteListComponentProps {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { createCustomErrorParams } from "@homarr/validation/form";
|
||||
|
||||
interface InnerProps {
|
||||
boardNames: string[];
|
||||
onSuccess: (props: { name: string; columnCount: number; isPublic: boolean }) => Promise<void>;
|
||||
}
|
||||
|
||||
export const AddBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useZodForm(
|
||||
validation.board.create.refine((value) => !innerProps.boardNames.includes(value.name), {
|
||||
params: createCustomErrorParams("boardAlreadyExists"),
|
||||
path: ["name"],
|
||||
}),
|
||||
{
|
||||
initialValues: {
|
||||
name: "",
|
||||
columnCount: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const columnCountChecks = validation.board.create.shape.columnCount._def.checks;
|
||||
const minColumnCount = columnCountChecks.find((check) => check.kind === "min")?.value;
|
||||
const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
void innerProps.onSuccess(values);
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("board.field.name.label")} data-autofocus {...form.getInputProps("name")} />
|
||||
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
||||
<Slider min={minColumnCount} max={maxColumnCount} step={1} {...form.getInputProps("columnCount")} />
|
||||
</InputWrapper>
|
||||
|
||||
<Switch
|
||||
label={t("board.field.isPublic.label")}
|
||||
description={t("board.field.isPublic.description")}
|
||||
{...form.getInputProps("isPublic")}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.create")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
||||
});
|
||||
@@ -1,5 +1,25 @@
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
import { createTRPCClient, createTRPCReact, httpLink } from "@trpc/react-query";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import type { AppRouter } from ".";
|
||||
|
||||
export const clientApi = createTRPCReact<AppRouter>();
|
||||
export const fetchApi = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
transformer: SuperJSON,
|
||||
headers() {
|
||||
const headers = new Headers();
|
||||
headers.set("x-trpc-source", "fetch");
|
||||
return headers;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,20 @@ import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publ
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
exists: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
.input(z.string())
|
||||
.query(async ({ ctx, input: name }) => {
|
||||
try {
|
||||
await noBoardWithSimilarNameAsync(ctx.db, name);
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError && error.code === "CONFLICT") {
|
||||
return true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
getAllBoards: publicProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./app-url/client";
|
||||
export * from "./revalidate-path-action";
|
||||
|
||||
9
packages/modals-collection/eslint.config.js
Normal file
9
packages/modals-collection/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
...baseConfig,
|
||||
];
|
||||
1
packages/modals-collection/index.ts
Normal file
1
packages/modals-collection/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
47
packages/modals-collection/package.json
Normal file
47
packages/modals-collection/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@homarr/modals-collection",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"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/api": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/form": "workspace:^0.1.0",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/old-schema": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.12.2",
|
||||
"@tabler/icons-react": "^3.17.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "^14.2.11",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.10.0",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config"
|
||||
}
|
||||
128
packages/modals-collection/src/boards/add-board-modal.tsx
Normal file
128
packages/modals-collection/src/boards/add-board-modal.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
interface InnerProps {
|
||||
onSettled: () => MaybePromise<void>;
|
||||
}
|
||||
|
||||
export const AddBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useZodForm(validation.board.create, {
|
||||
mode: "controlled",
|
||||
initialValues: {
|
||||
name: "",
|
||||
columnCount: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
});
|
||||
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
|
||||
onSettled: innerProps.onSettled,
|
||||
});
|
||||
|
||||
const boardNameStatus = useBoardNameStatus(form.values.name);
|
||||
|
||||
const columnCountChecks = validation.board.create.shape.columnCount._def.checks;
|
||||
const minColumnCount = columnCountChecks.find((check) => check.kind === "min")?.value;
|
||||
const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
// Prevent submit before name availability check
|
||||
if (!boardNameStatus.canSubmit) return;
|
||||
mutate(values, {
|
||||
onSuccess: () => {
|
||||
actions.closeModal();
|
||||
showSuccessNotification({
|
||||
title: "Board created",
|
||||
message: `Board ${values.name} has been created`,
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: "Failed to create board",
|
||||
message: `Board ${values.name} could not be created`,
|
||||
});
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("board.field.name.label")}
|
||||
data-autofocus
|
||||
{...form.getInputProps("name")}
|
||||
description={
|
||||
boardNameStatus.description ? (
|
||||
<Group c={boardNameStatus.description.color} gap="xs" align="center">
|
||||
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
|
||||
<span>{boardNameStatus.description.label}</span>
|
||||
</Group>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
|
||||
<Slider min={minColumnCount} max={maxColumnCount} step={1} {...form.getInputProps("columnCount")} />
|
||||
</InputWrapper>
|
||||
|
||||
<Switch
|
||||
label={t("board.field.isPublic.label")}
|
||||
description={t("board.field.isPublic.description")}
|
||||
{...form.getInputProps("isPublic")}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
{t("common.action.create")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
||||
});
|
||||
|
||||
export const useBoardNameStatus = (name: string) => {
|
||||
const t = useI18n();
|
||||
const [debouncedName] = useDebouncedValue(name, 250);
|
||||
const { data: boardExists, isLoading } = clientApi.board.exists.useQuery(debouncedName, {
|
||||
enabled: validation.board.create.shape.name.safeParse(debouncedName).success,
|
||||
});
|
||||
|
||||
return {
|
||||
canSubmit: !boardExists && !isLoading,
|
||||
description:
|
||||
debouncedName.trim() === ""
|
||||
? undefined
|
||||
: isLoading
|
||||
? {
|
||||
label: "Checking availability...",
|
||||
}
|
||||
: boardExists === undefined
|
||||
? undefined
|
||||
: boardExists
|
||||
? {
|
||||
icon: IconAlertTriangle,
|
||||
label: t("common.zod.errors.custom.boardAlreadyExists"), // The board ${debouncedName} already exists
|
||||
color: "red",
|
||||
}
|
||||
: {
|
||||
icon: IconCircleCheck,
|
||||
label: `${debouncedName} is available`,
|
||||
color: "green",
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { Button, Fieldset, FileInput, Grid, Group, Radio, Stack, Switch, TextInp
|
||||
import { IconFileUpload } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -10,24 +11,21 @@ import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { SelectWithDescription } from "@homarr/ui";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
import { createOldmarrImportConfigurationSchema, superRefineJsonImportFile, z } from "@homarr/validation";
|
||||
import { oldmarrImportConfigurationSchema, superRefineJsonImportFile, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { useBoardNameStatus } from "./add-board-modal";
|
||||
|
||||
interface InnerProps {
|
||||
boardNames: string[];
|
||||
}
|
||||
|
||||
export const ImportBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
export const ImportBoardModal = createModal(({ actions }) => {
|
||||
const tOldImport = useScopedI18n("board.action.oldImport");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const [fileValid, setFileValid] = useState(true);
|
||||
const form = useZodForm(
|
||||
z.object({
|
||||
file: z.instanceof(File).nullable().superRefine(superRefineJsonImportFile),
|
||||
configuration: createOldmarrImportConfigurationSchema(innerProps.boardNames),
|
||||
configuration: oldmarrImportConfigurationSchema,
|
||||
}),
|
||||
{
|
||||
mode: "controlled",
|
||||
initialValues: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
file: null!,
|
||||
@@ -67,6 +65,7 @@ export const ImportBoardModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
);
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation();
|
||||
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
|
||||
|
||||
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
|
||||
const formData = new FormData();
|
||||
@@ -94,7 +93,7 @@ export const ImportBoardModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
if (!fileValid) {
|
||||
if (!fileValid || !boardNameStatus.canSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,7 +138,19 @@ export const ImportBoardModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
</Grid>
|
||||
</Fieldset>
|
||||
|
||||
<TextInput withAsterisk label={tOldImport("form.name.label")} {...form.getInputProps("configuration.name")} />
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={tOldImport("form.name.label")}
|
||||
description={
|
||||
boardNameStatus.description ? (
|
||||
<Group c={boardNameStatus.description.color} gap="xs" align="center">
|
||||
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
|
||||
<span>{boardNameStatus.description.label}</span>
|
||||
</Group>
|
||||
) : null
|
||||
}
|
||||
{...form.getInputProps("configuration.name")}
|
||||
/>
|
||||
|
||||
<Radio.Group
|
||||
withAsterisk
|
||||
2
packages/modals-collection/src/boards/index.ts
Normal file
2
packages/modals-collection/src/boards/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AddBoardModal } from "./add-board-modal";
|
||||
export { ImportBoardModal } from "./import-board-modal";
|
||||
2
packages/modals-collection/src/index.ts
Normal file
2
packages/modals-collection/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./boards";
|
||||
export * from "./invites";
|
||||
2
packages/modals-collection/src/invites/index.ts
Normal file
2
packages/modals-collection/src/invites/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { InviteCopyModal } from "./invite-copy-modal";
|
||||
export { InviteCreateModal } from "./invite-create-modal";
|
||||
8
packages/modals-collection/tsconfig.json
Normal file
8
packages/modals-collection/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -69,23 +69,15 @@ const permissionsSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export const createOldmarrImportConfigurationSchema = (existingBoardNames: string[]) =>
|
||||
z.object({
|
||||
name: boardNameSchema.refine(
|
||||
(value) => {
|
||||
return existingBoardNames.every((name) => name.toLowerCase().trim() !== value.toLowerCase().trim());
|
||||
},
|
||||
{
|
||||
params: createCustomErrorParams("boardAlreadyExists"),
|
||||
},
|
||||
),
|
||||
onlyImportApps: z.boolean().default(false),
|
||||
distinctAppsByHref: z.boolean().default(true),
|
||||
screenSize: z.enum(["lg", "md", "sm"]).default("lg"),
|
||||
sidebarBehaviour: z.enum(["remove-items", "last-section"]).default("last-section"),
|
||||
});
|
||||
export const oldmarrImportConfigurationSchema = z.object({
|
||||
name: boardNameSchema,
|
||||
onlyImportApps: z.boolean().default(false),
|
||||
distinctAppsByHref: z.boolean().default(true),
|
||||
screenSize: z.enum(["lg", "md", "sm"]).default("lg"),
|
||||
sidebarBehaviour: z.enum(["remove-items", "last-section"]).default("last-section"),
|
||||
});
|
||||
|
||||
export type OldmarrImportConfiguration = z.infer<ReturnType<typeof createOldmarrImportConfigurationSchema>>;
|
||||
export type OldmarrImportConfiguration = z.infer<typeof oldmarrImportConfigurationSchema>;
|
||||
|
||||
export const superRefineJsonImportFile = (value: File | null, context: z.RefinementCtx) => {
|
||||
if (!value) {
|
||||
@@ -121,7 +113,7 @@ export const superRefineJsonImportFile = (value: File | null, context: z.Refinem
|
||||
|
||||
const importJsonFileSchema = zfd.formData({
|
||||
file: zfd.file().superRefine(superRefineJsonImportFile),
|
||||
configuration: zfd.json(createOldmarrImportConfigurationSchema([])),
|
||||
configuration: zfd.json(oldmarrImportConfigurationSchema),
|
||||
});
|
||||
|
||||
const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));
|
||||
|
||||
@@ -26,5 +26,5 @@ export {
|
||||
type BoardItemAdvancedOptions,
|
||||
} from "./shared";
|
||||
export { passwordRequirements } from "./user";
|
||||
export { createOldmarrImportConfigurationSchema, superRefineJsonImportFile } from "./board";
|
||||
export { oldmarrImportConfigurationSchema, superRefineJsonImportFile } from "./board";
|
||||
export type { OldmarrImportConfiguration } from "./board";
|
||||
|
||||
2633
pnpm-lock.yaml
generated
2633
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,8 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.9.1",
|
||||
"typescript": "^5.5.4"
|
||||
"eslint": "^9.10.0",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config"
|
||||
}
|
||||
Reference in New Issue
Block a user