feat(app-widget): show description in widget (#3876)

This commit is contained in:
Meier Lukas
2025-08-29 20:50:51 +02:00
committed by GitHub
parent 5168cba8e4
commit c3461677e8
6 changed files with 116 additions and 17 deletions

View File

@@ -0,0 +1,43 @@
import SuperJSON from "superjson";
import { eq } from "../..";
import type { Database } from "../..";
import { items } from "../../schema";
/**
* To support showing the description in the widget itself we replaced
* the tooltip show option with display mode.
*/
export async function migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync(db: Database) {
const existingAppItems = await db.query.items.findMany({
where: (table, { eq }) => eq(table.kind, "app"),
});
const itemsToUpdate = existingAppItems
.map((item) => ({
id: item.id,
options: SuperJSON.parse<{ showDescriptionTooltip?: boolean }>(item.options),
}))
.filter((item) => item.options.showDescriptionTooltip !== undefined);
console.log(
`Migrating app items with showDescriptionTooltip to descriptionDisplayMode count=${itemsToUpdate.length}`,
);
await Promise.all(
itemsToUpdate.map(async (item) => {
const { showDescriptionTooltip, ...options } = item.options;
await db
.update(items)
.set({
options: SuperJSON.stringify({
...options,
descriptionDisplayMode: showDescriptionTooltip ? "tooltip" : "hidden",
}),
})
.where(eq(items.id, item.id));
}),
);
console.log(`Migrated app items with showDescriptionTooltip to descriptionDisplayMode count=${itemsToUpdate.length}`);
}

View File

@@ -1,8 +1,10 @@
import type { Database } from "../..";
import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widget_provider_to_options";
import { migrateOpnsenseCredentialsAsync } from "./0001_opnsense_credentials";
import { migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync } from "./0002_app_widget_show_description_tooltip_to_display_mode";
export const applyCustomMigrationsAsync = async (db: Database) => {
await migrateReleaseWidgetProviderToOptionsAsync(db);
await migrateOpnsenseCredentialsAsync(db);
await migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync(db);
};

View File

@@ -37,9 +37,9 @@ export const mapApp = (
appId: appsMap.get(app.id)?.id!,
openInNewTab: app.behaviour.isOpeningNewTab,
pingEnabled: app.network.enabledStatusChecker,
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
showTitle: app.appearance.appNameStatus === "normal",
layout: app.appearance.positionAppName,
descriptionDisplayMode: app.behaviour.tooltipDescription !== "" ? "tooltip" : "hidden",
} satisfies WidgetComponentProps<"app">["options"]),
layouts: boardSizes.map((size) => {
const shapeForSize = app.shape[size];

View File

@@ -1266,9 +1266,6 @@
"showTitle": {
"label": "Show app name"
},
"showDescriptionTooltip": {
"label": "Show description tooltip"
},
"pingEnabled": {
"label": "Enable status check"
},
@@ -1280,6 +1277,15 @@
"column": "Vertical",
"column-reverse": "Vertical (reversed)"
}
},
"descriptionDisplayMode": {
"label": "Description display mode",
"description": "Choose how to display the app description",
"option": {
"normal": "Within widget",
"tooltip": "As tooltip",
"hidden": "Hidden"
}
}
},
"error": {

View File

@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import { Fragment, Suspense } from "react";
import { Flex, Text, Tooltip, UnstyledButton } from "@mantine/core";
import { Flex, rem, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
import { IconLoader } from "@tabler/icons-react";
import combineClasses from "clsx";
@@ -74,7 +74,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
))}
position="right-start"
multiline
disabled={!options.showDescriptionTooltip || !app.description}
disabled={options.descriptionDisplayMode !== "tooltip" || !app.description || isEditMode}
styles={{ tooltip: { maxWidth: 300 } }}
>
<Flex
@@ -87,16 +87,34 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
align="center"
gap={isColumnLayout ? 0 : "sm"}
>
{options.showTitle && (
<Text
className="app-title"
fw={700}
size={isTiny ? "8px" : "sm"}
ta={isColumnLayout ? "center" : undefined}
>
{app.name}
</Text>
)}
<Stack gap={0}>
{options.showTitle && (
<Text
className="app-title"
fw={700}
size={isTiny ? rem(8) : "sm"}
ta={isColumnLayout ? "center" : undefined}
>
{app.name}
</Text>
)}
{options.descriptionDisplayMode === "normal" && (
<Text
className="app-description"
size={isTiny ? rem(8) : "sm"}
ta={isColumnLayout ? "center" : undefined}
c="dimmed"
lineClamp={4}
>
{app.description?.split("\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</Text>
)}
</Stack>
<MaskedOrNormalImage
imageUrl={app.iconUrl}
hasColor={board.iconColor !== null}

View File

@@ -1,10 +1,13 @@
import {
IconApps,
IconDeviceDesktopX,
IconEyeOff,
IconLayoutBottombarExpand,
IconLayoutNavbarExpand,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
IconTextScan2,
IconTooltip,
} from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
@@ -18,7 +21,34 @@ export const { definition, componentLoader } = createWidgetDefinition("app", {
appId: factory.app(),
openInNewTab: factory.switch({ defaultValue: true }),
showTitle: factory.switch({ defaultValue: true }),
showDescriptionTooltip: factory.switch({ defaultValue: false }),
descriptionDisplayMode: factory.select({
options: [
{
label(t) {
return t("widget.app.option.descriptionDisplayMode.option.normal");
},
value: "normal",
icon: IconTextScan2,
},
{
label(t) {
return t("widget.app.option.descriptionDisplayMode.option.tooltip");
},
value: "tooltip",
icon: IconTooltip,
},
{
label(t) {
return t("widget.app.option.descriptionDisplayMode.option.hidden");
},
value: "hidden",
icon: IconEyeOff,
},
],
defaultValue: "hidden",
searchable: true,
withDescription: true,
}),
layout: factory.select({
options: [
{