FIXED - Make adding a portfolio project simpler and actually saveable

- Fix critical bug: TabsContent unmounted inactive panels, so the project
  form only submitted the active tab's fields and could never save; add an
  opt-in forceMount that keeps panels mounted (hidden) and use it on all
  four project-form tabs
- Make Sections and Assets optional: a project saves with just Basics and
  Localized Content; new projects start with no sections/assets, and the
  last one can now be removed
- Update wizard progress so empty sections/assets steps count as complete
- Update save hint copy and step labels to reflect the optional steps
- Correct .claude/launch.json dev port to 3014
- Update portfolio-form-progress tests for the optional-steps behaviour
This commit is contained in:
moh
2026-09-20 01:56:50 +02:00
parent a13e33edcd
commit d78450fc47
5 changed files with 56 additions and 25 deletions
+14 -16
View File
@@ -165,13 +165,13 @@ const wizardSteps: Array<{
},
{
key: "sections",
label: "Sections",
description: "Build the project story with ordered content blocks.",
label: "Sections (optional)",
description: "Optional — build the project story with ordered content blocks.",
},
{
key: "assets",
label: "Assets & Media",
description: "Cover and gallery assets from the media library.",
label: "Assets & Media (optional)",
description: "Optional — cover and gallery assets from the media library.",
},
];
@@ -233,7 +233,8 @@ function createInitialState(
function createInitialSections(project: PortfolioProjectView | null | undefined): SectionFormValue[] {
if (!project?.sections.length) {
return [createEmptySection(0)];
// Sections are optional — start empty so a project can be saved without them.
return [];
}
return project.sections.map((section, index) => ({
@@ -259,7 +260,8 @@ function createInitialSections(project: PortfolioProjectView | null | undefined)
function createInitialAssets(project: PortfolioProjectView | null | undefined): AssetFormValue[] {
if (!project?.assets.length) {
return [createEmptyAsset(0)];
// Gallery assets are optional — start empty so a project can be saved without them.
return [];
}
return project.assets.map((asset, index) => ({
@@ -732,7 +734,7 @@ export function PortfolioProjectForm({
</AppCard>
<div className="space-y-6">
<TabsContent value="basics" className="mt-0">
<TabsContent value="basics" className="mt-0" forceMount>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -894,7 +896,7 @@ export function PortfolioProjectForm({
</AppCard>
</TabsContent>
<TabsContent value="content" className="mt-0">
<TabsContent value="content" className="mt-0" forceMount>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -945,7 +947,7 @@ export function PortfolioProjectForm({
</AppCard>
</TabsContent>
<TabsContent value="sections" className="mt-0">
<TabsContent value="sections" className="mt-0" forceMount>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -1021,12 +1023,10 @@ export function PortfolioProjectForm({
variant="ghost"
className="text-destructive"
onClick={() =>
sections.length > 1 &&
setSections((current) =>
current.filter((_, currentIndex) => currentIndex !== index),
)
}
disabled={sections.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -1137,7 +1137,7 @@ export function PortfolioProjectForm({
</AppCard>
</TabsContent>
<TabsContent value="assets" className="mt-0">
<TabsContent value="assets" className="mt-0" forceMount>
<AppCard level={3} layer="single" padding="lg">
<div className="space-y-8">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
@@ -1228,12 +1228,10 @@ export function PortfolioProjectForm({
variant="ghost"
className="text-destructive"
onClick={() =>
assets.length > 1 &&
setAssets((current) =>
current.filter((_, currentIndex) => currentIndex !== index),
)
}
disabled={assets.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -1347,11 +1345,11 @@ export function PortfolioProjectForm({
<div className="flex flex-wrap items-center gap-3">
{firstIncompleteStep ? (
<p className="text-sm text-muted-foreground">
Save becomes available after all steps are complete.
Fill Basics and Localized Content to save. Sections and assets are optional.
</p>
) : (
<p className="text-sm text-muted-foreground">
All steps are ready. You can save now.
Ready to save. Sections and assets are optional.
</p>
)}
<SubmitButton />
+7 -1
View File
@@ -98,22 +98,28 @@ function TabsTrigger({
type TabsContentProps = React.HTMLAttributes<HTMLDivElement> & {
value: string;
// Keep the panel mounted while inactive (hidden via `hidden`) so its form
// fields still submit. Use for tabbed forms that share one submit button.
forceMount?: boolean;
};
function TabsContent({
className,
value,
children,
forceMount = false,
...props
}: TabsContentProps) {
const { value: activeValue } = useTabsContext();
const isActive = activeValue === value;
if (activeValue !== value) {
if (!isActive && !forceMount) {
return null;
}
return (
<div
hidden={!isActive}
className={cn(
"mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
className,