feat: add import for config files from oldmarr (#1019)
* wip: add oldmarr config import
* wip: add support for wrong amount of categories / sections with autofix, color mapping, position adjustments of wrappers
* fix: lockfile broken
* feat: add support for form data trpc requests
* wip: improve file upload
* refactor: restructure import, add import configuration
* wip: add configurations for import to modal
* refactor: move oldmarr import to old-import package
* fix: column count not respects screen size for board
* feat: add beta badge for oldmarr config import
* chore: address pull request feedback
* fix: format issues
* fix: inconsistent versions
* fix: deepsource issues
* fix: revert {} to Record<string, never> convertion to prevent typecheck issue
* fix: inconsistent zod version
* fix: format issue
* chore: address pull request feedback
* fix: wrong import
* fix: broken lock file
* fix: inconsistent versions
* fix: format issues
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "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",
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import { useState } from "react";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||||
import { createWSClient, loggerLink, unstable_httpBatchStreamLink, wsLink } from "@trpc/client";
|
import {
|
||||||
|
createWSClient,
|
||||||
|
httpLink,
|
||||||
|
isNonJsonSerializable,
|
||||||
|
loggerLink,
|
||||||
|
splitLink,
|
||||||
|
unstable_httpBatchStreamLink,
|
||||||
|
wsLink,
|
||||||
|
} from "@trpc/client";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import type { AppRouter } from "@homarr/api";
|
import type { AppRouter } from "@homarr/api";
|
||||||
@@ -34,18 +42,29 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
enabled: (opts) =>
|
enabled: (opts) =>
|
||||||
process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error),
|
process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error),
|
||||||
}),
|
}),
|
||||||
(args) => {
|
splitLink({
|
||||||
return ({ op, next }) => {
|
condition: ({ type }) => type === "subscription",
|
||||||
console.log("op", op.type, op.input, op.path, op.id);
|
true: wsLink<AppRouter>({
|
||||||
if (op.type === "subscription") {
|
client: wsClient,
|
||||||
const link = wsLink<AppRouter>({
|
transformer: superjson,
|
||||||
client: wsClient,
|
}),
|
||||||
transformer: superjson,
|
false: splitLink({
|
||||||
});
|
condition: ({ input }) => isNonJsonSerializable(input),
|
||||||
return link(args)({ op, next });
|
true: httpLink({
|
||||||
}
|
/**
|
||||||
|
* We don't want to transform the data here as we want to use form data
|
||||||
return unstable_httpBatchStreamLink({
|
*/
|
||||||
|
transformer: {
|
||||||
|
serialize(object: unknown) {
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
deserialize(data: unknown) {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
|
}),
|
||||||
|
false: unstable_httpBatchStreamLink({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
headers() {
|
headers() {
|
||||||
@@ -53,9 +72,9 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
headers.set("x-trpc-source", "nextjs-react");
|
headers.set("x-trpc-source", "nextjs-react");
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
})(args)({ op, next });
|
}),
|
||||||
};
|
}),
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { IconCategoryPlus } from "@tabler/icons-react";
|
import { Affix, Button, Group, Menu } from "@mantine/core";
|
||||||
|
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { BetaBadge } from "@homarr/ui";
|
||||||
|
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
||||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
import { ImportBoardModal } from "~/components/manage/boards/import-board-modal";
|
||||||
|
|
||||||
interface CreateBoardButtonProps {
|
interface CreateBoardButtonProps {
|
||||||
boardNames: string[];
|
boardNames: string[];
|
||||||
@@ -17,7 +19,8 @@ interface CreateBoardButtonProps {
|
|||||||
|
|
||||||
export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { openModal } = useModalAction(AddBoardModal);
|
const { openModal: openAddModal } = useModalAction(AddBoardModal);
|
||||||
|
const { openModal: openImportModal } = useModalAction(ImportBoardModal);
|
||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
@@ -25,8 +28,8 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onCreateClick = useCallback(() => {
|
||||||
openModal({
|
openAddModal({
|
||||||
onSuccess: async (values) => {
|
onSuccess: async (values) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: values.name,
|
name: values.name,
|
||||||
@@ -36,11 +39,41 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
|||||||
},
|
},
|
||||||
boardNames,
|
boardNames,
|
||||||
});
|
});
|
||||||
}, [mutateAsync, boardNames, openModal]);
|
}, [mutateAsync, boardNames, openAddModal]);
|
||||||
|
|
||||||
|
const onImportClick = useCallback(() => {
|
||||||
|
openImportModal({ boardNames });
|
||||||
|
}, [openImportModal, boardNames]);
|
||||||
|
|
||||||
|
const buttonGroupContent = (
|
||||||
|
<>
|
||||||
|
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={onCreateClick} loading={isPending}>
|
||||||
|
{t("management.page.board.action.new.label")}
|
||||||
|
</Button>
|
||||||
|
<Menu position="bottom-end">
|
||||||
|
<Menu.Target>
|
||||||
|
<Button px="xs" ms={1}>
|
||||||
|
<IconChevronDown size="1rem" />
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item onClick={onImportClick} leftSection={<IconFileImport size="1rem" />}>
|
||||||
|
<Group>
|
||||||
|
{t("board.action.oldImport.label")}
|
||||||
|
<BetaBadge size="xs" />
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileAffixButton leftSection={<IconCategoryPlus size="1rem" />} onClick={onClick} loading={isPending}>
|
<>
|
||||||
{t("management.page.board.action.new.label")}
|
<Button.Group visibleFrom="md">{buttonGroupContent}</Button.Group>
|
||||||
</MobileAffixButton>
|
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
|
||||||
|
<Button.Group>{buttonGroupContent}</Button.Group>
|
||||||
|
</Affix>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
189
apps/nextjs/src/components/manage/boards/import-board-modal.tsx
Normal file
189
apps/nextjs/src/components/manage/boards/import-board-modal.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
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",
|
||||||
|
});
|
||||||
@@ -29,6 +29,8 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
|
"@homarr/old-import": "workspace:^0.1.0",
|
||||||
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/ping": "workspace:^0.1.0",
|
"@homarr/ping": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
} from "@homarr/db/schema/sqlite";
|
} from "@homarr/db/schema/sqlite";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||||
|
import { importAsync } from "@homarr/old-import";
|
||||||
|
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||||
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
||||||
|
|
||||||
@@ -451,6 +453,13 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
importOldmarrConfig: protectedProcedure
|
||||||
|
.input(validation.board.importOldmarrConfig)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const content = await input.file.text();
|
||||||
|
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
|
||||||
|
await importAsync(ctx.db, oldmarr, input.configuration);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => {
|
const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => {
|
||||||
|
|||||||
9
packages/old-import/eslint.config.js
Normal file
9
packages/old-import/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/old-import/index.ts
Normal file
1
packages/old-import/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./src";
|
||||||
40
packages/old-import/package.json
Normal file
40
packages/old-import/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@homarr/old-import",
|
||||||
|
"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/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
|
"superjson": "2.2.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.5.4"
|
||||||
|
},
|
||||||
|
"prettier": "@homarr/prettier-config"
|
||||||
|
}
|
||||||
49
packages/old-import/src/fix-section-issues.ts
Normal file
49
packages/old-import/src/fix-section-issues.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
|
||||||
|
export const fixSectionIssues = (old: OldmarrConfig) => {
|
||||||
|
const wrappers = old.wrappers.sort((wrapperA, wrapperB) => wrapperA.position - wrapperB.position);
|
||||||
|
const categories = old.categories.sort((categoryA, categoryB) => categoryA.position - categoryB.position);
|
||||||
|
|
||||||
|
const neededSectionCount = categories.length * 2 + 1;
|
||||||
|
const hasToMuchEmptyWrappers = wrappers.length > categories.length + 1;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Fixing section issues neededSectionCount=${neededSectionCount} hasToMuchEmptyWrappers=${hasToMuchEmptyWrappers}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let position = 0; position < neededSectionCount; position++) {
|
||||||
|
const index = Math.floor(position / 2);
|
||||||
|
const isEmpty = position % 2 === 0;
|
||||||
|
const section = isEmpty ? wrappers[index] : categories[index];
|
||||||
|
if (!section) {
|
||||||
|
// If there are not enough empty sections for categories we need to insert them
|
||||||
|
if (isEmpty) {
|
||||||
|
// Insert empty wrapper for between categories
|
||||||
|
wrappers.push({
|
||||||
|
id: createId(),
|
||||||
|
position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all wrappers that should be merged into one
|
||||||
|
const wrapperIdsToMerge = wrappers.slice(categories.length).map((section) => section.id);
|
||||||
|
// Remove all wrappers after the first at the end
|
||||||
|
wrappers.splice(categories.length + 1);
|
||||||
|
|
||||||
|
if (wrapperIdsToMerge.length >= 2) {
|
||||||
|
logger.debug(`Found wrappers to merge count=${wrapperIdsToMerge.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrappers,
|
||||||
|
categories,
|
||||||
|
wrapperIdsToMerge,
|
||||||
|
};
|
||||||
|
};
|
||||||
59
packages/old-import/src/import-apps.ts
Normal file
59
packages/old-import/src/import-apps.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createId, inArray } from "@homarr/db";
|
||||||
|
import type { Database, InferInsertModel } from "@homarr/db";
|
||||||
|
import { apps as appsTable } from "@homarr/db/schema/sqlite";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrApp } from "@homarr/old-schema";
|
||||||
|
|
||||||
|
export const insertAppsAsync = async (
|
||||||
|
db: Database,
|
||||||
|
apps: OldmarrApp[],
|
||||||
|
distinctAppsByHref: boolean,
|
||||||
|
configName: string,
|
||||||
|
) => {
|
||||||
|
logger.info(
|
||||||
|
`Importing old homarr apps configuration=${configName} distinctAppsByHref=${distinctAppsByHref} apps=${apps.length}`,
|
||||||
|
);
|
||||||
|
const existingAppsWithHref = distinctAppsByHref
|
||||||
|
? await db.query.apps.findMany({
|
||||||
|
where: inArray(appsTable.href, [...new Set(apps.map((app) => app.url))]),
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
logger.debug(`Found existing apps with href count=${existingAppsWithHref.length}`);
|
||||||
|
|
||||||
|
const mappedApps = apps.map((app) => ({
|
||||||
|
// Use id of existing app when it has the same href and distinctAppsByHref is true
|
||||||
|
newId: distinctAppsByHref
|
||||||
|
? (existingAppsWithHref.find(
|
||||||
|
(existingApp) =>
|
||||||
|
existingApp.href === (app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl) &&
|
||||||
|
existingApp.name === app.name &&
|
||||||
|
existingApp.iconUrl === app.appearance.iconUrl,
|
||||||
|
)?.id ?? createId())
|
||||||
|
: createId(),
|
||||||
|
...app,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const appsToCreate = mappedApps
|
||||||
|
.filter((app) => !existingAppsWithHref.some((existingApp) => existingApp.id === app.newId))
|
||||||
|
.map(
|
||||||
|
(app) =>
|
||||||
|
({
|
||||||
|
id: app.newId,
|
||||||
|
name: app.name,
|
||||||
|
iconUrl: app.appearance.iconUrl,
|
||||||
|
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
|
||||||
|
description: app.behaviour.tooltipDescription,
|
||||||
|
}) satisfies InferInsertModel<typeof appsTable>,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`Creating apps count=${appsToCreate.length}`);
|
||||||
|
|
||||||
|
if (appsToCreate.length > 0) {
|
||||||
|
await db.insert(appsTable).values(appsToCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Imported apps count=${appsToCreate.length}`);
|
||||||
|
|
||||||
|
return mappedApps;
|
||||||
|
};
|
||||||
35
packages/old-import/src/import-board.ts
Normal file
35
packages/old-import/src/import-board.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { boards } from "@homarr/db/schema/sqlite";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { mapColor } from "./mappers/map-colors";
|
||||||
|
import { mapColumnCount } from "./mappers/map-column-count";
|
||||||
|
|
||||||
|
export const insertBoardAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
|
||||||
|
logger.info(`Importing old homarr board configuration=${old.configProperties.name}`);
|
||||||
|
const boardId = createId();
|
||||||
|
await db.insert(boards).values({
|
||||||
|
id: boardId,
|
||||||
|
name: configuration.name,
|
||||||
|
backgroundImageAttachment: old.settings.customization.backgroundImageAttachment,
|
||||||
|
backgroundImageUrl: old.settings.customization.backgroundImageUrl,
|
||||||
|
backgroundImageRepeat: old.settings.customization.backgroundImageRepeat,
|
||||||
|
backgroundImageSize: old.settings.customization.backgroundImageSize,
|
||||||
|
columnCount: mapColumnCount(old, configuration.screenSize),
|
||||||
|
faviconImageUrl: old.settings.customization.faviconUrl,
|
||||||
|
isPublic: old.settings.access.allowGuests,
|
||||||
|
logoImageUrl: old.settings.customization.logoImageUrl,
|
||||||
|
pageTitle: old.settings.customization.pageTitle,
|
||||||
|
metaTitle: old.settings.customization.metaTitle,
|
||||||
|
opacity: old.settings.customization.appOpacity,
|
||||||
|
primaryColor: mapColor(old.settings.customization.colors.primary, "#fa5252"),
|
||||||
|
secondaryColor: mapColor(old.settings.customization.colors.secondary, "#fd7e14"),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Imported board id=${boardId}`);
|
||||||
|
|
||||||
|
return boardId;
|
||||||
|
};
|
||||||
16
packages/old-import/src/import-error.ts
Normal file
16
packages/old-import/src/import-error.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
export class OldHomarrImportError extends Error {
|
||||||
|
constructor(oldConfig: OldmarrConfig, cause: unknown) {
|
||||||
|
super(`Failed to import old homarr configuration name=${oldConfig.configProperties.name}`, {
|
||||||
|
cause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OldHomarrScreenSizeError extends Error {
|
||||||
|
constructor(type: "app" | "widget", id: string, screenSize: OldmarrImportConfiguration["screenSize"]) {
|
||||||
|
super(`Screen size not found for type=${type} id=${id} screenSize=${screenSize}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
packages/old-import/src/import-items.ts
Normal file
98
packages/old-import/src/import-items.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { items } from "@homarr/db/schema/sqlite";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrApp, OldmarrWidget } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
import type { WidgetComponentProps } from "../../widgets/src/definition";
|
||||||
|
import { OldHomarrScreenSizeError } from "./import-error";
|
||||||
|
import { mapKind } from "./widgets/definitions";
|
||||||
|
import { mapOptions } from "./widgets/options";
|
||||||
|
|
||||||
|
export const insertItemsAsync = async (
|
||||||
|
db: Database,
|
||||||
|
widgets: OldmarrWidget[],
|
||||||
|
mappedApps: (OldmarrApp & { newId: string })[],
|
||||||
|
sectionIdMaps: Map<string, string>,
|
||||||
|
configuration: OldmarrImportConfiguration,
|
||||||
|
) => {
|
||||||
|
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${mappedApps.length}`);
|
||||||
|
|
||||||
|
for (const widget of widgets) {
|
||||||
|
// All items should have been moved to the last wrapper
|
||||||
|
if (widget.area.type === "sidebar") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = mapKind(widget.type);
|
||||||
|
|
||||||
|
logger.debug(`Mapped widget kind id=${widget.id} previous=${widget.type} current=${kind}`);
|
||||||
|
|
||||||
|
if (!kind) {
|
||||||
|
logger.error(`Widget has no kind id=${widget.id} type=${widget.type}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const sectionId = sectionIdMaps.get(widget.area.properties.id)!;
|
||||||
|
|
||||||
|
logger.debug(`Inserting widget id=${widget.id} sectionId=${sectionId}`);
|
||||||
|
|
||||||
|
const screenSizeShape = widget.shape[configuration.screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("widget", widget.id, configuration.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(items).values({
|
||||||
|
id: createId(),
|
||||||
|
sectionId,
|
||||||
|
height: screenSizeShape.size.height,
|
||||||
|
width: screenSizeShape.size.width,
|
||||||
|
xOffset: screenSizeShape.location.x,
|
||||||
|
yOffset: screenSizeShape.location.y,
|
||||||
|
kind,
|
||||||
|
options: SuperJSON.stringify(mapOptions(kind, widget.properties)),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`Inserted widget id=${widget.id} sectionId=${sectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const app of mappedApps) {
|
||||||
|
// All items should have been moved to the last wrapper
|
||||||
|
if (app.area.type === "sidebar") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const sectionId = sectionIdMaps.get(app.area.properties.id)!;
|
||||||
|
|
||||||
|
logger.debug(`Inserting app name=${app.name} sectionId=${sectionId}`);
|
||||||
|
|
||||||
|
const screenSizeShape = app.shape[configuration.screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("app", app.id, configuration.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(items).values({
|
||||||
|
id: createId(),
|
||||||
|
sectionId,
|
||||||
|
height: screenSizeShape.size.height,
|
||||||
|
width: screenSizeShape.size.width,
|
||||||
|
xOffset: screenSizeShape.location.x,
|
||||||
|
yOffset: screenSizeShape.location.y,
|
||||||
|
kind: "app",
|
||||||
|
options: SuperJSON.stringify({
|
||||||
|
appId: app.newId,
|
||||||
|
openInNewTab: app.behaviour.isOpeningNewTab,
|
||||||
|
pingEnabled: app.network.enabledStatusChecker,
|
||||||
|
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
|
||||||
|
showTitle: app.appearance.appNameStatus === "normal",
|
||||||
|
} satisfies WidgetComponentProps<"app">["options"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`Inserted app name=${app.name} sectionId=${sectionId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
51
packages/old-import/src/import-sections.ts
Normal file
51
packages/old-import/src/import-sections.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { sections } from "@homarr/db/schema/sqlite";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
|
||||||
|
export const insertSectionsAsync = async (
|
||||||
|
db: Database,
|
||||||
|
categories: OldmarrConfig["categories"],
|
||||||
|
wrappers: OldmarrConfig["wrappers"],
|
||||||
|
boardId: string,
|
||||||
|
) => {
|
||||||
|
logger.info(
|
||||||
|
`Importing old homarr sections boardId=${boardId} categories=${categories.length} wrappers=${wrappers.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const wrapperIds = wrappers.map((section) => section.id);
|
||||||
|
const categoryIds = categories.map((section) => section.id);
|
||||||
|
const idMaps = new Map<string, string>([...wrapperIds, ...categoryIds].map((id) => [id, createId()]));
|
||||||
|
|
||||||
|
const wrappersToInsert = wrappers.map((section) => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
id: idMaps.get(section.id)!,
|
||||||
|
boardId,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: section.position,
|
||||||
|
kind: "empty" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const categoriesToInsert = categories.map((section) => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
id: idMaps.get(section.id)!,
|
||||||
|
boardId,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: section.position,
|
||||||
|
kind: "category" as const,
|
||||||
|
name: section.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (wrappersToInsert.length > 0) {
|
||||||
|
await db.insert(sections).values(wrappersToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoriesToInsert.length > 0) {
|
||||||
|
await db.insert(sections).values(categoriesToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Imported sections count=${wrappersToInsert.length + categoriesToInsert.length}`);
|
||||||
|
|
||||||
|
return idMaps;
|
||||||
|
};
|
||||||
47
packages/old-import/src/index.ts
Normal file
47
packages/old-import/src/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { fixSectionIssues } from "./fix-section-issues";
|
||||||
|
import { insertAppsAsync } from "./import-apps";
|
||||||
|
import { insertBoardAsync } from "./import-board";
|
||||||
|
import { OldHomarrImportError, OldHomarrScreenSizeError } from "./import-error";
|
||||||
|
import { insertItemsAsync } from "./import-items";
|
||||||
|
import { insertSectionsAsync } from "./import-sections";
|
||||||
|
import { moveWidgetsAndAppsIfMerge } from "./move-widgets-and-apps-merge";
|
||||||
|
|
||||||
|
export const importAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
|
||||||
|
if (configuration.onlyImportApps) {
|
||||||
|
await db
|
||||||
|
.transaction(async (trasaction) => {
|
||||||
|
await insertAppsAsync(trasaction, old.apps, configuration.distinctAppsByHref, old.configProperties.name);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw new OldHomarrImportError(old, error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.transaction(async (trasaction) => {
|
||||||
|
const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(old);
|
||||||
|
const { apps, widgets } = moveWidgetsAndAppsIfMerge(old, wrapperIdsToMerge, configuration);
|
||||||
|
|
||||||
|
const boardId = await insertBoardAsync(trasaction, old, configuration);
|
||||||
|
const sectionIdMaps = await insertSectionsAsync(trasaction, categories, wrappers, boardId);
|
||||||
|
const mappedApps = await insertAppsAsync(
|
||||||
|
trasaction,
|
||||||
|
apps,
|
||||||
|
configuration.distinctAppsByHref,
|
||||||
|
old.configProperties.name,
|
||||||
|
);
|
||||||
|
await insertItemsAsync(trasaction, widgets, mappedApps, sectionIdMaps, configuration);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error instanceof OldHomarrScreenSizeError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OldHomarrImportError(old, error);
|
||||||
|
});
|
||||||
|
};
|
||||||
48
packages/old-import/src/mappers/map-colors.ts
Normal file
48
packages/old-import/src/mappers/map-colors.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const oldColors = [
|
||||||
|
"dark",
|
||||||
|
"gray",
|
||||||
|
"red",
|
||||||
|
"pink",
|
||||||
|
"grape",
|
||||||
|
"violet",
|
||||||
|
"indigo",
|
||||||
|
"blue",
|
||||||
|
"cyan",
|
||||||
|
"green",
|
||||||
|
"lime",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"teal",
|
||||||
|
] as const;
|
||||||
|
type OldColor = (typeof oldColors)[number];
|
||||||
|
|
||||||
|
export const mapColor = (color: string | undefined, fallback: string) => {
|
||||||
|
if (!color) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldColors.some((mantineColor) => color === mantineColor)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mantineColor = color as OldColor;
|
||||||
|
|
||||||
|
return mappedColors[mantineColor];
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedColors: Record<(typeof oldColors)[number], string> = {
|
||||||
|
blue: "#228be6",
|
||||||
|
cyan: "#15aabf",
|
||||||
|
dark: "#2e2e2e",
|
||||||
|
grape: "#be4bdb",
|
||||||
|
gray: "#868e96",
|
||||||
|
green: "#40c057",
|
||||||
|
indigo: "#4c6ef5",
|
||||||
|
lime: "#82c91e",
|
||||||
|
orange: "#fd7e14",
|
||||||
|
pink: "#e64980",
|
||||||
|
red: "#fa5252",
|
||||||
|
teal: "#12b886",
|
||||||
|
violet: "#7950f2",
|
||||||
|
yellow: "#fab005",
|
||||||
|
};
|
||||||
15
packages/old-import/src/mappers/map-column-count.ts
Normal file
15
packages/old-import/src/mappers/map-column-count.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
export const mapColumnCount = (old: OldmarrConfig, screenSize: OldmarrImportConfiguration["screenSize"]) => {
|
||||||
|
switch (screenSize) {
|
||||||
|
case "lg":
|
||||||
|
return old.settings.customization.gridstack.columnCountLarge;
|
||||||
|
case "md":
|
||||||
|
return old.settings.customization.gridstack.columnCountMedium;
|
||||||
|
case "sm":
|
||||||
|
return old.settings.customization.gridstack.columnCountSmall;
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
};
|
||||||
300
packages/old-import/src/move-widgets-and-apps-merge.ts
Normal file
300
packages/old-import/src/move-widgets-and-apps-merge.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import type { OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema";
|
||||||
|
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { OldHomarrScreenSizeError } from "./import-error";
|
||||||
|
import { mapColumnCount } from "./mappers/map-column-count";
|
||||||
|
|
||||||
|
export const moveWidgetsAndAppsIfMerge = (
|
||||||
|
old: OldmarrConfig,
|
||||||
|
wrapperIdsToMerge: string[],
|
||||||
|
configuration: OldmarrImportConfiguration,
|
||||||
|
) => {
|
||||||
|
const firstId = wrapperIdsToMerge[0];
|
||||||
|
if (!firstId) {
|
||||||
|
return { apps: old.apps, widgets: old.widgets };
|
||||||
|
}
|
||||||
|
|
||||||
|
const affectedMap = new Map<string, { apps: OldmarrApp[]; widgets: OldmarrWidget[] }>(
|
||||||
|
wrapperIdsToMerge.map((id) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
apps: old.apps.filter((app) => app.area.type !== "sidebar" && id === app.area.properties.id),
|
||||||
|
widgets: old.widgets.filter((app) => app.area.type !== "sidebar" && id === app.area.properties.id),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`Merging wrappers at the end of the board count=${wrapperIdsToMerge.length}`);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (const id of wrapperIdsToMerge) {
|
||||||
|
let requiredHeight = 0;
|
||||||
|
const affected = affectedMap.get(id);
|
||||||
|
if (!affected) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = affected.apps;
|
||||||
|
const widgets = affected.widgets;
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
if (app.area.type === "sidebar") continue;
|
||||||
|
// Move item to first wrapper
|
||||||
|
app.area.properties.id = firstId;
|
||||||
|
|
||||||
|
const screenSizeShape = app.shape[configuration.screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("app", app.id, configuration.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||||
|
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||||
|
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move item down as much as needed to not overlap with other items
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const widget of widgets) {
|
||||||
|
if (widget.area.type === "sidebar") continue;
|
||||||
|
// Move item to first wrapper
|
||||||
|
widget.area.properties.id = firstId;
|
||||||
|
|
||||||
|
const screenSizeShape = widget.shape[configuration.screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("widget", widget.id, configuration.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||||
|
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||||
|
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move item down as much as needed to not overlap with other items
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += requiredHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.sidebarBehaviour === "last-section") {
|
||||||
|
if (old.settings.customization.layout.enabledLeftSidebar) {
|
||||||
|
offset = moveWidgetsAndAppsInLeftSidebar(old, firstId, offset, configuration.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old.settings.customization.layout.enabledRightSidebar) {
|
||||||
|
moveWidgetsAndAppsInRightSidebar(old, firstId, offset, configuration.screenSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { apps: old.apps, widgets: old.widgets };
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveWidgetsAndAppsInLeftSidebar = (
|
||||||
|
old: OldmarrConfig,
|
||||||
|
firstId: string,
|
||||||
|
offset: number,
|
||||||
|
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||||
|
) => {
|
||||||
|
const columnCount = mapColumnCount(old, screenSize);
|
||||||
|
let requiredHeight = updateItems({
|
||||||
|
// This should work as the reference of the items did not change, only the array reference did
|
||||||
|
items: [...old.widgets, ...old.apps],
|
||||||
|
screenSize,
|
||||||
|
filter: (item) =>
|
||||||
|
item.area.type === "sidebar" &&
|
||||||
|
item.area.properties.location === "left" &&
|
||||||
|
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||||
|
update: (item) => {
|
||||||
|
const screenSizeShape = item.shape[screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||||
|
}
|
||||||
|
// Reduce width to one if column count is one
|
||||||
|
if (screenSizeShape.size.width > columnCount) {
|
||||||
|
screenSizeShape.size.width = columnCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.area = {
|
||||||
|
type: "wrapper",
|
||||||
|
properties: {
|
||||||
|
id: firstId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only increase offset if there are less than 3 columns because then the items have to be stacked
|
||||||
|
if (columnCount <= 3) {
|
||||||
|
offset += requiredHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When column count is 0 we need to stack the items of the sidebar on top of each other
|
||||||
|
if (columnCount !== 1) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredHeight = updateItems({
|
||||||
|
// This should work as the reference of the items did not change, only the array reference did
|
||||||
|
items: [...old.widgets, ...old.apps],
|
||||||
|
screenSize,
|
||||||
|
filter: (item) =>
|
||||||
|
item.area.type === "sidebar" &&
|
||||||
|
item.area.properties.location === "left" &&
|
||||||
|
item.shape[screenSize]?.location.x === 1,
|
||||||
|
update: (item) => {
|
||||||
|
const screenSizeShape = item.shape[screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.area = {
|
||||||
|
type: "wrapper",
|
||||||
|
properties: {
|
||||||
|
id: firstId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
screenSizeShape.location.x = 0;
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += requiredHeight;
|
||||||
|
return offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveWidgetsAndAppsInRightSidebar = (
|
||||||
|
old: OldmarrConfig,
|
||||||
|
firstId: string,
|
||||||
|
offset: number,
|
||||||
|
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||||
|
) => {
|
||||||
|
const columnCount = mapColumnCount(old, screenSize);
|
||||||
|
const xOffsetDelta = Math.max(columnCount - 2, 0);
|
||||||
|
const requiredHeight = updateItems({
|
||||||
|
// This should work as the reference of the items did not change, only the array reference did
|
||||||
|
items: [...old.widgets, ...old.apps],
|
||||||
|
screenSize,
|
||||||
|
filter: (item) =>
|
||||||
|
item.area.type === "sidebar" &&
|
||||||
|
item.area.properties.location === "right" &&
|
||||||
|
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||||
|
update: (item) => {
|
||||||
|
const screenSizeShape = item.shape[screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce width to one if column count is one
|
||||||
|
if (screenSizeShape.size.width > columnCount) {
|
||||||
|
screenSizeShape.size.width = columnCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.area = {
|
||||||
|
type: "wrapper",
|
||||||
|
properties: {
|
||||||
|
id: firstId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
screenSizeShape.location.x += xOffsetDelta;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// When column count is 0 we need to stack the items of the sidebar on top of each other
|
||||||
|
if (columnCount !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += requiredHeight;
|
||||||
|
|
||||||
|
updateItems({
|
||||||
|
// This should work as the reference of the items did not change, only the array reference did
|
||||||
|
items: [...old.widgets, ...old.apps],
|
||||||
|
screenSize,
|
||||||
|
filter: (item) =>
|
||||||
|
item.area.type === "sidebar" &&
|
||||||
|
item.area.properties.location === "left" &&
|
||||||
|
item.shape[screenSize]?.location.x === 1,
|
||||||
|
update: (item) => {
|
||||||
|
const screenSizeShape = item.shape[screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.area = {
|
||||||
|
type: "wrapper",
|
||||||
|
properties: {
|
||||||
|
id: firstId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
screenSizeShape.location.x = 0;
|
||||||
|
screenSizeShape.location.y += offset;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createItemSnapshot = (
|
||||||
|
item: OldmarrApp | OldmarrWidget,
|
||||||
|
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||||
|
) => ({
|
||||||
|
x: item.shape[screenSize]?.location.x,
|
||||||
|
y: item.shape[screenSize]?.location.y,
|
||||||
|
height: item.shape[screenSize]?.size.height,
|
||||||
|
width: item.shape[screenSize]?.size.width,
|
||||||
|
section:
|
||||||
|
item.area.type === "sidebar"
|
||||||
|
? {
|
||||||
|
type: "sidebar",
|
||||||
|
location: item.area.properties.location,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: item.area.type,
|
||||||
|
id: item.area.properties.id,
|
||||||
|
},
|
||||||
|
toString(): string {
|
||||||
|
return objectEntries(this)
|
||||||
|
.filter(([key]) => key !== "toString")
|
||||||
|
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
||||||
|
.join(" ");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateItems = (options: {
|
||||||
|
items: (OldmarrApp | OldmarrWidget)[];
|
||||||
|
filter: (item: OldmarrApp | OldmarrWidget) => boolean;
|
||||||
|
update: (item: OldmarrApp | OldmarrWidget) => void;
|
||||||
|
screenSize: OldmarrImportConfiguration["screenSize"];
|
||||||
|
}) => {
|
||||||
|
const items = options.items.filter(options.filter);
|
||||||
|
let requiredHeight = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
const before = createItemSnapshot(item, options.screenSize);
|
||||||
|
|
||||||
|
const screenSizeShape = item.shape[options.screenSize];
|
||||||
|
if (!screenSizeShape) {
|
||||||
|
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||||
|
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.update(item);
|
||||||
|
const after = createItemSnapshot(item, options.screenSize);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Moved item ${item.id}\n [snapshot before]: ${before.toString()}\n [snapshot after]: ${after.toString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requiredHeight;
|
||||||
|
};
|
||||||
18
packages/old-import/src/widgets/definitions/bookmark.ts
Normal file
18
packages/old-import/src/widgets/definitions/bookmark.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"bookmark",
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
iconUrl: string;
|
||||||
|
openNewTab: boolean;
|
||||||
|
hideHostname: boolean;
|
||||||
|
hideIcon: boolean;
|
||||||
|
}[];
|
||||||
|
layout: "autoGrid" | "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
>;
|
||||||
11
packages/old-import/src/widgets/definitions/calendar.ts
Normal file
11
packages/old-import/src/widgets/definitions/calendar.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrCalendarDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"calendar",
|
||||||
|
{
|
||||||
|
hideWeekDays: boolean;
|
||||||
|
showUnmonitored: boolean;
|
||||||
|
radarrReleaseType: "inCinemas" | "physicalRelease" | "digitalRelease";
|
||||||
|
fontSize: "xs" | "sm" | "md" | "lg" | "xl";
|
||||||
|
}
|
||||||
|
>;
|
||||||
9
packages/old-import/src/widgets/definitions/common.ts
Normal file
9
packages/old-import/src/widgets/definitions/common.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { OldmarrWidgetKind } from "@homarr/old-schema";
|
||||||
|
|
||||||
|
export interface CommonOldmarrWidgetDefinition<
|
||||||
|
TId extends OldmarrWidgetKind,
|
||||||
|
TOptions extends Record<string, unknown>,
|
||||||
|
> {
|
||||||
|
id: TId;
|
||||||
|
options: TOptions;
|
||||||
|
}
|
||||||
53
packages/old-import/src/widgets/definitions/dashdot.ts
Normal file
53
packages/old-import/src/widgets/definitions/dashdot.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrDashdotDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"dashdot",
|
||||||
|
{
|
||||||
|
dashName: string;
|
||||||
|
url: string;
|
||||||
|
usePercentages: boolean;
|
||||||
|
columns: number;
|
||||||
|
graphHeight: number;
|
||||||
|
graphsOrder: (
|
||||||
|
| {
|
||||||
|
key: "storage";
|
||||||
|
subValues: {
|
||||||
|
enabled: boolean;
|
||||||
|
compactView: boolean;
|
||||||
|
span: number;
|
||||||
|
multiView: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: "network";
|
||||||
|
subValues: {
|
||||||
|
enabled: boolean;
|
||||||
|
compactView: boolean;
|
||||||
|
span: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: "cpu";
|
||||||
|
subValues: {
|
||||||
|
enabled: boolean;
|
||||||
|
multiView: boolean;
|
||||||
|
span: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: "ram";
|
||||||
|
subValues: {
|
||||||
|
enabled: boolean;
|
||||||
|
span: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: "gpu";
|
||||||
|
subValues: {
|
||||||
|
enabled: boolean;
|
||||||
|
span: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
21
packages/old-import/src/widgets/definitions/date.ts
Normal file
21
packages/old-import/src/widgets/definitions/date.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrDateDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"date",
|
||||||
|
{
|
||||||
|
timezone: string;
|
||||||
|
customTitle: string;
|
||||||
|
display24HourFormat: boolean;
|
||||||
|
dateFormat:
|
||||||
|
| "hide"
|
||||||
|
| "dddd, MMMM D"
|
||||||
|
| "dddd, D MMMM"
|
||||||
|
| "MMM D"
|
||||||
|
| "D MMM"
|
||||||
|
| "DD/MM/YYYY"
|
||||||
|
| "MM/DD/YYYY"
|
||||||
|
| "DD/MM"
|
||||||
|
| "MM/DD";
|
||||||
|
titleState: "none" | "city" | "both";
|
||||||
|
}
|
||||||
|
>;
|
||||||
4
packages/old-import/src/widgets/definitions/dlspeed.ts
Normal file
4
packages/old-import/src/widgets/definitions/dlspeed.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export type OldmarrDlspeedDefinition = CommonOldmarrWidgetDefinition<"dlspeed", {}>;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrDnsHoleControlsDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"dns-hole-controls",
|
||||||
|
{
|
||||||
|
showToggleAllButtons: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrDnsHoleSummaryDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"dns-hole-summary",
|
||||||
|
{ usePiHoleColors: boolean; layout: "column" | "row" | "grid" }
|
||||||
|
>;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrHealthMonitoringDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"health-monitoring",
|
||||||
|
{
|
||||||
|
fahrenheit: boolean;
|
||||||
|
cpu: boolean;
|
||||||
|
memory: boolean;
|
||||||
|
fileSystem: boolean;
|
||||||
|
defaultTabState: "system" | "cluster";
|
||||||
|
node: string;
|
||||||
|
defaultViewState: "storage" | "none" | "node" | "vm" | "lxc";
|
||||||
|
summary: boolean;
|
||||||
|
showNode: boolean;
|
||||||
|
showVM: boolean;
|
||||||
|
showLXCs: boolean;
|
||||||
|
showStorage: boolean;
|
||||||
|
sectionIndicatorColor: "all" | "any";
|
||||||
|
ignoreCert: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
16
packages/old-import/src/widgets/definitions/iframe.ts
Normal file
16
packages/old-import/src/widgets/definitions/iframe.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrIframeDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"iframe",
|
||||||
|
{
|
||||||
|
embedUrl: string;
|
||||||
|
allowFullScreen: boolean;
|
||||||
|
allowScrolling: boolean;
|
||||||
|
allowTransparency: boolean;
|
||||||
|
allowPayment: boolean;
|
||||||
|
allowAutoPlay: boolean;
|
||||||
|
allowMicrophone: boolean;
|
||||||
|
allowCamera: boolean;
|
||||||
|
allowGeolocation: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
75
packages/old-import/src/widgets/definitions/index.ts
Normal file
75
packages/old-import/src/widgets/definitions/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import type { OldmarrBookmarkDefinition } from "./bookmark";
|
||||||
|
import type { OldmarrCalendarDefinition } from "./calendar";
|
||||||
|
import type { OldmarrDashdotDefinition } from "./dashdot";
|
||||||
|
import type { OldmarrDateDefinition } from "./date";
|
||||||
|
import type { OldmarrDlspeedDefinition } from "./dlspeed";
|
||||||
|
import type { OldmarrDnsHoleControlsDefinition } from "./dns-hole-controls";
|
||||||
|
import type { OldmarrDnsHoleSummaryDefinition } from "./dns-hole-summary";
|
||||||
|
import type { OldmarrHealthMonitoringDefinition } from "./health-monitoring";
|
||||||
|
import type { OldmarrIframeDefinition } from "./iframe";
|
||||||
|
import type { OldmarrIndexerManagerDefinition } from "./indexer-manager";
|
||||||
|
import type { OldmarrMediaRequestListDefinition } from "./media-requests-list";
|
||||||
|
import type { OldmarrMediaRequestStatsDefinition } from "./media-requests-stats";
|
||||||
|
import type { OldmarrMediaServerDefinition } from "./media-server";
|
||||||
|
import type { OldmarrMediaTranscodingDefinition } from "./media-transcoding";
|
||||||
|
import type { OldmarrNotebookDefinition } from "./notebook";
|
||||||
|
import type { OldmarrRssDefinition } from "./rss";
|
||||||
|
import type { OldmarrSmartHomeEntityStateDefinition } from "./smart-home-entity-state";
|
||||||
|
import type { OldmarrSmartHomeTriggerAutomationDefinition } from "./smart-home-trigger-automation";
|
||||||
|
import type { OldmarrTorrentStatusDefinition } from "./torrent-status";
|
||||||
|
import type { OldmarrUsenetDefinition } from "./usenet";
|
||||||
|
import type { OldmarrVideoStreamDefinition } from "./video-stream";
|
||||||
|
import type { OldmarrWeatherDefinition } from "./weather";
|
||||||
|
|
||||||
|
export type OldmarrWidgetDefinitions =
|
||||||
|
| OldmarrWeatherDefinition
|
||||||
|
| OldmarrDateDefinition
|
||||||
|
| OldmarrCalendarDefinition
|
||||||
|
| OldmarrIndexerManagerDefinition
|
||||||
|
| OldmarrDashdotDefinition
|
||||||
|
| OldmarrUsenetDefinition
|
||||||
|
| OldmarrTorrentStatusDefinition
|
||||||
|
| OldmarrDlspeedDefinition
|
||||||
|
| OldmarrRssDefinition
|
||||||
|
| OldmarrVideoStreamDefinition
|
||||||
|
| OldmarrIframeDefinition
|
||||||
|
| OldmarrMediaServerDefinition
|
||||||
|
| OldmarrMediaRequestListDefinition
|
||||||
|
| OldmarrMediaRequestStatsDefinition
|
||||||
|
| OldmarrDnsHoleSummaryDefinition
|
||||||
|
| OldmarrDnsHoleControlsDefinition
|
||||||
|
| OldmarrBookmarkDefinition
|
||||||
|
| OldmarrNotebookDefinition
|
||||||
|
| OldmarrSmartHomeEntityStateDefinition
|
||||||
|
| OldmarrSmartHomeTriggerAutomationDefinition
|
||||||
|
| OldmarrHealthMonitoringDefinition
|
||||||
|
| OldmarrMediaTranscodingDefinition;
|
||||||
|
|
||||||
|
export const widgetKindMapping = {
|
||||||
|
app: null, // In oldmarr apps were not widgets
|
||||||
|
clock: "date",
|
||||||
|
calendar: "calendar",
|
||||||
|
weather: "weather",
|
||||||
|
rssFeed: "rss",
|
||||||
|
video: "video-stream",
|
||||||
|
iframe: "iframe",
|
||||||
|
mediaServer: "media-server",
|
||||||
|
dnsHoleSummary: "dns-hole-summary",
|
||||||
|
dnsHoleControls: "dns-hole-controls",
|
||||||
|
notebook: "notebook",
|
||||||
|
"smartHome-entityState": "smart-home/entity-state",
|
||||||
|
"smartHome-executeAutomation": "smart-home/trigger-automation",
|
||||||
|
"mediaRequests-requestList": "media-requests-list",
|
||||||
|
"mediaRequests-requestStats": "media-requests-stats",
|
||||||
|
} satisfies Record<WidgetKind, OldmarrWidgetDefinitions["id"] | null>;
|
||||||
|
// Use null for widgets that did not exist in oldmarr
|
||||||
|
// TODO: revert assignment so that only old widgets are needed in the object,
|
||||||
|
// this can be done ones all widgets are implemented
|
||||||
|
|
||||||
|
export type WidgetMapping = typeof widgetKindMapping;
|
||||||
|
|
||||||
|
export const mapKind = (kind: OldmarrWidgetDefinitions["id"]): WidgetKind | undefined =>
|
||||||
|
objectEntries(widgetKindMapping).find(([_, value]) => value === kind)?.[0];
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrIndexerManagerDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"indexer-manager",
|
||||||
|
{
|
||||||
|
openIndexerSiteInNewTab: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrMediaRequestListDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"media-requests-list",
|
||||||
|
{
|
||||||
|
replaceLinksWithExternalHost: boolean;
|
||||||
|
openInNewTab: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrMediaRequestStatsDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"media-requests-stats",
|
||||||
|
{
|
||||||
|
replaceLinksWithExternalHost: boolean;
|
||||||
|
openInNewTab: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export type OldmarrMediaServerDefinition = CommonOldmarrWidgetDefinition<"media-server", {}>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrMediaTranscodingDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"media-transcoding",
|
||||||
|
{
|
||||||
|
defaultView: "workers" | "queue" | "statistics";
|
||||||
|
showHealthCheck: boolean;
|
||||||
|
showHealthChecksInQueue: boolean;
|
||||||
|
queuePageSize: number;
|
||||||
|
showAppIcon: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
10
packages/old-import/src/widgets/definitions/notebook.ts
Normal file
10
packages/old-import/src/widgets/definitions/notebook.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrNotebookDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"notebook",
|
||||||
|
{
|
||||||
|
showToolbar: boolean;
|
||||||
|
allowReadOnlyCheck: boolean;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
14
packages/old-import/src/widgets/definitions/rss.ts
Normal file
14
packages/old-import/src/widgets/definitions/rss.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrRssDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"rss",
|
||||||
|
{
|
||||||
|
rssFeedUrl: string[];
|
||||||
|
refreshInterval: number;
|
||||||
|
dangerousAllowSanitizedItemContent: boolean;
|
||||||
|
textLinesClamp: number;
|
||||||
|
sortByPublishDateAscending: boolean;
|
||||||
|
sortPostsWithoutPublishDateToTheTop: boolean;
|
||||||
|
maximumAmountOfPosts: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrSmartHomeEntityStateDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"smart-home/entity-state",
|
||||||
|
{
|
||||||
|
entityId: string;
|
||||||
|
appendUnit: boolean;
|
||||||
|
genericToggle: boolean;
|
||||||
|
automationId: string;
|
||||||
|
displayName: string;
|
||||||
|
displayFriendlyName: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrSmartHomeTriggerAutomationDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"smart-home/trigger-automation",
|
||||||
|
{
|
||||||
|
automationId: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrTorrentStatusDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"torrents-status",
|
||||||
|
{
|
||||||
|
displayCompletedTorrents: boolean;
|
||||||
|
displayActiveTorrents: boolean;
|
||||||
|
speedLimitOfActiveTorrents: number;
|
||||||
|
displayStaleTorrents: boolean;
|
||||||
|
labelFilterIsWhitelist: boolean;
|
||||||
|
labelFilter: string[];
|
||||||
|
displayRatioWithFilter: boolean;
|
||||||
|
columnOrdering: boolean;
|
||||||
|
rowSorting: boolean;
|
||||||
|
columns: ("up" | "down" | "eta" | "progress")[];
|
||||||
|
nameColumnSize: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
4
packages/old-import/src/widgets/definitions/usenet.ts
Normal file
4
packages/old-import/src/widgets/definitions/usenet.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export type OldmarrUsenetDefinition = CommonOldmarrWidgetDefinition<"usenet", {}>;
|
||||||
11
packages/old-import/src/widgets/definitions/video-stream.ts
Normal file
11
packages/old-import/src/widgets/definitions/video-stream.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrVideoStreamDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"video-stream",
|
||||||
|
{
|
||||||
|
FeedUrl: string;
|
||||||
|
autoPlay: boolean;
|
||||||
|
muted: boolean;
|
||||||
|
controls: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
16
packages/old-import/src/widgets/definitions/weather.ts
Normal file
16
packages/old-import/src/widgets/definitions/weather.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||||
|
|
||||||
|
export type OldmarrWeatherDefinition = CommonOldmarrWidgetDefinition<
|
||||||
|
"weather",
|
||||||
|
{
|
||||||
|
displayInFahrenheit: boolean;
|
||||||
|
displayCityName: boolean;
|
||||||
|
displayWeekly: boolean;
|
||||||
|
forecastDays: number;
|
||||||
|
location: {
|
||||||
|
name: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>;
|
||||||
121
packages/old-import/src/widgets/options.ts
Normal file
121
packages/old-import/src/widgets/options.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import type { WidgetComponentProps } from "../../../widgets/src/definition";
|
||||||
|
import type { OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
|
||||||
|
|
||||||
|
// This type enforces, that for all widget mappings there is a corresponding option mapping,
|
||||||
|
// each option of newmarr can be mapped from the value of the oldmarr options
|
||||||
|
type OptionMapping = {
|
||||||
|
[WidgetKey in keyof WidgetMapping]: WidgetMapping[WidgetKey] extends null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
|
||||||
|
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[WidgetKey] }>["options"],
|
||||||
|
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionMapping: OptionMapping = {
|
||||||
|
"mediaRequests-requestList": {
|
||||||
|
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
|
||||||
|
},
|
||||||
|
"mediaRequests-requestStats": {},
|
||||||
|
calendar: {
|
||||||
|
filterFutureMonths: () => undefined,
|
||||||
|
filterPastMonths: () => undefined,
|
||||||
|
},
|
||||||
|
clock: {
|
||||||
|
customTitle: (oldOptions) => oldOptions.customTitle,
|
||||||
|
customTitleToggle: (oldOptions) => oldOptions.titleState !== "none",
|
||||||
|
dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat),
|
||||||
|
is24HourFormat: (oldOptions) => oldOptions.display24HourFormat,
|
||||||
|
showDate: (oldOptions) => oldOptions.dateFormat !== "hide",
|
||||||
|
showSeconds: () => undefined,
|
||||||
|
timezone: (oldOptions) => oldOptions.timezone,
|
||||||
|
useCustomTimezone: () => true,
|
||||||
|
},
|
||||||
|
weather: {
|
||||||
|
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
|
||||||
|
hasForecast: (oldOptions) => oldOptions.displayWeekly,
|
||||||
|
isFormatFahrenheit: (oldOptions) => oldOptions.displayInFahrenheit,
|
||||||
|
location: (oldOptions) => oldOptions.location,
|
||||||
|
showCity: (oldOptions) => oldOptions.displayCityName,
|
||||||
|
},
|
||||||
|
iframe: {
|
||||||
|
embedUrl: (oldOptions) => oldOptions.embedUrl,
|
||||||
|
allowAutoPlay: (oldOptions) => oldOptions.allowAutoPlay,
|
||||||
|
allowFullScreen: (oldOptions) => oldOptions.allowFullScreen,
|
||||||
|
allowPayment: (oldOptions) => oldOptions.allowPayment,
|
||||||
|
allowCamera: (oldOptions) => oldOptions.allowCamera,
|
||||||
|
allowMicrophone: (oldOptions) => oldOptions.allowMicrophone,
|
||||||
|
allowGeolocation: (oldOptions) => oldOptions.allowGeolocation,
|
||||||
|
allowScrolling: (oldOptions) => oldOptions.allowScrolling,
|
||||||
|
allowTransparency: (oldOptions) => oldOptions.allowTransparency,
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
feedUrl: (oldOptions) => oldOptions.FeedUrl,
|
||||||
|
hasAutoPlay: (oldOptions) => oldOptions.autoPlay,
|
||||||
|
hasControls: (oldOptions) => oldOptions.controls,
|
||||||
|
isMuted: (oldOptions) => oldOptions.muted,
|
||||||
|
},
|
||||||
|
dnsHoleControls: {
|
||||||
|
showToggleAllButtons: (oldOptions) => oldOptions.showToggleAllButtons,
|
||||||
|
},
|
||||||
|
dnsHoleSummary: {
|
||||||
|
layout: (oldOptions) => oldOptions.layout,
|
||||||
|
usePiHoleColors: (oldOptions) => oldOptions.usePiHoleColors,
|
||||||
|
},
|
||||||
|
rssFeed: {
|
||||||
|
feedUrls: (oldOptions) => oldOptions.rssFeedUrl,
|
||||||
|
maximumAmountPosts: (oldOptions) => oldOptions.maximumAmountOfPosts,
|
||||||
|
textLinesClamp: (oldOptions) => oldOptions.textLinesClamp,
|
||||||
|
},
|
||||||
|
notebook: {
|
||||||
|
allowReadOnlyCheck: (oldOptions) => oldOptions.allowReadOnlyCheck,
|
||||||
|
content: (oldOptions) => oldOptions.content,
|
||||||
|
showToolbar: (oldOptions) => oldOptions.showToolbar,
|
||||||
|
},
|
||||||
|
"smartHome-entityState": {
|
||||||
|
entityId: (oldOptions) => oldOptions.entityId,
|
||||||
|
displayName: (oldOptions) => oldOptions.displayName,
|
||||||
|
clickable: () => undefined,
|
||||||
|
entityUnit: () => undefined,
|
||||||
|
},
|
||||||
|
"smartHome-executeAutomation": {
|
||||||
|
automationId: (oldOptions) => oldOptions.automationId,
|
||||||
|
displayName: (oldOptions) => oldOptions.displayName,
|
||||||
|
},
|
||||||
|
mediaServer: {},
|
||||||
|
app: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the oldmarr options to the newmarr options
|
||||||
|
* @param kind item kind to map
|
||||||
|
* @param oldOptions oldmarr options for this item
|
||||||
|
* @returns newmarr options for this item or null if the item did not exist in oldmarr
|
||||||
|
*/
|
||||||
|
export const mapOptions = <K extends WidgetKind>(
|
||||||
|
kind: K,
|
||||||
|
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[K] }>["options"],
|
||||||
|
) => {
|
||||||
|
logger.debug(`Mapping old homarr options for widget kind=${kind} options=${JSON.stringify(oldOptions)}`);
|
||||||
|
if (optionMapping[kind] === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapping = optionMapping[kind];
|
||||||
|
return objectEntries(mapping).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
const newValue = value(oldOptions as never);
|
||||||
|
logger.debug(`Mapping old homarr option kind=${kind} key=${key as string} newValue=${newValue as string}`);
|
||||||
|
if (newValue !== undefined) {
|
||||||
|
acc[key as string] = newValue;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>,
|
||||||
|
) as WidgetComponentProps<K>["options"];
|
||||||
|
};
|
||||||
8
packages/old-import/tsconfig.json
Normal file
8
packages/old-import/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"]
|
||||||
|
}
|
||||||
9
packages/old-schema/eslint.config.js
Normal file
9
packages/old-schema/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/old-schema/index.ts
Normal file
1
packages/old-schema/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./src";
|
||||||
34
packages/old-schema/package.json
Normal file
34
packages/old-schema/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "@homarr/old-schema",
|
||||||
|
"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": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"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.5.4"
|
||||||
|
},
|
||||||
|
"prettier": "@homarr/prettier-config"
|
||||||
|
}
|
||||||
77
packages/old-schema/src/app.ts
Normal file
77
packages/old-schema/src/app.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { tileBaseSchema } from "./tile";
|
||||||
|
|
||||||
|
const appBehaviourSchema = z.object({
|
||||||
|
externalUrl: z.string(),
|
||||||
|
isOpeningNewTab: z.boolean(),
|
||||||
|
tooltipDescription: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appNetworkSchema = z.object({
|
||||||
|
enabledStatusChecker: z.boolean(),
|
||||||
|
okStatus: z.array(z.number()).optional(),
|
||||||
|
statusCodes: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appAppearanceSchema = z.object({
|
||||||
|
iconUrl: z.string(),
|
||||||
|
appNameStatus: z.union([z.literal("normal"), z.literal("hover"), z.literal("hidden")]),
|
||||||
|
positionAppName: z.union([
|
||||||
|
z.literal("row"),
|
||||||
|
z.literal("column"),
|
||||||
|
z.literal("row-reverse"),
|
||||||
|
z.literal("column-reverse"),
|
||||||
|
]),
|
||||||
|
appNameFontSize: z.number(),
|
||||||
|
lineClampAppName: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationSchema = z.enum([
|
||||||
|
"readarr",
|
||||||
|
"radarr",
|
||||||
|
"sonarr",
|
||||||
|
"lidarr",
|
||||||
|
"prowlarr",
|
||||||
|
"sabnzbd",
|
||||||
|
"jellyseerr",
|
||||||
|
"overseerr",
|
||||||
|
"deluge",
|
||||||
|
"qBittorrent",
|
||||||
|
"transmission",
|
||||||
|
"plex",
|
||||||
|
"jellyfin",
|
||||||
|
"nzbGet",
|
||||||
|
"pihole",
|
||||||
|
"adGuardHome",
|
||||||
|
"homeAssistant",
|
||||||
|
"openmediavault",
|
||||||
|
"proxmox",
|
||||||
|
"tdarr",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appIntegrationPropertySchema = z.object({
|
||||||
|
type: z.enum(["private", "public"]),
|
||||||
|
field: z.enum(["apiKey", "password", "username"]),
|
||||||
|
value: z.string().nullable().optional(),
|
||||||
|
isDefined: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appIntegrationSchema = z.object({
|
||||||
|
type: integrationSchema.optional().nullable(),
|
||||||
|
properties: z.array(appIntegrationPropertySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oldmarrAppSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
behaviour: appBehaviourSchema,
|
||||||
|
network: appNetworkSchema,
|
||||||
|
appearance: appAppearanceSchema,
|
||||||
|
integration: appIntegrationSchema.optional(),
|
||||||
|
})
|
||||||
|
.and(tileBaseSchema);
|
||||||
|
|
||||||
|
export type OldmarrApp = z.infer<typeof oldmarrAppSchema>;
|
||||||
30
packages/old-schema/src/config.ts
Normal file
30
packages/old-schema/src/config.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { oldmarrAppSchema } from "./app";
|
||||||
|
import { settingsSchema } from "./setting";
|
||||||
|
import { oldmarrWidgetSchema } from "./widget";
|
||||||
|
|
||||||
|
const categorySchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
position: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapperSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
position: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oldmarrConfigSchema = z.object({
|
||||||
|
schemaVersion: z.number(),
|
||||||
|
configProperties: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
categories: z.array(categorySchema),
|
||||||
|
wrappers: z.array(wrapperSchema),
|
||||||
|
apps: z.array(oldmarrAppSchema),
|
||||||
|
widgets: z.array(oldmarrWidgetSchema),
|
||||||
|
settings: settingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OldmarrConfig = z.infer<typeof oldmarrConfigSchema>;
|
||||||
5
packages/old-schema/src/index.ts
Normal file
5
packages/old-schema/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type { OldmarrConfig } from "./config";
|
||||||
|
export { oldmarrConfigSchema } from "./config";
|
||||||
|
export type { OldmarrApp } from "./app";
|
||||||
|
export type { OldmarrWidget, OldmarrWidgetKind } from "./widget";
|
||||||
|
export { oldmarrWidgetKinds } from "./widget";
|
||||||
75
packages/old-schema/src/setting.ts
Normal file
75
packages/old-schema/src/setting.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const baseSearchEngineSchema = z.object({
|
||||||
|
properties: z.object({
|
||||||
|
openInNewTab: z.boolean().default(true),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const commonSearchEngineSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.enum(["google", "duckDuckGo", "bing"]),
|
||||||
|
})
|
||||||
|
.and(baseSearchEngineSchema);
|
||||||
|
|
||||||
|
const customSearchEngineSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.literal("custom"),
|
||||||
|
properties: z.object({
|
||||||
|
template: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.and(baseSearchEngineSchema);
|
||||||
|
|
||||||
|
const searchEngineSchema = z.union([commonSearchEngineSchema, customSearchEngineSchema]);
|
||||||
|
|
||||||
|
const commonSettingsSchema = z.object({
|
||||||
|
searchEngine: searchEngineSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const accessSettingsSchema = z.object({
|
||||||
|
allowGuests: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridstackSettingsSchema = z.object({
|
||||||
|
columnCountSmall: z.number(),
|
||||||
|
columnCountMedium: z.number(),
|
||||||
|
columnCountLarge: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutSettingsSchema = z.object({
|
||||||
|
enabledLeftSidebar: z.boolean(),
|
||||||
|
enabledRightSidebar: z.boolean(),
|
||||||
|
enabledDocker: z.boolean(),
|
||||||
|
enabledPing: z.boolean(),
|
||||||
|
enabledSearchbar: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorsSettingsSchema = z.object({
|
||||||
|
primary: z.string().optional(),
|
||||||
|
secondary: z.string().optional(),
|
||||||
|
shade: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customizationSettingsSchema = z.object({
|
||||||
|
layout: layoutSettingsSchema,
|
||||||
|
pageTitle: z.string().optional(),
|
||||||
|
metaTitle: z.string().optional(),
|
||||||
|
logoImageUrl: z.string().optional(),
|
||||||
|
faviconUrl: z.string().optional(),
|
||||||
|
backgroundImageUrl: z.string().optional(),
|
||||||
|
backgroundImageAttachment: z.enum(["fixed", "scroll"]).optional(),
|
||||||
|
backgroundImageSize: z.enum(["cover", "contain"]).optional(),
|
||||||
|
backgroundImageRepeat: z.enum(["no-repeat", "repeat", "repeat-x", "repeat-y"]).optional(),
|
||||||
|
customCss: z.string().optional(),
|
||||||
|
colors: colorsSettingsSchema,
|
||||||
|
appOpacity: z.number().optional(),
|
||||||
|
gridstack: gridstackSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const settingsSchema = z.object({
|
||||||
|
common: commonSettingsSchema,
|
||||||
|
customization: customizationSettingsSchema,
|
||||||
|
access: accessSettingsSchema,
|
||||||
|
});
|
||||||
55
packages/old-schema/src/tile.ts
Normal file
55
packages/old-schema/src/tile.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const createAreaSchema = <TType extends string, TPropertiesSchema extends z.AnyZodObject>(
|
||||||
|
type: TType,
|
||||||
|
propertiesSchema: TPropertiesSchema,
|
||||||
|
) =>
|
||||||
|
z.object({
|
||||||
|
type: z.literal(type),
|
||||||
|
properties: propertiesSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapperAreaSchema = createAreaSchema(
|
||||||
|
"wrapper",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const categoryAreaSchema = createAreaSchema(
|
||||||
|
"category",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidebarAreaSchema = createAreaSchema(
|
||||||
|
"sidebar",
|
||||||
|
z.object({
|
||||||
|
location: z.union([z.literal("right"), z.literal("left")]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaSchema = z.union([wrapperAreaSchema, categoryAreaSchema, sidebarAreaSchema]);
|
||||||
|
|
||||||
|
const sizedShapeSchema = z.object({
|
||||||
|
location: z.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
}),
|
||||||
|
size: z.object({
|
||||||
|
width: z.number(),
|
||||||
|
height: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const shapeSchema = z.object({
|
||||||
|
lg: sizedShapeSchema.optional(),
|
||||||
|
md: sizedShapeSchema.optional(),
|
||||||
|
sm: sizedShapeSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tileBaseSchema = z.object({
|
||||||
|
area: areaSchema,
|
||||||
|
shape: shapeSchema,
|
||||||
|
});
|
||||||
40
packages/old-schema/src/widget.ts
Normal file
40
packages/old-schema/src/widget.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { tileBaseSchema } from "./tile";
|
||||||
|
|
||||||
|
export const oldmarrWidgetKinds = [
|
||||||
|
"calendar",
|
||||||
|
"indexer-manager",
|
||||||
|
"dashdot",
|
||||||
|
"usenet",
|
||||||
|
"weather",
|
||||||
|
"torrents-status",
|
||||||
|
"dlspeed",
|
||||||
|
"date",
|
||||||
|
"rss",
|
||||||
|
"video-stream",
|
||||||
|
"iframe",
|
||||||
|
"media-server",
|
||||||
|
"media-requests-list",
|
||||||
|
"media-requests-stats",
|
||||||
|
"dns-hole-summary",
|
||||||
|
"dns-hole-controls",
|
||||||
|
"bookmark",
|
||||||
|
"notebook",
|
||||||
|
"smart-home/entity-state",
|
||||||
|
"smart-home/trigger-automation",
|
||||||
|
"health-monitoring",
|
||||||
|
"media-transcoding",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type OldmarrWidgetKind = (typeof oldmarrWidgetKinds)[number];
|
||||||
|
|
||||||
|
export const oldmarrWidgetSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.enum(oldmarrWidgetKinds),
|
||||||
|
properties: z.record(z.unknown()),
|
||||||
|
})
|
||||||
|
.and(tileBaseSchema);
|
||||||
|
|
||||||
|
export type OldmarrWidget = z.infer<typeof oldmarrWidgetSchema>;
|
||||||
8
packages/old-schema/tsconfig.json
Normal file
8
packages/old-schema/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@homarr/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src", "../old-import/src/widgets/options.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -530,6 +530,7 @@ export default {
|
|||||||
symbols: {
|
symbols: {
|
||||||
colon: ": ",
|
colon: ": ",
|
||||||
},
|
},
|
||||||
|
beta: "Beta",
|
||||||
error: "Error",
|
error: "Error",
|
||||||
errors: {
|
errors: {
|
||||||
noData: "No data to show",
|
noData: "No data to show",
|
||||||
@@ -541,6 +542,7 @@ export default {
|
|||||||
backToOverview: "Back to overview",
|
backToOverview: "Back to overview",
|
||||||
create: "Create",
|
create: "Create",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
|
import: "Import",
|
||||||
insert: "Insert",
|
insert: "Insert",
|
||||||
remove: "Remove",
|
remove: "Remove",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
@@ -644,6 +646,9 @@ export default {
|
|||||||
passwordsDoNotMatch: "Passwords do not match",
|
passwordsDoNotMatch: "Passwords do not match",
|
||||||
passwordRequirements: "Password does not meet the requirements",
|
passwordRequirements: "Password does not meet the requirements",
|
||||||
boardAlreadyExists: "A board with this name already exists",
|
boardAlreadyExists: "A board with this name already exists",
|
||||||
|
invalidFileType: "Invalid file type, expected {expected}",
|
||||||
|
fileTooLarge: "File is too large, maximum size is {maxSize}",
|
||||||
|
invalidConfiguration: "Invalid configuration",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1202,6 +1207,61 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
oldImport: {
|
||||||
|
label: "Import from homarr before 1.0.0",
|
||||||
|
notification: {
|
||||||
|
success: {
|
||||||
|
title: "Import successful",
|
||||||
|
message: "The board was successfully imported",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Import failed",
|
||||||
|
message: "The board could not be imported, check the logs for further details",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
file: {
|
||||||
|
label: "Select JSON file",
|
||||||
|
invalidError: "Invalid configuration file",
|
||||||
|
},
|
||||||
|
apps: {
|
||||||
|
label: "Apps",
|
||||||
|
avoidDuplicates: {
|
||||||
|
label: "Avoid duplicates",
|
||||||
|
description: "Ignores apps where an app with the same href already exists",
|
||||||
|
},
|
||||||
|
onlyImportApps: {
|
||||||
|
label: "Only import apps",
|
||||||
|
description: "Only adds the apps, the board need to be recreated manually",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
label: "Board name",
|
||||||
|
},
|
||||||
|
screenSize: {
|
||||||
|
label: "Screen size",
|
||||||
|
option: {
|
||||||
|
sm: "Small",
|
||||||
|
md: "Medium",
|
||||||
|
lg: "Large",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sidebarBehavior: {
|
||||||
|
label: "Sidebar behavior",
|
||||||
|
description: "Sidebars were removed in 1.0, you can select what should happen with the items inside them.",
|
||||||
|
option: {
|
||||||
|
lastSection: {
|
||||||
|
label: "Last section",
|
||||||
|
description: "Sidebar will be displayed below the last section",
|
||||||
|
},
|
||||||
|
removeItems: {
|
||||||
|
label: "Remove items",
|
||||||
|
description: "Items contained in the sidebar will be removed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
field: {
|
field: {
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
|
|||||||
17
packages/ui/src/components/beta-badge.tsx
Normal file
17
packages/ui/src/components/beta-badge.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { BadgeProps } from "@mantine/core";
|
||||||
|
import { Badge } from "@mantine/core";
|
||||||
|
|
||||||
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
interface BetaBadgeProps {
|
||||||
|
size: BadgeProps["size"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BetaBadge = ({ size }: BetaBadgeProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<Badge size={size} color="green" variant="outline">
|
||||||
|
{t("common.beta")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,3 +8,4 @@ export { TextMultiSelect } from "./text-multi-select";
|
|||||||
export { UserAvatar } from "./user-avatar";
|
export { UserAvatar } from "./user-avatar";
|
||||||
export { UserAvatarGroup } from "./user-avatar-group";
|
export { UserAvatarGroup } from "./user-avatar-group";
|
||||||
export { CustomPasswordInput } from "./password-input/password-input";
|
export { CustomPasswordInput } from "./password-input/password-input";
|
||||||
|
export { BetaBadge } from "./beta-badge";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface BaseSelectItem {
|
|||||||
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
|
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
|
||||||
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder"> {
|
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder"> {
|
||||||
data: TSelectItem[];
|
data: TSelectItem[];
|
||||||
|
description?: string;
|
||||||
|
withAsterisk?: boolean;
|
||||||
onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8",
|
||||||
|
"zod-form-data": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { zfd } from "zod-form-data";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
backgroundImageAttachments,
|
backgroundImageAttachments,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
|
|
||||||
import { zodEnumFromArray } from "./enums";
|
import { zodEnumFromArray } from "./enums";
|
||||||
|
import { createCustomErrorParams } from "./form/i18n";
|
||||||
import { createSavePermissionsSchema } from "./permissions";
|
import { createSavePermissionsSchema } from "./permissions";
|
||||||
import { commonItemSchema, createSectionSchema } from "./shared";
|
import { commonItemSchema, createSectionSchema } from "./shared";
|
||||||
|
|
||||||
@@ -67,6 +69,61 @@ const permissionsSchema = z.object({
|
|||||||
id: z.string(),
|
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 type OldmarrImportConfiguration = z.infer<ReturnType<typeof createOldmarrImportConfigurationSchema>>;
|
||||||
|
|
||||||
|
export const superRefineJsonImportFile = (value: File | null, context: z.RefinementCtx) => {
|
||||||
|
if (!value) {
|
||||||
|
return context.addIssue({
|
||||||
|
code: "invalid_type",
|
||||||
|
expected: "object",
|
||||||
|
received: "null",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.type !== "application/json") {
|
||||||
|
return context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
params: createCustomErrorParams({
|
||||||
|
key: "invalidFileType",
|
||||||
|
params: { expected: "JSON" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.size > 1024 * 1024) {
|
||||||
|
return context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
params: createCustomErrorParams({
|
||||||
|
key: "fileTooLarge",
|
||||||
|
params: { maxSize: "1 MB" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importJsonFileSchema = zfd.formData({
|
||||||
|
file: zfd.file().superRefine(superRefineJsonImportFile),
|
||||||
|
configuration: zfd.json(createOldmarrImportConfigurationSchema([])),
|
||||||
|
});
|
||||||
|
|
||||||
const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));
|
const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));
|
||||||
|
|
||||||
z.object({
|
z.object({
|
||||||
@@ -88,4 +145,5 @@ export const boardSchemas = {
|
|||||||
changeVisibility: changeVisibilitySchema,
|
changeVisibility: changeVisibilitySchema,
|
||||||
permissions: permissionsSchema,
|
permissions: permissionsSchema,
|
||||||
savePermissions: savePermissionsSchema,
|
savePermissions: savePermissionsSchema,
|
||||||
|
importOldmarrConfig: importJsonFileSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ParamsObject } from "international-types";
|
||||||
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
|
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
|
||||||
import { ZodIssueCode } from "zod";
|
import { ZodIssueCode } from "zod";
|
||||||
|
|
||||||
@@ -114,16 +115,17 @@ const handleZodError = (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
|
|||||||
if (issue.code === ZodIssueCode.too_big) {
|
if (issue.code === ZodIssueCode.too_big) {
|
||||||
return handleTooBigError(issue);
|
return handleTooBigError(issue);
|
||||||
}
|
}
|
||||||
if (issue.code === ZodIssueCode.invalid_type && ctx.data === "") {
|
if (issue.code === ZodIssueCode.invalid_type && (ctx.data === "" || issue.received === "null")) {
|
||||||
return {
|
return {
|
||||||
key: "errors.required",
|
key: "errors.required",
|
||||||
params: {},
|
params: {},
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
|
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
|
||||||
const { i18n } = issue.params as CustomErrorParams;
|
const { i18n } = issue.params as CustomErrorParams<CustomErrorKey>;
|
||||||
return {
|
return {
|
||||||
key: `errors.custom.${i18n.key}`,
|
key: `errors.custom.${i18n.key}`,
|
||||||
|
params: i18n.params,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +134,17 @@ const handleZodError = (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CustomErrorParams {
|
type CustomErrorKey = keyof TranslationObject["common"]["zod"]["errors"]["custom"];
|
||||||
|
|
||||||
|
export interface CustomErrorParams<TKey extends CustomErrorKey> {
|
||||||
i18n: {
|
i18n: {
|
||||||
key: keyof TranslationObject["common"]["zod"]["errors"]["custom"];
|
key: TKey;
|
||||||
params?: Record<string, unknown>;
|
params: ParamsObject<TranslationObject["common"]["zod"]["errors"]["custom"][TKey]>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createCustomErrorParams = (i18n: CustomErrorParams["i18n"] | CustomErrorParams["i18n"]["key"]) =>
|
export const createCustomErrorParams = <TKey extends CustomErrorKey>(
|
||||||
typeof i18n === "string" ? { i18n: { key: i18n } } : { i18n };
|
i18n: keyof CustomErrorParams<TKey>["i18n"]["params"] extends never
|
||||||
|
? CustomErrorParams<TKey>["i18n"]["key"]
|
||||||
|
: CustomErrorParams<TKey>["i18n"],
|
||||||
|
) => (typeof i18n === "string" ? { i18n: { key: i18n, params: {} } } : { i18n });
|
||||||
|
|||||||
@@ -26,3 +26,5 @@ export {
|
|||||||
type BoardItemAdvancedOptions,
|
type BoardItemAdvancedOptions,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
export { passwordRequirements } from "./user";
|
export { passwordRequirements } from "./user";
|
||||||
|
export { createOldmarrImportConfigurationSchema, superRefineJsonImportFile } from "./board";
|
||||||
|
export type { OldmarrImportConfiguration } from "./board";
|
||||||
|
|||||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -94,6 +94,9 @@ importers:
|
|||||||
'@homarr/notifications':
|
'@homarr/notifications':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/notifications
|
version: link:../../packages/notifications
|
||||||
|
'@homarr/old-schema':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../packages/old-schema
|
||||||
'@homarr/server-settings':
|
'@homarr/server-settings':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/server-settings
|
version: link:../../packages/server-settings
|
||||||
@@ -476,6 +479,12 @@ importers:
|
|||||||
'@homarr/log':
|
'@homarr/log':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../log
|
version: link:../log
|
||||||
|
'@homarr/old-import':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../old-import
|
||||||
|
'@homarr/old-schema':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../old-schema
|
||||||
'@homarr/ping':
|
'@homarr/ping':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../ping
|
version: link:../ping
|
||||||
@@ -1072,6 +1081,68 @@ importers:
|
|||||||
specifier: ^5.5.4
|
specifier: ^5.5.4
|
||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
|
|
||||||
|
packages/old-import:
|
||||||
|
dependencies:
|
||||||
|
'@homarr/common':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../common
|
||||||
|
'@homarr/db':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../db
|
||||||
|
'@homarr/definitions':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../definitions
|
||||||
|
'@homarr/log':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../log
|
||||||
|
'@homarr/old-schema':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../old-schema
|
||||||
|
'@homarr/validation':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../validation
|
||||||
|
superjson:
|
||||||
|
specifier: 2.2.1
|
||||||
|
version: 2.2.1
|
||||||
|
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: ^9.10.0
|
||||||
|
version: 9.10.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.4
|
||||||
|
version: 5.5.4
|
||||||
|
|
||||||
|
packages/old-schema:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.8
|
||||||
|
version: 3.23.8
|
||||||
|
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: ^9.10.0
|
||||||
|
version: 9.10.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.4
|
||||||
|
version: 5.5.4
|
||||||
|
|
||||||
packages/ping:
|
packages/ping:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@homarr/common':
|
'@homarr/common':
|
||||||
@@ -1289,12 +1360,18 @@ importers:
|
|||||||
'@homarr/definitions':
|
'@homarr/definitions':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../definitions
|
version: link:../definitions
|
||||||
|
'@homarr/old-schema':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../old-schema
|
||||||
'@homarr/translation':
|
'@homarr/translation':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../translation
|
version: link:../translation
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.23.8
|
specifier: ^3.23.8
|
||||||
version: 3.23.8
|
version: 3.23.8
|
||||||
|
zod-form-data:
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2(zod@3.23.8)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@homarr/eslint-config':
|
'@homarr/eslint-config':
|
||||||
specifier: workspace:^0.2.0
|
specifier: workspace:^0.2.0
|
||||||
@@ -7488,6 +7565,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
zod-form-data@2.0.2:
|
||||||
|
resolution: {integrity: sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: '>= 3.11.0'
|
||||||
|
|
||||||
zod-to-json-schema@3.23.0:
|
zod-to-json-schema@3.23.0:
|
||||||
resolution: {integrity: sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==}
|
resolution: {integrity: sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -14192,6 +14274,10 @@ snapshots:
|
|||||||
compress-commons: 6.0.2
|
compress-commons: 6.0.2
|
||||||
readable-stream: 4.5.2
|
readable-stream: 4.5.2
|
||||||
|
|
||||||
|
zod-form-data@2.0.2(zod@3.23.8):
|
||||||
|
dependencies:
|
||||||
|
zod: 3.23.8
|
||||||
|
|
||||||
zod-to-json-schema@3.23.0(zod@3.23.8):
|
zod-to-json-schema@3.23.0(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.9.1",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
Reference in New Issue
Block a user