From 7a573a7994d92e98604051858ec97f9e98586620 Mon Sep 17 00:00:00 2001 From: Janet Cobb Date: Fri, 12 Jul 2024 09:55:48 -0400 Subject: [PATCH] Improve creator UI --- src/Creator.scss | 12 + src/Creator.tsx | 161 ++++---- src/components/DdfNumberInput.tsx | 62 ++++ src/components/SimpleButton.tsx | 26 ++ src/components/StringArrayInput.scss | 14 + src/components/StringArrayInput.tsx | 121 ++++++ src/components/creator/CreatorPreview.tsx | 83 +++-- src/components/creator/CreatorWizard.tsx | 243 ++++++------ src/components/creator/context.ts | 13 + src/components/creator/schema.tsx | 346 +++++------------- src/components/creator/steps/common.ts | 25 ++ src/components/creator/steps/details.tsx | 141 +++++++ src/components/creator/steps/download.tsx | 52 +++ src/components/creator/steps/kind.tsx | 42 +++ .../creator/steps/panel-overview.tsx | 87 +++++ src/components/creator/steps/task.tsx | 116 ++++++ src/components/creator/types.ts | 4 + 17 files changed, 1068 insertions(+), 480 deletions(-) create mode 100644 src/Creator.scss create mode 100644 src/components/DdfNumberInput.tsx create mode 100644 src/components/SimpleButton.tsx create mode 100644 src/components/StringArrayInput.scss create mode 100644 src/components/StringArrayInput.tsx create mode 100644 src/components/creator/context.ts create mode 100644 src/components/creator/steps/common.ts create mode 100644 src/components/creator/steps/details.tsx create mode 100644 src/components/creator/steps/download.tsx create mode 100644 src/components/creator/steps/kind.tsx create mode 100644 src/components/creator/steps/panel-overview.tsx create mode 100644 src/components/creator/steps/task.tsx create mode 100644 src/components/creator/types.ts diff --git a/src/Creator.scss b/src/Creator.scss new file mode 100644 index 00000000..04bfb089 --- /dev/null +++ b/src/Creator.scss @@ -0,0 +1,12 @@ +.rc-header { + background-color: var(--pf-v5-global--BackgroundColor--dark-300); + color: var(--pf-v5-global--Color--light-100); +} + +.rc-header a { + color: var(--pf-v5-global--link--Color--light); +} + +.rc-header a:hover { + color: var(--pf-v5-global--link--Color--light--hover); +} diff --git a/src/Creator.tsx b/src/Creator.tsx index cc0f4369..42096980 100644 --- a/src/Creator.tsx +++ b/src/Creator.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import YAML, { YAMLError } from 'yaml'; +import YAML from 'yaml'; import { Grid, GridItem, @@ -7,18 +7,12 @@ import { PageSection, Title, } from '@patternfly/react-core'; -import { - QuickStart, - QuickStartSpec, - QuickStartTask, -} from '@patternfly/quickstarts'; +import { QuickStart, QuickStartSpec } from '@patternfly/quickstarts'; import CreatorWizard, { EMPTY_TASK } from './components/creator/CreatorWizard'; import { ItemKind, metaForKind } from './components/creator/meta'; import CreatorPreview from './components/creator/CreatorPreview'; - -export type CreatorErrors = { - taskErrors: Map; -}; +import './Creator.scss'; +import { CreatorWizardStage } from './components/creator/schema'; const BASE_METADATA = { name: 'test-quickstart', @@ -26,56 +20,21 @@ const BASE_METADATA = { function makeDemoQuickStart( kind: ItemKind | null, - baseQuickStart: QuickStart, - taskContents: string[] -): [QuickStart, CreatorErrors] { + baseQuickStart: QuickStart +): QuickStart { const kindMeta = kind !== null ? metaForKind(kind) : null; - const [tasks, taskErrors] = (() => { - if (kindMeta?.hasTasks !== true) return [undefined, new Map()]; - - const out: QuickStartTask[] = []; - const errors: CreatorErrors['taskErrors'] = new Map(); - - if (baseQuickStart.spec.tasks !== undefined) { - for (let index = 0; index < baseQuickStart.spec.tasks.length; ++index) { - const task = baseQuickStart.spec.tasks[index]; - - try { - out.push({ - ...YAML.parse(taskContents[index]), - title: task.title, - }); - } catch (e) { - if (!(e instanceof YAMLError)) throw e; - - out.push({ ...EMPTY_TASK, title: task.title }); - errors.set(index, e.message); - } - } - } - - return [out, errors]; - })(); - - return [ - { - ...baseQuickStart, - metadata: { - ...baseQuickStart.metadata, - name: 'test-quickstart', - ...(kindMeta?.extraMetadata ?? {}), - }, - spec: { - ...baseQuickStart.spec, - tasks: tasks, - }, + return { + ...baseQuickStart, + metadata: { + ...baseQuickStart.metadata, + name: 'test-quickstart', + ...(kindMeta?.extraMetadata ?? {}), }, - { taskErrors }, - ]; + }; } -const Creator = () => { +const CreatorInternal = ({ resetCreator }: { resetCreator: () => void }) => { const [rawKind, setRawKind] = useState(null); const [rawQuickStart, setRawQuickStart] = useState({ @@ -93,9 +52,11 @@ const Creator = () => { rawKind !== null ? { id: rawKind, meta: metaForKind(rawKind) } : null; const [bundles, setBundles] = useState([]); - const [taskContents, setTaskContents] = useState([]); + const [currentStage, setCurrentStage] = useState({ + type: 'card', + }); - const [currentTask, setCurrentTask] = useState(null); + const isDownloadStage = currentStage.type !== 'download'; const updateSpec = ( updater: (old: QuickStartSpec) => Partial @@ -143,20 +104,14 @@ const Creator = () => { return { ...old, ...updates }; }); - - if (meta.hasTasks) { - setTaskContents((old) => (old.length === 0 ? [''] : old)); - } else if (!meta.hasTasks) { - setTaskContents([]); - } } setRawKind(newKind); }; - const [quickStart, errors] = useMemo( - () => makeDemoQuickStart(rawKind, rawQuickStart, taskContents), - [rawKind, rawQuickStart, taskContents] + const quickStart = useMemo( + () => makeDemoQuickStart(rawKind, rawQuickStart), + [rawKind, rawQuickStart] ); const files = useMemo(() => { @@ -165,15 +120,18 @@ const Creator = () => { .replaceAll(/\s/g, '-') .replaceAll(/(^-+)|(-+$)/g, ''); - const adjustedQuickstart = { ...quickStart }; - adjustedQuickstart.spec = { ...adjustedQuickstart.spec }; - adjustedQuickstart.metadata = { - ...adjustedQuickstart.metadata, - name: effectiveName, + const adjustedQuickstart = { + ...quickStart, + spec: { + ...quickStart.spec, + icon: undefined, + }, + metadata: { + ...quickStart.metadata, + name: effectiveName, + }, }; - delete adjustedQuickstart.spec['icon']; - return [ { name: 'metadata.yaml', @@ -192,49 +150,64 @@ const Creator = () => { ]; }, [quickStart, bundles]); - if ((quickStart.spec.tasks?.length ?? 0) != taskContents.length) { - throw new Error( - `Mismatch between quickstart tasks and task contents: ${quickStart.spec.tasks?.length} vs ${taskContents.length}` - ); - } - return ( - + - Add new learning resources + Add new learning resource -

Description

+

+ Add cards to the learning resources spaces within console.redhat.com.{' '} + + Learn more about Hybrid Cloud Console Learning Resources. + +

- + - + { updateSpec(() => spec); }} onChangeBundles={setBundles} - onChangeTaskContents={setTaskContents} - onChangeCurrentTask={setCurrentTask} - errors={errors} + onChangeCurrentStage={setCurrentStage} + resetCreator={resetCreator} files={files} /> - - - + {isDownloadStage ? ( + + + + ) : null}
); }; +const Creator = () => { + const [resetCount, setResetCount] = useState(0n); + + return ( + setResetCount((old) => old + 1n)} + /> + ); +}; + export default Creator; diff --git a/src/components/DdfNumberInput.tsx b/src/components/DdfNumberInput.tsx new file mode 100644 index 00000000..82d27885 --- /dev/null +++ b/src/components/DdfNumberInput.tsx @@ -0,0 +1,62 @@ +import { + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + NumberInput, +} from '@patternfly/react-core'; +import React, { useId } from 'react'; +import { + UseFieldApiConfig, + useFieldApi, +} from '@data-driven-forms/react-form-renderer'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; + +const DdfNumberInput = (props: UseFieldApiConfig) => { + const { input, meta, ...rest } = useFieldApi(props); + const reactId = useId(); + const effectiveId = rest.id || reactId; + + const showInvalid = meta.touched && meta.invalid; + + const focusProps = { + onFocus: input.onFocus, + onBlur: input.onBlur, + }; + + return ( + + input.onChange((input.value ?? 0) + 1)} + onMinus={() => input.onChange((input.value ?? 0) - 1)} + onChange={(e) => input.onChange(e.currentTarget.valueAsNumber)} + {...focusProps} + plusBtnProps={focusProps} + minusBtnProps={focusProps} + /> + + {showInvalid ? ( + + + }> + {meta.error} + + + + ) : null} + + ); +}; + +export default DdfNumberInput; diff --git a/src/components/SimpleButton.tsx b/src/components/SimpleButton.tsx new file mode 100644 index 00000000..ad57a5f8 --- /dev/null +++ b/src/components/SimpleButton.tsx @@ -0,0 +1,26 @@ +import React, { ButtonHTMLAttributes, ReactNode } from 'react'; + +const SimpleButton = ({ + type = 'button', + children, + icon, + ...rest +}: ButtonHTMLAttributes & { + icon?: ReactNode; +}) => { + return ( + + ); +}; + +export default SimpleButton; diff --git a/src/components/StringArrayInput.scss b/src/components/StringArrayInput.scss new file mode 100644 index 00000000..507c8536 --- /dev/null +++ b/src/components/StringArrayInput.scss @@ -0,0 +1,14 @@ +.lr-string-array { + display: grid; + grid-template-columns: [label-start] auto [label-end value-start] 1fr [value-end remove-start] auto [remove-end]; + + .lr-string-array-label { + grid-column-start: label-start; + grid-column-end: label-end; + } + + .lr-string-array-value { + grid-column-start: value-start; + grid-column-end: value-end; + } +} diff --git a/src/components/StringArrayInput.tsx b/src/components/StringArrayInput.tsx new file mode 100644 index 00000000..eb990583 --- /dev/null +++ b/src/components/StringArrayInput.tsx @@ -0,0 +1,121 @@ +import React, { useId } from 'react'; +import { + Button, + Flex, + FlexItem, + FormGroup, + FormSection, + TextInput, +} from '@patternfly/react-core'; +import MinusCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/minus-circle-icon'; +import { + UseFieldApiConfig, + useFieldApi, +} from '@data-driven-forms/react-form-renderer'; + +const DEFAULT_VALUE: string[] = []; +const DEFAULT_LABEL = (index: number) => `Item ${index + 1}`; + +const StringArrayInput = (props: UseFieldApiConfig) => { + const id = useId(); + + const { input, ...rest } = useFieldApi(props); + const reactId = useId(); + const effectiveId = rest.id || reactId; + + const defaultItem = rest.defaultItem ?? ''; + + const value: string[] = input.value ?? DEFAULT_VALUE; + const itemLabel: (index: number) => string = rest.itemLabel ?? DEFAULT_LABEL; + + const minItems: number | undefined = rest.minItems; + const maxItems: number | undefined = rest.maxItems; + + if (minItems !== undefined) { + if (value.length < minItems) { + const newValue = [...value]; + + for (let i = 0; i < minItems - value.length; ++i) { + newValue.push(defaultItem); + } + + input.onChange(newValue); + } + } + + const canRemove = minItems === undefined || value.length > minItems; + const canAdd = maxItems === undefined || value.length < maxItems; + + const addLabel = rest.addLabel ?? 'Add item'; + const addLabelIcon = rest.addLabelIcon; + + const fullMessage = rest.fullMessage ?? 'No more items can be added'; + + return ( + +
+ {value.map((element, index) => { + const elementId = `${id}-${index}-title`; + + return ( + + + + { + const newValue = [...value]; + newValue[index] = newItem; + + input.onChange(newValue); + }} + /> + + + {canRemove ? ( + +
+ + {canAdd ? ( + + ) : ( + {fullMessage} + )} +
+ ); +}; + +export default StringArrayInput; diff --git a/src/components/creator/CreatorPreview.tsx b/src/components/creator/CreatorPreview.tsx index ddd6c5ff..2c5885e6 100644 --- a/src/components/creator/CreatorPreview.tsx +++ b/src/components/creator/CreatorPreview.tsx @@ -6,34 +6,40 @@ import { QuickStartStatus, useValuesForQuickStartContext, } from '@patternfly/quickstarts'; -import { Title } from '@patternfly/react-core'; +import { Flex, FlexItem, Title } from '@patternfly/react-core'; import WrappedQuickStartTile from '../WrappedQuickStartTile'; import React, { useContext, useMemo, useState } from 'react'; import { ItemMeta } from './meta'; import './CreatorPreview.scss'; +import { CreatorWizardStage } from './schema'; const CreatorPreview = ({ kindMeta, quickStart, - currentTask, + currentStage, }: { kindMeta: ItemMeta | null; quickStart: QuickStart; - currentTask: number | null; + currentStage: CreatorWizardStage; }) => { const allQuickStarts = useMemo(() => [quickStart], [quickStart]); const [quickStartStates, setQuickStartStates] = useState( {} ); - const [prevTask, setPrevTask] = useState(currentTask); + const [prevStage, setPrevStage] = useState( + currentStage + ); const parentContext = useContext(QuickStartContext); + const showPanel = + kindMeta?.hasTasks && + (currentStage.type === 'panel-overview' || currentStage.type === 'task'); + const quickstartValues = useValuesForQuickStartContext({ allQuickStarts: [quickStart], - activeQuickStartID: - kindMeta?.hasTasks === true ? quickStart.metadata.name : '', + activeQuickStartID: showPanel ? quickStart.metadata.name : '', setActiveQuickStartID: () => {}, allQuickStartStates: quickStartStates, setAllQuickStartStates: (states) => setQuickStartStates(states), @@ -46,44 +52,53 @@ const CreatorPreview = ({ quickstartValues.setAllQuickStarts?.([quickStart]); } - if ( - prevTask !== currentTask || - quickstartValues?.activeQuickStartState === undefined - ) { - setPrevTask(currentTask); + if (prevStage !== currentStage) { + setPrevStage(currentStage); - if (currentTask !== null) { - quickstartValues.setQuickStartTaskNumber?.( - quickStart.metadata.name, - currentTask - ); - } else { + if (currentStage.type === 'panel-overview') { quickstartValues.restartQuickStart?.( quickStart.metadata.name, quickStart.spec.tasks?.length ?? 0 ); + } else if (currentStage.type === 'task') { + quickstartValues.setQuickStartTaskNumber?.( + quickStart.metadata.name, + currentStage.index + ); } } return ( - - -
- - Live card preview - + + + + Live {showPanel ? kindMeta.displayName : 'card'} preview + + -
- -
-
-
-
+ + + +
+ {!showPanel ? ( +
+ +
+ ) : null} +
+
+
+
+ ); }; diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx index 58214e2a..25b4928e 100644 --- a/src/components/creator/CreatorWizard.tsx +++ b/src/components/creator/CreatorWizard.tsx @@ -1,13 +1,20 @@ import { + Banner, Button, ClipboardCopy, ClipboardCopyVariant, + Flex, + FlexItem, + Stack, + StackItem, + Text, + Title, } from '@patternfly/react-core'; +import CheckCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/check-circle-icon'; import DownloadIcon from '@patternfly/react-icons/dist/dynamic/icons/download-icon'; import React, { useContext, useEffect, useMemo } from 'react'; import { ItemKind, isItemKind, metaForKind } from './meta'; -import { CreatorErrors } from '../../Creator'; -import { QuickStartSpec } from '@patternfly/quickstarts'; +import { QuickStartSpec, QuickStartTask } from '@patternfly/quickstarts'; import { AnyObject, FormRenderer, @@ -15,6 +22,11 @@ import { } from '@data-driven-forms/react-form-renderer'; import DdfWizardContext from '@data-driven-forms/react-form-renderer/wizard-context'; import pf4ComponentMapper from '@data-driven-forms/pf4-component-mapper/component-mapper'; +import { CreatorWizardStage, makeSchema, stageFromStepName } from './schema'; +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 { NAME_BUNDLES, NAME_DESCRIPTION, @@ -26,35 +38,18 @@ import { NAME_TASK_TITLES, NAME_TITLE, NAME_URL, - makeSchema, - taskFromStepName, -} from './schema'; -import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'; -import { downloadFile } from '@redhat-cloud-services/frontend-components-utilities/helpers'; - -export type TaskState = { - title: string; - yamlContent: string; -}; - -export const EMPTY_TASK: TaskState = { - title: '', - yamlContent: '', -}; +} from './steps/common'; +import StringArrayInput from '../StringArrayInput'; +import { CreatorWizardContext } from './context'; +import { CreatorFiles } from './types'; -type CreatorFiles = { - name: string; - content: string; -}[]; - -type CreatorWizardProps = { +export type CreatorWizardProps = { onChangeKind: (newKind: ItemKind | null) => void; onChangeQuickStartSpec: (newValue: QuickStartSpec) => void; onChangeBundles: (newValue: string[]) => void; - onChangeTaskContents: (contents: string[]) => void; - onChangeCurrentTask: (index: number | null) => void; + onChangeCurrentStage: (stage: CreatorWizardStage) => void; + resetCreator: () => void; files: CreatorFiles; - errors: CreatorErrors; }; type FormValue = AnyObject; @@ -64,17 +59,24 @@ type UpdaterProps = { onChangeKind: (newKind: ItemKind | null) => void; onChangeBundles: (bundles: string[]) => void; onChangeQuickStartSpec: (newValue: QuickStartSpec) => void; - onChangeTaskContents: (contents: string[]) => void; }; const DEFAULT_TASK_TITLES: string[] = ['']; +export const EMPTY_TASK: QuickStartTask = {}; + +type FormTaskValue = { + description?: string; + enable_work_check?: boolean; + work_check_instructions?: string; + work_check_help?: string; +}; + const PropUpdater = ({ values, onChangeKind, onChangeBundles, onChangeQuickStartSpec, - onChangeTaskContents, }: UpdaterProps) => { const bundles = values[NAME_BUNDLES]; @@ -91,8 +93,7 @@ const PropUpdater = ({ const introduction: string | undefined = values[NAME_PANEL_INTRODUCTION]; const taskTitles: string[] = values[NAME_TASK_TITLES] ?? DEFAULT_TASK_TITLES; - const taskValues: { content: string | undefined }[] | undefined = - values[NAME_TASKS_ARRAY]; + const taskValues: FormTaskValue[] | undefined = values[NAME_TASKS_ARRAY]; const kind = typeof rawKind === 'string' && isItemKind(rawKind) ? rawKind : null; @@ -103,18 +104,28 @@ const PropUpdater = ({ onChangeKind(kind); }, [kind]); - const taskContents = useMemo(() => { - if (meta?.hasTasks !== true) { - return []; - } + const effectiveTasks = useMemo(() => { + if (meta?.hasTasks !== true) return undefined; + + const out: QuickStartTask[] = []; - const effective = []; + // The task titles array determines how many tasks there should be. + for (let i = 0; i < taskTitles.length; ++i) { + const taskValue = taskValues?.[i]; - for (let i = 0; i < (taskTitles?.length ?? 0); ++i) { - effective.push(taskValues?.[i]?.content ?? ''); + out.push({ + title: taskTitles[i], + description: taskValue?.description ?? '', + review: taskValue?.enable_work_check + ? { + instructions: taskValue?.work_check_instructions, + failedTaskHelp: taskValue?.work_check_help, + } ?? '' + : undefined, + }); } - return effective; + return out; }, [meta, taskTitles, taskValues]); useEffect(() => { @@ -142,11 +153,10 @@ const PropUpdater = ({ : undefined, prerequisites: meta?.hasTasks === true ? prerequisites : undefined, introduction: meta?.hasTasks === true ? introduction : undefined, - tasks: - meta?.hasTasks === true - ? (taskTitles ?? []).map((t) => ({ title: t })) - : undefined, + tasks: effectiveTasks, }); + + onChangeKind(kind); }, [ meta, rawKind, @@ -156,73 +166,86 @@ const PropUpdater = ({ duration, prerequisites, introduction, - taskTitles, + effectiveTasks, ]); - useEffect(() => { - onChangeTaskContents(taskContents); - }, [taskContents]); - // Allow use as JSX component return undefined; }; -const CreatorWizardContext = React.createContext<{ - errors: CreatorErrors; - files: CreatorFiles; - onChangeCurrentTask: (index: number | null) => void; -}>({ - errors: { - taskErrors: new Map(), - }, - files: [], - onChangeCurrentTask: () => {}, -}); - -const TaskErrorPreview = ({ index }: { index: number }) => { - const context = useContext(CreatorWizardContext); - const error = context.errors.taskErrors.get(index); - - return error !== undefined ? ( -
{error}
- ) : undefined; -}; - const FileDownload = () => { const { files } = useContext(CreatorWizardContext); + function doDownload(file: { content: string; name: string }) { + const dotIndex = file.name.lastIndexOf('.'); + const baseName = + dotIndex !== -1 ? file.name.substring(0, dotIndex) : file.name; + const extension = + dotIndex !== -1 ? file.name.substring(dotIndex + 1) : 'txt'; + + downloadFile(file.content, baseName, extension); + } + return (
- Download these files. - {files.map((file) => ( -
+ + + + + + Files successfully generated! + + + + + + + Download these files and use them to create the learning resource PR + in the{' '} + + {' '} + correct repo + + . + + + + - - - {file.content} - -
- ))} + + + {files.map((file) => ( + + } + className="pf-v5-u-mb-sm" + onClick={() => doDownload(file)} + > + {file.name} + + + + {file.content} + + + ))} +
); }; @@ -234,40 +257,53 @@ const WizardSpy = () => { const creatorContext = useContext(CreatorWizardContext); useEffect(() => { - creatorContext.onChangeCurrentTask( - taskFromStepName(wizardContext.currentStep.name) + creatorContext.onChangeCurrentStage( + stageFromStepName(wizardContext.currentStep.name) ); }, [wizardContext.currentStep.name]); return undefined; }; +const TaskTitlePreview = ({ index }: { index: number }) => { + return ( + + {(state) => ( + + {state.values?.[NAME_TASK_TITLES]?.[index] ?? ''} + + )} + + ); +}; + const CreatorWizard = ({ onChangeKind, onChangeQuickStartSpec, onChangeBundles, - onChangeTaskContents, - onChangeCurrentTask, + onChangeCurrentStage, + resetCreator, files, - errors, }: CreatorWizardProps) => { const chrome = useChrome(); const schema = useMemo(() => makeSchema(chrome), []); const context = useMemo( () => ({ - errors, files, - onChangeCurrentTask, + onChangeCurrentStage, + resetCreator, }), - [errors, files] + [files, onChangeCurrentStage] ); const componentMapper = { ...pf4ComponentMapper, - 'lr-task-error': TaskErrorPreview, + 'lr-number-input': DdfNumberInput, 'lr-download-files': FileDownload, 'lr-wizard-spy': WizardSpy, + 'lr-task-title-preview': TaskTitlePreview, + 'lr-string-array': StringArrayInput, }; return ( @@ -298,7 +334,6 @@ const CreatorWizard = ({ onChangeKind={onChangeKind} onChangeBundles={onChangeBundles} onChangeQuickStartSpec={onChangeQuickStartSpec} - onChangeTaskContents={onChangeTaskContents} /> )} diff --git a/src/components/creator/context.ts b/src/components/creator/context.ts new file mode 100644 index 00000000..5c58b828 --- /dev/null +++ b/src/components/creator/context.ts @@ -0,0 +1,13 @@ +import React from 'react'; +import { CreatorWizardStage } from './schema'; +import { CreatorFiles } from './types'; + +export const CreatorWizardContext = React.createContext<{ + files: CreatorFiles; + onChangeCurrentStage: (stage: CreatorWizardStage) => void; + resetCreator: () => void; +}>({ + files: [], + onChangeCurrentStage: () => {}, + resetCreator: () => {}, +}); diff --git a/src/components/creator/schema.tsx b/src/components/creator/schema.tsx index 65562bb0..1a499a54 100644 --- a/src/components/creator/schema.tsx +++ b/src/components/creator/schema.tsx @@ -1,28 +1,29 @@ import { - ConditionProp, - Field, FormSpy, Schema, componentTypes, - dataTypes, - validatorTypes, } from '@data-driven-forms/react-form-renderer'; -import { - ALL_ITEM_KINDS, - ALL_KIND_ENTRIES, - ItemKind, - ItemMeta, - isItemKind, - metaForKind, -} from './meta'; +import { ALL_ITEM_KINDS, metaForKind } from './meta'; import { ChromeAPI } from '@redhat-cloud-services/types'; import { WizardButtonsProps, WizardProps, } from '@data-driven-forms/pf4-component-mapper'; -import { WizardNextStepFunctionArgument } from '@data-driven-forms/pf4-component-mapper/wizard/wizard'; import React from 'react'; import { Button } from '@patternfly/react-core'; +import { isTaskStep, makeTaskStep, taskFromStepName } from './steps/task'; +import { isDetailsStep, makeDetailsStep } from './steps/details'; +import { + isPanelOverviewStep, + makePanelOverviewStep, +} from './steps/panel-overview'; +import { isKindStep, makeKindStep } from './steps/kind'; +import { + STEP_DOWNLOAD, + isDownloadStep, + makeDownloadStep, +} from './steps/download'; +import { MAX_TASKS, NAME_KIND, NAME_TASK_TITLES } from './steps/common'; const CustomButtons = (props: WizardButtonsProps) => { return ( @@ -44,9 +45,6 @@ const CustomButtons = (props: WizardButtonsProps) => { return ( <> - {computedNext !== undefined - ? props.renderNextButton({ submitLabel: 'Next' }) - : null} + + {computedNext !== undefined + ? props.renderNextButton({ submitLabel: 'Next' }) + : null} ); }} @@ -62,153 +64,37 @@ const CustomButtons = (props: WizardButtonsProps) => { ); }; -const REQUIRED = { - type: validatorTypes.REQUIRED, -} as const; - -function kindMetaCondition(test: (meta: ItemMeta) => boolean): ConditionProp { - return { - when: NAME_KIND, - is: (kind: string | undefined) => { - return ( - typeof kind === 'string' && isItemKind(kind) && test(metaForKind(kind)) - ); - }, - }; -} - -type Bundles = ReturnType; +const STEP_TITLE_PANEL_PARENT = 'Create panel'; -function detailsStepName(kind: ItemKind): string { - return `step-details-${kind}`; -} - -export const NAME_KIND = 'kind'; -export const NAME_TITLE = 'title'; -export const NAME_BUNDLES = 'bundles'; -export const NAME_DESCRIPTION = 'description'; -export const NAME_DURATION = 'duration'; -export const NAME_URL = 'url'; - -export const NAME_PANEL_INTRODUCTION = 'panel-overview'; -export const NAME_PREREQUISITES = 'prerequisites'; -export const NAME_TASK_TITLES = 'task-titles'; - -const STEP_PANEL_OVERVIEW = 'step-panel-overview'; -const STEP_DOWNLOAD = 'step-download'; - -function makeDetailsStep(kind: ItemKind, bundles: Bundles) { - const meta = metaForKind(kind); - - const fields: Field[] = []; - - fields.push( - { - component: componentTypes.TEXT_FIELD, - name: NAME_TITLE, - label: 'Title', - isRequired: true, - validate: [REQUIRED], - }, - { - component: componentTypes.SELECT, - name: NAME_BUNDLES, - label: 'Bundles', - simpleValue: true, - isMulti: true, - options: bundles.map((b) => ({ - value: b.id, - label: `${b.title} (${b.id})`, - })), - }, - { - component: componentTypes.TEXT_FIELD, - name: NAME_DESCRIPTION, - label: 'Description', - isRequired: true, - validate: [REQUIRED], +export type CreatorWizardStage = + | { type: 'card' } + | { type: 'panel-overview' } + | { + type: 'task'; + index: number; } - ); + | { type: 'download' }; - if (meta.fields.duration) { - fields.push({ - component: componentTypes.TEXT_FIELD, - name: NAME_DURATION, - label: 'Duration', - dataType: dataTypes.NUMBER, - isRequired: true, - validate: [REQUIRED], - }); - } +export function stageFromStepName(name: string): CreatorWizardStage { + if (isKindStep(name) || isDetailsStep(name)) return { type: 'card' }; - if (meta.fields.url) { - fields.push({ - component: componentTypes.TEXT_FIELD, - name: NAME_URL, - label: 'URL', - isRequired: true, - validate: [ - REQUIRED, - { - type: validatorTypes.URL, - }, - ], - condition: kindMetaCondition((meta) => meta.fields.url === true), - }); - } + if (isPanelOverviewStep(name)) return { type: 'panel-overview' }; - return { - name: detailsStepName(kind), - title: `${meta.displayName} details`, - fields: fields, - nextStep: meta.hasTasks ? STEP_PANEL_OVERVIEW : STEP_DOWNLOAD, - }; -} - -const MAX_TASKS = 10; - -export const NAME_TASKS_ARRAY = 'tasks'; -export const NAME_TASK_CONTENT = 'content'; - -const TASK_STEP_PREFIX = 'step-task-detail-'; + if (isTaskStep(name)) { + return { + type: 'task', + index: (() => { + const index = taskFromStepName(name); + if (index === null) throw new Error('unable to parse task index'); -function taskStepName(index: number): string { - return `${TASK_STEP_PREFIX}${index}`; -} - -export function taskFromStepName(name: string): number | null { - if (name.startsWith(TASK_STEP_PREFIX)) { - return parseInt(name.substring(TASK_STEP_PREFIX.length)); + return index; + })(), + }; } - return null; -} - -function makeTaskStep(index: number) { - return { - name: taskStepName(index), - title: `Task ${index + 1}`, - fields: [ - { - component: componentTypes.TEXTAREA, - name: `${NAME_TASKS_ARRAY}[${index}].${NAME_TASK_CONTENT}`, - label: 'Task data (YAML)', - resizeOrientation: 'vertical', - }, - { - component: 'lr-task-error', - name: `internal-task-errors[${index}]`, - index: index, - }, - ], - nextStep: ({ values }: WizardNextStepFunctionArgument) => { - if (index + 1 < (values?.[NAME_TASK_TITLES]?.length ?? 0)) { - return taskStepName(index + 1); - } + if (isDownloadStep(name)) return { type: 'download' }; - return STEP_DOWNLOAD; - }, - }; + throw new Error('unable to parse step name: ' + name); } export function makeSchema(chrome: ChromeAPI): Schema { @@ -217,10 +103,16 @@ export function makeSchema(chrome: ChromeAPI): Schema { const taskSteps = []; for (let i = 0; i < MAX_TASKS; ++i) { - taskSteps.push(makeTaskStep(i)); + taskSteps.push( + makeTaskStep({ + index: i, + panelStepTitle: STEP_TITLE_PANEL_PARENT, + downloadStep: STEP_DOWNLOAD, + }) + ); } - const wizardProps: WizardProps & { + const rawWizardProps: WizardProps & { component: string; name: string; } = { @@ -229,106 +121,64 @@ export function makeSchema(chrome: ChromeAPI): Schema { isDynamic: true, crossroads: [NAME_KIND, NAME_TASK_TITLES], fields: [ - { - name: 'step-kind', - title: 'Select content type', - fields: [ - { - component: componentTypes.SELECT, - name: NAME_KIND, - label: 'Type', - simpleValue: true, - options: ALL_KIND_ENTRIES.map(([name, value]) => ({ - value: name, - label: value.displayName, - })), - isRequired: true, - validate: [REQUIRED], - }, - ], - nextStep: { - when: NAME_KIND, - stepMapper: Object.fromEntries( - ALL_ITEM_KINDS.map((kind) => [kind, detailsStepName(kind)]) - ), - }, - }, - ...ALL_ITEM_KINDS.map((kind) => makeDetailsStep(kind, bundles)), - { - name: STEP_PANEL_OVERVIEW, - title: 'Panel overview', - fields: [ - { - component: componentTypes.TEXTAREA, - name: NAME_PANEL_INTRODUCTION, - label: 'Introduction (Markdown)', - resizeOrientation: 'vertical', - }, - { - component: componentTypes.FIELD_ARRAY, - name: NAME_PREREQUISITES, - label: 'Prerequisites', - noItemsMessage: 'No prerequisites have been added.', - fields: [ - { - component: componentTypes.TEXT_FIELD, - label: 'Prerequisite', - }, - ], - }, - { - component: componentTypes.FIELD_ARRAY, - name: NAME_TASK_TITLES, - label: 'Tasks', - minItems: 1, - maxItems: MAX_TASKS, - noItemsMessage: 'No tasks have been added.', - initialValue: [''], - fields: [ - { - component: componentTypes.TEXT_FIELD, - label: 'Title', - }, - ], - }, - ], - nextStep: taskStepName(0), - }, + makeKindStep(), + ...ALL_ITEM_KINDS.map((kind) => + makeDetailsStep({ kind, bundles, downloadStep: STEP_DOWNLOAD }) + ), + ...ALL_ITEM_KINDS.filter((kind) => metaForKind(kind).hasTasks).map( + (kind) => + makePanelOverviewStep({ + kind, + panelStepTitle: STEP_TITLE_PANEL_PARENT, + }) + ), ...taskSteps, - { - name: STEP_DOWNLOAD, - title: 'Download files', - fields: [ - { - component: 'lr-download-files', - name: 'internal-download', - }, - ], - }, + makeDownloadStep(), ], }; - const schema = { - fields: [wizardProps], - }; - - for (const step of schema.fields) { - if (step.component === componentTypes.WIZARD) { - for (const page of step.fields) { - // Add an lr-wizard-spy component to all wizard steps. It must be here (rather - // than at the top level of the schema) so that it is inside the WizardContext. - page.fields.push({ + const wizardProps = { + ...rawWizardProps, + fields: rawWizardProps.fields.map((rawPage) => { + const page: typeof rawPage & { + buttonLabels?: { [key: string]: string }; + } = { ...rawPage }; + + // Add an lr-wizard-spy component to all wizard steps. It must be here (rather + // than at the top level of the schema) so that it is inside the WizardContext. + page.fields = [ + ...page.fields, + { component: 'lr-wizard-spy', name: `internal-wizard-spies.${page.name}`, - }); + }, + ]; + + // Use custom buttons for each step. + if (page.buttons === undefined) { + page.buttons = CustomButtons; + } - // Use custom buttons for each step. - if (page.buttons === undefined) { - page.buttons = CustomButtons; + if (page.buttonLabels !== undefined) { + // Fix missing prop errors for button labels by adding defaults. + page.buttonLabels = { + next: 'Next', + cancel: 'Cancel', + back: 'Back', + ...page.buttonLabels, + }; + + // Don't show "Submit" as a label, since this form is never submitted. + if (page.buttonLabels.submit === undefined) { + page.buttonLabels.submit = page.buttonLabels.next; } } - } - } - return schema; + return page; + }), + }; + + return { + fields: [wizardProps], + }; } diff --git a/src/components/creator/steps/common.ts b/src/components/creator/steps/common.ts new file mode 100644 index 00000000..61870100 --- /dev/null +++ b/src/components/creator/steps/common.ts @@ -0,0 +1,25 @@ +import { validatorTypes } from '@data-driven-forms/react-form-renderer'; + +export const REQUIRED = { + type: validatorTypes.REQUIRED, +} as const; + +export const NAME_KIND = 'kind'; +export const NAME_TITLE = 'title'; +export const NAME_BUNDLES = 'bundles'; +export const NAME_DESCRIPTION = 'description'; +export const NAME_DURATION = 'duration'; +export const NAME_URL = 'url'; + +export const NAME_PANEL_INTRODUCTION = 'panel-overview'; +export const NAME_PREREQUISITES = 'prerequisites'; + +export const NAME_TASKS_ARRAY = 'tasks'; +export const NAME_TASK_TITLES = 'task-titles'; + +export const NAME_TASK_DESCRIPTION = 'description'; +export const NAME_TASK_ENABLE_WORK_CHECK = 'enable_work_check'; +export const NAME_TASK_WORK_CHECK_INSTRUCTIONS = 'work_check_instructions'; +export const NAME_TASK_WORK_CHECK_HELP = 'work_check_help'; + +export const MAX_TASKS = 10; diff --git a/src/components/creator/steps/details.tsx b/src/components/creator/steps/details.tsx new file mode 100644 index 00000000..5f3d28da --- /dev/null +++ b/src/components/creator/steps/details.tsx @@ -0,0 +1,141 @@ +import { ItemKind, ItemMeta, isItemKind, metaForKind } from '../meta'; +import { + ConditionProp, + Field, + componentTypes, + dataTypes, + validatorTypes, +} from '@data-driven-forms/react-form-renderer'; +import React from 'react'; +import { ChromeAPI } from '@redhat-cloud-services/types'; +import { + NAME_BUNDLES, + NAME_DESCRIPTION, + NAME_DURATION, + NAME_KIND, + NAME_TITLE, + NAME_URL, + REQUIRED, +} from './common'; +import { panelOverviewStepName } from './panel-overview'; + +export type Bundles = ReturnType; + +function kindMetaCondition(test: (meta: ItemMeta) => boolean): ConditionProp { + return { + when: NAME_KIND, + is: (kind: string | undefined) => { + return ( + typeof kind === 'string' && isItemKind(kind) && test(metaForKind(kind)) + ); + }, + }; +} + +const DETAILS_STEP_PREFIX = 'step-details-'; + +export function isDetailsStep(name: string): boolean { + return name.startsWith(DETAILS_STEP_PREFIX); +} + +export function detailsStepName(kind: ItemKind): string { + return `${DETAILS_STEP_PREFIX}${kind}`; +} + +export function makeDetailsStep({ + kind, + bundles, + downloadStep, +}: { + kind: ItemKind; + bundles: Bundles; + downloadStep: string; +}) { + const meta = metaForKind(kind); + + const fields: Field[] = []; + + fields.push( + { + component: componentTypes.PLAIN_TEXT, + name: 'internal-text-details-description', + label: + 'Share the details required to populate a card in the correct places.', + }, + { + component: componentTypes.SELECT, + name: NAME_BUNDLES, + label: 'Associated bundle(s)', + simpleValue: true, + isMulti: true, + placeholder: 'Select all that apply', + options: bundles.map((b) => ({ + value: b.id, + label: `${b.title} (${b.id})`, + })), + isRequired: true, + validate: [REQUIRED], + }, + { + component: componentTypes.TEXT_FIELD, + name: NAME_TITLE, + label: 'Resource title', + placeholder: 'Title to display on card', + isRequired: true, + validate: [REQUIRED], + }, + { + component: componentTypes.TEXTAREA, + name: NAME_DESCRIPTION, + label: 'Resource description', + placeholder: + "Short description of resource and will auto-truncate on card with '...' after 3 lines of text.", + isRequired: true, + validate: [REQUIRED], + resizeOrientation: 'vertical', + } + ); + + if (meta.fields.duration) { + fields.push({ + component: 'lr-number-input', + name: NAME_DURATION, + label: 'Duration', + unit: minutes, + dataType: dataTypes.NUMBER, + initialValue: 0, + minValue: 0, + isRequired: true, + validate: [REQUIRED], + }); + } + + if (meta.fields.url) { + fields.push({ + component: componentTypes.TEXT_FIELD, + name: NAME_URL, + label: 'Endpoint URL', + placeholder: 'http://url.redhat.com/docs-n-things', + isRequired: true, + validate: [ + REQUIRED, + { + type: validatorTypes.URL, + }, + ], + condition: kindMetaCondition((meta) => meta.fields.url === true), + }); + } + + return { + name: detailsStepName(kind), + title: `${meta.displayName} details`, + fields: fields, + nextStep: meta.hasTasks ? panelOverviewStepName(kind) : downloadStep, + buttonLabels: { + next: meta.hasTasks + ? `Approve card and create ${meta.displayName} panel` + : 'Approve card and generate files', + }, + }; +} diff --git a/src/components/creator/steps/download.tsx b/src/components/creator/steps/download.tsx new file mode 100644 index 00000000..265225a8 --- /dev/null +++ b/src/components/creator/steps/download.tsx @@ -0,0 +1,52 @@ +import React, { useContext } from 'react'; +import { WizardButtonsProps } from '@data-driven-forms/pf4-component-mapper'; +import { Button } from '@patternfly/react-core'; +import { CreatorWizardContext } from '../context'; +import PlusCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/plus-circle-icon'; + +export const STEP_DOWNLOAD = 'step-download'; + +export function isDownloadStep(name: string) { + return name === STEP_DOWNLOAD; +} + +const DownloadStepButtons = (props: WizardButtonsProps) => { + const { resetCreator } = useContext(CreatorWizardContext); + + return ( + <> + + + + + ); +}; + +export function makeDownloadStep() { + return { + name: STEP_DOWNLOAD, + title: 'Download files', + fields: [ + { + component: 'lr-download-files', + name: 'internal-download', + }, + ], + hasNoBodyPadding: true, + buttons: DownloadStepButtons, + }; +} diff --git a/src/components/creator/steps/kind.tsx b/src/components/creator/steps/kind.tsx new file mode 100644 index 00000000..52669994 --- /dev/null +++ b/src/components/creator/steps/kind.tsx @@ -0,0 +1,42 @@ +import { componentTypes } from '@data-driven-forms/react-form-renderer'; +import { ALL_ITEM_KINDS, ALL_KIND_ENTRIES } from '../meta'; +import { NAME_KIND, REQUIRED } from './common'; +import { detailsStepName } from './details'; + +const STEP_KIND = 'step-kind'; + +export function isKindStep(name: string): boolean { + return name === STEP_KIND; +} + +export function makeKindStep() { + return { + name: STEP_KIND, + title: 'Select content type', + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'internal-text-kind-description', + label: "Learning resources are grouped by their 'content type'.", + }, + { + component: componentTypes.RADIO, + name: NAME_KIND, + label: 'Select content type', + simpleValue: true, + options: ALL_KIND_ENTRIES.map(([name, value]) => ({ + value: name, + label: value.displayName, + })), + isRequired: true, + validate: [REQUIRED], + }, + ], + nextStep: { + when: NAME_KIND, + stepMapper: Object.fromEntries( + ALL_ITEM_KINDS.map((kind) => [kind, detailsStepName(kind)]) + ), + }, + }; +} diff --git a/src/components/creator/steps/panel-overview.tsx b/src/components/creator/steps/panel-overview.tsx new file mode 100644 index 00000000..6f0a3dcd --- /dev/null +++ b/src/components/creator/steps/panel-overview.tsx @@ -0,0 +1,87 @@ +import { ItemKind, metaForKind } from '../meta'; +import { WizardField } from '@data-driven-forms/pf4-component-mapper'; +import { componentTypes } from '@data-driven-forms/react-form-renderer'; +import { Title } from '@patternfly/react-core'; +import { taskStepName } from './task'; +import React from 'react'; +import { + MAX_TASKS, + NAME_PANEL_INTRODUCTION, + NAME_PREREQUISITES, + NAME_TASK_TITLES, +} from './common'; +import PlusCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/plus-circle-icon'; + +export const PANEL_OVERVIEW_STEP_PREFIX = 'step-panel-overview-'; + +export function isPanelOverviewStep(name: string): boolean { + return name.startsWith(PANEL_OVERVIEW_STEP_PREFIX); +} + +export function panelOverviewStepName(kind: ItemKind): string { + return `${PANEL_OVERVIEW_STEP_PREFIX}${kind}`; +} + +export function makePanelOverviewStep({ + kind, + panelStepTitle, +}: { + kind: ItemKind; + panelStepTitle: string; +}) { + const meta = metaForKind(kind); + + const step: WizardField & { buttonLabels: { [key: string]: string } } = { + name: panelOverviewStepName(kind), + title: 'Create overview', + substepOf: panelStepTitle, + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'internal-text-overview-instructions', + label: `Share the required details to show on the introduction (first view) in the ${meta.displayName}. Details that you entered in the previous steps have been brought in automatically.`, + }, + { + component: componentTypes.PLAIN_TEXT, + name: 'internal-text-overview-header', + label: {meta.displayName} overview, + }, + { + component: componentTypes.TEXTAREA, + name: NAME_PANEL_INTRODUCTION, + label: 'Introduction (Markdown)', + resizeOrientation: 'vertical', + }, + { + component: componentTypes.FIELD_ARRAY, + name: NAME_PREREQUISITES, + label: 'Prerequisites', + noItemsMessage: 'No prerequisites have been added.', + fields: [ + { + component: componentTypes.TEXT_FIELD, + label: 'Prerequisite', + }, + ], + }, + { + component: 'lr-string-array', + name: NAME_TASK_TITLES, + label: 'Tasks', + minItems: 1, + maxItems: MAX_TASKS, + initialValue: [''], + fullMessage: `Only ${MAX_TASKS} tasks can be added.`, + itemLabel: (index: number) => `Task ${index + 1}`, + addLabel: 'Add another task', + addLabelIcon: , + }, + ], + nextStep: taskStepName(0), + buttonLabels: { + next: 'Create task 1 content', + }, + }; + + return step; +} diff --git a/src/components/creator/steps/task.tsx b/src/components/creator/steps/task.tsx new file mode 100644 index 00000000..7ac3bfb5 --- /dev/null +++ b/src/components/creator/steps/task.tsx @@ -0,0 +1,116 @@ +import { + FormSpy, + componentTypes, +} from '@data-driven-forms/react-form-renderer'; +import { WizardNextStepFunctionArgument } from '@data-driven-forms/pf4-component-mapper/wizard/wizard'; +import React from 'react'; +import { + NAME_TASKS_ARRAY, + NAME_TASK_DESCRIPTION, + NAME_TASK_ENABLE_WORK_CHECK, + NAME_TASK_TITLES, + NAME_TASK_WORK_CHECK_HELP, + NAME_TASK_WORK_CHECK_INSTRUCTIONS, +} from './common'; + +const TASK_STEP_PREFIX = 'step-task-detail-'; + +export function isTaskStep(name: string) { + return name.startsWith(TASK_STEP_PREFIX); +} + +export function taskStepName(index: number): string { + return `${TASK_STEP_PREFIX}${index}`; +} + +export function taskFromStepName(name: string): number | null { + if (name.startsWith(TASK_STEP_PREFIX)) { + return parseInt(name.substring(TASK_STEP_PREFIX.length)); + } + + return null; +} + +export function makeTaskStep({ + index, + panelStepTitle, + downloadStep, +}: { + index: number; + panelStepTitle: string; + downloadStep: string; +}) { + const taskName = `${NAME_TASKS_ARRAY}[${index}]`; + + const workCheckEnabledCondition = { + when: `${taskName}.${NAME_TASK_ENABLE_WORK_CHECK}`, + is: true, + }; + + return { + name: taskStepName(index), + title: `Task ${index + 1}`, + substepOf: panelStepTitle, + fields: [ + { + component: 'lr-task-title-preview', + name: `internal-task-title-preview[${index}]`, + index: index, + }, + { + component: componentTypes.PLAIN_TEXT, + name: `internal-text-task-step-description`, + label: 'Add the content for this step of the panel.', + }, + { + component: componentTypes.TEXTAREA, + name: `${taskName}.${NAME_TASK_DESCRIPTION}`, + label: 'Description', + resizeOrientation: 'vertical', + }, + { + component: componentTypes.SWITCH, + name: `${taskName}.${NAME_TASK_ENABLE_WORK_CHECK}`, + label: "Show 'Work check' section", + }, + { + component: componentTypes.PLAIN_TEXT, + name: `internal-text-check-work-explanation`, + condition: workCheckEnabledCondition, + label: "Add the content to display in the 'Check your work box.", + }, + { + component: componentTypes.TEXTAREA, + name: `${taskName}.${NAME_TASK_WORK_CHECK_INSTRUCTIONS}`, + condition: workCheckEnabledCondition, + label: 'Work check instructions', + resizeOrientation: 'vertical', + }, + { + component: componentTypes.TEXT_FIELD, + condition: workCheckEnabledCondition, + label: 'Optional failure message', + name: `${taskName}.${NAME_TASK_WORK_CHECK_HELP}`, + placeholder: 'Try completing the task again', + }, + ], + nextStep: ({ values }: WizardNextStepFunctionArgument) => { + if (index + 1 < (values?.[NAME_TASK_TITLES]?.length ?? 0)) { + return taskStepName(index + 1); + } + + return downloadStep; + }, + buttonLabels: { + next: ( + + {(state) => { + return index + 1 < state.values[NAME_TASK_TITLES].length + ? `Create task ${index + 2} content` + : 'Approve and generate files'; + }} + + ), + }, + }; +} diff --git a/src/components/creator/types.ts b/src/components/creator/types.ts new file mode 100644 index 00000000..eddd6162 --- /dev/null +++ b/src/components/creator/types.ts @@ -0,0 +1,4 @@ +export type CreatorFiles = { + name: string; + content: string; +}[];