fix: simplify widget package publishing and navigation
This commit is contained in:
1 parent
84058544a4
commit
9ff1019a3d
21 files changed
+811
-396
No files matched your search
@@ -8,8 +8,8 @@ import { packagePreviewProcedures } from "./preview-procedures";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { eq } from "@homarr/db";
|
||||
import { customWidgetInstallations, items } from "@homarr/db/schema";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { customWidgetArtifacts, customWidgetInstallations, items } from "@homarr/db/schema";
|
||||
import { customWidgetPackageSchema } from "@homarr/custom-widgets/package";
|
||||
|
||||
import { createTRPCRouter } from "../../../trpc";
|
||||
@@ -30,6 +30,7 @@ import { getOrBuildWidgetArtifact, packageTransferProcedures } from "./transfer"
|
||||
import { withWidgetInstallationLock } from "./coordination";
|
||||
import { publishWidgetPackageChange } from "./events";
|
||||
import { packageWorkshopProcedures } from "./workshop";
|
||||
import { getWidgetWorkshopUrl, readWidgetWorkshopOrigin } from "./workshop-client";
|
||||
import { setWidgetPackageEnabled } from "./enabled";
|
||||
import { packageWebhookProcedures } from "./webhook-procedures";
|
||||
import { packageArtifactReviewProcedures } from "./artifact-review";
|
||||
@@ -67,7 +68,7 @@ export const customWidgetPackageRouter = createTRPCRouter({
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const [rows, placements] = await Promise.all([
|
||||
ctx.db.query.customWidgetInstallations.findMany({ columns: { draft: false, previousSnapshot: false } }),
|
||||
ctx.db.query.customWidgetInstallations.findMany({ columns: { previousSnapshot: false } }),
|
||||
ctx.db.query.items.findMany({ where: eq(items.kind, "customApi"), columns: { options: true } }),
|
||||
]);
|
||||
const counts = new Map<string, number>();
|
||||
@@ -75,11 +76,30 @@ export const customWidgetPackageRouter = createTRPCRouter({
|
||||
const options = parsePackagePlacement(placement.options);
|
||||
if (options) counts.set(options.definitionId, (counts.get(options.definitionId) ?? 0) + 1);
|
||||
}
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
bindings: parseBindings(row.bindings),
|
||||
placementCount: counts.get(row.id) ?? 0,
|
||||
}));
|
||||
const artifactIds = rows.flatMap((row) => (row.activeArtifactId ? [row.activeArtifactId] : []));
|
||||
const artifacts =
|
||||
artifactIds.length > 0
|
||||
? await ctx.db.query.customWidgetArtifacts.findMany({
|
||||
where: inArray(customWidgetArtifacts.id, artifactIds),
|
||||
columns: { id: true, version: true },
|
||||
})
|
||||
: [];
|
||||
const versions = new Map(artifacts.map((artifact) => [artifact.id, artifact.version]));
|
||||
return rows.map(({ draft, ...row }) => {
|
||||
const source = customWidgetPackageSchema.safeParse(readPackageDraft(draft));
|
||||
const origin = readWidgetWorkshopOrigin(row.origin);
|
||||
return {
|
||||
...row,
|
||||
bindings: parseBindings(row.bindings),
|
||||
placementCount: counts.get(row.id) ?? 0,
|
||||
activeVersion: versions.get(row.activeArtifactId ?? "") ?? null,
|
||||
draftVersion: source.data?.manifest.version ?? null,
|
||||
description: source.data?.manifest.description ?? "",
|
||||
author: origin?.authorName || source.data?.manifest.author || "",
|
||||
workshop: origin,
|
||||
workshopUrl: origin ? getWidgetWorkshopUrl(origin.submissionId) : null,
|
||||
};
|
||||
});
|
||||
}),
|
||||
get: admin
|
||||
.meta({
|
||||
@@ -95,6 +115,7 @@ export const customWidgetPackageRouter = createTRPCRouter({
|
||||
const { previousSnapshot: _snapshot, draft: _draft, ...installation } = row;
|
||||
return {
|
||||
...installation,
|
||||
workshop: readWidgetWorkshopOrigin(row.origin),
|
||||
source: readPackageDraft(row.draft),
|
||||
draftDigest: packageDigest(row.draft),
|
||||
bindings: parseBindings(row.bindings),
|
||||
|
||||
@@ -48,16 +48,19 @@ export function preserveWidgetCollectionOrigin(raw: string | null, nextOrigin: o
|
||||
export function readPortableWidgetOrigin(raw: string | null): WidgetPortableOrigin | undefined {
|
||||
const value = readOrigin(raw);
|
||||
if (!value) return undefined;
|
||||
const { collection: _collection, ...origin } = value;
|
||||
const { collection: _collection, authorName: _authorName, ...origin } = value;
|
||||
const direct = widgetPortableOriginSchema.safeParse(origin);
|
||||
if (direct.success) return direct.data;
|
||||
if (typeof value.forkedFrom !== "string") return undefined;
|
||||
if (value.kind !== undefined || typeof value.forkedFrom !== "string") return undefined;
|
||||
const upstream = readPortableWidgetOrigin(typeof value.origin === "string" ? value.origin : null);
|
||||
let workshopUpstream;
|
||||
if (upstream?.kind === "workshop") workshopUpstream = upstream;
|
||||
if (upstream?.kind === "fork") workshopUpstream = upstream.upstream;
|
||||
const parsed = widgetPortableOriginSchema.safeParse({
|
||||
kind: "fork",
|
||||
packageId: value.forkedFrom,
|
||||
artifactDigest: value.artifact ?? undefined,
|
||||
upstream: upstream?.kind === "workshop" ? upstream : undefined,
|
||||
upstream: workshopUpstream,
|
||||
});
|
||||
if (parsed.success) return parsed.data;
|
||||
return undefined;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { assertWidgetArtifactIntegrity } from "@homarr/custom-widgets/package/server";
|
||||
import type { WorkshopPackageRelease } from "@homarr/workshop/package-releases";
|
||||
import { parseWorkshopPackageRelease } from "@homarr/workshop/package-releases";
|
||||
import { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import { resolveHomarrUrlConfig } from "@homarr/workshop/schema";
|
||||
@@ -24,9 +25,13 @@ export const widgetWorkshopOriginSchema = z.object({
|
||||
sourceDigest: z.string(),
|
||||
artifactDigest: z.string(),
|
||||
author: z.string(),
|
||||
authorName: z.string().optional(),
|
||||
forkedFrom: z.string(),
|
||||
});
|
||||
|
||||
export const getWidgetWorkshopUrl = (submissionId: string) =>
|
||||
`${urls.workshopWebUrl}/${encodeURIComponent(submissionId)}`;
|
||||
|
||||
export function readWidgetWorkshopOrigin(raw: string | null) {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
@@ -41,27 +46,31 @@ export function readWidgetWorkshopOrigin(raw: string | null) {
|
||||
export async function getWidgetWorkshopRelease(releaseId: string) {
|
||||
return withWorkshopError(async () => {
|
||||
const release = await widgetWorkshop.packages.get(releaseId, AbortSignal.timeout(15_000));
|
||||
const parsed = parseWorkshopPackageRelease(release);
|
||||
if (
|
||||
packageDigest(parsed.source) !== release.sourceDigest ||
|
||||
parsed.source.manifest.id !== release.packageId ||
|
||||
parsed.source.manifest.version !== release.version
|
||||
) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Workshop release metadata does not match its package" });
|
||||
}
|
||||
if (parsed.artifact) {
|
||||
assertWidgetArtifactIntegrity(parsed.artifact, parsed.source);
|
||||
if (parsed.artifact.digest !== release.artifactDigest) throw new Error("Release artifact checksum mismatch");
|
||||
}
|
||||
return {
|
||||
...parsed,
|
||||
release,
|
||||
origin: { kind: "workshop" as const, workshopApiUrl: urls.workshopApiUrl, ...parsed.origin },
|
||||
workshopUrl: `${urls.workshopWebUrl}/${encodeURIComponent(release.submission)}`,
|
||||
};
|
||||
return describeWidgetWorkshopRelease(release);
|
||||
});
|
||||
}
|
||||
|
||||
export function describeWidgetWorkshopRelease(release: WorkshopPackageRelease) {
|
||||
const parsed = parseWorkshopPackageRelease(release);
|
||||
if (
|
||||
packageDigest(parsed.source) !== release.sourceDigest ||
|
||||
parsed.source.manifest.id !== release.packageId ||
|
||||
parsed.source.manifest.version !== release.version
|
||||
) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Workshop release metadata does not match its package" });
|
||||
}
|
||||
if (parsed.artifact) {
|
||||
assertWidgetArtifactIntegrity(parsed.artifact, parsed.source);
|
||||
if (parsed.artifact.digest !== release.artifactDigest) throw new Error("Release artifact checksum mismatch");
|
||||
}
|
||||
return {
|
||||
...parsed,
|
||||
release,
|
||||
origin: { kind: "workshop" as const, workshopApiUrl: urls.workshopApiUrl, ...parsed.origin },
|
||||
workshopUrl: getWidgetWorkshopUrl(release.submission),
|
||||
};
|
||||
}
|
||||
|
||||
export function authenticatedWidgetWorkshop(token: string) {
|
||||
const backend = new WorkshopBackend(urls.workshopApiUrl);
|
||||
backend.pocketBase.authStore.save(token);
|
||||
|
||||
@@ -3,18 +3,19 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { customWidgetPackageSchema } from "@homarr/custom-widgets/package";
|
||||
import { customWidgetPackageSchema, widgetPackageManifestSchema } from "@homarr/custom-widgets/package";
|
||||
import type { CustomWidgetArtifact } from "@homarr/custom-widgets/package";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { customWidgetInstallations } from "@homarr/db/schema";
|
||||
import { preserveWidgetCollectionOrigin } from "./portable-collection-origin";
|
||||
import { preserveWidgetCollectionOrigin, readPortableWidgetOrigin } from "./portable-collection-origin";
|
||||
|
||||
import { persistPackageArtifact } from "./preview-store";
|
||||
import { withWidgetInstallationLock } from "./coordination";
|
||||
import { withWidgetArtifactReferenceLock } from "./artifact-references";
|
||||
import { getArtifact, getInstallation, packageDigest } from "./records";
|
||||
import { getArtifact, getInstallation, packageDigest, readPackageDraft } from "./records";
|
||||
import {
|
||||
authenticatedWidgetWorkshop,
|
||||
describeWidgetWorkshopRelease,
|
||||
getWidgetWorkshopRelease,
|
||||
readWidgetWorkshopOrigin,
|
||||
widgetWorkshop,
|
||||
@@ -91,7 +92,7 @@ export const packageWorkshopProcedures = {
|
||||
widgetWorkshop.packages.list(origin.submissionId, AbortSignal.timeout(15_000)),
|
||||
);
|
||||
const latest = releases[0];
|
||||
const draft = customWidgetPackageSchema.safeParse(JSON.parse(installation.draft));
|
||||
const draft = customWidgetPackageSchema.safeParse(readPackageDraft(installation.draft));
|
||||
let activeVersion: string | null = null;
|
||||
if (installation.activeArtifactId)
|
||||
activeVersion = (await getArtifact(ctx, installation.activeArtifactId)).source.manifest.version;
|
||||
@@ -153,7 +154,11 @@ export const packageWorkshopProcedures = {
|
||||
.input(
|
||||
idInput.extend({
|
||||
token: z.string().min(1).max(20_000),
|
||||
mode: z.enum(["new", "update", "fork"]),
|
||||
mode: z.enum(["auto", "new", "update", "fork"]).default("auto"),
|
||||
title: z.string().trim().min(3).max(100).optional(),
|
||||
description: z.string().max(2048).optional(),
|
||||
version: widgetPackageManifestSchema.shape.version.optional(),
|
||||
expectedDraftDigest: z.string().min(1).optional(),
|
||||
submissionId: z.string().optional(),
|
||||
expectedRevision: z.number().int().positive().optional(),
|
||||
forkedFrom: z.string().optional(),
|
||||
@@ -161,56 +166,89 @@ export const packageWorkshopProcedures = {
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const installation = await getInstallation(ctx, input.id);
|
||||
const source = customWidgetPackageSchema.parse(JSON.parse(installation.draft));
|
||||
const origin = readWidgetWorkshopOrigin(installation.origin);
|
||||
let submissionId: string | undefined;
|
||||
let forkedFrom: string | undefined;
|
||||
if (input.mode === "update") {
|
||||
submissionId = input.submissionId ?? origin?.submissionId;
|
||||
if (!submissionId || !input.expectedRevision)
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "An update requires its Workshop submission and current revision",
|
||||
});
|
||||
} else if (input.mode === "fork") {
|
||||
forkedFrom = input.forkedFrom ?? origin?.releaseId;
|
||||
if (!forkedFrom)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Choose the original release to publish a fork" });
|
||||
const original = await getWidgetWorkshopRelease(forkedFrom);
|
||||
if (original.source.manifest.id === source.manifest.id)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "A fork must have its own package identity" });
|
||||
}
|
||||
let artifact: CustomWidgetArtifact | undefined;
|
||||
if (installation.activeArtifactId) {
|
||||
const active = await getArtifact(ctx, installation.activeArtifactId);
|
||||
if (packageDigest(active.source) === packageDigest(source)) artifact = active.artifact;
|
||||
}
|
||||
const release = await withWorkshopError(() =>
|
||||
authenticatedWidgetWorkshop(input.token).packages.publish({
|
||||
source,
|
||||
artifact,
|
||||
submissionId,
|
||||
expectedRevision: input.expectedRevision,
|
||||
forkedFrom,
|
||||
changelog: input.changelog,
|
||||
title: installation.name,
|
||||
description: source.manifest.description,
|
||||
}),
|
||||
);
|
||||
const published = await getWidgetWorkshopRelease(release.id);
|
||||
await ctx.db
|
||||
.update(customWidgetInstallations)
|
||||
.set({
|
||||
origin: JSON.stringify(preserveWidgetCollectionOrigin(installation.origin, published.origin)),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(customWidgetInstallations.id, input.id));
|
||||
return {
|
||||
releaseId: release.id,
|
||||
submissionId: release.submission,
|
||||
version: release.version,
|
||||
workshopUrl: published.workshopUrl,
|
||||
};
|
||||
return withWidgetInstallationLock(input.id, async () => {
|
||||
const installation = await getInstallation(ctx, input.id);
|
||||
if (input.expectedDraftDigest && packageDigest(installation.draft) !== input.expectedDraftDigest)
|
||||
throw new TRPCError({ code: "CONFLICT", message: "The saved draft changed. Reload it before publishing." });
|
||||
const source = customWidgetPackageSchema.parse(readPackageDraft(installation.draft));
|
||||
const origin = readWidgetWorkshopOrigin(installation.origin);
|
||||
const backend = authenticatedWidgetWorkshop(input.token);
|
||||
const user = await withWorkshopError(() => backend.refreshAuth());
|
||||
if (!user) throw new TRPCError({ code: "UNAUTHORIZED", message: "Sign in to Workshop before publishing." });
|
||||
let submissionId: string | undefined;
|
||||
let forkedFrom: string | undefined;
|
||||
let expectedRevision = input.expectedRevision;
|
||||
if (input.mode === "auto") {
|
||||
if (!input.expectedDraftDigest)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Review the saved draft before publishing." });
|
||||
const portable = readPortableWidgetOrigin(installation.origin);
|
||||
if (origin?.author === user.id) {
|
||||
const listing = await withWorkshopError(() => backend.get(origin.submissionId));
|
||||
submissionId = listing.id;
|
||||
expectedRevision = listing.revision;
|
||||
} else if (origin) {
|
||||
forkedFrom = origin.releaseId;
|
||||
} else if (portable?.kind === "fork" && portable.upstream) {
|
||||
const upstream = readWidgetWorkshopOrigin(JSON.stringify(portable.upstream));
|
||||
forkedFrom = upstream?.releaseId;
|
||||
}
|
||||
if (forkedFrom) {
|
||||
const original = await getWidgetWorkshopRelease(forkedFrom);
|
||||
if (original.source.manifest.id === source.manifest.id) source.manifest.id = `widget-${createId()}`;
|
||||
}
|
||||
source.manifest.author = user.name;
|
||||
source.manifest.name = input.title ?? installation.name;
|
||||
source.manifest.description = input.description ?? source.manifest.description;
|
||||
source.manifest.version = input.version ?? source.manifest.version;
|
||||
} else if (input.mode === "update") {
|
||||
submissionId = input.submissionId ?? origin?.submissionId;
|
||||
if (!submissionId || !expectedRevision)
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "An update requires its Workshop submission and current revision",
|
||||
});
|
||||
} else if (input.mode === "fork") {
|
||||
forkedFrom = input.forkedFrom ?? origin?.releaseId;
|
||||
if (!forkedFrom)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Choose the original release to publish a fork" });
|
||||
const original = await getWidgetWorkshopRelease(forkedFrom);
|
||||
if (original.source.manifest.id === source.manifest.id)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "A fork must have its own package identity" });
|
||||
}
|
||||
let artifact: CustomWidgetArtifact | undefined;
|
||||
if (installation.activeArtifactId) {
|
||||
const active = await getArtifact(ctx, installation.activeArtifactId);
|
||||
if (packageDigest(active.source) === packageDigest(source)) artifact = active.artifact;
|
||||
}
|
||||
const release = await withWorkshopError(() =>
|
||||
backend.packages.publish({
|
||||
source,
|
||||
artifact,
|
||||
submissionId,
|
||||
expectedRevision,
|
||||
forkedFrom,
|
||||
changelog: input.changelog,
|
||||
title: input.title ?? installation.name,
|
||||
description: input.description ?? source.manifest.description,
|
||||
}),
|
||||
);
|
||||
const published = describeWidgetWorkshopRelease(release);
|
||||
await ctx.db
|
||||
.update(customWidgetInstallations)
|
||||
.set({
|
||||
origin: JSON.stringify(
|
||||
preserveWidgetCollectionOrigin(installation.origin, { ...published.origin, authorName: user.name }),
|
||||
),
|
||||
draft: JSON.stringify(source),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(customWidgetInstallations.id, input.id));
|
||||
return {
|
||||
releaseId: release.id,
|
||||
submissionId: release.submission,
|
||||
version: release.version,
|
||||
workshopUrl: published.workshopUrl,
|
||||
};
|
||||
});
|
||||
}),
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const widgetPortableWorkshopOriginSchema = z.strictObject({
|
||||
releaseId: z.string().max(128),
|
||||
version: z.string().max(128),
|
||||
sourceDigest: digest,
|
||||
artifactDigest: digest,
|
||||
artifactDigest: digest.or(z.literal("")),
|
||||
author: z.string().max(256),
|
||||
forkedFrom: z.string().max(128),
|
||||
});
|
||||
|
||||
@@ -1191,6 +1191,17 @@
|
||||
"savedPlaceholder": "••••••• (saved, leave empty to keep)"
|
||||
},
|
||||
"package": {
|
||||
"workbenchTitle": "Widget editor",
|
||||
"publishDescription": "Add a title, description, and screenshots using the Workshop publishing form. Your Workshop account supplies the attribution.",
|
||||
"saveBeforePublish": "Save your draft before publishing.",
|
||||
"draftVersion": "Draft v{version}",
|
||||
"byAuthor": "By {name}",
|
||||
"yourWorkshopListing": "Your Workshop listing",
|
||||
"upToDate": "No newer Workshop release",
|
||||
"reviewUpdate": "Review update",
|
||||
"updateReviewDescription": "Load this release into the editor for review. Your active widgets keep running until you activate the update. Connections and placement settings are preserved.",
|
||||
"replaceModifiedDraft": "Replace my modified saved draft with this release",
|
||||
"duplicate": "Duplicate",
|
||||
"title": "Widget packages",
|
||||
"create": "Create package",
|
||||
"library": "Packages",
|
||||
@@ -1379,7 +1390,7 @@
|
||||
"changelog": "Release notes",
|
||||
"publishAcknowledgement": "I reviewed the complete saved source for secrets and local information, and I have permission to share this code and its dependencies publicly.",
|
||||
"published": "Published release {version}.",
|
||||
"openWorkshop": "Open Workshop page",
|
||||
"openWorkshop": "View on Workshop",
|
||||
"manifestFile": "widget.json",
|
||||
"filePathExample": "components/Status.tsx",
|
||||
"serverUrlExample": "http://sonarr:8989",
|
||||
@@ -1564,7 +1575,7 @@
|
||||
"useDescription": "Save a draft to keep your work. Activate it when you want boards to use that code, then add it to a board below. One package can appear on several boards.",
|
||||
"previewSavesFirst": "Your first preview also saves a draft. It does not add or change dashboard tiles.",
|
||||
"workshopTab": "Workshop",
|
||||
"workshopDescription": "Share your finished widget, publish a new release, or review upstream updates. Local connections and credentials stay on your Homarr instance.",
|
||||
"workshopDescription": "Share your saved widget using the Workshop publishing form. Check for updates and view installed versions in the Packages library.",
|
||||
"releaseTools": "Release details, disable and rollback",
|
||||
"chooseConnection": "Choose a connection or Homarr integration",
|
||||
"noCompatibleConnections": "No compatible connections yet. Create one below, then refresh this list.",
|
||||
@@ -1579,7 +1590,7 @@
|
||||
"runtimeLogs": "Build and preview errors appear here. Server output is included in Homarr logs under custom-widget-package. The active release process can be inspected below.",
|
||||
"buildState": "Preview build: {state}",
|
||||
"actionsMenu": "Widget package actions",
|
||||
"forkDefaultName": "{name} fork",
|
||||
"forkDefaultName": "{name} copy",
|
||||
"previewBuilding": "Building preview…",
|
||||
"previewBuildReady": "Preview build ready",
|
||||
"previewBuildFailed": "Preview build failed",
|
||||
@@ -1675,6 +1686,18 @@
|
||||
"publish": {
|
||||
"title": "Publish to Workshop",
|
||||
"introTitle": "Share this widget with the community",
|
||||
"packageDescription": "Share the saved package source. Connections, credentials, and stored widget data stay in your Homarr instance. Review code and fixtures before publishing.",
|
||||
"publishingAs": "Publishing as {name}",
|
||||
"updatingListing": "A new release will update your existing Workshop listing.",
|
||||
"attributedCopy": "Your copy will have its own listing and retain a link to the original author’s release.",
|
||||
"releaseDetails": "Release details",
|
||||
"version": "Version",
|
||||
"releaseNotes": "Release notes (optional)",
|
||||
"releaseNotesDescription": "Tell existing users what changed in this release.",
|
||||
"updateAction": "Publish update",
|
||||
"packageInvalid": "This saved package needs repair before it can be published. Open the editor to fix its configuration or source.",
|
||||
"screenshotLimit": "This listing can have up to five screenshots. Remove existing images on Workshop before adding more.",
|
||||
"screenshotUploadFailed": "The package is published, but screenshots could not be uploaded. Add them using View and manage submission.",
|
||||
"introDescription": "Homarr exports a credential-free copy of the current widget. Add clear listing details so people know what it does before installing it.",
|
||||
"listingDetails": "Workshop listing",
|
||||
"listingDetailsDescription": "These details appear publicly alongside the exported widget definition.",
|
||||
@@ -6154,6 +6177,8 @@
|
||||
},
|
||||
"management": {
|
||||
"metaTitle": "Management",
|
||||
"collapseSidebar": "Collapse sidebar",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
"notFound": {
|
||||
"title": "Not Found",
|
||||
"text": "Could not find requested resource",
|
||||
|
||||
@@ -466,6 +466,18 @@ export class WorkshopBackend {
|
||||
}
|
||||
}
|
||||
|
||||
public async addScreenshots(id: string, screenshots: File[]) {
|
||||
workshopScreenshotsSchema.parse(screenshots);
|
||||
if (screenshots.length === 0) return;
|
||||
const current = await this.get(id);
|
||||
if (current.screenshots.length + screenshots.length > MAX_WORKSHOP_SCREENSHOTS)
|
||||
throw new Error(`A submission can have up to ${MAX_WORKSHOP_SCREENSHOTS} screenshots`);
|
||||
await this.pocketBase.collection("submissions").update(id, {
|
||||
"screenshots+": screenshots,
|
||||
expectedRevision: current.revision,
|
||||
});
|
||||
}
|
||||
|
||||
public async update(id: string, input: WorkshopSubmissionInput, screenshotChanges: WorkshopScreenshotChanges = {}) {
|
||||
const parsed = workshopSubmissionInputSchema.parse(input);
|
||||
const additions = screenshotChanges.additions ?? [];
|
||||
|
||||
Reference in new issue
Block a user