fix: address widget review feedback
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user