From 06b336650bc9a8d7a872e3e42aa69a8961b7abd3 Mon Sep 17 00:00:00 2001 From: platex-rehor-bot Date: Thu, 30 Apr 2026 11:14:11 +0000 Subject: [PATCH 1/2] feat(creator): add YAML editor with wizard sync and validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RHCLOUD-40758 Add Creator YAML editor with bidirectional state sync between wizard and YAML tabs, schema validation, metadata file creation, and file load/download support. - Wizard → YAML: switching to YAML tab serializes current wizard state (spec, bundles, tags) into the editor - YAML → Wizard: switching back populates wizard form fields from shared state via initialValues - Kind detection: extracts ItemKind from spec.type.text, propagates to parent for correct metadata generation - Schema validation: warns on missing required fields, unknown tag kinds, unknown spec fields, and unrecognized types - File operations: Load from File, Load Sample Template, Download Files (uses parent-generated files for consistency with wizard) - Sample template updated with metadata tags (bundle, content) - 27 unit tests covering sync, validation, and file operations Co-Authored-By: Claude Opus 4.6 --- jest.config.js | 2 +- src/Creator.tsx | 5 + src/components/creator/CreatorWizard.tsx | 43 ++ .../creator/CreatorYAMLView.test.tsx | 635 ++++++++++++++++++ src/components/creator/CreatorYAMLView.tsx | 382 ++++++++++- src/data/quickstart-templates.ts | 8 +- 6 files changed, 1050 insertions(+), 25 deletions(-) create mode 100644 src/components/creator/CreatorYAMLView.test.tsx diff --git a/jest.config.js b/jest.config.js index 4c79597e..718a9993 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ -const transformIgnorePatterns = ['node_modules/(?!(uuid)/)']; +const transformIgnorePatterns = ['node_modules/(?!(uuid|yaml)/)']; /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { diff --git a/src/Creator.tsx b/src/Creator.tsx index 87aa5bbc..c205d9e3 100644 --- a/src/Creator.tsx +++ b/src/Creator.tsx @@ -238,6 +238,11 @@ const CreatorInternal = ({ onChangeCurrentStage={setCurrentStage} resetCreator={resetCreator} files={files} + quickStart={quickStart} + currentBundles={bundles} + currentTags={tags} + currentKind={rawKind} + onChangeKindDirect={setRawKind} /> diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index e6c82964..aa5d47cb 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -36,6 +36,7 @@ import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome' import { downloadFile } from '@redhat-cloud-services/frontend-components-utilities/helpers'; import SimpleButton from '../SimpleButton'; import DdfNumberInput from '../DdfNumberInput'; +import { ExtendedQuickstart } from '../../utils/fetchQuickstarts'; import { NAME_BUNDLES, NAME_DESCRIPTION, @@ -66,6 +67,11 @@ export type CreatorWizardProps = { filterData: FilterData; onChangeTags: (tags: { [kind: string]: string[] }) => void; onChangeMetadataTags: (tags: Array<{ kind: string; value: string }>) => void; + quickStart?: ExtendedQuickstart; + currentBundles?: string[]; + currentTags?: { [kind: string]: string[] }; + currentKind?: ItemKind | null; + onChangeKindDirect?: (kind: ItemKind | null) => void; }; type ViewMode = 'wizard' | 'creator'; @@ -312,11 +318,43 @@ const CreatorWizard = ({ onChangeMetadataTags, files, filterData, + quickStart, + currentBundles, + currentTags, + currentKind, + onChangeKindDirect, }: CreatorWizardProps) => { const chrome = useChrome(); const [viewMode, setViewMode] = useState('wizard'); const schema = useMemo(() => makeSchema(chrome, filterData), []); + // Derive initialValues from shared state so wizard ↔ YAML stays in sync. + // FormRenderer is conditionally rendered (unmounted in YAML mode), so it + // picks up fresh initialValues each time the user switches back to wizard. + const initialValues = useMemo(() => { + if (!quickStart) return undefined; + return { + [NAME_KIND]: currentKind || undefined, + [NAME_BUNDLES]: currentBundles, + [NAME_TAGS]: currentTags, + [NAME_TITLE]: quickStart.spec.displayName || '', + [NAME_DESCRIPTION]: quickStart.spec.description || '', + [NAME_DURATION]: quickStart.spec.durationMinutes, + [NAME_URL]: quickStart.spec.link?.href, + [NAME_PREREQUISITES]: quickStart.spec.prerequisites, + [NAME_PANEL_INTRODUCTION]: quickStart.spec.introduction, + [NAME_TASK_TITLES]: quickStart.spec.tasks?.map((t) => t.title || '') || [ + '', + ], + [NAME_TASKS_ARRAY]: quickStart.spec.tasks?.map((t) => ({ + description: t.description, + enable_work_check: !!t.review, + work_check_instructions: t.review?.instructions, + work_check_help: t.review?.failedTaskHelp, + })), + }; + }, [quickStart, currentKind, currentBundles, currentTags]); + // Update stage when switching to creator mode to show preview const handleViewModeChange = (newMode: ViewMode) => { setViewMode(newMode); @@ -368,6 +406,7 @@ const CreatorWizard = ({ {}} schema={schema} + initialValues={initialValues} componentMapper={componentMapper} > {({ formFields }) => ( @@ -409,6 +448,10 @@ const CreatorWizard = ({ onChangeBundles={onChangeBundles} onChangeTags={onChangeTags} onChangeMetadataTags={onChangeMetadataTags} + onChangeKind={onChangeKindDirect} + quickStart={quickStart} + currentBundles={currentBundles} + currentTags={currentTags} /> )} diff --git a/src/components/creator/CreatorYAMLView.test.tsx b/src/components/creator/CreatorYAMLView.test.tsx new file mode 100644 index 00000000..93b1fc43 --- /dev/null +++ b/src/components/creator/CreatorYAMLView.test.tsx @@ -0,0 +1,635 @@ +import React from 'react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import CreatorYAMLView from './CreatorYAMLView'; +import { CreatorWizardContext } from './context'; +import { CreatorFiles } from './types'; +import { DEFAULT_QUICKSTART_YAML } from '../../data/quickstart-templates'; +import { ExtendedQuickstart } from '../../utils/fetchQuickstarts'; + +// Mock downloadFile from frontend-components-utilities +const mockDownloadFile = jest.fn(); +jest.mock( + '@redhat-cloud-services/frontend-components-utilities/helpers', + () => ({ + downloadFile: (...args: unknown[]) => mockDownloadFile(...args), + }) +); + +// Mock Monaco Editor — render a simple textarea that mirrors onChange behavior +jest.mock('@monaco-editor/react', () => { + const MockEditor = ({ + value, + onChange, + }: { + value: string; + onChange: (value: string | undefined) => void; + }) => ( +