feat: add widget preview pages (#9)
* feat: add widget definition system * fix: wrong typecheck command in turbo generator * chore: fix formatting * feat: add widget preview page * chore: fix formatting and type errors * chore: fix from widget edit modal and remove some never casts * chore: address pull request feedback
This commit is contained in:
33
packages/widgets/src/_inputs/common.tsx
Normal file
33
packages/widgets/src/_inputs/common.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetSort } from "..";
|
||||
import type { WidgetOptionOfType, WidgetOptionType } from "../options";
|
||||
|
||||
export interface CommonWidgetInputProps<TKey extends WidgetOptionType> {
|
||||
sort: WidgetSort;
|
||||
property: string;
|
||||
options: Omit<WidgetOptionOfType<TKey>, "defaultValue" | "type">;
|
||||
}
|
||||
|
||||
type UseWidgetInputTranslationReturnType = (
|
||||
key: "label" | "description",
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Short description why as and unknown convertions are used below:
|
||||
* Typescript was not smart enought to work with the generic of the WidgetSort to only allow properties that are relying within that specified sort.
|
||||
* This does not mean, that the function useWidgetInputTranslation can be called with invalid arguments without type errors and rather means that the above widget.<sort>.option.<property> string
|
||||
* is not recognized as valid argument for the scoped i18n hook. Because the typesafety should remain outside the usage of those methods I (Meierschlumpf) decided to provide this fully typesafe useWidgetInputTranslation method.
|
||||
*
|
||||
* Some notes about it:
|
||||
* - The label translation can be used for every input, especially considering that all options should have defined a label for themself. The description translation should only be used when withDescription
|
||||
* is defined for the option. The method does sadly not reconize issues with those definitions. So it does not yell at you when you somewhere show the label without having it defined in the translations.
|
||||
*/
|
||||
export const useWidgetInputTranslation = (
|
||||
sort: WidgetSort,
|
||||
property: string,
|
||||
): UseWidgetInputTranslationReturnType => {
|
||||
return useScopedI18n(
|
||||
`widget.${sort}.option.${property}` as never, // Because the type is complex and not recognized by typescript, we need to cast it to never to make it work.
|
||||
) as unknown as UseWidgetInputTranslationReturnType;
|
||||
};
|
||||
6
packages/widgets/src/_inputs/form.ts
Normal file
6
packages/widgets/src/_inputs/form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createFormContext } from "@homarr/form";
|
||||
|
||||
export const [FormProvider, useFormContext, useForm] =
|
||||
createFormContext<Record<string, unknown>>();
|
||||
24
packages/widgets/src/_inputs/index.ts
Normal file
24
packages/widgets/src/_inputs/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { WidgetOptionType } from "../options";
|
||||
import { WidgetMultiSelectInput } from "./widget-multiselect-input";
|
||||
import { WidgetNumberInput } from "./widget-number-input";
|
||||
import { WidgetSelectInput } from "./widget-select-input";
|
||||
import { WidgetSliderInput } from "./widget-slider-input";
|
||||
import { WidgetSwitchInput } from "./widget-switch-input";
|
||||
import { WidgetTextInput } from "./widget-text-input";
|
||||
|
||||
const mapping = {
|
||||
text: WidgetTextInput,
|
||||
location: () => null,
|
||||
multiSelect: WidgetMultiSelectInput,
|
||||
multiText: () => null,
|
||||
number: WidgetNumberInput,
|
||||
select: WidgetSelectInput,
|
||||
slider: WidgetSliderInput,
|
||||
switch: WidgetSwitchInput,
|
||||
} satisfies Record<WidgetOptionType, unknown>;
|
||||
|
||||
export const getInputForType = <TType extends WidgetOptionType>(
|
||||
type: TType,
|
||||
) => {
|
||||
return mapping[type];
|
||||
};
|
||||
25
packages/widgets/src/_inputs/widget-multiselect-input.tsx
Normal file
25
packages/widgets/src/_inputs/widget-multiselect-input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { MultiSelect } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetMultiSelectInput = ({
|
||||
property,
|
||||
sort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"multiSelect">) => {
|
||||
const t = useWidgetInputTranslation(sort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
label={t("label")}
|
||||
data={options.options as unknown as string[]}
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
{...form.getInputProps(property)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
27
packages/widgets/src/_inputs/widget-number-input.tsx
Normal file
27
packages/widgets/src/_inputs/widget-number-input.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { NumberInput } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetNumberInput = ({
|
||||
property,
|
||||
sort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"number">) => {
|
||||
const t = useWidgetInputTranslation(sort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
label={t("label")}
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
min={options.validate.minValue ?? undefined}
|
||||
max={options.validate.maxValue ?? undefined}
|
||||
step={options.step}
|
||||
{...form.getInputProps(property)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
25
packages/widgets/src/_inputs/widget-select-input.tsx
Normal file
25
packages/widgets/src/_inputs/widget-select-input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Select } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetSelectInput = ({
|
||||
property,
|
||||
sort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"select">) => {
|
||||
const t = useWidgetInputTranslation(sort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t("label")}
|
||||
data={options.options as unknown as string[]}
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
{...form.getInputProps(property)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
30
packages/widgets/src/_inputs/widget-slider-input.tsx
Normal file
30
packages/widgets/src/_inputs/widget-slider-input.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { InputWrapper, Slider } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetSliderInput = ({
|
||||
property,
|
||||
sort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"slider">) => {
|
||||
const t = useWidgetInputTranslation(sort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<InputWrapper
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
>
|
||||
<Slider
|
||||
label={t("label")}
|
||||
min={options.validate.minValue ?? undefined}
|
||||
max={options.validate.maxValue ?? undefined}
|
||||
step={options.step}
|
||||
{...form.getInputProps(property)}
|
||||
/>
|
||||
</InputWrapper>
|
||||
);
|
||||
};
|
||||
24
packages/widgets/src/_inputs/widget-switch-input.tsx
Normal file
24
packages/widgets/src/_inputs/widget-switch-input.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { Switch } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetSwitchInput = ({
|
||||
property,
|
||||
sort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"switch">) => {
|
||||
const t = useWidgetInputTranslation(sort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
label={t("label")}
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
{...form.getInputProps(property, { type: "checkbox" })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
24
packages/widgets/src/_inputs/widget-text-input.tsx
Normal file
24
packages/widgets/src/_inputs/widget-text-input.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { TextInput } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetTextInput = ({
|
||||
property,
|
||||
sort: widgetSort,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"text">) => {
|
||||
const t = useWidgetInputTranslation(widgetSort, property);
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t("label")}
|
||||
description={options.withDescription ? t("description") : undefined}
|
||||
{...form.getInputProps(property)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user