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:
@@ -347,16 +347,26 @@ function SectionHeader({
|
||||
);
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
function SubmitButton({ onSelect }: { onSelect: () => void }) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button type="submit" disabled={pending}>
|
||||
<Button type="submit" onClick={onSelect} disabled={pending}>
|
||||
{pending ? "Saving..." : "Save Project"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function DraftButton({ onSelect }: { onSelect: () => void }) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button type="submit" onClick={onSelect} variant="outline" disabled={pending}>
|
||||
Save as draft
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewModeCard({
|
||||
active,
|
||||
label,
|
||||
@@ -466,6 +476,12 @@ export function PortfolioProjectForm({
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const assetsInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const intentRef = useRef<HTMLInputElement | null>(null);
|
||||
const setIntent = (value: "save" | "draft") => {
|
||||
if (intentRef.current) {
|
||||
intentRef.current.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
const sectionsPayload = JSON.stringify(sections.map((section, index) => ({ ...section, sortOrder: index })));
|
||||
const assetsPayload = JSON.stringify(assets.map((asset, index) => ({ ...asset, sortOrder: index })));
|
||||
@@ -540,6 +556,11 @@ export function PortfolioProjectForm({
|
||||
action={action}
|
||||
className="space-y-8"
|
||||
onSubmit={(event) => {
|
||||
// A draft save skips the completeness gate entirely.
|
||||
if (intentRef.current?.value === "draft") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstIncompleteStep) {
|
||||
return;
|
||||
}
|
||||
@@ -554,6 +575,7 @@ export function PortfolioProjectForm({
|
||||
<input type="hidden" name="viewMode" value={projectState.viewMode} />
|
||||
<input ref={sectionsInputRef} type="hidden" name="sections" value={sectionsPayload} />
|
||||
<input ref={assetsInputRef} type="hidden" name="assets" value={assetsPayload} />
|
||||
<input ref={intentRef} type="hidden" name="intent" defaultValue="save" />
|
||||
|
||||
<AppCard level={3} layer="single" padding="lg" className="space-y-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -1186,14 +1208,15 @@ export function PortfolioProjectForm({
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{firstIncompleteStep ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fill Basics and Localized Content to save. Sections and assets are optional.
|
||||
Fill Basics and Localized Content to publish — or save as a draft anytime.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ready to save. Sections and assets are optional.
|
||||
</p>
|
||||
)}
|
||||
<SubmitButton />
|
||||
<DraftButton onSelect={() => setIntent("draft")} />
|
||||
<SubmitButton onSelect={() => setIntent("save")} />
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
Reference in New Issue
Block a user