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:
Meier Lukas
2024-09-07 18:13:24 +02:00
committed by GitHub
parent fc1bff2110
commit 5404cebf5b
65 changed files with 2132 additions and 34 deletions

View 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"];
};