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:
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user