Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/backend/define_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export function defineConfig(config: Partial<CmsConfig>): CmsConfig {
storiesHasEditReview:
config.storiesHasEditReview === undefined ? false : config.storiesHasEditReview,

storiesHasSections:
config.storiesHasSections === undefined ? false : config.storiesHasSections,

storyTemplates: config.storyTemplates || [],
} satisfies CmsConfig;
}
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/backend/services/cms_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 0 additions & 8 deletions src/backend/services/story_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 11 additions & 3 deletions src/backend/stubs/config/cms.stub
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -68,6 +67,8 @@ const devotionStream: StreamSpec = {
// CMS configuration
// ---------------------------------------

const storiesHasSections = false;

const cmsConfig: Partial<CmsConfig> = {
/**
* The name of the app
Expand Down Expand Up @@ -125,6 +126,11 @@ const cmsConfig: Partial<CmsConfig> = {
*/
storiesHasEditReview: false,

/**
* Flag to enable the Sections tab and section type field on story create/edit
*/
storiesHasSections,

bespokeTemplates: [
{
id: 'daily-grace',
Expand All @@ -141,7 +147,9 @@ const cmsConfig: Partial<CmsConfig> = {
{
id: 'course',
name: 'Course',
fields: [courseFields(videoUploadConfig, imageUploadConfig)],
fields: courseFields(videoUploadConfig, imageUploadConfig, {
hasSections: storiesHasSections,
}),
},
],

Expand Down
1 change: 1 addition & 0 deletions src/backend/stubs/tests/helpers/cms_mock.stub
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const testCmsConfig: CmsConfig = {
},
],
storiesHasEditReview: false,
storiesHasSections: false,
bespokeTemplates: [
{
id: 'test-story',
Expand Down
4 changes: 2 additions & 2 deletions src/backend/stubs/tests/unit/story_service.stub
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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');
});

Expand Down
12 changes: 9 additions & 3 deletions src/backend/validators/course.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<any> {
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',
Expand All @@ -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),
}),
});
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/stories/components/story-edit-details.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const translationLocaleModel = {
title: '',
coverImage: '',
description: '',
tags: 'gospel,john',
tags: '',
};

const translationWithoutTagsModel = {
Expand Down Expand Up @@ -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
Expand Down
34 changes: 21 additions & 13 deletions src/frontend/stories/components/story-edit-details.vue
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const tagsField: FieldSpec = {
placeholderText: 'Add tags...',
};

const contentClassificationField: FieldSpec = {
const contentClassificationField = computed((): FieldSpec => ({
label: 'Content Classification',
name: 'contentClassification',
widget: 'panel',
Expand All @@ -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:
Expand All @@ -238,18 +242,22 @@ 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',
widget: 'string',
placeholderText: 'e.g., Session, Devotion, Day',
},
],
};
}));
</script>
14 changes: 9 additions & 5 deletions src/frontend/stories/story-create.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

<Variant title="Prefilled" :setup-app="loadNormalData">
<StoryCreate
:config="sharedProps.config"
:config="configWithSections"
:user="sharedProps.user"
:language="sharedProps.language"
:errors="{}"
Expand Down Expand Up @@ -63,7 +63,12 @@ import StoryCreate from './story-create.vue';
import { sharedProps, miniSidebar, availableResources } from '../test/mocks';
import { useSharedStore } from '../store';
import type { StoryHandler } from '../shared/helpers';
import type { BundleTemplate, StoryCreateProps } from '../../types';
import type { BundleTemplate, StoryCreateProps, UiConfig } from '../../types';

const configWithSections: UiConfig = {
...sharedProps.config,
storiesHasSections: true,
};

const emptyModel: StoryCreateProps['model'] = {
title: '',
Expand Down Expand Up @@ -150,9 +155,8 @@ const loadResourcesTab: StoryHandler = (context): void => {
<docs lang="md">
# 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).
Expand Down
Loading
Loading