diff --git a/src/Creator.tsx b/src/Creator.tsx index 65734996..37172446 100644 --- a/src/Creator.tsx +++ b/src/Creator.tsx @@ -54,11 +54,13 @@ const CreatorInternal = ({ const filterMap = useFilterMap({ data: filterData }); const [rawQuickStart, setRawQuickStart] = useState({ + apiVersion: 'console.openshift.io/v1', metadata: { name: 'test-quickstart', tags: [], }, spec: { + version: 0.1, displayName: '', icon: null, description: '', @@ -189,15 +191,17 @@ const CreatorInternal = ({ .replaceAll(/(^-+)|(-+$)/g, ''); const adjustedQuickstart = { - ...quickStart, - spec: { - ...quickStart.spec, - icon: undefined, - }, + apiVersion: quickStart.apiVersion || 'console.openshift.io/v1', + kind: 'QuickStarts', metadata: { ...quickStart.metadata, name: effectiveName, }, + spec: { + version: quickStart.spec.version ?? 0.1, + ...quickStart.spec, + icon: quickStart.spec.icon ?? null, + }, }; const allTags = bundles.toSorted().map((bundle) => ({ @@ -221,7 +225,7 @@ const CreatorInternal = ({ }, { name: `${effectiveName}.yaml`, - content: YAML.stringify(adjustedQuickstart), + content: YAML.stringify(adjustedQuickstart, { nullStr: '~' }), }, ]; }, [quickStart, bundles, tags]); diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index 7ef22edc..225af4d9 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -42,6 +42,7 @@ import { NAME_BUNDLES, NAME_DESCRIPTION, NAME_DURATION, + NAME_ICON, NAME_KIND, NAME_METADATA_NAME, NAME_PANEL_INTRODUCTION, @@ -143,6 +144,7 @@ const PropUpdater = ({ const rawKind: string | undefined = values[NAME_KIND]; const title: string | undefined = values[NAME_TITLE]; const description: string | undefined = values[NAME_DESCRIPTION]; + const icon: string | null | undefined = values[NAME_ICON]; const url: string | undefined = values[NAME_URL]; const duration: number | string | undefined = values[NAME_DURATION]; const prerequisites: string[] | undefined = values[NAME_PREREQUISITES]; @@ -195,7 +197,7 @@ const PropUpdater = ({ : undefined, displayName: title ?? '', description: description ?? '', - icon: null, + icon: icon ?? null, link: meta?.fields?.url && url !== undefined && isValidUrl(url) ? { @@ -218,6 +220,7 @@ const PropUpdater = ({ rawKind, title, description, + icon, url, duration, prerequisites, @@ -237,7 +240,7 @@ const FileDownload = () => { const quickstartName = useMemo(() => { const yamlFile = files.find( - (f) => f.name !== 'metadata.yaml' && f.name.endsWith('.yaml') + (f) => !f.name.startsWith('metadata.') && f.name.endsWith('.yaml') ); if (!yamlFile) return null; const name = yamlFile.name.replace(/\.yaml$/, ''); @@ -447,6 +450,7 @@ const CreatorWizard = ({ [NAME_BUNDLES]: currentBundles, [NAME_TAGS]: currentTags, [NAME_TITLE]: quickStart.spec.displayName || '', + [NAME_ICON]: quickStart.spec.icon ?? null, [NAME_DESCRIPTION]: quickStart.spec.description || '', [NAME_DURATION]: quickStart.spec.durationMinutes, [NAME_URL]: quickStart.spec.link?.href, diff --git a/src/components/creator/CreatorYAMLView.test.tsx b/src/components/creator/CreatorYAMLView.test.tsx index 3ce956cd..7aec1609 100644 --- a/src/components/creator/CreatorYAMLView.test.tsx +++ b/src/components/creator/CreatorYAMLView.test.tsx @@ -939,5 +939,54 @@ spec: expect(editorValue).toContain('name: getting-started'); expect(editorValue).toContain('displayName: GS'); }); + + it('loads content YAML and merges tags from metadata.yml', async () => { + jest.useRealTimers(); + + mockedListRepoQuickstarts.mockResolvedValueOnce([ + { name: 'subs-simple', displayName: 'Simple Content Access' }, + ]); + const contentYaml = + 'metadata:\n name: subs-simple\nspec:\n displayName: Simple Content Access\n'; + mockedGetRepoQuickstartContent.mockResolvedValueOnce({ + name: 'subs-simple', + files: [ + { + name: 'metadata.yml', + content: + 'kind: QuickStarts\nname: subs-simple\ntags:\n- kind: bundle\n value: subscriptions\n', + }, + { name: 'subs-simple.yaml', content: contentYaml }, + ], + }); + + renderWithContext(); + + await act(async () => { + fireEvent.click( + screen.getByRole('button', { name: /load from repo/i }) + ); + }); + + await waitFor(() => { + expect(screen.getByText('Simple Content Access')).toBeInTheDocument(); + }); + + fireEvent.click( + screen.getByRole('button', { name: 'Simple Content Access' }) + ); + + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + + const editor = screen.getByTestId('mock-monaco-editor'); + const editorValue = (editor as HTMLTextAreaElement).value; + expect(editorValue).toContain('displayName: Simple Content Access'); + expect(editorValue).toContain('value: subscriptions'); + }); }); }); diff --git a/src/components/creator/CreatorYAMLView.tsx b/src/components/creator/CreatorYAMLView.tsx index f0e7f3a6..9a680f2e 100644 --- a/src/components/creator/CreatorYAMLView.tsx +++ b/src/components/creator/CreatorYAMLView.tsx @@ -213,18 +213,19 @@ function serializeToYaml( // Build document matching the expected YAML structure const doc: Record = { + apiVersion: quickStart.apiVersion || 'console.openshift.io/v1', kind: 'QuickStarts', metadata: { name: quickStart.metadata.name || 'untitled-quickstart', + ...(quickStart.metadata.externalDocumentation + ? { externalDocumentation: true } + : {}), + ...(quickStart.metadata.learningPath ? { learningPath: true } : {}), + ...(quickStart.metadata.otherResource ? { otherResource: true } : {}), ...(allTags.length > 0 ? { tags: allTags } : {}), }, spec: { - ...(quickStart.spec.displayName - ? { displayName: quickStart.spec.displayName } - : {}), - ...(quickStart.spec.description - ? { description: quickStart.spec.description } - : {}), + version: quickStart.spec.version ?? 0.1, ...(quickStart.spec.type ? { type: { @@ -233,6 +234,13 @@ function serializeToYaml( }, } : {}), + ...(quickStart.spec.displayName + ? { displayName: quickStart.spec.displayName } + : {}), + icon: quickStart.spec.icon ?? null, + ...(quickStart.spec.description + ? { description: quickStart.spec.description } + : {}), ...(quickStart.spec.durationMinutes !== undefined ? { durationMinutes: quickStart.spec.durationMinutes } : {}), @@ -256,7 +264,7 @@ function serializeToYaml( }, }; - return YAML.stringify(doc, { lineWidth: 0 }); + return YAML.stringify(doc, { lineWidth: 0, nullStr: '~' }); } /** Bundle entry from chrome.getAvailableBundles() */ @@ -555,14 +563,21 @@ const CreatorYAMLView: React.FC = ({ // Build the quickstart object const quickstartObj: ExtendedQuickstart = { + apiVersion: parsed.apiVersion || 'console.openshift.io/v1', metadata: { name: metadata.name || 'untitled-quickstart', tags: metadata.tags || [], + ...(metadata.externalDocumentation + ? { externalDocumentation: true } + : {}), + ...(metadata.learningPath ? { learningPath: true } : {}), + ...(metadata.otherResource ? { otherResource: true } : {}), }, spec: { + version: spec.version ?? 0.1, displayName: spec.displayName || '', description: spec.description || '', - icon: spec.icon || null, + icon: spec.icon ?? null, type: spec.type, durationMinutes: spec.durationMinutes, link: spec.link, @@ -739,17 +754,33 @@ const CreatorYAMLView: React.FC = ({ const yamlFile = content.files.find( (f) => (f.name.endsWith('.yml') || f.name.endsWith('.yaml')) && - f.name !== 'metadata.yaml' + !f.name.startsWith('metadata.') ); if (yamlFile) { let finalContent = yamlFile.content; try { const parsed = YAML.parse(finalContent); - if (parsed && !parsed.kind) { - const { metadata, spec, ...rest } = parsed; + if (parsed) { + if (!parsed.metadata) parsed.metadata = {}; + + const metadataFile = content.files.find((f) => + f.name.startsWith('metadata.') + ); + if (metadataFile) { + const meta = YAML.parse(metadataFile.content); + if (Array.isArray(meta?.tags) && meta.tags.length > 0) { + parsed.metadata.tags = meta.tags; + } + } + + if (!parsed.kind) { + parsed.kind = 'QuickStarts'; + } + + const { kind, metadata, spec, ...rest } = parsed; finalContent = YAML.stringify( - { kind: 'QuickStarts', metadata, spec, ...rest }, - { lineWidth: 0 } + { kind, metadata, spec, ...rest }, + { lineWidth: 0, nullStr: '~' } ); } } catch { diff --git a/src/components/creator/SourceSelector.tsx b/src/components/creator/SourceSelector.tsx index da76d7b8..df4cb175 100644 --- a/src/components/creator/SourceSelector.tsx +++ b/src/components/creator/SourceSelector.tsx @@ -25,6 +25,7 @@ import { NAME_BUNDLES, NAME_DESCRIPTION, NAME_DURATION, + NAME_ICON, NAME_KIND, NAME_METADATA_NAME, NAME_PANEL_INTRODUCTION, @@ -108,6 +109,7 @@ const SourceSelector = (props: UseFieldApiConfig) => { formApi.change(NAME_PANEL_INTRODUCTION, undefined); formApi.change(NAME_TASK_TITLES, undefined); formApi.change(NAME_TASKS_ARRAY, undefined); + formApi.change(NAME_ICON, undefined); }; const handleSelectScratch = () => { @@ -125,7 +127,7 @@ const SourceSelector = (props: UseFieldApiConfig) => { const yamlFile = content.files.find( (f) => (f.name.endsWith('.yml') || f.name.endsWith('.yaml')) && - f.name !== 'metadata.yaml' + !f.name.startsWith('metadata.') ); if (!yamlFile) { setError(`No quickstart YAML file found in "${name}".`); @@ -150,6 +152,7 @@ const SourceSelector = (props: UseFieldApiConfig) => { if (spec.displayName) formApi.change(NAME_TITLE, spec.displayName); if (spec.description) formApi.change(NAME_DESCRIPTION, spec.description); + if (spec.icon !== undefined) formApi.change(NAME_ICON, spec.icon); if (spec.durationMinutes !== undefined) formApi.change(NAME_DURATION, spec.durationMinutes); if (spec.link?.href) formApi.change(NAME_URL, spec.link.href); @@ -184,15 +187,21 @@ const SourceSelector = (props: UseFieldApiConfig) => { const bundles: string[] = []; const tagsByKind: { [kind: string]: string[] } = {}; - if (Array.isArray(metadata.tags)) { - metadata.tags.forEach((tag: { kind?: string; value?: string }) => { - if (tag.kind === 'bundle' && tag.value) { - bundles.push(tag.value); - } else if (tag.kind && tag.value) { - if (!tagsByKind[tag.kind]) tagsByKind[tag.kind] = []; - tagsByKind[tag.kind].push(tag.value); - } - }); + const metadataFile = content.files.find((f) => + f.name.startsWith('metadata.') + ); + if (metadataFile) { + const meta = YAML.parse(metadataFile.content); + if (Array.isArray(meta?.tags)) { + meta.tags.forEach((tag: { kind?: string; value?: string }) => { + if (tag.kind === 'bundle' && tag.value) { + bundles.push(tag.value); + } else if (tag.kind && tag.value) { + if (!tagsByKind[tag.kind]) tagsByKind[tag.kind] = []; + tagsByKind[tag.kind].push(tag.value); + } + }); + } } if (bundles.length > 0) formApi.change(NAME_BUNDLES, bundles); if (Object.keys(tagsByKind).length > 0) diff --git a/src/components/creator/steps/common.ts b/src/components/creator/steps/common.ts index f5dde6a8..dec7f9fe 100644 --- a/src/components/creator/steps/common.ts +++ b/src/components/creator/steps/common.ts @@ -12,6 +12,7 @@ export const NAME_BUNDLES = 'bundles'; export const NAME_DESCRIPTION = 'description'; export const NAME_DURATION = 'duration'; export const NAME_URL = 'url'; +export const NAME_ICON = 'icon'; export const NAME_PANEL_INTRODUCTION = 'panel-overview'; export const NAME_PREREQUISITES = 'prerequisites';