fix: address widget review feedback

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:08 +02:00
1 parent b1b17ad69f
commit a38e731209
79 files changed
+2138 -244

No files matched your search

@@ -4,23 +4,31 @@ import z from "zod";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import { createWidgetDefinition } from "../../definition";
import { createWidgetDefinition, widgetQueryInputMatches } from "../../definition";
import { optionsBuilder } from "../../options";
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
queryKey: [["widget", "immich"]],
queryKey: [["widget", "immich", "getAlbum"]],
queryMatcher: ({ input }, scope) =>
widgetQueryInputMatches(input, {
integrationId: scope.integrationIds[0] ?? "",
albumId:
typeof scope.options.albumId === "string" && scope.options.albumId !== ALL_PHOTOS_ALBUM_ID
? scope.options.albumId
: undefined,
}),
refetchInterval: null,
supportedIntegrations: ["immich"],
integrationsRequired: true,
maxIntegrations: 1,
contextActions({ widgetStateRef }) {
const call = (key: string) => () => {
const action = widgetStateRef.current?.[key];
const action = widgetStateRef?.current?.[key];
if (typeof action === "function") action();
};
const disabled = (key: string) => typeof widgetStateRef.current?.[key] !== "function";
const disabled = (key: string) => typeof widgetStateRef?.current?.[key] !== "function";
return [
{
key: "previousPhoto",
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { WidgetComponentProps } from "../definition";
import ImmichServerStatsWidget from "./server-stats/component";
import { definition as albumCarouselDefinition } from "./album-carousel";
const mocks = vi.hoisted(() => ({
getAlbums: vi.fn(() => ({ data: [], isPending: false, isError: false })),
}));
vi.mock("@homarr/api/client", () => ({
clientApi: {
widget: {
immich: {
getAlbums: { useQuery: mocks.getAlbums },
getServerStats: { useQuery: () => ({ data: undefined }) },
},
},
},
}));
vi.mock("@homarr/translation/client", () => ({
useI18n: () => (key: string) => key,
useScopedI18n: () => (key: string) => key,
}));
describe("Immich album queries", () => {
beforeEach(() => vi.clearAllMocks());
test("server stats requests a bounded album response", () => {
ImmichServerStatsWidget({
integrationIds: ["immich-id"],
options: { showUsers: true, showPhotos: true, showVideos: true, showStorage: true },
displayMode: "advanced",
width: 800,
height: 600,
isEditMode: false,
boardId: "board-id",
itemId: "item-id",
setOptions: vi.fn(),
} satisfies WidgetComponentProps<"immich-serverStats">);
expect(mocks.getAlbums).toHaveBeenCalledWith(
{ integrationId: "immich-id", limit: 50 },
expect.objectContaining({ enabled: true }),
);
});
test("album carousel keeps requesting every album for its selector", () => {
const options = albumCarouselDefinition.createOptions();
options.albumId.useOptions(["immich-id"]);
expect(mocks.getAlbums).toHaveBeenCalledWith(
{ integrationId: "immich-id" },
expect.objectContaining({ enabled: true }),
);
});
});
@@ -10,30 +10,37 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import { setWidgetRuntimeQueries } from "../../definition";
import classes from "./component.module.css";
const MAX_ADVANCED_ALBUMS = 50;
export default function ImmichServerStatsWidget({
integrationIds,
options,
displayMode = "compact",
width,
widgetStateRef,
}: WidgetComponentProps<"immich-serverStats">) {
const t = useI18n();
const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({
integrationId: integrationIds[0] ?? "",
const input = { integrationId: integrationIds[0] ?? "" };
const albumsInput = { ...input, limit: MAX_ADVANCED_ALBUMS };
const isAdvanced = displayMode === "advanced";
const albumsEnabled = isAdvanced && integrationIds.length > 0;
const { data: stats } = clientApi.widget.immich.getServerStats.useQuery(input);
const { data: albums = [] } = clientApi.widget.immich.getAlbums.useQuery(albumsInput, {
enabled: albumsEnabled,
staleTime: 15 * 60 * 1000,
});
const { data: albums = [] } = clientApi.widget.immich.getAlbums.useQuery(
{ integrationId: integrationIds[0] ?? "" },
{ enabled: displayMode === "advanced" && integrationIds.length > 0, staleTime: 15 * 60 * 1000 },
);
setWidgetRuntimeQueries(widgetStateRef, [
{ path: ["widget", "immich", "getServerStats"], input },
...(albumsEnabled ? [{ path: ["widget", "immich", "getAlbums"], input: albumsInput }] : []),
]);
if (!stats) return <WidgetEmptyState />;
const statsContent = (
<SimpleGrid
cols={displayMode === "advanced" ? (width >= 720 ? 4 : width >= 360 ? 2 : 1) : width >= 320 ? 2 : 1}
spacing="sm"
>
<SimpleGrid cols={isAdvanced ? (width >= 720 ? 4 : width >= 360 ? 2 : 1) : width >= 320 ? 2 : 1} spacing="sm">
{options.showUsers && (
<StatItem icon={<IconUsers size={20} />} label={t("widget.immich-serverStats.users")} value={stats.userCount} />
)}
@@ -61,7 +68,7 @@ export default function ImmichServerStatsWidget({
</SimpleGrid>
);
if (displayMode === "compact") {
if (!isAdvanced) {
return (
<Stack gap="md" h="100%" p="md" justify="center">
{statsContent}
@@ -69,7 +76,7 @@ export default function ImmichServerStatsWidget({
);
}
const sortedAlbums = [...albums].toSorted((left, right) => right.assetCount - left.assetCount);
const sortedAlbums = albums;
const maxAssets = sortedAlbums[0]?.assetCount ?? 1;
return (
<Stack gap="lg" h="100%" p="lg">
@@ -1,11 +1,12 @@
import { IconGraphFilled } from "@tabler/icons-react";
import { createWidgetDefinition } from "../../definition";
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../../definition";
import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", {
icon: IconGraphFilled,
queryKey: [["widget", "immich"]],
queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]],
queryMatcher: matchesWidgetRuntimeQuery,
refetchInterval: null,
supportedIntegrations: ["immich"],
integrationsRequired: true,