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:
@@ -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";
|
||||
|
||||
7
packages/common/src/revalidate-path-action.ts
Normal file
7
packages/common/src/revalidate-path-action.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidatePathActionAsync(path: string) {
|
||||
return new Promise((resolve) => resolve(revalidatePath(path, "page")));
|
||||
}
|
||||
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",
|
||||
},
|
||||
};
|
||||
};
|
||||
200
packages/modals-collection/src/boards/import-board-modal.tsx
Normal file
200
packages/modals-collection/src/boards/import-board-modal.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Fieldset, FileInput, Grid, Group, Radio, Stack, Switch, TextInput } from "@mantine/core";
|
||||
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";
|
||||
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { SelectWithDescription } from "@homarr/ui";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
import { oldmarrImportConfigurationSchema, superRefineJsonImportFile, z } from "@homarr/validation";
|
||||
|
||||
import { useBoardNameStatus } from "./add-board-modal";
|
||||
|
||||
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: oldmarrImportConfigurationSchema,
|
||||
}),
|
||||
{
|
||||
mode: "controlled",
|
||||
initialValues: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
file: null!,
|
||||
configuration: {
|
||||
distinctAppsByHref: true,
|
||||
onlyImportApps: false,
|
||||
screenSize: "lg",
|
||||
sidebarBehaviour: "last-section",
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
onValuesChange(values, previous) {
|
||||
// This is a workarround until async validation is supported by mantine
|
||||
void (async () => {
|
||||
if (values.file === previous.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await values.file.text();
|
||||
const result = oldmarrConfigSchema.safeParse(JSON.parse(content));
|
||||
|
||||
if (!result.success) {
|
||||
console.error(result.error.errors);
|
||||
setFileValid(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setFileValid(true);
|
||||
form.setFieldValue("configuration.name", result.data.configProperties.name);
|
||||
})();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
formData.set("file", values.file);
|
||||
formData.set("configuration", JSON.stringify(values.configuration));
|
||||
|
||||
await mutateAsync(formData, {
|
||||
async onSuccess() {
|
||||
actions.closeModal();
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
showSuccessNotification({
|
||||
title: tOldImport("notification.success.title"),
|
||||
message: tOldImport("notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: tOldImport("notification.error.title"),
|
||||
message: tOldImport("notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
if (!fileValid || !boardNameStatus.canSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
void handleSubmitAsync({
|
||||
// It's checked for null in the superrefine
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
file: values.file!,
|
||||
configuration: values.configuration,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<FileInput
|
||||
rightSection={<IconFileUpload />}
|
||||
withAsterisk
|
||||
accept="application/json"
|
||||
{...form.getInputProps("file")}
|
||||
error={
|
||||
(form.getInputProps("file").error as string | undefined) ??
|
||||
(!fileValid && form.isDirty("file") ? tOldImport("form.file.invalidError") : undefined)
|
||||
}
|
||||
type="button"
|
||||
label={tOldImport("form.file.label")}
|
||||
/>
|
||||
|
||||
<Fieldset legend={tOldImport("form.apps.label")}>
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Switch
|
||||
label={tOldImport("form.apps.avoidDuplicates.label")}
|
||||
description={tOldImport("form.apps.avoidDuplicates.description")}
|
||||
{...form.getInputProps("configuration.distinctAppsByHref", { type: "checkbox" })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Switch
|
||||
label={tOldImport("form.apps.onlyImportApps.label")}
|
||||
description={tOldImport("form.apps.onlyImportApps.description")}
|
||||
{...form.getInputProps("configuration.onlyImportApps", { type: "checkbox" })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Fieldset>
|
||||
|
||||
<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
|
||||
label={tOldImport("form.screenSize.label")}
|
||||
{...form.getInputProps("configuration.screenSize")}
|
||||
>
|
||||
<Group mt="xs">
|
||||
<Radio value="sm" label={tOldImport("form.screenSize.option.sm")} />
|
||||
<Radio value="md" label={tOldImport("form.screenSize.option.md")} />
|
||||
<Radio value="lg" label={tOldImport("form.screenSize.option.lg")} />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
<SelectWithDescription
|
||||
withAsterisk
|
||||
label={tOldImport("form.sidebarBehavior.label")}
|
||||
description={tOldImport("form.sidebarBehavior.description")}
|
||||
data={[
|
||||
{
|
||||
value: "last-section",
|
||||
label: tOldImport("form.sidebarBehavior.option.lastSection.label"),
|
||||
description: tOldImport("form.sidebarBehavior.option.lastSection.description"),
|
||||
},
|
||||
{
|
||||
value: "remove-items",
|
||||
label: tOldImport("form.sidebarBehavior.option.removeItems.label"),
|
||||
description: tOldImport("form.sidebarBehavior.option.removeItems.description"),
|
||||
},
|
||||
]}
|
||||
{...form.getInputProps("configuration.sidebarBehaviour")}
|
||||
/>
|
||||
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{tCommon("action.import")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("board.action.oldImport.label"),
|
||||
size: "lg",
|
||||
});
|
||||
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";
|
||||
57
packages/modals-collection/src/invites/invite-copy-modal.tsx
Normal file
57
packages/modals-collection/src/invites/invite-copy-modal.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button, CopyButton, Mark, Stack, Text } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
export const InviteCopyModal = createModal<RouterOutputs["invite"]["createInvite"]>(({ actions, innerProps }) => {
|
||||
const t = useScopedI18n("management.page.user.invite");
|
||||
const inviteUrl = useInviteUrl(innerProps);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t("action.copy.description")}</Text>
|
||||
{/* TODO: When next-international v2 is released the descriptions bold element can be implemented, see https://github.com/QuiiBz/next-international/pull/361 for progress */}
|
||||
<Link href={createPath(innerProps)}>{t("action.copy.link")}</Link>
|
||||
<Stack gap="xs">
|
||||
<Text fw="bold">{t("field.id.label")}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.id}
|
||||
</Mark>
|
||||
|
||||
<Text fw="bold">{t("field.token.label")}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.token}
|
||||
</Mark>
|
||||
</Stack>
|
||||
<CopyButton value={inviteUrl}>
|
||||
{({ copy }) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copy();
|
||||
actions.closeModal();
|
||||
}}
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
{t("action.copy.button")}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Stack>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("management.page.user.invite.action.copy.title");
|
||||
},
|
||||
});
|
||||
|
||||
const createPath = ({ id, token }: RouterOutputs["invite"]["createInvite"]) => `/auth/invite/${id}?token=${token}`;
|
||||
|
||||
const useInviteUrl = ({ id, token }: RouterOutputs["invite"]["createInvite"]) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return window.location.href.replace(pathname, createPath({ id, token }));
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useForm } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { InviteCopyModal } from "./invite-copy-modal";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface FormType {
|
||||
expirationDate: Date;
|
||||
}
|
||||
|
||||
export const InviteCreateModal = createModal<void>(({ actions }) => {
|
||||
const tInvite = useScopedI18n("management.page.user.invite");
|
||||
const t = useI18n();
|
||||
const { openModal } = useModalAction(InviteCopyModal);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
const { mutate, isPending } = clientApi.invite.createInvite.useMutation();
|
||||
const minDate = dayjs().add(1, "hour").toDate();
|
||||
const maxDate = dayjs().add(6, "months").toDate();
|
||||
|
||||
const form = useForm<FormType>({
|
||||
initialValues: {
|
||||
expirationDate: dayjs().add(4, "hours").toDate(),
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: FormType) => {
|
||||
mutate(values, {
|
||||
onSuccess: (result) => {
|
||||
void utils.invite.getAll.invalidate();
|
||||
actions.closeModal();
|
||||
openModal(result);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<Text>{tInvite("action.new.description")}</Text>
|
||||
|
||||
<DateTimePicker
|
||||
popoverProps={{ withinPortal: true }}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
withAsterisk
|
||||
valueFormat="DD MMM YYYY HH:mm"
|
||||
label={tInvite("field.expirationDate.label")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("expirationDate")}
|
||||
/>
|
||||
|
||||
<Group justify="end">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} color="teal">
|
||||
{t("common.action.create")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("management.page.user.invite.action.new.title");
|
||||
},
|
||||
});
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user