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,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,189 +0,0 @@
|
||||
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 { 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 { createOldmarrImportConfigurationSchema, superRefineJsonImportFile, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
|
||||
interface InnerProps {
|
||||
boardNames: string[];
|
||||
}
|
||||
|
||||
export const ImportBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
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),
|
||||
}),
|
||||
{
|
||||
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 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) {
|
||||
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")} {...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",
|
||||
});
|
||||
Reference in New Issue
Block a user