From d41177e314ed183563f2b2c103422221e58b5bf5 Mon Sep 17 00:00:00 2001 From: Hossam Farid Date: Mon, 10 Aug 2026 12:22:52 -0400 Subject: [PATCH 1/4] fix(git-service): correct YAML fields, add kind specific meta flags, api and spec versions --- src/Creator.tsx | 20 ++++++----- src/components/creator/CreatorWizard.tsx | 8 ++--- src/components/creator/CreatorYAMLView.tsx | 39 ++++++++++++++++------ 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Creator.tsx b/src/Creator.tsx index 65734996..bf36d59c 100644 --- a/src/Creator.tsx +++ b/src/Creator.tsx @@ -47,18 +47,18 @@ const CreatorInternal = ({ filterLoader: UnwrappedLoader; }) => { const { data: filterData } = filterLoader(); - const showGitService = useFlag( - 'platform.learning-resources.quickstarts.git-service' - ); + const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); const [rawKind, setRawKind] = useState(null); 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 +189,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 +223,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..4f0eef72 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -230,9 +230,7 @@ const PropUpdater = ({ }; const FileDownload = () => { - const showGitService = useFlag( - 'platform.learning-resources.quickstarts.git-service' - ); + const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); const { files } = useContext(CreatorWizardContext); const quickstartName = useMemo(() => { @@ -427,9 +425,7 @@ const CreatorWizard = ({ onChangeKindDirect, }: CreatorWizardProps) => { const chrome = useChrome(); - const showGitService = useFlag( - 'platform.learning-resources.quickstarts.git-service' - ); + const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); const [viewMode, setViewMode] = useState('wizard'); const schema = useMemo( () => makeSchema(chrome, filterData, showGitService), diff --git a/src/components/creator/CreatorYAMLView.tsx b/src/components/creator/CreatorYAMLView.tsx index f0e7f3a6..6d655835 100644 --- a/src/components/creator/CreatorYAMLView.tsx +++ b/src/components/creator/CreatorYAMLView.tsx @@ -213,18 +213,23 @@ function serializeToYaml( // Build document matching the expected YAML structure const doc: Record = { + 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: 0.1, ...(quickStart.spec.type ? { type: { @@ -233,6 +238,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 +268,7 @@ function serializeToYaml( }, }; - return YAML.stringify(doc, { lineWidth: 0 }); + return YAML.stringify(doc, { lineWidth: 0, nullStr: '~' }); } /** Bundle entry from chrome.getAvailableBundles() */ @@ -440,9 +452,7 @@ const CreatorYAMLView: React.FC = ({ }) => { const { files } = useContext(CreatorWizardContext); - const showCreatePR = useFlag( - 'platform.learning-resources.quickstarts.git-service' - ); + const showCreatePR = true; // useFlag('platform.learning-resources.quickstarts.git-service'); const [parsedName, setParsedName] = useState(null); const { @@ -555,14 +565,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, From 0ed619b5d6f393ef58dcd9ea085fc7482e737c40 Mon Sep 17 00:00:00 2001 From: Hossam Farid Date: Mon, 10 Aug 2026 13:22:07 -0400 Subject: [PATCH 2/4] fix(git-service): turn on feature flag for git-service --- src/Creator.tsx | 4 +++- src/components/creator/CreatorWizard.tsx | 8 ++++++-- src/components/creator/CreatorYAMLView.tsx | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Creator.tsx b/src/Creator.tsx index bf36d59c..37172446 100644 --- a/src/Creator.tsx +++ b/src/Creator.tsx @@ -47,7 +47,9 @@ const CreatorInternal = ({ filterLoader: UnwrappedLoader; }) => { const { data: filterData } = filterLoader(); - const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); + const showGitService = useFlag( + 'platform.learning-resources.quickstarts.git-service' + ); const [rawKind, setRawKind] = useState(null); const filterMap = useFilterMap({ data: filterData }); diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index 4f0eef72..7ef22edc 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -230,7 +230,9 @@ const PropUpdater = ({ }; const FileDownload = () => { - const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); + const showGitService = useFlag( + 'platform.learning-resources.quickstarts.git-service' + ); const { files } = useContext(CreatorWizardContext); const quickstartName = useMemo(() => { @@ -425,7 +427,9 @@ const CreatorWizard = ({ onChangeKindDirect, }: CreatorWizardProps) => { const chrome = useChrome(); - const showGitService = true; // useFlag('platform.learning-resources.quickstarts.git-service'); + const showGitService = useFlag( + 'platform.learning-resources.quickstarts.git-service' + ); const [viewMode, setViewMode] = useState('wizard'); const schema = useMemo( () => makeSchema(chrome, filterData, showGitService), diff --git a/src/components/creator/CreatorYAMLView.tsx b/src/components/creator/CreatorYAMLView.tsx index 6d655835..bf0aea83 100644 --- a/src/components/creator/CreatorYAMLView.tsx +++ b/src/components/creator/CreatorYAMLView.tsx @@ -452,7 +452,9 @@ const CreatorYAMLView: React.FC = ({ }) => { const { files } = useContext(CreatorWizardContext); - const showCreatePR = true; // useFlag('platform.learning-resources.quickstarts.git-service'); + const showCreatePR = useFlag( + 'platform.learning-resources.quickstarts.git-service' + ); const [parsedName, setParsedName] = useState(null); const { From ebff2c4bc147dc48cada88ca44bdb7eb01684770 Mon Sep 17 00:00:00 2001 From: Hossam Farid Date: Mon, 17 Aug 2026 10:01:50 -0400 Subject: [PATCH 3/4] fix(git-service): update selector and wizard components --- src/components/creator/CreatorWizard.tsx | 7 ++- .../creator/CreatorYAMLView.test.tsx | 49 +++++++++++++++++++ src/components/creator/CreatorYAMLView.tsx | 32 ++++++++---- src/components/creator/SourceSelector.tsx | 28 +++++++---- src/components/creator/steps/common.ts | 1 + 5 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index 7ef22edc..5987b43a 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$/, ''); 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 bf0aea83..0a45a662 100644 --- a/src/components/creator/CreatorYAMLView.tsx +++ b/src/components/creator/CreatorYAMLView.tsx @@ -220,12 +220,8 @@ function serializeToYaml( ...(quickStart.metadata.externalDocumentation ? { externalDocumentation: true } : {}), - ...(quickStart.metadata.learningPath - ? { learningPath: true } - : {}), - ...(quickStart.metadata.otherResource - ? { otherResource: true } - : {}), + ...(quickStart.metadata.learningPath ? { learningPath: true } : {}), + ...(quickStart.metadata.otherResource ? { otherResource: true } : {}), ...(allTags.length > 0 ? { tags: allTags } : {}), }, spec: { @@ -758,16 +754,32 @@ 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 }, + { kind, metadata, spec, ...rest }, { lineWidth: 0 } ); } diff --git a/src/components/creator/SourceSelector.tsx b/src/components/creator/SourceSelector.tsx index da76d7b8..0b00ad9c 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, @@ -125,7 +126,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 +151,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 +186,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'; From 1b66f7a8b07b9a74fb0ec0faffe06d0cfda2fb4c Mon Sep 17 00:00:00 2001 From: Hossam Farid Date: Tue, 18 Aug 2026 11:17:44 -0400 Subject: [PATCH 4/4] fix(git-service): implement coderabbit suggestions --- src/components/creator/CreatorWizard.tsx | 1 + src/components/creator/CreatorYAMLView.tsx | 6 +++--- src/components/creator/SourceSelector.tsx | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index 5987b43a..225af4d9 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -450,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.tsx b/src/components/creator/CreatorYAMLView.tsx index 0a45a662..9a680f2e 100644 --- a/src/components/creator/CreatorYAMLView.tsx +++ b/src/components/creator/CreatorYAMLView.tsx @@ -213,7 +213,7 @@ function serializeToYaml( // Build document matching the expected YAML structure const doc: Record = { - apiVersion: 'console.openshift.io/v1', + apiVersion: quickStart.apiVersion || 'console.openshift.io/v1', kind: 'QuickStarts', metadata: { name: quickStart.metadata.name || 'untitled-quickstart', @@ -225,7 +225,7 @@ function serializeToYaml( ...(allTags.length > 0 ? { tags: allTags } : {}), }, spec: { - version: 0.1, + version: quickStart.spec.version ?? 0.1, ...(quickStart.spec.type ? { type: { @@ -780,7 +780,7 @@ const CreatorYAMLView: React.FC = ({ const { kind, metadata, spec, ...rest } = parsed; finalContent = YAML.stringify( { kind, metadata, spec, ...rest }, - { lineWidth: 0 } + { lineWidth: 0, nullStr: '~' } ); } } catch { diff --git a/src/components/creator/SourceSelector.tsx b/src/components/creator/SourceSelector.tsx index 0b00ad9c..df4cb175 100644 --- a/src/components/creator/SourceSelector.tsx +++ b/src/components/creator/SourceSelector.tsx @@ -109,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 = () => {