fix(custom-widgets): address final v2 review feedback (#6451)

This commit is contained in:
Thomas authored and GitHub committed 2026-07-23 02:11:32 +02:00
1 parent df967751f6
commit 4f6e335f72
20 files changed
+186 -92

No files matched your search

@@ -1,8 +1,8 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
getPrivateWorkshopSourceNames,
publishWorkshopDefinition,
serializeWorkshopDefinition,
workshopDefinitionChanged,
} from "./workshop-publish-definition";
@@ -26,16 +26,27 @@ describe("Workshop publish definition inspection", () => {
expect(workshopDefinitionChanged(definition, { ...definition, name: "Updated" })).toBe(true);
});
it("submits the same refreshed export that passed the change check", () => {
const source = readFileSync(
`${process.cwd()}/apps/nextjs/src/components/workshop/workshop-publish-modal.tsx`,
"utf8",
);
expect(source).toContain("customWidget.export.useQuery");
expect(source).toContain("await definition.refetch()");
expect(source).toContain("workshopDefinitionChanged(inspectedDefinition, refreshed.data)");
expect(source).toContain("content: serializeWorkshopDefinition(refreshed.data)");
expect(source).toMatch(/disabled=\{[\s\S]*!definition\.data[\s\S]*definition\.isError/u);
expect(serializeWorkshopDefinition(definition)).toBe(JSON.stringify(definition));
it("blocks publishing when the definition changes after inspection", async () => {
const publish = vi.fn(async () => undefined);
await expect(
publishWorkshopDefinition({
inspectedDefinition: definition,
refetchDefinition: async () => ({ ...definition, name: "Updated" }),
publish,
}),
).resolves.toBe("changed");
expect(publish).not.toHaveBeenCalled();
});
it("publishes the same refreshed export that passed the change check", async () => {
const publish = vi.fn(async () => undefined);
await expect(
publishWorkshopDefinition({
inspectedDefinition: definition,
refetchDefinition: async () => definition,
publish,
}),
).resolves.toBe("published");
expect(publish).toHaveBeenCalledWith(serializeWorkshopDefinition(definition));
});
});
@@ -11,6 +11,22 @@ export const serializeWorkshopDefinition = (definition: unknown): string => {
export const workshopDefinitionChanged = (previous: unknown, current: unknown) =>
serializeWorkshopDefinition(previous) !== serializeWorkshopDefinition(current);
export async function publishWorkshopDefinition({
inspectedDefinition,
refetchDefinition,
publish,
}: {
inspectedDefinition: unknown;
refetchDefinition: () => Promise<unknown | undefined>;
publish: (content: string) => Promise<unknown>;
}) {
const refreshedDefinition = await refetchDefinition();
if (refreshedDefinition === undefined) return "unavailable" as const;
if (workshopDefinitionChanged(inspectedDefinition, refreshedDefinition)) return "changed" as const;
await publish(serializeWorkshopDefinition(refreshedDefinition));
return "published" as const;
}
export const getPrivateWorkshopSourceNames = (definition: PublishableWorkshopDefinition | undefined) =>
Object.entries(definition?.sources ?? {}).flatMap(([sourceId, source]) =>
source.networkScope === "public" ? [] : [source.name ?? sourceId],
@@ -12,8 +12,8 @@ import { useScopedI18n } from "@homarr/translation/client";
import { createWorkshopClient } from "./workshop-client";
import {
getPrivateWorkshopSourceNames,
publishWorkshopDefinition,
serializeWorkshopDefinition,
workshopDefinitionChanged,
} from "./workshop-publish-definition";
export function WorkshopPublishModal({
@@ -60,22 +60,26 @@ export function WorkshopPublishModal({
try {
if (!definition.data) throw new Error(t("publish.error"));
const inspectedDefinition = definition.data;
const refreshed = await definition.refetch();
if (!refreshed.data) throw new Error(t("publish.error"));
if (workshopDefinitionChanged(inspectedDefinition, refreshed.data)) {
const result = await publishWorkshopDefinition({
inspectedDefinition,
refetchDefinition: async () => (await definition.refetch()).data,
publish: (content) =>
createSubmission.mutateAsync({
input: {
type: "customWidget",
title,
description,
content,
},
screenshots,
}),
});
if (result === "unavailable") throw new Error(t("publish.error"));
if (result === "changed") {
setSourceUrlsReviewed(false);
setError(t("publish.error"));
setError(t("publish.definitionChanged"));
return;
}
await createSubmission.mutateAsync({
input: {
type: "customWidget",
title,
description,
content: serializeWorkshopDefinition(refreshed.data),
},
screenshots,
});
setPublished(true);
} catch (cause) {
setError(cause instanceof Error ? cause.message : t("publish.error"));