feat: modernize dashboard widgets

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:28 +02:00
1 parent 1a5e083fd0
commit 6f2cd7fc2b
270 files changed
+10189 -3544

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { getBoardSaveScopeId, shouldRefetchAfterSaveError } from "./updater";
describe("board persistence coordination", () => {
it("uses one mutation scope for every save of the same board", () => {
expect(getBoardSaveScopeId("board-1")).toBe(getBoardSaveScopeId("board-1"));
expect(getBoardSaveScopeId("board-1")).not.toBe(getBoardSaveScopeId("board-2"));
});
it("only refetches after a failed save when no newer local board exists", () => {
const failedBoard = { id: "board-1", items: ["first"] };
const newerBoard = { id: "board-1", items: ["first", "second"] };
expect(shouldRefetchAfterSaveError(failedBoard, failedBoard)).toBe(true);
expect(shouldRefetchAfterSaveError(newerBoard, failedBoard)).toBe(false);
});
});
+48 -4
View File
@@ -5,13 +5,19 @@ import { useCallback } from "react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
type Board = RouterOutputs["board"]["getHomeBoard"];
type UpdateCallback = (previous: Board) => Board;
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const getBoardSaveScopeId = (boardId: string) => `board-save:${boardId}`;
export const shouldRefetchAfterSaveError = <T>(currentBoard: T | undefined, failedBoard: T) =>
currentBoard === failedBoard;
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
@@ -21,9 +27,13 @@ export const useUpdateBoard = () => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
let updatedBoard: Board | undefined;
utils.board.getBoardByName.setData({ name: boardName }, (previous) => {
if (!previous) return previous;
updatedBoard = updaterWithoutUndefined(previous);
return updatedBoard;
});
return updatedBoard;
},
[utils],
);
@@ -32,3 +42,37 @@ export const useUpdateBoard = () => {
updateBoard,
};
};
interface PersistBoardOptions {
onError?: (error: unknown) => void;
}
export const usePersistBoard = ({ id, name }: Pick<Board, "id" | "name">) => {
const utils = clientApi.useUtils();
const { updateBoard } = useUpdateBoard();
const { mutateAsync: saveBoardAsync } = clientApi.board.saveBoard.useMutation({
scope: { id: getBoardSaveScopeId(id) },
});
const updateAndPersistBoard = useCallback(
(updater: UpdateCallback, options?: PersistBoardOptions) => {
const updatedBoard = updateBoard(updater);
if (!updatedBoard) return undefined;
const persistedBoard = utils.board.getBoardByName.getData({ name });
if (!persistedBoard) return undefined;
void saveBoardAsync(persistedBoard).catch((error: unknown) => {
const currentBoard = utils.board.getBoardByName.getData({ name });
if (shouldRefetchAfterSaveError(currentBoard, persistedBoard)) {
void utils.board.getBoardByName.invalidate({ name });
}
options?.onError?.(error);
});
return persistedBoard;
},
[name, saveBoardAsync, updateBoard, utils],
);
return { updateAndPersistBoard };
};