refactor(cms): deduplicate shared admin components and fix studio batch completion
CI / check (push) Failing after 1m35s
CI / deploy (push) Skipped
CI / e2e (push) Skipped

- fix(studio): stop markBatchDone infinite recursion so batches complete
- refactor(api): merge api-response into api and drop the duplicate module
- refactor(media): extract shared media loader and URL validator
- refactor(ui): extract shared LoadingSpinner for site and admin groups
- refactor(dates): consolidate raw date formatting into formatDate util
- refactor(logs): share a single generic log-list loader across tables
- refactor(theme): merge both ColorField components and reuse contrast helpers
- refactor(import): extract shared ImportErrorBanner and SearchInput
- chore(): remove dead theme-editor-tabs after inlining tab components
This commit is contained in:
openhands committed 2026-09-05 20:58:42 +02:00
1 parent b12e405b34
commit 8c12fc6c60
93 files changed
+1447 -1005

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
import { NextResponse } from "next/server";
/** Standard success response: { ok: true, ...data } */
export function apiOk(data?: Record<string, unknown>) {
return NextResponse.json({ ok: true, ...data });
}
/** Standard error response: { error: message } with given status */
export function apiError(message: string, status: number = 400) {
return NextResponse.json({ error: message }, { status });
}
+18 -1
View File
@@ -1,5 +1,22 @@
import { describe, expect, it } from "vitest";
import { apiUnavailable, pagination, positiveBigInt } from "@/lib/api";
import { apiOk, apiUnavailable, pagination, positiveBigInt } from "@/lib/api";
describe("apiOk", () => {
it("returns a BigInt-safe success envelope with ok: true", async () => {
const response = apiOk({ id: 9007199254740993n });
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("no-store");
await expect(response.json()).resolves.toEqual({
ok: true,
id: "9007199254740993",
});
});
it("allows empty payloads", async () => {
await expect(apiOk().json()).resolves.toEqual({ ok: true });
});
});
describe("apiUnavailable", () => {
it("returns a no-store 503 error without exposing internal details", async () => {
+5
View File
@@ -22,6 +22,11 @@ export function apiJson(data: unknown, init?: ResponseInit): NextResponse {
});
}
/** Standard success envelope: { ok: true, ...data }. */
export function apiOk(data?: Record<string, unknown>): NextResponse {
return apiJson({ ok: true, ...data });
}
/** Standard error envelope. */
export function apiError(message: string, status = 400): NextResponse {
return apiJson({ error: message }, { status });
+25
View File
@@ -0,0 +1,25 @@
export type MediaFile = {
name: string;
url: string;
size: number;
uploaded: number;
};
/** Only allow safe, relative /api/media URLs as image src. */
export function validImageUrl(url: string): string {
if (!url.startsWith("/")) return "";
if (/[<>"']/.test(url)) return "";
if (/(data|javascript|vbscript|file):/i.test(url)) return "";
return url;
}
/** Load the media library from /api/media, tolerating transient failures. */
export async function loadMediaFiles(): Promise<MediaFile[]> {
try {
const res = await fetch("/api/media");
const data = await res.json();
return Array.isArray(data?.files) ? (data.files as MediaFile[]) : [];
} catch {
return [];
}
}