diff --git a/apps/nextjs/src/components/board/sections/content.tsx b/apps/nextjs/src/components/board/sections/content.tsx index 541e96967..cf579191f 100644 --- a/apps/nextjs/src/components/board/sections/content.tsx +++ b/apps/nextjs/src/components/board/sections/content.tsx @@ -39,12 +39,21 @@ interface ItemProps { } export const SectionContentItem = ({ item, integrations }: ItemProps) => { - const minWidth = useMinSize(item, "x"); - const minHeight = useMinSize(item, "y"); + if (item.type === "section") return ; + return ( - - {item.type === "item" && item.kind !== "app" && } - {item.type === "item" ? : } + + {item.kind !== "app" && } + + + ); +}; + +const ContainerSectionContentItem = ({ item }: { item: ContainerSectionItem }) => { + const { minWidth, minHeight } = useMinSize(item); + return ( + + ); }; @@ -52,14 +61,18 @@ export const SectionContentItem = ({ item, integrations }: ItemProps) => { /** * Calculates a container's minimum width or height from its direct content. */ -export const useMinSize = (item: ContainerSectionItem | SectionItem, direction: "x" | "y") => { +export const useMinSize = (item: ContainerSectionItem) => { const { items, innerSections } = useSectionItems(item.id); - if (item.type === "item") return undefined; - - const size = direction === "x" ? "width" : "height"; - return Math.max( - 1, - ...items.map((childItem) => childItem[`${direction}Offset`] + childItem[size]), - ...innerSections.map((childSection) => childSection[`${direction}Offset`] + childSection[size]), - ); + return { + minWidth: Math.max( + 1, + ...items.map((child) => child.xOffset + child.width), + ...innerSections.map((child) => child.xOffset + child.width), + ), + minHeight: Math.max( + 1, + ...items.map((child) => child.yOffset + child.height), + ...innerSections.map((child) => child.yOffset + child.height), + ), + }; }; diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index 138369c57..d78d11660 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -34,8 +34,6 @@ import { IconTriangleFilled, IconZoomScan, } from "@tabler/icons-react"; -import { escapeForRegEx } from "@tiptap/react"; - import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { createId } from "@homarr/common"; @@ -63,6 +61,8 @@ interface FormValidation { errors: FormErrors; } +const escapeForRegEx = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + export const WidgetMultiReleasesRepositoriesInput = ({ property, kind, diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 193650b5c..8475de4e0 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -59,17 +59,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget return ( - ( - - {line} -
-
- ))} - position="right-start" - multiline - disabled={options.descriptionDisplayMode !== "tooltip" || !app.description || isEditMode} - styles={{ tooltip: { maxWidth: 300 } }} + -
+ {options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && @@ -148,6 +140,29 @@ interface AppLinkProps { enabled: boolean; } +const AppDescriptionTooltip = ({ + description, + enabled, + children, +}: PropsWithChildren<{ description?: string | null; enabled: boolean }>) => + enabled ? ( + ( + + {line} +
+
+ ))} + position="right-start" + multiline + styles={{ tooltip: { maxWidth: 300 } }} + > + {children} +
+ ) : ( + children + ); + const AppLink = ({ href, openInNewTab, enabled, children }: PropsWithChildren) => enabled ? ( parseStatus(a.status ?? "") === "running", @@ -140,6 +138,7 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey, variant="filled" chevronPosition="right" multiple + keepMounted={false} value={isAdvanced ? advancedOpenSections : openSections} onChange={isAdvanced ? setAdvancedOpenSections : setOpenSections} > @@ -168,19 +167,27 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey, {!hideFooter && ( - - - v{instance.instanceInfo.version} - - - {t("footer.updated", { when: relativeTime })} - - + )} ); } + +const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => { + const t = useScopedI18n("widget.coolify"); + const relativeTime = useTimeAgo(updatedAt, 60_000); + return ( + + + v{version} + + + {t("footer.updated", { when: relativeTime })} + + + ); +}; diff --git a/packages/widgets/src/coolify/single-instance-layout.tsx b/packages/widgets/src/coolify/single-instance-layout.tsx index b7dd45b60..4b9252eb5 100644 --- a/packages/widgets/src/coolify/single-instance-layout.tsx +++ b/packages/widgets/src/coolify/single-instance-layout.tsx @@ -50,8 +50,6 @@ export function SingleInstanceLayout({ const baseUrl = getSafeApplicationUrl(instance.integrationUrl)?.replace(/\/+$/, "") ?? ""; const displayUrl = baseUrl ? baseUrl.replace(/^https?:\/\//, "") : "—"; - const relativeTime = useTimeAgo(instance.updatedAt); - return ( @@ -87,6 +85,7 @@ export function SingleInstanceLayout({ variant="contained" chevronPosition="right" multiple + keepMounted={false} value={isAdvanced ? advancedOpenSections : openSections} onChange={isAdvanced ? setAdvancedOpenSections : setOpenSections} > @@ -115,23 +114,31 @@ export function SingleInstanceLayout({ {!hideFooter && ( - - - Coolify - - v{instance.instanceInfo.version} - - - - {t("footer.updated", { when: relativeTime })} - - + )} ); } + +const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => { + const t = useScopedI18n("widget.coolify"); + const relativeTime = useTimeAgo(updatedAt, 60_000); + return ( + + + Coolify + + v{version} + + + + {t("footer.updated", { when: relativeTime })} + + + ); +}; diff --git a/packages/widgets/src/media-missing/component.tsx b/packages/widgets/src/media-missing/component.tsx index 15ad851f2..3d4ec2503 100644 --- a/packages/widgets/src/media-missing/component.tsx +++ b/packages/widgets/src/media-missing/component.tsx @@ -168,6 +168,7 @@ export default function MediaMissingWidget({ return ( { if (value === "missing" || value === "queued") setSelectedTab(value); }} @@ -193,12 +194,12 @@ export default function MediaMissingWidget({ {showMissing && ( - {renderPanel(missing, t("empty.missing"))} + {activeTab === "missing" && renderPanel(missing, t("empty.missing"))} )} {showQueued && ( - {renderPanel(queued, t("empty.queued"))} + {activeTab === "queued" && renderPanel(queued, t("empty.queued"))} )} diff --git a/packages/widgets/src/notebook/notebook.tsx b/packages/widgets/src/notebook/notebook.tsx index b190d2a82..25ea9586d 100644 --- a/packages/widgets/src/notebook/notebook.tsx +++ b/packages/widgets/src/notebook/notebook.tsx @@ -874,27 +874,30 @@ function TaskListToggle() { ); } -function ListIndentIncrease() { +function useActiveListItemType() { const { editor } = useRichTextEditorContext(); const [itemType, setItemType] = useState("listItem"); + + useEffect(() => { + if (!editor) return; + const updateItemType = () => setItemType(editor.isActive("taskItem") ? "taskItem" : "listItem"); + updateItemType(); + editor.on("selectionUpdate", updateItemType); + return () => { + editor.off("selectionUpdate", updateItemType); + }; + }, [editor]); + + return { editor, itemType }; +} + +function ListIndentIncrease() { + const { editor, itemType } = useActiveListItemType(); const tControls = useScopedI18n("widget.notebook.controls"); const handleIncreaseIndent = useCallback(() => { editor?.chain().focus().sinkListItem(itemType).run(); }, [editor, itemType]); - useEffect(() => { - if (!editor) return; - - const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => { - setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem"); - }; - - editor.on("selectionUpdate", handleSelectionUpdate); - return () => { - editor.off("selectionUpdate", handleSelectionUpdate); - }; - }, [editor]); - return ( { editor?.chain().focus().liftListItem(itemType).run(); }, [editor, itemType]); - useEffect(() => { - if (!editor) return; - - const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => { - setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem"); - }; - - editor.on("selectionUpdate", handleSelectionUpdate); - return () => { - editor.off("selectionUpdate", handleSelectionUpdate); - }; - }, [editor]); - return ( { + const resultById = new Map(results.flat().map((result) => [result.id, result])); const formattedResults = normalizedRepositories .map((repository) => { if (!repository.provider) return { ...repository, error: { code: "noProviderSelected" } }; - const repositoryResult = results.flat().find(({ id }) => id === repository.id); + const repositoryResult = resultById.get(repository.id); if (!repositoryResult) return { ...repository, error: { code: "noProviderResponse" } }; if (!repositoryResult.success) return { ...repository, error: repositoryResult.error }; diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx index d88e3beca..c608b432a 100644 --- a/packages/widgets/src/umami/umami-content.tsx +++ b/packages/widgets/src/umami/umami-content.tsx @@ -63,7 +63,7 @@ export function UmamiContent({ { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, { enabled: viewMode === "events" && eventNames.length > 0, ...umamiQueryOptions }, ); - const multiEventSeries = multiEventQuery.data; + const multiEventSeries = getUsableWidgetQueryData(multiEventQuery); const multiEventTotal = multiEventSeries ? multiEventSeries.flatMap(({ dataPoints }) => dataPoints).reduce((sum, { y }) => sum + y, 0) @@ -106,10 +106,9 @@ export function UmamiContent({ const selectedView = viewMode === "events" ? ( 0} chartType={chartType} showXAxis={showXAxis} /> diff --git a/packages/widgets/src/umami/umami-events-content.tsx b/packages/widgets/src/umami/umami-events-content.tsx index 752d5f33b..fd7eb7fec 100644 --- a/packages/widgets/src/umami/umami-events-content.tsx +++ b/packages/widgets/src/umami/umami-events-content.tsx @@ -3,27 +3,23 @@ import { BarChart, LineChart } from "@mantine/charts"; import { Box, Group, Stack, Text } from "@mantine/core"; -import { clientApi } from "@homarr/api/client"; import type { UmamiEventSeries } from "@homarr/integrations/types"; import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; -import { EVENT_COLORS, formatXLabel, umamiQueryOptions } from "./umami-utils"; -import { getUsableWidgetQueryData } from "../common/query-state"; +import { EVENT_COLORS, formatXLabel } from "./umami-utils"; interface UmamiEventsContentProps { - integrationIds: string[]; - websiteId: string; + series: UmamiEventSeries[]; timeFrame: string; - eventNames: string[]; + hasSelectedEvents: boolean; chartType: string; showXAxis: boolean; } export function UmamiEventsContent({ - integrationIds, - websiteId, + series, timeFrame, - eventNames, + hasSelectedEvents, chartType, showXAxis, }: UmamiEventsContentProps) { @@ -31,15 +27,7 @@ export function UmamiEventsContent({ const locale = useCurrentIntlLocale(); const tickColor = "var(--mantine-color-dimmed)"; - const series = - getUsableWidgetQueryData( - clientApi.widget.umami.getMultiEventTimeSeries.useQuery( - { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, - umamiQueryOptions, - ), - ) ?? []; - - if (eventNames.length === 0) { + if (!hasSelectedEvents) { return (