feat(settings): add simple-ping settings (#2118)
This commit is contained in:
@@ -7,6 +7,8 @@ import { IconLoader } from "@tabler/icons-react";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -17,6 +19,8 @@ import { PingIndicator } from "./ping/ping-indicator";
|
||||
|
||||
export default function AppWidget({ options, isEditMode }: WidgetComponentProps<"app">) {
|
||||
const t = useI18n();
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const [app] = clientApi.app.byId.useSuspenseQuery(
|
||||
{
|
||||
id: options.appId,
|
||||
@@ -81,7 +85,7 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
||||
<img src={app.iconUrl} alt={app.name} className={combineClasses(classes.appIcon, "app-icon")} />
|
||||
</Flex>
|
||||
</Tooltip.Floating>
|
||||
{options.pingEnabled && app.href ? (
|
||||
{options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? (
|
||||
<Suspense fallback={<PingDot icon={IconLoader} color="blue" tooltip={`${t("common.action.loading")}…`} />}>
|
||||
<PingIndicator href={app.href} />
|
||||
</Suspense>
|
||||
|
||||
@@ -5,13 +5,24 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("app", {
|
||||
icon: IconApps,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
appId: factory.app(),
|
||||
openInNewTab: factory.switch({ defaultValue: true }),
|
||||
showTitle: factory.switch({ defaultValue: true }),
|
||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||
pingEnabled: factory.switch({ defaultValue: false }),
|
||||
})),
|
||||
createOptions(settings) {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
appId: factory.app(),
|
||||
openInNewTab: factory.switch({ defaultValue: true }),
|
||||
showTitle: factory.switch({ defaultValue: true }),
|
||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||
pingEnabled: factory.switch({ defaultValue: settings.enableStatusByDefault }),
|
||||
}),
|
||||
{
|
||||
pingEnabled: {
|
||||
shouldHide() {
|
||||
return settings.forceDisableStatus;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
errors: {
|
||||
NOT_FOUND: {
|
||||
icon: IconDeviceDesktopX,
|
||||
|
||||
@@ -10,50 +10,52 @@ import { BookmarkAddButton } from "./add-button";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("bookmarks", {
|
||||
icon: IconClock,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
title: factory.text(),
|
||||
layout: factory.select({
|
||||
options: (["grid", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`),
|
||||
})),
|
||||
defaultValue: "column",
|
||||
}),
|
||||
hideIcon: factory.switch({ defaultValue: false }),
|
||||
hideHostname: factory.switch({ defaultValue: false }),
|
||||
openNewTab: factory.switch({ defaultValue: true }),
|
||||
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
|
||||
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
|
||||
return (
|
||||
<Group {...rootAttributes} tabIndex={0} justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Handle />
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
title: factory.text(),
|
||||
layout: factory.select({
|
||||
options: (["grid", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`),
|
||||
})),
|
||||
defaultValue: "column",
|
||||
}),
|
||||
hideIcon: factory.switch({ defaultValue: false }),
|
||||
hideHostname: factory.switch({ defaultValue: false }),
|
||||
openNewTab: factory.switch({ defaultValue: true }),
|
||||
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
|
||||
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
|
||||
return (
|
||||
<Group {...rootAttributes} tabIndex={0} justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Handle />
|
||||
|
||||
<Group>
|
||||
<Avatar src={item.iconUrl} alt={item.name} />
|
||||
<Stack gap={0}>
|
||||
<Text>{item.name}</Text>
|
||||
</Stack>
|
||||
<Group>
|
||||
<Avatar src={item.iconUrl} alt={item.name} />
|
||||
<Stack gap={0}>
|
||||
<Text>{item.name}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<ActionIcon variant="transparent" color="red" onClick={removeItem}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
AddButton: BookmarkAddButton,
|
||||
uniqueIdentifier: (item) => item.id,
|
||||
useData: (initialIds) => {
|
||||
const { data, error, isLoading } = clientApi.app.byIds.useQuery(initialIds);
|
||||
|
||||
<ActionIcon variant="transparent" color="red" onClick={removeItem}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
AddButton: BookmarkAddButton,
|
||||
uniqueIdentifier: (item) => item.id,
|
||||
useData: (initialIds) => {
|
||||
const { data, error, isLoading } = clientApi.app.byIds.useQuery(initialIds);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
},
|
||||
}),
|
||||
})),
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
},
|
||||
}),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -9,23 +9,25 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("calendar", {
|
||||
icon: IconCalendar,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
releaseType: factory.multiSelect({
|
||||
defaultValue: ["inCinemas", "digitalRelease"],
|
||||
options: radarrReleaseTypes.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.calendar.option.releaseType.options.${value}`),
|
||||
})),
|
||||
}),
|
||||
filterPastMonths: factory.number({
|
||||
validate: z.number().min(2).max(9999),
|
||||
defaultValue: 2,
|
||||
}),
|
||||
filterFutureMonths: factory.number({
|
||||
validate: z.number().min(2).max(9999),
|
||||
defaultValue: 2,
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
releaseType: factory.multiSelect({
|
||||
defaultValue: ["inCinemas", "digitalRelease"],
|
||||
options: radarrReleaseTypes.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.calendar.option.releaseType.options.${value}`),
|
||||
})),
|
||||
}),
|
||||
filterPastMonths: factory.number({
|
||||
validate: z.number().min(2).max(9999),
|
||||
defaultValue: 2,
|
||||
}),
|
||||
filterFutureMonths: factory.number({
|
||||
validate: z.number().min(2).max(9999),
|
||||
defaultValue: 2,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("calendar"),
|
||||
integrationsRequired: false,
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -6,65 +6,67 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("clock", {
|
||||
icon: IconClock,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
customTitleToggle: factory.switch({
|
||||
defaultValue: false,
|
||||
withDescription: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
customTitleToggle: factory.switch({
|
||||
defaultValue: false,
|
||||
withDescription: true,
|
||||
}),
|
||||
customTitle: factory.text({
|
||||
defaultValue: "",
|
||||
}),
|
||||
is24HourFormat: factory.switch({
|
||||
defaultValue: true,
|
||||
withDescription: true,
|
||||
}),
|
||||
showSeconds: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
useCustomTimezone: factory.switch({ defaultValue: false }),
|
||||
timezone: factory.select({
|
||||
options: Intl.supportedValuesOf("timeZone").map((value) => value),
|
||||
defaultValue: "Europe/London",
|
||||
searchable: true,
|
||||
withDescription: true,
|
||||
}),
|
||||
showDate: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
dateFormat: factory.select({
|
||||
options: [
|
||||
{ value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") },
|
||||
{ value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") },
|
||||
{ value: "MMM D", label: dayjs().format("MMM D") },
|
||||
{ value: "D MMM", label: dayjs().format("D MMM") },
|
||||
{ value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") },
|
||||
{ value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") },
|
||||
{ value: "DD/MM", label: dayjs().format("DD/MM") },
|
||||
{ value: "MM/DD", label: dayjs().format("MM/DD") },
|
||||
],
|
||||
defaultValue: "dddd, MMMM D",
|
||||
withDescription: true,
|
||||
}),
|
||||
customTimeFormat: factory.text({
|
||||
defaultValue: "",
|
||||
withDescription: true,
|
||||
}),
|
||||
customDateFormat: factory.text({
|
||||
defaultValue: "",
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
customTitle: factory.text({
|
||||
defaultValue: "",
|
||||
}),
|
||||
is24HourFormat: factory.switch({
|
||||
defaultValue: true,
|
||||
withDescription: true,
|
||||
}),
|
||||
showSeconds: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
useCustomTimezone: factory.switch({ defaultValue: false }),
|
||||
timezone: factory.select({
|
||||
options: Intl.supportedValuesOf("timeZone").map((value) => value),
|
||||
defaultValue: "Europe/London",
|
||||
searchable: true,
|
||||
withDescription: true,
|
||||
}),
|
||||
showDate: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
dateFormat: factory.select({
|
||||
options: [
|
||||
{ value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") },
|
||||
{ value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") },
|
||||
{ value: "MMM D", label: dayjs().format("MMM D") },
|
||||
{ value: "D MMM", label: dayjs().format("D MMM") },
|
||||
{ value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") },
|
||||
{ value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") },
|
||||
{ value: "DD/MM", label: dayjs().format("DD/MM") },
|
||||
{ value: "MM/DD", label: dayjs().format("MM/DD") },
|
||||
],
|
||||
defaultValue: "dddd, MMMM D",
|
||||
withDescription: true,
|
||||
}),
|
||||
customTimeFormat: factory.text({
|
||||
defaultValue: "",
|
||||
withDescription: true,
|
||||
}),
|
||||
customDateFormat: factory.text({
|
||||
defaultValue: "",
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
customTitle: {
|
||||
shouldHide: (options) => !options.customTitleToggle,
|
||||
{
|
||||
customTitle: {
|
||||
shouldHide: (options) => !options.customTitleToggle,
|
||||
},
|
||||
timezone: {
|
||||
shouldHide: (options) => !options.useCustomTimezone,
|
||||
},
|
||||
dateFormat: {
|
||||
shouldHide: (options) => !options.showDate,
|
||||
},
|
||||
},
|
||||
timezone: {
|
||||
shouldHide: (options) => !options.useCustomTimezone,
|
||||
},
|
||||
dateFormat: {
|
||||
shouldHide: (options) => !options.showDate,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { LoaderComponent } from "next/dynamic";
|
||||
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
|
||||
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import type { SettingsContextProps } from "@homarr/settings";
|
||||
import type { stringOrTranslation } from "@homarr/translation";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetImports } from ".";
|
||||
import type { inferOptionsFromDefinition, WidgetOptionsRecord } from "./options";
|
||||
import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options";
|
||||
|
||||
const createWithDynamicImport =
|
||||
<TKind extends WidgetKind, TDefinition extends WidgetDefinition>(kind: TKind, definition: TDefinition) =>
|
||||
@@ -30,7 +32,7 @@ export interface WidgetDefinition {
|
||||
icon: TablerIcon;
|
||||
supportedIntegrations?: IntegrationKind[];
|
||||
integrationsRequired?: boolean;
|
||||
options: WidgetOptionsRecord;
|
||||
createOptions: (settings: SettingsContextProps) => WidgetOptionsRecord;
|
||||
errors?: Partial<
|
||||
Record<
|
||||
DefaultErrorData["code"],
|
||||
@@ -44,7 +46,7 @@ export interface WidgetDefinition {
|
||||
}
|
||||
|
||||
export interface WidgetProps<TKind extends WidgetKind> {
|
||||
options: inferOptionsFromDefinition<WidgetOptionsRecordOf<TKind>>;
|
||||
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
|
||||
integrationIds: string[];
|
||||
itemId: string | undefined; // undefined when in preview mode
|
||||
}
|
||||
@@ -52,13 +54,19 @@ export interface WidgetProps<TKind extends WidgetKind> {
|
||||
export type WidgetComponentProps<TKind extends WidgetKind> = WidgetProps<TKind> & {
|
||||
boardId: string | undefined; // undefined when in preview mode
|
||||
isEditMode: boolean;
|
||||
setOptions: ({
|
||||
newOptions,
|
||||
}: {
|
||||
newOptions: Partial<inferOptionsFromDefinition<WidgetOptionsRecordOf<TKind>>>;
|
||||
}) => void;
|
||||
setOptions: ({ newOptions }: { newOptions: Partial<inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>> }) => void;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type WidgetOptionsRecordOf<TKind extends WidgetKind> = WidgetImports[TKind]["definition"]["options"];
|
||||
export type WidgetOptionsRecordOf<TKind extends WidgetKind> = WidgetImports[TKind]["definition"]["createOptions"];
|
||||
|
||||
/**
|
||||
* The following type should only include values that can be available for user (including anonymous).
|
||||
* Because they need to be provided to the client to for example set certain default values.
|
||||
*/
|
||||
export interface WidgetOptionsSettings {
|
||||
server: {
|
||||
board: Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ export const widgetKind = "dnsHoleControls";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
icon: IconDeviceGamepad,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
showToggleAllButtons: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showToggleAllButtons: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("dnsHole"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
|
||||
@@ -9,18 +9,20 @@ export const widgetKind = "dnsHoleSummary";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
icon: IconAd,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
usePiHoleColors: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
layout: factory.select({
|
||||
options: (["grid", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.dnsHoleSummary.option.layout.option.${value}.label`),
|
||||
})),
|
||||
defaultValue: "grid",
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
usePiHoleColors: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
layout: factory.select({
|
||||
options: (["grid", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.dnsHoleSummary.option.layout.option.${value}.label`),
|
||||
})),
|
||||
defaultValue: "grid",
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("dnsHole"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
|
||||
@@ -33,76 +33,78 @@ const columnsSort = columnsList.filter((column) =>
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
|
||||
icon: IconDownload,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
columns: factory.multiSelect({
|
||||
defaultValue: ["integration", "name", "progress", "time", "actions"],
|
||||
options: columnsList.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
searchable: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
columns: factory.multiSelect({
|
||||
defaultValue: ["integration", "name", "progress", "time", "actions"],
|
||||
options: columnsList.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
searchable: true,
|
||||
}),
|
||||
enableRowSorting: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
defaultSort: factory.select({
|
||||
defaultValue: "type",
|
||||
options: columnsSort.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
}),
|
||||
descendingDefaultSort: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
showCompletedUsenet: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
showCompletedTorrent: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
activeTorrentThreshold: factory.number({
|
||||
//in KiB/s
|
||||
validate: z.number().min(0),
|
||||
defaultValue: 0,
|
||||
step: 1,
|
||||
}),
|
||||
categoryFilter: factory.multiText({
|
||||
defaultValue: [] as string[],
|
||||
validate: z.string(),
|
||||
}),
|
||||
filterIsWhitelist: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
applyFilterToRatio: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
}),
|
||||
enableRowSorting: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
defaultSort: factory.select({
|
||||
defaultValue: "type",
|
||||
options: columnsSort.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.downloads.items.${value}.columnTitle`),
|
||||
})),
|
||||
}),
|
||||
descendingDefaultSort: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
showCompletedUsenet: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
showCompletedTorrent: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
activeTorrentThreshold: factory.number({
|
||||
//in KiB/s
|
||||
validate: z.number().min(0),
|
||||
defaultValue: 0,
|
||||
step: 1,
|
||||
}),
|
||||
categoryFilter: factory.multiText({
|
||||
defaultValue: [] as string[],
|
||||
validate: z.string(),
|
||||
}),
|
||||
filterIsWhitelist: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
applyFilterToRatio: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
defaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
{
|
||||
defaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
},
|
||||
descendingDefaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
},
|
||||
showCompletedUsenet: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
showCompletedTorrent: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
activeTorrentThreshold: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
applyFilterToRatio: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
},
|
||||
descendingDefaultSort: {
|
||||
shouldHide: (options) => !options.enableRowSorting,
|
||||
},
|
||||
showCompletedUsenet: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
showCompletedTorrent: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
activeTorrentThreshold: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
applyFilterToRatio: {
|
||||
shouldHide: (_, integrationKinds) =>
|
||||
!getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("downloadClient"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -7,34 +7,36 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
|
||||
icon: IconHeartRateMonitor,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
fahrenheit: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
cpu: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
memory: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
fileSystem: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
defaultTab: factory.select({
|
||||
defaultValue: "system",
|
||||
options: [
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "cluster", label: "Cluster" },
|
||||
] as const,
|
||||
}),
|
||||
sectionIndicatorRequirement: factory.select({
|
||||
defaultValue: "all",
|
||||
options: [
|
||||
{ value: "all", label: "All active" },
|
||||
{ value: "any", label: "Any active" },
|
||||
] as const,
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
fahrenheit: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
cpu: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
memory: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
fileSystem: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
defaultTab: factory.select({
|
||||
defaultValue: "system",
|
||||
options: [
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "cluster", label: "Cluster" },
|
||||
] as const,
|
||||
}),
|
||||
sectionIndicatorRequirement: factory.select({
|
||||
defaultValue: "all",
|
||||
options: [
|
||||
{ value: "all", label: "All active" },
|
||||
{ value: "any", label: "Any active" },
|
||||
] as const,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
|
||||
@@ -5,17 +5,19 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("iframe", {
|
||||
icon: IconBrowser,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
embedUrl: factory.text(),
|
||||
allowFullScreen: factory.switch(),
|
||||
allowScrolling: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowTransparency: factory.switch(),
|
||||
allowPayment: factory.switch(),
|
||||
allowAutoPlay: factory.switch(),
|
||||
allowMicrophone: factory.switch(),
|
||||
allowCamera: factory.switch(),
|
||||
allowGeolocation: factory.switch(),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
embedUrl: factory.text(),
|
||||
allowFullScreen: factory.switch(),
|
||||
allowScrolling: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowTransparency: factory.switch(),
|
||||
allowPayment: factory.switch(),
|
||||
allowAutoPlay: factory.switch(),
|
||||
allowMicrophone: factory.switch(),
|
||||
allowCamera: factory.switch(),
|
||||
allowGeolocation: factory.switch(),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Center, Loader as UiLoader } from "@mantine/core";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import type { SettingsContextProps } from "@homarr/settings";
|
||||
|
||||
import * as app from "./app";
|
||||
import * as bookmarks from "./bookmarks";
|
||||
@@ -31,7 +32,7 @@ import * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
|
||||
import * as video from "./video";
|
||||
import * as weather from "./weather";
|
||||
|
||||
export type { WidgetDefinition } from "./definition";
|
||||
export type { WidgetDefinition, WidgetOptionsSettings } from "./definition";
|
||||
export type { WidgetComponentProps };
|
||||
|
||||
export const widgetImports = {
|
||||
@@ -94,9 +95,13 @@ export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (Widget
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: never[])[number];
|
||||
|
||||
export const reduceWidgetOptionsWithDefaultValues = (kind: WidgetKind, currentValue: Record<string, unknown> = {}) => {
|
||||
export const reduceWidgetOptionsWithDefaultValues = (
|
||||
kind: WidgetKind,
|
||||
settings: SettingsContextProps,
|
||||
currentValue: Record<string, unknown> = {},
|
||||
) => {
|
||||
const definition = widgetImports[kind].definition;
|
||||
const options = definition.options as Record<string, WidgetOptionDefinition>;
|
||||
const options = definition.createOptions(settings) as Record<string, WidgetOptionDefinition>;
|
||||
return objectEntries(options).reduce(
|
||||
(prev, [key, value]) => ({
|
||||
...prev,
|
||||
|
||||
@@ -7,11 +7,13 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("indexerManager", {
|
||||
icon: IconReportSearch,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
openIndexerSiteInNewTab: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
openIndexerSiteInNewTab: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("indexerManager"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", {
|
||||
icon: IconZoomQuestion,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
linksTargetNewTab: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
linksTargetNewTab: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createWidgetDefinition } from "../../definition";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
|
||||
icon: IconChartBar,
|
||||
options: {},
|
||||
createOptions() {
|
||||
return {};
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createWidgetDefinition } from "../definition";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
|
||||
icon: IconVideo,
|
||||
options: {},
|
||||
createOptions() {
|
||||
return {};
|
||||
},
|
||||
supportedIntegrations: ["jellyfin", "plex"],
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -6,16 +6,18 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
|
||||
icon: IconTransform,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
defaultView: factory.select({
|
||||
defaultValue: "statistics",
|
||||
options: [
|
||||
{ label: "Workers", value: "workers" },
|
||||
{ label: "Queue", value: "queue" },
|
||||
{ label: "Statistics", value: "statistics" },
|
||||
],
|
||||
}),
|
||||
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
defaultView: factory.select({
|
||||
defaultValue: "statistics",
|
||||
options: [
|
||||
{ label: "Workers", value: "workers" },
|
||||
{ label: "Queue", value: "queue" },
|
||||
{ label: "Statistics", value: "statistics" },
|
||||
],
|
||||
}),
|
||||
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["tdarr"],
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -6,9 +6,11 @@ import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", {
|
||||
icon: IconBrandMinecraft,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
title: factory.text({ defaultValue: "" }),
|
||||
domain: factory.text({ defaultValue: "hypixel.net", validate: z.string().nonempty() }),
|
||||
isBedrockServer: factory.switch({ defaultValue: false }),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
title: factory.text({ defaultValue: "" }),
|
||||
domain: factory.text({ defaultValue: "hypixel.net", validate: z.string().nonempty() }),
|
||||
isBedrockServer: factory.switch({ defaultValue: false }),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -8,6 +8,7 @@ import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { zodResolver } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import type { SettingsContextProps } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { zodErrorMap } from "@homarr/validation/form";
|
||||
|
||||
@@ -32,6 +33,7 @@ interface ModalProps<TSort extends WidgetKind> {
|
||||
onSuccessfulEdit: (value: WidgetEditModalState) => void;
|
||||
integrationData: IntegrationSelectOption[];
|
||||
integrationSupport: boolean;
|
||||
settings: SettingsContextProps;
|
||||
}
|
||||
|
||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||
@@ -40,13 +42,16 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
|
||||
// Translate the error messages
|
||||
z.setErrorMap(zodErrorMap(t));
|
||||
const { definition } = widgetImports[innerProps.kind];
|
||||
const options = definition.createOptions(innerProps.settings) as Record<string, OptionsBuilderResult[string]>;
|
||||
|
||||
const form = useForm({
|
||||
mode: "controlled",
|
||||
initialValues: innerProps.value,
|
||||
validate: zodResolver(
|
||||
z.object({
|
||||
options: z.object(
|
||||
objectEntries(widgetImports[innerProps.kind].definition.options).reduce(
|
||||
objectEntries(options).reduce(
|
||||
(acc, [key, value]: [string, { type: string; validate?: z.ZodType<unknown> }]) => {
|
||||
if (value.validate) {
|
||||
acc[key] = value.type === "multiText" ? z.array(value.validate).optional() : value.validate;
|
||||
@@ -68,8 +73,6 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
});
|
||||
const { openModal } = useModalAction(WidgetAdvancedOptionsModal);
|
||||
|
||||
const { definition } = widgetImports[innerProps.kind];
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
@@ -89,7 +92,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
{...form.getInputProps("integrationIds")}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(definition.options).map(([key, value]: [string, OptionsBuilderResult[string]]) => {
|
||||
{Object.entries(options).map(([key, value]) => {
|
||||
const Input = getInputForType(value.type);
|
||||
|
||||
if (
|
||||
|
||||
@@ -6,22 +6,24 @@ import { defaultContent } from "./default-content";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("notebook", {
|
||||
icon: IconNotes,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
showToolbar: factory.switch({
|
||||
defaultValue: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
showToolbar: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowReadOnlyCheck: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
content: factory.text({
|
||||
defaultValue: defaultContent,
|
||||
}),
|
||||
}),
|
||||
allowReadOnlyCheck: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
content: factory.text({
|
||||
defaultValue: defaultContent,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
content: {
|
||||
shouldHide: () => true, // Hide the content option as it can be modified in the editor
|
||||
{
|
||||
content: {
|
||||
shouldHide: () => true, // Hide the content option as it can be modified in the editor
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -149,6 +149,10 @@ export type WidgetOptionType = WidgetOptionDefinition["type"];
|
||||
export type WidgetOptionOfType<TType extends WidgetOptionType> = Extract<WidgetOptionDefinition, { type: TType }>;
|
||||
|
||||
type inferOptionFromDefinition<TDefinition extends WidgetOptionDefinition> = TDefinition["defaultValue"];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type inferOptionsFromCreator<TOptions extends (settings: any) => WidgetOptionsRecord> =
|
||||
inferOptionsFromDefinition<ReturnType<TOptions>>;
|
||||
export type inferOptionsFromDefinition<TOptions extends WidgetOptionsRecord> = {
|
||||
[key in keyof TOptions]: inferOptionFromDefinition<TOptions[key]>;
|
||||
};
|
||||
|
||||
@@ -12,21 +12,23 @@ import { optionsBuilder } from "../options";
|
||||
*/
|
||||
export const { definition, componentLoader } = createWidgetDefinition("rssFeed", {
|
||||
icon: IconRss,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
feedUrls: factory.multiText({
|
||||
defaultValue: [],
|
||||
validate: z.string().url(),
|
||||
}),
|
||||
enableRtl: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
textLinesClamp: factory.number({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(50),
|
||||
}),
|
||||
maximumAmountPosts: factory.number({
|
||||
defaultValue: 100,
|
||||
validate: z.number().min(1).max(9999),
|
||||
}),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
feedUrls: factory.multiText({
|
||||
defaultValue: [],
|
||||
validate: z.string().url(),
|
||||
}),
|
||||
enableRtl: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
textLinesClamp: factory.number({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(50),
|
||||
}),
|
||||
maximumAmountPosts: factory.number({
|
||||
defaultValue: 100,
|
||||
validate: z.number().min(1).max(9999),
|
||||
}),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -7,15 +7,17 @@ import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", {
|
||||
icon: IconBinaryTree,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
entityId: factory.text({
|
||||
defaultValue: "sun.sun",
|
||||
}),
|
||||
displayName: factory.text({
|
||||
defaultValue: "Sun",
|
||||
}),
|
||||
entityUnit: factory.text(),
|
||||
clickable: factory.switch(),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
entityId: factory.text({
|
||||
defaultValue: "sun.sun",
|
||||
}),
|
||||
displayName: factory.text({
|
||||
defaultValue: "Sun",
|
||||
}),
|
||||
entityUnit: factory.text(),
|
||||
clickable: factory.switch(),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -7,9 +7,11 @@ import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", {
|
||||
icon: IconBinaryTree,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
displayName: factory.text(),
|
||||
automationId: factory.text(),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
displayName: factory.text(),
|
||||
automationId: factory.text(),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { SettingsContextProps } from "@homarr/settings";
|
||||
import { createLanguageMapping } from "@homarr/translation";
|
||||
|
||||
import { widgetImports } from "..";
|
||||
@@ -8,26 +9,25 @@ import { widgetImports } from "..";
|
||||
describe("Widget properties with description should have matching translations", async () => {
|
||||
const enTranslation = await createLanguageMapping().en();
|
||||
objectEntries(widgetImports).forEach(([key, value]) => {
|
||||
Object.entries(value.definition.options).forEach(
|
||||
([optionKey, optionValue]: [string, { withDescription?: boolean }]) => {
|
||||
it(`should have matching translations for ${optionKey} option description of ${key} widget`, () => {
|
||||
const option = enTranslation.default.widget[key].option;
|
||||
if (!(optionKey in option)) {
|
||||
throw new Error(`Option ${optionKey} not found in translation`);
|
||||
}
|
||||
const value = option[optionKey as keyof typeof option];
|
||||
Object.entries(value.definition.createOptions({} as SettingsContextProps)).forEach(([optionKey, optionValue_]) => {
|
||||
const optionValue = optionValue_ as { withDescription: boolean };
|
||||
it(`should have matching translations for ${optionKey} option description of ${key} widget`, () => {
|
||||
const option = enTranslation.default.widget[key].option;
|
||||
if (!(optionKey in option)) {
|
||||
throw new Error(`Option ${optionKey} not found in translation`);
|
||||
}
|
||||
const value = option[optionKey as keyof typeof option];
|
||||
|
||||
expect("description" in value).toBe(optionValue.withDescription);
|
||||
});
|
||||
},
|
||||
);
|
||||
expect("description" in value).toBe(optionValue.withDescription);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Widget properties should have matching name translations", async () => {
|
||||
const enTranslation = await createLanguageMapping().en();
|
||||
objectEntries(widgetImports).forEach(([key, value]) => {
|
||||
Object.keys(value.definition.options).forEach((optionKey) => {
|
||||
Object.keys(value.definition.createOptions({} as SettingsContextProps)).forEach((optionKey) => {
|
||||
it(`should have matching translations for ${optionKey} option name of ${key} widget`, () => {
|
||||
const option = enTranslation.default.widget[key].option;
|
||||
if (!(optionKey in option)) {
|
||||
|
||||
@@ -5,16 +5,18 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("video", {
|
||||
icon: IconDeviceCctv,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
feedUrl: factory.text({
|
||||
defaultValue: "",
|
||||
}),
|
||||
hasAutoPlay: factory.switch({
|
||||
withDescription: true,
|
||||
}),
|
||||
isMuted: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
hasControls: factory.switch(),
|
||||
})),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
feedUrl: factory.text({
|
||||
defaultValue: "",
|
||||
}),
|
||||
hasAutoPlay: factory.switch({
|
||||
withDescription: true,
|
||||
}),
|
||||
isMuted: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
hasControls: factory.switch(),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -7,47 +7,49 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("weather", {
|
||||
icon: IconCloud,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
isFormatFahrenheit: factory.switch(),
|
||||
disableTemperatureDecimals: factory.switch(),
|
||||
showCurrentWindSpeed: factory.switch({ withDescription: true }),
|
||||
location: factory.location({
|
||||
defaultValue: {
|
||||
name: "Paris",
|
||||
latitude: 48.85341,
|
||||
longitude: 2.3488,
|
||||
},
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
isFormatFahrenheit: factory.switch(),
|
||||
disableTemperatureDecimals: factory.switch(),
|
||||
showCurrentWindSpeed: factory.switch({ withDescription: true }),
|
||||
location: factory.location({
|
||||
defaultValue: {
|
||||
name: "Paris",
|
||||
latitude: 48.85341,
|
||||
longitude: 2.3488,
|
||||
},
|
||||
}),
|
||||
dateFormat: factory.select({
|
||||
options: [
|
||||
{ value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") },
|
||||
{ value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") },
|
||||
{ value: "MMM D", label: dayjs().format("MMM D") },
|
||||
{ value: "D MMM", label: dayjs().format("D MMM") },
|
||||
{ value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") },
|
||||
{ value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") },
|
||||
{ value: "DD/MM", label: dayjs().format("DD/MM") },
|
||||
{ value: "MM/DD", label: dayjs().format("MM/DD") },
|
||||
],
|
||||
defaultValue: "dddd, MMMM D",
|
||||
withDescription: true,
|
||||
}),
|
||||
showCity: factory.switch(),
|
||||
hasForecast: factory.switch(),
|
||||
forecastDayCount: factory.slider({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(7),
|
||||
step: 1,
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
dateFormat: factory.select({
|
||||
options: [
|
||||
{ value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") },
|
||||
{ value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") },
|
||||
{ value: "MMM D", label: dayjs().format("MMM D") },
|
||||
{ value: "D MMM", label: dayjs().format("D MMM") },
|
||||
{ value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") },
|
||||
{ value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") },
|
||||
{ value: "DD/MM", label: dayjs().format("DD/MM") },
|
||||
{ value: "MM/DD", label: dayjs().format("MM/DD") },
|
||||
],
|
||||
defaultValue: "dddd, MMMM D",
|
||||
withDescription: true,
|
||||
}),
|
||||
showCity: factory.switch(),
|
||||
hasForecast: factory.switch(),
|
||||
forecastDayCount: factory.slider({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(7),
|
||||
step: 1,
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
forecastDayCount: {
|
||||
shouldHide({ hasForecast }) {
|
||||
return !hasForecast;
|
||||
{
|
||||
forecastDayCount: {
|
||||
shouldHide({ hasForecast }) {
|
||||
return !hasForecast;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
Reference in New Issue
Block a user