feat(app-widget): add layout option (#3875)
This commit is contained in:
@@ -39,6 +39,7 @@ export const mapApp = (
|
|||||||
pingEnabled: app.network.enabledStatusChecker,
|
pingEnabled: app.network.enabledStatusChecker,
|
||||||
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
|
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
|
||||||
showTitle: app.appearance.appNameStatus === "normal",
|
showTitle: app.appearance.appNameStatus === "normal",
|
||||||
|
layout: app.appearance.positionAppName,
|
||||||
} satisfies WidgetComponentProps<"app">["options"]),
|
} satisfies WidgetComponentProps<"app">["options"]),
|
||||||
layouts: boardSizes.map((size) => {
|
layouts: boardSizes.map((size) => {
|
||||||
const shapeForSize = app.shape[size];
|
const shapeForSize = app.shape[size];
|
||||||
|
|||||||
@@ -1271,6 +1271,15 @@
|
|||||||
},
|
},
|
||||||
"pingEnabled": {
|
"pingEnabled": {
|
||||||
"label": "Enable status check"
|
"label": "Enable status check"
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"label": "Layout",
|
||||||
|
"option": {
|
||||||
|
"row": "Horizontal",
|
||||||
|
"row-reverse": "Horizontal (reversed)",
|
||||||
|
"column": "Vertical",
|
||||||
|
"column-reverse": "Vertical (reversed)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Select } from "@mantine/core";
|
import { Group, Select } from "@mantine/core";
|
||||||
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { translateIfNecessary } from "@homarr/translation";
|
import { translateIfNecessary } from "@homarr/translation";
|
||||||
import type { stringOrTranslation } from "@homarr/translation";
|
import type { stringOrTranslation } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
import type { CommonWidgetInputProps } from "./common";
|
import type { CommonWidgetInputProps } from "./common";
|
||||||
import { useWidgetInputTranslation } from "./common";
|
import { useWidgetInputTranslation } from "./common";
|
||||||
@@ -12,6 +14,7 @@ import { useFormContext } from "./form";
|
|||||||
|
|
||||||
export type SelectOption =
|
export type SelectOption =
|
||||||
| {
|
| {
|
||||||
|
icon?: TablerIcon;
|
||||||
value: string;
|
value: string;
|
||||||
label: stringOrTranslation;
|
label: stringOrTranslation;
|
||||||
}
|
}
|
||||||
@@ -23,10 +26,19 @@ export type inferSelectOptionValue<TOption extends SelectOption> = TOption exten
|
|||||||
? TValue
|
? TValue
|
||||||
: TOption;
|
: TOption;
|
||||||
|
|
||||||
|
const getIconFor = (options: SelectOption[], value: string) => {
|
||||||
|
const current = options.find((option) => (typeof option === "string" ? option : option.value) === value);
|
||||||
|
if (!current) return null;
|
||||||
|
if (typeof current === "string") return null;
|
||||||
|
return current.icon;
|
||||||
|
};
|
||||||
|
|
||||||
export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"select">) => {
|
export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"select">) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const tWidget = useWidgetInputTranslation(kind, property);
|
const tWidget = useWidgetInputTranslation(kind, property);
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
|
const inputProps = form.getInputProps(`options.${property}`);
|
||||||
|
const CurrentIcon = getIconFor(options.options, inputProps.value as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -39,9 +51,29 @@ export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInput
|
|||||||
label: translateIfNecessary(t, option.label) ?? option.value,
|
label: translateIfNecessary(t, option.label) ?? option.value,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
leftSection={CurrentIcon && <CurrentIcon size={16} stroke={1.5} />}
|
||||||
|
renderOption={({ option, checked }) => {
|
||||||
|
const Icon = getIconFor(options.options, option.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group flex="1" gap="xs">
|
||||||
|
{Icon && <Icon color="currentColor" opacity={0.6} size={18} stroke={1.5} />}
|
||||||
|
{option.label}
|
||||||
|
{checked && (
|
||||||
|
<IconCheck
|
||||||
|
style={{ marginInlineStart: "auto" }}
|
||||||
|
color="currentColor"
|
||||||
|
opacity={0.6}
|
||||||
|
size={18}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}}
|
||||||
description={options.withDescription ? tWidget("description") : undefined}
|
description={options.withDescription ? tWidget("description") : undefined}
|
||||||
searchable={options.searchable}
|
searchable={options.searchable}
|
||||||
{...form.getInputProps(`options.${property}`)}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
|||||||
[app, options.openInNewTab],
|
[app, options.openInNewTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tinyText = height < 100 || width < 100;
|
const isTiny = height < 100 || width < 100;
|
||||||
|
const isColumnLayout = options.layout.startsWith("column");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLink
|
<AppLink
|
||||||
@@ -77,15 +78,22 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
|||||||
styles={{ tooltip: { maxWidth: 300 } }}
|
styles={{ tooltip: { maxWidth: 300 } }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
|
p={isTiny ? 4 : "sm"}
|
||||||
className={combineClasses("app-flex-wrapper", app.name, app.id, app.href && classes.appWithUrl)}
|
className={combineClasses("app-flex-wrapper", app.name, app.id, app.href && classes.appWithUrl)}
|
||||||
h="100%"
|
h="100%"
|
||||||
w="100%"
|
w="100%"
|
||||||
direction="column"
|
direction={options.layout}
|
||||||
justify="center"
|
justify="center"
|
||||||
align="center"
|
align="center"
|
||||||
|
gap={isColumnLayout ? 0 : "sm"}
|
||||||
>
|
>
|
||||||
{options.showTitle && (
|
{options.showTitle && (
|
||||||
<Text className="app-title" fw={700} size={tinyText ? "8px" : "sm"} ta="center">
|
<Text
|
||||||
|
className="app-title"
|
||||||
|
fw={700}
|
||||||
|
size={isTiny ? "8px" : "sm"}
|
||||||
|
ta={isColumnLayout ? "center" : undefined}
|
||||||
|
>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -97,6 +105,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
|||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
minWidth: "20%",
|
||||||
|
maxWidth: isColumnLayout ? undefined : "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { IconApps, IconDeviceDesktopX } from "@tabler/icons-react";
|
import {
|
||||||
|
IconApps,
|
||||||
|
IconDeviceDesktopX,
|
||||||
|
IconLayoutBottombarExpand,
|
||||||
|
IconLayoutNavbarExpand,
|
||||||
|
IconLayoutSidebarLeftExpand,
|
||||||
|
IconLayoutSidebarRightExpand,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
import { optionsBuilder } from "../options";
|
||||||
@@ -12,6 +19,40 @@ export const { definition, componentLoader } = createWidgetDefinition("app", {
|
|||||||
openInNewTab: factory.switch({ defaultValue: true }),
|
openInNewTab: factory.switch({ defaultValue: true }),
|
||||||
showTitle: factory.switch({ defaultValue: true }),
|
showTitle: factory.switch({ defaultValue: true }),
|
||||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||||
|
layout: factory.select({
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label(t) {
|
||||||
|
return t("widget.app.option.layout.option.column");
|
||||||
|
},
|
||||||
|
value: "column",
|
||||||
|
icon: IconLayoutNavbarExpand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label(t) {
|
||||||
|
return t("widget.app.option.layout.option.column-reverse");
|
||||||
|
},
|
||||||
|
value: "column-reverse",
|
||||||
|
icon: IconLayoutBottombarExpand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label(t) {
|
||||||
|
return t("widget.app.option.layout.option.row");
|
||||||
|
},
|
||||||
|
value: "row",
|
||||||
|
icon: IconLayoutSidebarLeftExpand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label(t) {
|
||||||
|
return t("widget.app.option.layout.option.row-reverse");
|
||||||
|
},
|
||||||
|
value: "row-reverse",
|
||||||
|
icon: IconLayoutSidebarRightExpand,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultValue: "column",
|
||||||
|
searchable: true,
|
||||||
|
}),
|
||||||
pingEnabled: factory.switch({ defaultValue: settings.enableStatusByDefault }),
|
pingEnabled: factory.switch({ defaultValue: settings.enableStatusByDefault }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user