feat(widgets): add title to advanced options (#2909)

This commit is contained in:
Meier Lukas
2025-04-22 18:33:15 +02:00
committed by GitHub
parent f98750d0b3
commit c64d903f2b
9 changed files with 78 additions and 25 deletions

View File

@@ -48,6 +48,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}), options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}),
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },

View File

@@ -29,6 +29,7 @@ export const createItemCallback =
layouts: createItemLayouts(previous, firstSection), layouts: createItemLayouts(previous, firstSection),
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },

View File

@@ -20,7 +20,7 @@ describe("item actions duplicate-item", () => {
kind: itemKind, kind: itemKind,
integrationIds: ["1"], integrationIds: ["1"],
options: { address: "localhost" }, options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"], borderColor: "#ff0000" }, advancedOptions: { title: "The best one", customCssClasses: ["test"], borderColor: "#ff0000" },
}) })
.addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize }) .addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize })
.build(); .build();

View File

@@ -13,6 +13,7 @@ export class ItemMockBuilder {
layouts: [], layouts: [],
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },

View File

@@ -0,0 +1,12 @@
.badge {
@mixin dark {
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
}
@mixin light {
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}

View File

@@ -1,4 +1,4 @@
import { Card } from "@mantine/core"; import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks"; import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query"; import { QueryErrorResetBoundary } from "@tanstack/react-query";
import combineClasses from "clsx"; import combineClasses from "clsx";
@@ -14,6 +14,7 @@ import { WidgetError } from "@homarr/widgets/errors";
import type { SectionItem } from "~/app/[locale]/boards/_types"; import type { SectionItem } from "~/app/[locale]/boards/_types";
import classes from "../sections/item.module.css"; import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
import itemContentClasses from "./item-content.module.css";
import { BoardItemMenu } from "./item-menu"; import { BoardItemMenu } from "./item-menu";
interface BoardItemContentProps { interface BoardItemContentProps {
@@ -25,28 +26,51 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
return ( return (
<Card <>
ref={ref} <Card
className={combineClasses( ref={ref}
classes.itemCard, className={combineClasses(
`${item.kind}-wrapper`, classes.itemCard,
"grid-stack-item-content", `${item.kind}-wrapper`,
item.advancedOptions.customCssClasses.join(" "), "grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
)}
radius={board.itemRadius}
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
{item.advancedOptions.title?.trim() && (
<Badge
pos="absolute"
// It's 4 because of the mantine-react-table that has z-index 3
style={{ zIndex: 4 }}
top={2}
left={16}
size="xs"
radius={board.itemRadius}
styles={{
root: {
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
"--opacity": board.opacity / 100,
},
}}
className={itemContentClasses.badge}
c="var(--mantine-color-text)"
>
{item.advancedOptions.title}
</Badge>
)} )}
radius={board.itemRadius} </>
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
); );
}; };

View File

@@ -1092,6 +1092,9 @@
"integrations": { "integrations": {
"label": "Integrations" "label": "Integrations"
}, },
"title": {
"label": "Title"
},
"customCssClasses": { "customCssClasses": {
"label": "Custom css classes" "label": "Custom css classes"
}, },

View File

@@ -14,6 +14,7 @@ export const integrationSchema = z.object({
export type BoardItemIntegration = z.infer<typeof integrationSchema>; export type BoardItemIntegration = z.infer<typeof integrationSchema>;
export const itemAdvancedOptionsSchema = z.object({ export const itemAdvancedOptionsSchema = z.object({
title: z.string().max(64).nullable().default(null),
customCssClasses: z.array(z.string()).default([]), customCssClasses: z.array(z.string()).default([]),
borderColor: z.string().default(""), borderColor: z.string().default(""),
}); });

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core"; import { Button, CloseButton, ColorInput, Group, Input, Stack, TextInput, useMantineTheme } from "@mantine/core";
import { useForm } from "@homarr/form"; import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
@@ -20,13 +20,23 @@ export const WidgetAdvancedOptionsModal = createModal<InnerProps>(({ actions, in
initialValues: innerProps.advancedOptions, initialValues: innerProps.advancedOptions,
}); });
const handleSubmit = (values: BoardItemAdvancedOptions) => { const handleSubmit = (values: BoardItemAdvancedOptions) => {
innerProps.onSuccess(values); innerProps.onSuccess({
...values,
// we want to fallback to null if the title is empty
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
title: values.title?.trim() || null,
});
actions.closeModal(); actions.closeModal();
}; };
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack> <Stack>
<TextInput
label={t("item.edit.field.title.label")}
{...form.getInputProps("title")}
rightSection={<Input.ClearButton onClick={() => form.setFieldValue("title", "")} />}
/>
<TextMultiSelect <TextMultiSelect
label={t("item.edit.field.customCssClasses.label")} label={t("item.edit.field.customCssClasses.label")}
{...form.getInputProps("customCssClasses")} {...form.getInputProps("customCssClasses")}