feat(app-widget): add layout option (#3875)
This commit is contained in:
@@ -39,6 +39,7 @@ export const mapApp = (
|
||||
pingEnabled: app.network.enabledStatusChecker,
|
||||
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
|
||||
showTitle: app.appearance.appNameStatus === "normal",
|
||||
layout: app.appearance.positionAppName,
|
||||
} satisfies WidgetComponentProps<"app">["options"]),
|
||||
layouts: boardSizes.map((size) => {
|
||||
const shapeForSize = app.shape[size];
|
||||
|
||||
@@ -1271,6 +1271,15 @@
|
||||
},
|
||||
"pingEnabled": {
|
||||
"label": "Enable status check"
|
||||
},
|
||||
"layout": {
|
||||
"label": "Layout",
|
||||
"option": {
|
||||
"row": "Horizontal",
|
||||
"row-reverse": "Horizontal (reversed)",
|
||||
"column": "Vertical",
|
||||
"column-reverse": "Vertical (reversed)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"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 type { stringOrTranslation } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
@@ -12,6 +14,7 @@ import { useFormContext } from "./form";
|
||||
|
||||
export type SelectOption =
|
||||
| {
|
||||
icon?: TablerIcon;
|
||||
value: string;
|
||||
label: stringOrTranslation;
|
||||
}
|
||||
@@ -23,10 +26,19 @@ export type inferSelectOptionValue<TOption extends SelectOption> = TOption exten
|
||||
? TValue
|
||||
: 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">) => {
|
||||
const t = useI18n();
|
||||
const tWidget = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const inputProps = form.getInputProps(`options.${property}`);
|
||||
const CurrentIcon = getIconFor(options.options, inputProps.value as string);
|
||||
|
||||
return (
|
||||
<Select
|
||||
@@ -39,9 +51,29 @@ export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInput
|
||||
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}
|
||||
searchable={options.searchable}
|
||||
{...form.getInputProps(`options.${property}`)}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,7 +56,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
[app, options.openInNewTab],
|
||||
);
|
||||
|
||||
const tinyText = height < 100 || width < 100;
|
||||
const isTiny = height < 100 || width < 100;
|
||||
const isColumnLayout = options.layout.startsWith("column");
|
||||
|
||||
return (
|
||||
<AppLink
|
||||
@@ -77,15 +78,22 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
styles={{ tooltip: { maxWidth: 300 } }}
|
||||
>
|
||||
<Flex
|
||||
p={isTiny ? 4 : "sm"}
|
||||
className={combineClasses("app-flex-wrapper", app.name, app.id, app.href && classes.appWithUrl)}
|
||||
h="100%"
|
||||
w="100%"
|
||||
direction="column"
|
||||
direction={options.layout}
|
||||
justify="center"
|
||||
align="center"
|
||||
gap={isColumnLayout ? 0 : "sm"}
|
||||
>
|
||||
{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}
|
||||
</Text>
|
||||
)}
|
||||
@@ -97,6 +105,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
minWidth: "20%",
|
||||
maxWidth: isColumnLayout ? undefined : "50%",
|
||||
}}
|
||||
/>
|
||||
</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 { optionsBuilder } from "../options";
|
||||
@@ -12,6 +19,40 @@ export const { definition, componentLoader } = createWidgetDefinition("app", {
|
||||
openInNewTab: factory.switch({ defaultValue: true }),
|
||||
showTitle: factory.switch({ defaultValue: true }),
|
||||
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 }),
|
||||
}),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user