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 && (
-
-
-
-
- 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/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 (