ADDED - Save an unfinished project as a draft

- Add a Save as draft button that bypasses the completeness gate: the user
  can save partial work and finish later
- projectDraftInputSchema makes the user-facing copy (title, summary, service
  label, client) optional; the action auto-generates a slug from the title
  (or draft-<timestamp>), defaults the year to the current year, and forces
  the project unpublished
- Drafts drop not-yet-valid sections/assets instead of failing the save
- Pass intent via a hidden field set on click so the server reliably sees it
- Tests: draft schema accepts empty copy a strict save rejects, and still
  requires a slug and a valid year
This commit is contained in:
moh
2026-09-20 16:52:13 +02:00
parent 39e459fa09
commit 4b535694bc
4 changed files with 164 additions and 14 deletions
+59
View File
@@ -9,6 +9,7 @@ import {
import {
assetInputSchema,
categoryInputSchema,
projectDraftInputSchema,
projectInputSchema,
sectionInputSchema,
} from "../lib/portfolio-validation";
@@ -65,6 +66,64 @@ describe("portfolio validation", () => {
).toThrow(/slug/i);
});
it("draft schema accepts empty user-facing copy that a strict save rejects", () => {
const draftPayload = {
categoryId: "cat_1",
slug: "draft-123",
viewMode: "GRID" as const,
titleAr: "",
titleEn: "",
titleDe: "",
summaryAr: "",
summaryEn: "",
summaryDe: "",
clientName: "",
projectYear: 2026,
serviceLabelAr: "",
serviceLabelEn: "",
serviceLabelDe: "",
previewUrl: "",
currentCoverImagePath: "",
sortOrder: 0,
isFeatured: false,
isPublished: false,
sections: [],
assets: [],
};
expect(projectDraftInputSchema.parse(draftPayload).slug).toBe("draft-123");
expect(() => projectInputSchema.parse(draftPayload)).toThrow();
});
it("draft schema still requires a slug and a valid year", () => {
const base = {
categoryId: "cat_1",
slug: "draft-1",
viewMode: "GRID" as const,
titleAr: "",
titleEn: "",
titleDe: "",
summaryAr: "",
summaryEn: "",
summaryDe: "",
clientName: "",
projectYear: 2026,
serviceLabelAr: "",
serviceLabelEn: "",
serviceLabelDe: "",
previewUrl: "",
currentCoverImagePath: "",
sortOrder: 0,
isFeatured: false,
isPublished: false,
sections: [],
assets: [],
};
expect(() => projectDraftInputSchema.parse({ ...base, slug: "" })).toThrow(/slug/i);
expect(() => projectDraftInputSchema.parse({ ...base, projectYear: 1999 })).toThrow();
});
it("accepts valid section and asset payloads", () => {
expect(
sectionInputSchema.parse({