fix: preserve landing page section content when toggled off (#838)

* preserve section content when toggled off

* Update types.ts

* Remove comment on LandingPageLinks enabled property

Removed comment about the default visibility of the section.

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Victor Jack
2026-08-11 14:31:58 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 0dc32b5210
commit 6537613000
12 changed files with 102 additions and 101 deletions
+1 -1
View File
@@ -168,7 +168,7 @@ The repository also contains shared packages under `packages/` (for example `pac
PUBLIC_SERVER_URL="http://localhost:3002" PUBLIC_SERVER_URL="http://localhost:3002"
PRIVATE_SERVER_URL="http://localhost:3002" # used by the SSR auth proxy — required, or login returns "API upstream not configured" PRIVATE_SERVER_URL="http://localhost:3002" # used by the SSR auth proxy — required, or login returns "API upstream not configured"
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # must match the API (paste the value generated above) PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # must match the API (paste the value generated above)
PUBLIC_IS_SELFHOSTED=true PUBLIC_IS_SELFHOSTED=false
``` ```
- Running the background workers? Also create `apps/jobs/.env` (e.g. `cp apps/api/.env apps/jobs/.env`); it needs `REDIS_URL`. The `packages/db/.env` used by the DB scripts is set up in step 6. - Running the background workers? Also create `apps/jobs/.env` (e.g. `cp apps/api/.env apps/jobs/.env`); it needs `REDIS_URL`. The `packages/db/.env` used by the DB scripts is set up in step 6.
@@ -125,6 +125,7 @@ type LegacyLandingPageJson = {
footerText?: string; footerText?: string;
theme?: OrgLandingPageTheme; theme?: OrgLandingPageTheme;
embed?: { embed?: {
enabled?: boolean;
title?: string; title?: string;
description?: string; description?: string;
code?: string; code?: string;
@@ -138,6 +139,7 @@ type LegacyLandingPageJson = {
sandbox?: string; sandbox?: string;
}; };
callout?: { callout?: {
enabled?: boolean;
heading?: string; heading?: string;
description?: string; description?: string;
action?: { action?: {
@@ -413,6 +415,7 @@ function normalizeCallout(callout: NonNullable<LegacyLandingPageJson['callout']>
} }
return { return {
enabled: callout.enabled !== false,
heading, heading,
description: normalizeText(callout.description, ''), description: normalizeText(callout.description, ''),
action: { action: {
@@ -468,6 +471,7 @@ function normalizeLinks(raw: unknown): OrgLandingPageLinks | undefined {
const classicLearnMoreLabel = normalizeText(raw.classicLearnMoreLabel, ''); const classicLearnMoreLabel = normalizeText(raw.classicLearnMoreLabel, '');
return { return {
enabled: raw.enabled !== false,
heading, heading,
description: description || undefined, description: description || undefined,
boldVisitLabel: boldVisitLabel || undefined, boldVisitLabel: boldVisitLabel || undefined,
@@ -541,6 +545,7 @@ export function normalizeLandingPageSettings(value: unknown): OrgLandingPageJson
footer: resolveFooterConfig(landingPage), footer: resolveFooterConfig(landingPage),
embed: landingPage.embed embed: landingPage.embed
? { ? {
enabled: landingPage.embed.enabled !== false,
title: normalizeText(landingPage.embed.title, 'Section title'), title: normalizeText(landingPage.embed.title, 'Section title'),
description: normalizeText(landingPage.embed.description, ''), description: normalizeText(landingPage.embed.description, ''),
code: normalizeEmbedCode(landingPage.embed), code: normalizeEmbedCode(landingPage.embed),
@@ -587,6 +592,7 @@ export function normalizeLandingPageSettings(value: unknown): OrgLandingPageJson
footer: resolveFooterConfig(landingPage), footer: resolveFooterConfig(landingPage),
embed: landingPage.embed embed: landingPage.embed
? { ? {
enabled: landingPage.embed.enabled !== false,
title: normalizeText(landingPage.embed.title, 'Section title'), title: normalizeText(landingPage.embed.title, 'Section title'),
description: normalizeText(landingPage.embed.description, ''), description: normalizeText(landingPage.embed.description, ''),
code: normalizeEmbedCode(landingPage.embed) code: normalizeEmbedCode(landingPage.embed)
@@ -624,6 +630,7 @@ export function normalizeLandingPageSettings(value: unknown): OrgLandingPageJson
footer: resolveFooterConfig(landingPage), footer: resolveFooterConfig(landingPage),
embed: landingPage.embed embed: landingPage.embed
? { ? {
enabled: landingPage.embed.enabled !== false,
title: normalizeText(landingPage.embed.title, 'Section title'), title: normalizeText(landingPage.embed.title, 'Section title'),
description: normalizeText(landingPage.embed.description, ''), description: normalizeText(landingPage.embed.description, ''),
code: normalizeEmbedCode(landingPage.embed), code: normalizeEmbedCode(landingPage.embed),
@@ -33,6 +33,7 @@
} }
const callout: OrgLandingPageCallout = { const callout: OrgLandingPageCallout = {
enabled: true,
heading: t.get('settings.landing_page.editor.callout.heading_default'), heading: t.get('settings.landing_page.editor.callout.heading_default'),
description: '', description: '',
action: { action: {
@@ -51,16 +52,7 @@
} }
function setCalloutEnabled(enabled: boolean) { function setCalloutEnabled(enabled: boolean) {
if (!enabled) { setter({ ...ensureCallout(), enabled }, 'callout');
settings = {
...settings,
callout: undefined
};
markDirty();
return;
}
setter(ensureCallout(), 'callout');
} }
</script> </script>
@@ -71,13 +63,13 @@
<div class="space-y-6"> <div class="space-y-6">
<Field.Field orientation="horizontal"> <Field.Field orientation="horizontal">
<Switch <Switch
checked={Boolean(settings.callout)} checked={settings.callout?.enabled ?? false}
onCheckedChange={(checked) => setCalloutEnabled(checked === true)} onCheckedChange={(checked) => setCalloutEnabled(checked === true)}
/> />
<Field.Label>{$t('settings.landing_page.editor.callout.toggle')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.callout.toggle')}</Field.Label>
</Field.Field> </Field.Field>
{#if settings.callout} {#if settings.callout?.enabled}
<Field.Field> <Field.Field>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Field.Label>{$t('settings.landing_page.editor.callout.heading')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.callout.heading')}</Field.Label>
@@ -34,6 +34,7 @@
} }
const embed = { const embed = {
enabled: true,
title: t.get('settings.landing_page.editor.embed.title'), title: t.get('settings.landing_page.editor.embed.title'),
description: '', description: '',
code: '', code: '',
@@ -50,16 +51,7 @@
} }
function setEmbedEnabled(enabled: boolean) { function setEmbedEnabled(enabled: boolean) {
if (!enabled) { setter({ ...ensureEmbed(), enabled }, 'embed');
settings = {
...settings,
embed: undefined
};
markDirty();
return;
}
setter(ensureEmbed(), 'embed');
} }
function updateSecondaryAction(label: string, href: string) { function updateSecondaryAction(label: string, href: string) {
@@ -83,63 +75,68 @@
<div class="space-y-6"> <div class="space-y-6">
<Field.Field orientation="horizontal"> <Field.Field orientation="horizontal">
<Switch checked={Boolean(settings.embed)} onCheckedChange={(checked) => setEmbedEnabled(checked === true)} /> <Switch
checked={settings.embed?.enabled ?? false}
onCheckedChange={(checked) => setEmbedEnabled(checked === true)}
/>
<Field.Label>{$t('settings.landing_page.editor.embed.toggle')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.embed.toggle')}</Field.Label>
</Field.Field> </Field.Field>
{#if settings.embed} {#if settings.embed?.enabled}
<Field.Field> <div class="pb-5">
<div class="flex items-center justify-between"> <Field.Field>
<Field.Label>{$t('settings.landing_page.editor.embed.title')}</Field.Label> <div class="flex items-center justify-between">
<AIGenerateButton <Field.Label>{$t('settings.landing_page.editor.embed.title')}</Field.Label>
context="the embed section title on an organization's course platform landing page" <AIGenerateButton
onInsert={(text) => setter(text, 'embed.title')} context="the embed section title on an organization's course platform landing page"
onInsert={(text) => setter(text, 'embed.title')}
/>
</div>
<Input
value={settings.embed.title}
placeholder={$t('settings.landing_page.editor.embed.title_placeholder')}
oninput={(event) => setter(event.currentTarget.value, 'embed.title')}
/> />
</div> </Field.Field>
<Input
value={settings.embed.title}
placeholder={$t('settings.landing_page.editor.embed.title_placeholder')}
oninput={(event) => setter(event.currentTarget.value, 'embed.title')}
/>
</Field.Field>
<Field.Field class="min-w-0"> <Field.Field class="min-w-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Field.Label>{$t('settings.landing_page.editor.embed.description')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.embed.description')}</Field.Label>
<AIGenerateButton <AIGenerateButton
context="the embed section description on an organization's course platform landing page" context="the embed section description on an organization's course platform landing page"
onInsert={(text) => setter(text, 'embed.description')} onInsert={(text) => setter(text, 'embed.description')}
/>
</div>
<Textarea
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y ui:break-all ui:overflow-x-auto"
value={settings.embed.description ?? ''}
placeholder={$t('settings.landing_page.editor.embed.description_placeholder')}
oninput={(event) => setter(event.currentTarget.value, 'embed.description')}
/> />
</div> </Field.Field>
<Textarea
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y ui:break-all ui:overflow-x-auto"
value={settings.embed.description ?? ''}
placeholder={$t('settings.landing_page.editor.embed.description_placeholder')}
oninput={(event) => setter(event.currentTarget.value, 'embed.description')}
/>
</Field.Field>
<Field.Field class="min-w-0"> <Field.Field class="min-w-0">
<Field.Label>{$t('settings.landing_page.editor.embed.code')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.embed.code')}</Field.Label>
<Field.Description>{$t('settings.landing_page.editor.embed.code_description')}</Field.Description> <Field.Description>{$t('settings.landing_page.editor.embed.code_description')}</Field.Description>
<Textarea <Textarea
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y ui:break-all ui:overflow-x-auto" class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y ui:break-all ui:overflow-x-auto"
value={settings.embed.code} value={settings.embed.code}
placeholder={$t('settings.landing_page.editor.embed.code_placeholder')} placeholder={$t('settings.landing_page.editor.embed.code_placeholder')}
oninput={(event) => setter(event.currentTarget.value, 'embed.code')} oninput={(event) => setter(event.currentTarget.value, 'embed.code')}
/> />
</Field.Field> </Field.Field>
<ButtonLinkFields <ButtonLinkFields
label={settings.embed.secondaryAction?.label ?? ''} label={settings.embed.secondaryAction?.label ?? ''}
href={settings.embed.secondaryAction?.href ?? ''} href={settings.embed.secondaryAction?.href ?? ''}
labelFieldLabel={$t('settings.landing_page.editor.embed.secondary_label')} labelFieldLabel={$t('settings.landing_page.editor.embed.secondary_label')}
hrefFieldLabel={$t('settings.landing_page.editor.embed.secondary_href')} hrefFieldLabel={$t('settings.landing_page.editor.embed.secondary_href')}
labelPlaceholder={$t('settings.landing_page.editor.embed.secondary_label')} labelPlaceholder={$t('settings.landing_page.editor.embed.secondary_label')}
hrefPlaceholder={$t('settings.landing_page.editor.embed.secondary_href')} hrefPlaceholder={$t('settings.landing_page.editor.embed.secondary_href')}
onLabelInput={(value) => updateSecondaryAction(value, settings.embed.secondaryAction?.href ?? '')} onLabelInput={(value) => updateSecondaryAction(value, settings.embed.secondaryAction?.href ?? '')}
onHrefInput={(value) => updateSecondaryAction(settings.embed.secondaryAction?.label ?? '', value)} onHrefInput={(value) => updateSecondaryAction(settings.embed.secondaryAction?.label ?? '', value)}
/> />
</div>
{/if} {/if}
</div> </div>
</Field.Set> </Field.Set>
@@ -54,6 +54,7 @@
} }
const links: NonNullable<OrgLandingPageJson['links']> = { const links: NonNullable<OrgLandingPageJson['links']> = {
enabled: true,
heading: t.get('settings.landing_page.editor.links.heading_default'), heading: t.get('settings.landing_page.editor.links.heading_default'),
description: '', description: '',
boldVisitLabel: t.get('settings.landing_page.editor.links.bold_visit_default'), boldVisitLabel: t.get('settings.landing_page.editor.links.bold_visit_default'),
@@ -90,16 +91,11 @@
} }
function setLinksEnabled(enabled: boolean) { function setLinksEnabled(enabled: boolean) {
if (!enabled) { settings = {
settings = { ...settings,
...settings, links: { ...ensureLinks(), enabled }
links: undefined };
}; markDirty();
markDirty();
return;
}
ensureLinks();
} }
function addCard() { function addCard() {
@@ -137,11 +133,14 @@
<div class="space-y-6"> <div class="space-y-6">
<Field.Field orientation="horizontal"> <Field.Field orientation="horizontal">
<Switch checked={Boolean(settings.links)} onCheckedChange={(checked) => setLinksEnabled(checked === true)} /> <Switch
checked={settings.links?.enabled ?? false}
onCheckedChange={(checked) => setLinksEnabled(checked === true)}
/>
<Field.Label>{$t('settings.landing_page.editor.links.toggle')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.links.toggle')}</Field.Label>
</Field.Field> </Field.Field>
{#if settings.links} {#if settings.links?.enabled}
<Field.Field> <Field.Field>
<Field.Label>{$t('settings.landing_page.editor.links.heading')}</Field.Label> <Field.Label>{$t('settings.landing_page.editor.links.heading')}</Field.Label>
<Input <Input
@@ -50,6 +50,7 @@ export type OrgLandingPageFooterConfig = {
}; };
export interface OrgLandingPageEmbed { export interface OrgLandingPageEmbed {
enabled: boolean;
title: string; title: string;
description?: string; description?: string;
code: string; code: string;
@@ -60,6 +61,7 @@ export interface OrgLandingPageEmbed {
} }
export interface OrgLandingPageCallout { export interface OrgLandingPageCallout {
enabled: boolean;
heading: string; heading: string;
description: string; description: string;
action: { action: {
@@ -88,6 +90,7 @@ export interface OrgLandingPageLinkCard {
} }
export interface OrgLandingPageLinks { export interface OrgLandingPageLinks {
enabled: boolean;
heading: string; heading: string;
description?: string; description?: string;
boldVisitLabel?: string; boldVisitLabel?: string;
+11 -11
View File
@@ -152,17 +152,17 @@ Composable page shell used across dashboard list and settings screens. Import as
The bar also sits at `z-50`, matching the editor root so it paints above editor content (later in DOM wins the tie) while staying below body-portaled dialogs and sheets. Its shadow appears only while it is pinned away from its flow position, tracked by an `IntersectionObserver` on a 1px sentinel rendered as its next sibling. The bar also sits at `z-50`, matching the editor root so it paints above editor content (later in DOM wins the tie) while staying below body-portaled dialogs and sheets. Its shadow appears only while it is pinned away from its flow position, tracked by an `IntersectionObserver` on a 1px sentinel rendered as its next sibling.
| Prop | Description | | Prop | Description |
| -------------- | -------------------------------------------------------- | | -------------- | ------------------------------------------------------ |
| `hasChanges` | Enables Save and Discard when the form is dirty | | `hasChanges` | Enables Save and Discard when the form is dirty |
| `loading` | Shows loading state on Save | | `loading` | Shows loading state on Save |
| `disabled` | Disables Save only (e.g. while not initialized) | | `disabled` | Disables Save only (e.g. while not initialized) |
| `contentClass` | Extra classes on the inner card (width, padding, etc.) | | `contentClass` | Extra classes on the inner card (width, padding, etc.) |
| `statusLabel` | Left-side status text (e.g. "Unsaved changes") | | `statusLabel` | Left-side status text (e.g. "Unsaved changes") |
| `discardLabel` | Discard button label | | `discardLabel` | Discard button label |
| `saveLabel` | Save button label | | `saveLabel` | Save button label |
| `onSave` | Save handler | | `onSave` | Save handler |
| `onDiscard` | Discard handler | | `onDiscard` | Discard handler |
```svelte ```svelte
<Page.Root> <Page.Root>
@@ -107,7 +107,7 @@
`; `;
</script> </script>
{#if callout} {#if callout && callout.enabled !== false}
<EditableLandingSection sectionKey="callout"> <EditableLandingSection sectionKey="callout">
<section <section
class={sectionClasses[variant]} class={sectionClasses[variant]}
@@ -15,7 +15,7 @@
} = $props(); } = $props();
</script> </script>
{#if embed} {#if embed && embed.enabled !== false}
<EditableLandingSection sectionKey="embed"> <EditableLandingSection sectionKey="embed">
{#if variant === 'terminal'} {#if variant === 'terminal'}
<section class="ui:py-24 ui:px-6" style="border-top: 1px solid #1c1f28; background: #06070a;"> <section class="ui:py-24 ui:px-6" style="border-top: 1px solid #1c1f28; background: #06070a;">
@@ -33,7 +33,7 @@
} }
</script> </script>
{#if links && links.cards.length > 0} {#if links && links.enabled !== false && links.cards.length > 0}
<EditableLandingSection sectionKey="links"> <EditableLandingSection sectionKey="links">
{#if variant === 'minimal'} {#if variant === 'minimal'}
<section class="ui:py-20 ui:px-4"> <section class="ui:py-20 ui:px-4">
@@ -81,17 +81,17 @@
</div> </div>
</section> </section>
{#if links && links.cards.length > 0} {#if links && links.enabled !== false && links.cards.length > 0}
<div class="plus-row"></div> <div class="plus-row"></div>
<OrgLandingPageLinks {links} {labels} variant="saas" /> <OrgLandingPageLinks {links} {labels} variant="saas" />
{/if} {/if}
{#if embed} {#if embed && embed.enabled !== false}
<div class="plus-row"></div> <div class="plus-row"></div>
<OrgLandingPageEmbed {embed} {labels} variant="saas" /> <OrgLandingPageEmbed {embed} {labels} variant="saas" />
{/if} {/if}
{#if callout} {#if callout && callout.enabled !== false}
<div class="plus-row"></div> <div class="plus-row"></div>
<OrgLandingPageCallout {callout} {labels} variant="saas" /> <OrgLandingPageCallout {callout} {labels} variant="saas" />
{/if} {/if}
@@ -91,6 +91,7 @@ export type OrgLandingPageFooterConfig = {
}; };
}; };
export type LandingPageEmbed = { export type LandingPageEmbed = {
enabled?: boolean;
title: string; title: string;
description?: string; description?: string;
code: string; code: string;
@@ -101,6 +102,7 @@ export type LandingPageEmbed = {
}; };
export type LandingPageCallout = { export type LandingPageCallout = {
enabled?: boolean;
heading: string; heading: string;
description: string; description: string;
action: { action: {
@@ -129,6 +131,7 @@ export type LandingPageLinkCard = {
}; };
export type LandingPageLinks = { export type LandingPageLinks = {
enabled?: boolean;
heading: string; heading: string;
description?: string; description?: string;
/** Bold template card footer chip (e.g. Visit). */ /** Bold template card footer chip (e.g. Visit). */