feat(cms): recover drafts and failed imports with verified UI workflows
This commit is contained in:
1 parent
410a1e466c
commit
88790d1a0d
84 files changed
+3952
-288
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,7 @@ import { useFormatter, useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { retryableJobItems } from "@/lib/furni/import-job-retry";
|
||||
import { ImportAttachmentRecovery } from "./import-attachment-recovery";
|
||||
import type { useFurnitureJobs } from "./use-furniture-jobs";
|
||||
export function FurnitureJobHistory({
|
||||
state,
|
||||
@@ -98,6 +99,18 @@ export function FurnitureJobHistory({
|
||||
{item.error}
|
||||
</p>
|
||||
)}
|
||||
{!job.syncKind &&
|
||||
item.state === "failed" &&
|
||||
retryItems.some(
|
||||
(retryItem) => retryItem.classname === item.classname,
|
||||
) && (
|
||||
<ImportAttachmentRecovery
|
||||
jobId={job.id}
|
||||
classname={item.classname}
|
||||
disabled={state.busy}
|
||||
onResumed={state.refresh}
|
||||
/>
|
||||
)}
|
||||
{[...new Set(item.warnings ?? [])].map((warning) => (
|
||||
<p
|
||||
key={warning}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -17,6 +17,7 @@ import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { previewAutoCatalog } from "@/lib/furni/auto-catalog";
|
||||
import { compareFurniture } from "@/lib/furni/studio-inspection";
|
||||
import { FurnitureComparison } from "./furniture-inspector";
|
||||
import { ImportSourceDiagnostics } from "./import-source-diagnostics";
|
||||
import type { FurniItem } from "./studio-types";
|
||||
import { useFurnitureInspection } from "./use-furniture-inspection";
|
||||
import { useSourceAssetChecks } from "./use-source-asset-checks";
|
||||
@@ -38,6 +39,7 @@ export function ImportReview({
|
||||
busy?: boolean;
|
||||
onConfirm: (items: FurniItem[]) => void;
|
||||
}) {
|
||||
const diagnosticsT = useTranslations("admin.studio.importDiagnostics");
|
||||
const completenessT = useTranslations("admin.studio.completeness");
|
||||
const [attachments, setAttachments] = useState<Record<string, string>>({});
|
||||
const [uploading, setUploading] = useState<string | null>(null);
|
||||
@@ -57,7 +59,7 @@ export function ImportReview({
|
||||
...current,
|
||||
[item.classname]: data.attachmentId,
|
||||
}));
|
||||
toast.success("Matching .nitro attached to this import");
|
||||
toast.success(diagnosticsT("attached"));
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Upload failed");
|
||||
} finally {
|
||||
@@ -260,8 +262,8 @@ export function ImportReview({
|
||||
{local.nitro.exists !== true && (
|
||||
<label className="mt-3 block text-sm">
|
||||
{attachments[item.classname]
|
||||
? "Matching .nitro attached"
|
||||
: "Attach the original .nitro"}
|
||||
? diagnosticsT("attached")
|
||||
: diagnosticsT("attach")}
|
||||
<input
|
||||
className="mt-2 block max-w-full"
|
||||
type="file"
|
||||
@@ -278,6 +280,15 @@ export function ImportReview({
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{assets.items.find(
|
||||
(check) => check.classname === item.classname,
|
||||
) && (
|
||||
<ImportSourceDiagnostics
|
||||
check={assets.items.find(
|
||||
(check) => check.classname === item.classname,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FurnitureComparison item={item} local={local} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { ImportSourceDiagnostics } from "./import-source-diagnostics";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string, args?: Record<string, unknown>) =>
|
||||
`${key}${args?.status ?? ""}`,
|
||||
}));
|
||||
it("shows filenames, attempted addresses and distinct reasons inline", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ImportSourceDiagnostics
|
||||
check={{
|
||||
classname: "chair",
|
||||
revision: 1,
|
||||
state: "missing",
|
||||
alternatives: [],
|
||||
attempts: [
|
||||
{
|
||||
file: "chair.swf",
|
||||
url: "https://source.test/1/chair.swf",
|
||||
state: "missing",
|
||||
reason: "http",
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
file: "chair.nitro",
|
||||
url: "https://source.test/chair.nitro",
|
||||
state: "unknown",
|
||||
reason: "timeout",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("chair.swf");
|
||||
expect(html).toContain("http404");
|
||||
expect(html).toContain("timeout");
|
||||
expect(html).toContain("https://source.test/1/chair.swf");
|
||||
expect(html).not.toContain("href=");
|
||||
});
|
||||
it("does not invent diagnostics for earlier responses", () => {
|
||||
expect(
|
||||
renderToStaticMarkup(
|
||||
<ImportSourceDiagnostics
|
||||
check={{
|
||||
classname: "chair",
|
||||
revision: 1,
|
||||
state: "missing",
|
||||
alternatives: [],
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
).toBe("");
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { SourceAssetCheck } from "@/lib/furni/source-assets";
|
||||
export function ImportSourceDiagnostics({
|
||||
check,
|
||||
}: {
|
||||
check?: SourceAssetCheck;
|
||||
}) {
|
||||
const t = useTranslations("admin.studio.importDiagnostics");
|
||||
if (!check?.attempts?.length) return null;
|
||||
return (
|
||||
<details className="mt-2 text-xs">
|
||||
<summary className="cursor-pointer">{t("attempts")}</summary>
|
||||
<p className="mt-2 text-[var(--admin-text-muted)]">{t("preliminary")}</p>
|
||||
<ul className="space-y-2 py-2">
|
||||
{[
|
||||
...new Map(
|
||||
check.attempts.map((attempt) => [attempt.url, attempt]),
|
||||
).values(),
|
||||
].map((attempt) => (
|
||||
<li key={attempt.url}>
|
||||
<strong>{attempt.file}</strong> ·{" "}
|
||||
{attempt.reason === "http"
|
||||
? t("http", { status: attempt.status ?? 0 })
|
||||
: t(attempt.reason)}
|
||||
<code className="block break-all">{attempt.url}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user