diff --git a/src/backend/define_config.ts b/src/backend/define_config.ts index 2cf966dc..df8acca0 100644 --- a/src/backend/define_config.ts +++ b/src/backend/define_config.ts @@ -57,6 +57,9 @@ export function defineConfig(config: Partial): CmsConfig { storiesHasEditReview: config.storiesHasEditReview === undefined ? false : config.storiesHasEditReview, + storiesHasSections: + config.storiesHasSections === undefined ? false : config.storiesHasSections, + storyTemplates: config.storyTemplates || [], } satisfies CmsConfig; } @@ -72,18 +75,32 @@ export interface mediaConfig { maxSize: number; } -export function courseFields(video: mediaConfig, image: mediaConfig): FieldSpec[] { +export interface CourseFieldsOptions { + hasSections?: boolean; +} + +export function courseFields( + video: mediaConfig, + image: mediaConfig, + options: CourseFieldsOptions = {}, +): FieldSpec[] { + const { hasSections = false } = options; + return [ { name: 'title', label: 'Title', widget: 'string', }, - { - name: 'section', - label: 'Section', - widget: 'string', - }, + ...(hasSections + ? [ + { + name: 'section', + label: 'Section', + widget: 'string', + } as FieldSpec, + ] + : []), { name: 'imageUrl', label: 'Cover Image', diff --git a/src/backend/services/cms_service.ts b/src/backend/services/cms_service.ts index 0d2bdd86..053179b7 100644 --- a/src/backend/services/cms_service.ts +++ b/src/backend/services/cms_service.ts @@ -136,6 +136,7 @@ export class CmsService { helpUrl: this.#config.helpUrl, supportEmail: this.#config.supportEmail, hasAppPreview: this.#config.hasAppPreview, + storiesHasSections: this.#config.storiesHasSections, videoCollectionId: this.#config.videoCollectionId, languages: this.#config.languages, subscriptions: this.#config.subscriptions, diff --git a/src/backend/services/story_service.ts b/src/backend/services/story_service.ts index 355c992b..6f69461f 100644 --- a/src/backend/services/story_service.ts +++ b/src/backend/services/story_service.ts @@ -181,10 +181,6 @@ export class StoryService { target.coverImage = source.coverImage; } - if (locale !== sourceLocale && !target.tags) { - target.tags = source.tags; - } - const resourceService = new ResourceService(); const availableResources = await resourceService.listForLocale(locale); const targetFields = this.localisationFields(target); @@ -246,10 +242,6 @@ export class StoryService { target.coverImage = source.coverImage; } - if (locale !== sourceLocale && !target.tags) { - target.tags = source.tags; - } - const targetFields = this.localisationFields(target); return { diff --git a/src/backend/stubs/config/cms.stub b/src/backend/stubs/config/cms.stub index ef8df06e..bf96703e 100644 --- a/src/backend/stubs/config/cms.stub +++ b/src/backend/stubs/config/cms.stub @@ -1,8 +1,7 @@ {{{ exports({ to: app.configPath('cms.ts') }) }}} -import type { FieldSpec, CmsConfig, mediaConfig } from '@story-cms/kit'; -import { cou +import type { FieldSpec, CmsConfig, mediaConfig, StreamSpec, courseFields } from '@story-cms/kit'; // --------------------------------------- // Media upload configurations @@ -68,6 +67,8 @@ const devotionStream: StreamSpec = { // CMS configuration // --------------------------------------- +const storiesHasSections = false; + const cmsConfig: Partial = { /** * The name of the app @@ -125,6 +126,11 @@ const cmsConfig: Partial = { */ storiesHasEditReview: false, + /** + * Flag to enable the Sections tab and section type field on story create/edit + */ + storiesHasSections, + bespokeTemplates: [ { id: 'daily-grace', @@ -141,7 +147,9 @@ const cmsConfig: Partial = { { id: 'course', name: 'Course', - fields: [courseFields(videoUploadConfig, imageUploadConfig)], + fields: courseFields(videoUploadConfig, imageUploadConfig, { + hasSections: storiesHasSections, + }), }, ], diff --git a/src/backend/stubs/tests/helpers/cms_mock.stub b/src/backend/stubs/tests/helpers/cms_mock.stub index 6631a3e5..9473c0fd 100644 --- a/src/backend/stubs/tests/helpers/cms_mock.stub +++ b/src/backend/stubs/tests/helpers/cms_mock.stub @@ -53,6 +53,7 @@ export const testCmsConfig: CmsConfig = { }, ], storiesHasEditReview: false, + storiesHasSections: false, bespokeTemplates: [ { id: 'test-story', diff --git a/src/backend/stubs/tests/unit/story_service.stub b/src/backend/stubs/tests/unit/story_service.stub index 0b2aa8ef..daae9080 100644 --- a/src/backend/stubs/tests/unit/story_service.stub +++ b/src/backend/stubs/tests/unit/story_service.stub @@ -171,7 +171,7 @@ test.group('Story service', (group) => { assert.isUndefined(props); }); - test('editProps prefills tags from source when translation tags are empty', async ({ + test('editProps does not prefill tags from source when translation tags are empty', async ({ assert, }) => { const story = await StoryFactory.with('localisations').create(); @@ -188,7 +188,7 @@ test.group('Story service', (group) => { ); assert.isDefined(props); - assert.equal(props!.model.tags, 'gospel,john'); + assert.equal(props!.model.tags, ''); assert.equal(props!.source!.tags, 'gospel,john'); }); diff --git a/src/backend/validators/course.ts b/src/backend/validators/course.ts index 2a663e48..78172734 100644 --- a/src/backend/validators/course.ts +++ b/src/backend/validators/course.ts @@ -1,4 +1,5 @@ import vine, { SimpleMessagesProvider } from '@vinejs/vine'; +import type { CourseFieldsOptions } from '../define_config.js'; import videoRule from '../validators/video_rule.js'; import type { ValidatorType } from '../../types'; @@ -20,11 +21,16 @@ const screenSchema = vine.object({ }); export class CourseValidator implements ValidatorType { + #hasSections: boolean; + + constructor(options: CourseFieldsOptions = {}) { + this.#hasSections = options.hasSections === true; + } + validate(data: any): Promise { vine.messagesProvider = new SimpleMessagesProvider({ 'bundle.title.required': 'The chapter must have a title', 'bundle.section.required': 'The chapter must have a section', - 'bundle.imageUrl.required': 'The chapter must have a cover image', 'bundle.screens.required': 'The chapter must have at least one screen', 'bundle.screens.*.screenName.required': 'Each screen must have a name', 'bundle.screens.*.sessionVideo.videoSchema': 'Please upload a valid video file', @@ -33,8 +39,8 @@ export class CourseValidator implements ValidatorType { const schema = vine.create({ bundle: vine.object({ title: vine.string(), - section: vine.string(), - imageUrl: vine.string(), + ...(this.#hasSections ? { section: vine.string() } : {}), + imageUrl: vine.string().optional(), screens: vine.array(screenSchema).minLength(1), }), }); diff --git a/src/frontend/stories/components/story-edit-details.story.vue b/src/frontend/stories/components/story-edit-details.story.vue index 4fa27ee2..4d97a538 100644 --- a/src/frontend/stories/components/story-edit-details.story.vue +++ b/src/frontend/stories/components/story-edit-details.story.vue @@ -63,7 +63,7 @@ const translationLocaleModel = { title: '', coverImage: '', description: '', - tags: 'gospel,john', + tags: '', }; const translationWithoutTagsModel = { @@ -167,7 +167,7 @@ source locale only) classification fields, chapter template, and visibility. ## Variants - **Source locale** — single-column editable fields including chapter count and visibility -- **Translation locale** — side-by-side editable and read-only columns; tags prefilled from source when empty +- **Translation locale** — side-by-side editable and read-only columns; source tags shown in read-only column only - **Translation without tags** — translation mode where the source locale has no tags; both editable and read-only tag fields are empty - **Single template** — shows read-only Chapter Template when only one template is configured - **Multiple templates on edit** — read-only Chapter Template even when multiple templates are configured diff --git a/src/frontend/stories/components/story-edit-details.vue b/src/frontend/stories/components/story-edit-details.vue index c649a424..63de3657 100644 --- a/src/frontend/stories/components/story-edit-details.vue +++ b/src/frontend/stories/components/story-edit-details.vue @@ -205,7 +205,7 @@ const tagsField: FieldSpec = { placeholderText: 'Add tags...', }; -const contentClassificationField: FieldSpec = { +const contentClassificationField = computed((): FieldSpec => ({ label: 'Content Classification', name: 'contentClassification', widget: 'panel', @@ -217,11 +217,15 @@ const contentClassificationField: FieldSpec = { description: "The overall theme or category of your course (e.g., 'Educational', 'Devotional', 'Bible Study').", }, - { - title: 'Section Type', - description: - "How the course is divided into major sections (e.g., 'Weekly', 'Daily', 'Module').", - }, + ...(shared.config.storiesHasSections + ? [ + { + title: 'Section Type', + description: + "How the course is divided into major sections (e.g., 'Weekly', 'Daily', 'Module').", + }, + ] + : []), { title: 'Chapter Type', description: @@ -238,12 +242,16 @@ const contentClassificationField: FieldSpec = { widget: 'string', placeholderText: 'e.g., Course, Devotional, Plan', }, - { - label: 'Section Type', - name: 'sectionType', - widget: 'string', - placeholderText: 'e.g., Part, Module', - }, + ...(shared.config.storiesHasSections + ? [ + { + label: 'Section Type', + name: 'sectionType', + widget: 'string', + placeholderText: 'e.g., Part, Module', + }, + ] + : []), { label: 'Chapter Type', name: 'chapterType', @@ -251,5 +259,5 @@ const contentClassificationField: FieldSpec = { placeholderText: 'e.g., Session, Devotion, Day', }, ], -}; +})); diff --git a/src/frontend/stories/story-create.story.vue b/src/frontend/stories/story-create.story.vue index 619e4559..a515a605 100644 --- a/src/frontend/stories/story-create.story.vue +++ b/src/frontend/stories/story-create.story.vue @@ -30,7 +30,7 @@ { # Story Create -Full-page form for creating a new story. Tabbed navigation for Details, Sections, and -Resources. Collects title, cover image, classification fields, optional sections and -attached resources, and chapter template when multiple templates are available. +Full-page form for creating a new story. Tabbed navigation for Details and Resources by default; Sections and the section type field appear when `config.storiesHasSections` is enabled. +Collects title, cover image, classification fields, optional sections and attached resources, and chapter template when multiple templates are available. In production, this page is reached from the Story Gallery **+** button when `multi-story` is included in `config.subscriptions`. See [Story Gallery](./story-gallery.story.vue). diff --git a/src/frontend/stories/story-create.vue b/src/frontend/stories/story-create.vue index fb992929..1d020e6b 100644 --- a/src/frontend/stories/story-create.vue +++ b/src/frontend/stories/story-create.vue @@ -27,7 +27,9 @@ :templates="props.templates" /> @@ -103,38 +105,62 @@ model.$subscribe(() => { const headerSubtitle = computed(() => title.value?.trim() || 'Create Story'); +const hasSections = computed(() => shared.config.storiesHasSections); + const sectionTabLabel = computed(() => `${sectionType.value ?? 'Section'}s`); -const storyEditTabs = computed((): NavigationPaneTab[] => [ - { - label: 'Details', - hasError: storyEditTabHasError('details', errors.value), - }, - { - label: sectionTabLabel.value, - hasError: storyEditTabHasError('sections', errors.value), - }, - { +const storyEditTabs = computed((): NavigationPaneTab[] => { + const tabs: NavigationPaneTab[] = [ + { + label: 'Details', + hasError: storyEditTabHasError('details', errors.value, hasSections.value), + }, + ]; + + if (hasSections.value) { + tabs.push({ + label: sectionTabLabel.value, + hasError: storyEditTabHasError('sections', errors.value, hasSections.value), + }); + } + + tabs.push({ label: 'Resources', - hasError: storyEditTabHasError('resources', errors.value), - }, -]); - -const storyEditTabIcons = computed(() => ({ - Details: BookOpen, - [sectionTabLabel.value]: LayoutList, - Resources: FolderClosed, -})); - -const initialSectionType = props.model.sectionType || 'Section'; -const initialTabs: NavigationPaneTab[] = [ - { label: 'Details' }, - { label: `${initialSectionType}s` }, - { label: 'Resources' }, -]; + hasError: storyEditTabHasError('resources', errors.value, hasSections.value), + }); + + return tabs; +}); + +const storyEditTabIcons = computed(() => { + const icons: Record = { + Details: BookOpen, + Resources: FolderClosed, + }; + + if (hasSections.value) { + icons[sectionTabLabel.value] = LayoutList; + } + + return icons; +}); + +const buildInitialTabs = (): NavigationPaneTab[] => { + const tabs: NavigationPaneTab[] = [{ label: 'Details' }]; + + if (props.config.storiesHasSections) { + tabs.push({ label: `${props.model.sectionType || 'Section'}s` }); + } + + tabs.push({ label: 'Resources' }); + return tabs; +}; const currentStoryTab = ref( - resolveStoryTab(new URLSearchParams(window.location.search).get('tab'), initialTabs), + resolveStoryTab( + new URLSearchParams(window.location.search).get('tab'), + buildInitialTabs(), + ), ); const currentStoryTabIcon = computed( @@ -146,7 +172,11 @@ const onStoryTabChange = (tab: string) => { }; const focusFirstErroredTab = () => { - const tab = firstStoryEditTabWithError(errors.value, sectionTabLabel.value); + const tab = firstStoryEditTabWithError( + errors.value, + sectionTabLabel.value, + hasSections.value, + ); if (tab) { currentStoryTab.value = tab; } diff --git a/src/frontend/stories/story-edit-tab-errors.ts b/src/frontend/stories/story-edit-tab-errors.ts index 282ce3b0..2596df55 100644 --- a/src/frontend/stories/story-edit-tab-errors.ts +++ b/src/frontend/stories/story-edit-tab-errors.ts @@ -1,16 +1,25 @@ export type StoryEditTab = 'details' | 'sections' | 'resources'; -const STORY_EDIT_TAB_ORDER: StoryEditTab[] = ['details', 'sections', 'resources']; - -const STORY_EDIT_TAB_LABELS: Record string)> = { +const STORY_EDIT_TAB_LABELS: Record< + StoryEditTab, + string | ((sectionTabLabel: string) => string) +> = { details: 'Details', sections: (sectionTabLabel) => sectionTabLabel, resources: 'Resources', }; -function errorKeyBelongsToTab(key: string, tab: StoryEditTab): boolean { +function storyEditTabOrder(hasSections: boolean): StoryEditTab[] { + return hasSections ? ['details', 'sections', 'resources'] : ['details', 'resources']; +} + +function errorKeyBelongsToTab( + key: string, + tab: StoryEditTab, + hasSections: boolean, +): boolean { if (tab === 'sections') { - return key.startsWith('bundle.sections'); + return hasSections && key.startsWith('bundle.sections'); } if (tab === 'resources') { return key.startsWith('bundle.resources'); @@ -25,16 +34,20 @@ function errorKeyBelongsToTab(key: string, tab: StoryEditTab): boolean { export function storyEditTabHasError( tab: StoryEditTab, errors: Record, + hasSections = true, ): boolean { - return Object.keys(errors).some((key) => errorKeyBelongsToTab(key, tab)); + if (!hasSections && tab === 'sections') return false; + + return Object.keys(errors).some((key) => errorKeyBelongsToTab(key, tab, hasSections)); } export function firstStoryEditTabWithError( errors: Record, sectionTabLabel: string, + hasSections = true, ): string | null { - for (const tab of STORY_EDIT_TAB_ORDER) { - if (!storyEditTabHasError(tab, errors)) continue; + for (const tab of storyEditTabOrder(hasSections)) { + if (!storyEditTabHasError(tab, errors, hasSections)) continue; const label = STORY_EDIT_TAB_LABELS[tab]; return typeof label === 'function' ? label(sectionTabLabel) : label; diff --git a/src/frontend/stories/story-edit.story.vue b/src/frontend/stories/story-edit.story.vue index 9db7c28a..76634f5b 100644 --- a/src/frontend/stories/story-edit.story.vue +++ b/src/frontend/stories/story-edit.story.vue @@ -77,7 +77,7 @@ { # Story Edit -Full-page story editor with tabbed navigation for Details, Sections, and Resources. +Full-page story editor with tabbed navigation for Details, Resources, and optionally Sections when `config.storiesHasSections` is enabled. Saving posts attached resource IDs to the story localisation. ## Variants -- **Save changes** — published story with content; shows Save Changes only (`secondary`) +- **Save changes** — published story with content; shows Save Changes only (`secondary`); Sections tab hidden by default - **Delete and save** — empty story, published; shows Delete (`red`) and Save Changes (`secondary`) - **Delete and save draft** — empty story, unpublished; shows Delete (`red`) and Save Changes (`secondary`) -- **Without sections** — English source locale with no sections; Sections tab shows empty state message -- **Validation errors on tabs** — failed publish validation; Details and Sections tabs show error indicators and inline field messages -- **Translation locale** — Spanish edit with English source column; tags prefilled from source on Details tab -- **Arabic translation locale** — Arabic edit on the left, English source on the right; RTL text in the translation column; side-by-side fields on Details and Sections tabs -- **Translation without sections** — Spanish edit with English source column; source localisation has no sections, so the Sections tab is empty +- **Without sections** — `storiesHasSections: true`; English source locale with no sections; Sections tab shows empty state message +- **Validation errors on tabs** — `storiesHasSections: true`; failed publish validation; Details and Sections tabs show error indicators and inline field messages +- **Translation locale** — Spanish edit with English source column; source tags shown in read-only column only on Details tab +- **Arabic translation locale** — `storiesHasSections: true`; Arabic edit on the left, English source on the right; RTL text in the translation column; side-by-side fields on Details and Sections tabs +- **Translation without sections** — `storiesHasSections: true`; Spanish edit with English source column; source localisation has no sections, so the Sections tab is empty ## Usage diff --git a/src/frontend/stories/story-edit.vue b/src/frontend/stories/story-edit.vue index cd52ccf3..6b0b026a 100644 --- a/src/frontend/stories/story-edit.vue +++ b/src/frontend/stories/story-edit.vue @@ -35,7 +35,9 @@ :templates="props.templates" /> @@ -124,38 +126,62 @@ model.$subscribe(() => { const headerSubtitle = computed(() => title.value?.trim() || 'Edit Story'); +const hasSections = computed(() => shared.config.storiesHasSections); + const sectionTabLabel = computed(() => `${sectionType.value ?? 'Section'}s`); -const storyEditTabs = computed((): NavigationPaneTab[] => [ - { - label: 'Details', - hasError: storyEditTabHasError('details', errors.value), - }, - { - label: sectionTabLabel.value, - hasError: storyEditTabHasError('sections', errors.value), - }, - { +const storyEditTabs = computed((): NavigationPaneTab[] => { + const tabs: NavigationPaneTab[] = [ + { + label: 'Details', + hasError: storyEditTabHasError('details', errors.value, hasSections.value), + }, + ]; + + if (hasSections.value) { + tabs.push({ + label: sectionTabLabel.value, + hasError: storyEditTabHasError('sections', errors.value, hasSections.value), + }); + } + + tabs.push({ label: 'Resources', - hasError: storyEditTabHasError('resources', errors.value), - }, -]); - -const storyEditTabIcons = computed(() => ({ - Details: BookOpen, - [sectionTabLabel.value]: LayoutList, - Resources: FolderClosed, -})); - -const initialSectionType = props.model.sectionType || 'Section'; -const initialTabs: NavigationPaneTab[] = [ - { label: 'Details' }, - { label: `${initialSectionType}s` }, - { label: 'Resources' }, -]; + hasError: storyEditTabHasError('resources', errors.value, hasSections.value), + }); + + return tabs; +}); + +const storyEditTabIcons = computed(() => { + const icons: Record = { + Details: BookOpen, + Resources: FolderClosed, + }; + + if (hasSections.value) { + icons[sectionTabLabel.value] = LayoutList; + } + + return icons; +}); + +const buildInitialTabs = (): NavigationPaneTab[] => { + const tabs: NavigationPaneTab[] = [{ label: 'Details' }]; + + if (props.config.storiesHasSections) { + tabs.push({ label: `${props.model.sectionType || 'Section'}s` }); + } + + tabs.push({ label: 'Resources' }); + return tabs; +}; const currentStoryTab = ref( - resolveStoryTab(new URLSearchParams(window.location.search).get('tab'), initialTabs), + resolveStoryTab( + new URLSearchParams(window.location.search).get('tab'), + buildInitialTabs(), + ), ); const currentStoryTabIcon = computed( @@ -167,7 +193,11 @@ const onStoryTabChange = (tab: string) => { }; const focusFirstErroredTab = () => { - const tab = firstStoryEditTabWithError(errors.value, sectionTabLabel.value); + const tab = firstStoryEditTabWithError( + errors.value, + sectionTabLabel.value, + hasSections.value, + ); if (tab) { currentStoryTab.value = tab; } diff --git a/src/frontend/test/mocks.ts b/src/frontend/test/mocks.ts index 607993b4..f0fd8a8e 100644 --- a/src/frontend/test/mocks.ts +++ b/src/frontend/test/mocks.ts @@ -1728,6 +1728,7 @@ export const config: UiConfig = { logo: 'https://res.cloudinary.com/theword121/image/upload/v1687245360/episodes/viseg2hegowcrapio6pt.svg', helpUrl: 'https://www.theword121.com/', hasAppPreview: false, + storiesHasSections: false, videoCollectionId: 'temporary-id', languages, subscriptions: [ diff --git a/src/types.ts b/src/types.ts index 3a71c568..adcb4c90 100644 --- a/src/types.ts +++ b/src/types.ts @@ -387,6 +387,7 @@ export interface StoryEditProps { title: string; coverImage: string; description: string; + tags: string | null; sections: StorySection[]; resources: ResourceItem[]; }; @@ -706,6 +707,7 @@ export type CmsConfig = { // streamTemplates: BundleTemplate[]; storiesHasEditReview: boolean; + storiesHasSections: boolean; storyTemplates: BundleTemplate[]; }; @@ -720,6 +722,7 @@ export interface UiConfig { helpUrl: string; supportEmail: string; hasAppPreview: boolean; + storiesHasSections: boolean; videoCollectionId: string; languages: LanguageSpecification[]; subscriptions: Subscription[]; diff --git a/tests/unit/course.spec.ts b/tests/unit/course.spec.ts new file mode 100644 index 00000000..04d921cb --- /dev/null +++ b/tests/unit/course.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { CourseValidator } from '../../src/backend/validators/course.js'; +import { courseFields } from '../../src/backend/define_config.js'; + +const videoConfig = { + collection: 'test-video', + description: 'MP4 files', + extensions: ['.mp4'], + maxSize: 1000, +}; + +const imageConfig = { + collection: 'test-image', + description: 'PNG files', + extensions: ['.png'], + maxSize: 1000, +}; + +const validBundle = () => ({ + bundle: { + title: 'Introduction to Alongsiding', + imageUrl: 'https://example.com/cover.png', + screens: [ + { + screenName: 'Welcome', + }, + ], + }, +}); + +const validBundleWithSection = () => ({ + bundle: { + ...validBundle().bundle, + section: 'Module 1', + }, +}); + +test.describe('CourseValidator', () => { + test('accepts bundle without section by default', async () => { + const validator = new CourseValidator(); + const result = await validator.validate(validBundle()); + + expect(result.bundle.title).toBe('Introduction to Alongsiding'); + expect(result.bundle.section).toBeUndefined(); + }); + + test('rejects missing section when hasSections is true', async () => { + const validator = new CourseValidator({ hasSections: true }); + + await expect(validator.validate(validBundle())).rejects.toThrow(); + }); + + test('accepts valid bundle with section when hasSections is true', async () => { + const validator = new CourseValidator({ hasSections: true }); + const result = await validator.validate(validBundleWithSection()); + + expect(result.bundle.section).toBe('Module 1'); + }); + + test('accepts bundle without imageUrl', async () => { + const validator = new CourseValidator(); + const { title, screens } = validBundle().bundle; + const result = await validator.validate({ bundle: { title, screens } }); + + expect(result.bundle.title).toBe('Introduction to Alongsiding'); + expect(result.bundle.imageUrl).toBeUndefined(); + }); +}); + +test.describe('courseFields', () => { + test('omits section field when hasSections is false', () => { + const fields = courseFields(videoConfig, imageConfig, { hasSections: false }); + + expect(fields.some((field) => field.name === 'section')).toBe(false); + }); + + test('includes section field when hasSections is true', () => { + const fields = courseFields(videoConfig, imageConfig, { hasSections: true }); + + expect(fields.some((field) => field.name === 'section')).toBe(true); + }); +}); diff --git a/tests/unit/draftService.spec.ts b/tests/unit/draftService.spec.ts index 49a9710a..c4a537c5 100644 --- a/tests/unit/draftService.spec.ts +++ b/tests/unit/draftService.spec.ts @@ -53,6 +53,7 @@ function createMockCmsConfig(sourceLocale: string = 'en'): CmsConfig { ], streams: [], storiesHasEditReview: false, + storiesHasSections: false, storyTemplates: [], bespokeTemplates: [], pagesTracking: '', diff --git a/tests/unit/story-edit-tab-errors.spec.ts b/tests/unit/story-edit-tab-errors.spec.ts index 348ee566..7afae1e4 100644 --- a/tests/unit/story-edit-tab-errors.spec.ts +++ b/tests/unit/story-edit-tab-errors.spec.ts @@ -49,4 +49,18 @@ test.describe('firstStoryEditTabWithError', () => { test('returns null when there are no errors', () => { expect(firstStoryEditTabWithError({}, 'Chapters')).toBeNull(); }); + + test('returns null for section errors when sections are disabled', () => { + const errors = { 'bundle.sections.0.title': ['Section title is required'] }; + + expect(firstStoryEditTabWithError(errors, 'Chapters', false)).toBeNull(); + }); +}); + +test.describe('storyEditTabHasError with sections disabled', () => { + test('does not flag Sections for bundle.sections errors', () => { + const errors = { 'bundle.sections.1.title': ['Title is required'] }; + + expect(storyEditTabHasError('sections', errors, false)).toBe(false); + }); });