diff --git a/.changeset/tidy-planets-trade.md b/.changeset/tidy-planets-trade.md new file mode 100644 index 0000000000..49c1643e55 --- /dev/null +++ b/.changeset/tidy-planets-trade.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-scaffolder-react': patch +--- + +Make it possible to define control buttons text (Back, Create, Review) per template diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 15f7c11eba..194120f0f9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -363,8 +363,12 @@ export async function createRouter( const template = await authorizeTemplate(req.params, token); const parameters = [template.spec.parameters ?? []].flat(); + + const presentation = template.spec.presentation; + res.json({ title: template.metadata.title ?? template.metadata.name, + ...(presentation ? { presentation } : {}), description: template.metadata.description, 'ui:options': template.metadata['ui:options'], steps: parameters.map(schema => ({ diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 6113c72659..8cfbd2aaac 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -66,6 +66,7 @@ export interface TemplateEntityV1beta3 extends Entity { kind: 'Template'; spec: { type: string; + presentation?: TemplatePresentationV1beta3; parameters?: TemplateParametersV1beta3 | TemplateParametersV1beta3[]; steps: Array; output?: { @@ -98,4 +99,13 @@ export interface TemplatePermissionsV1beta3 extends JsonObject { // (undocumented) tags?: string[]; } + +// @public +export interface TemplatePresentationV1beta3 extends JsonObject { + buttonLabels?: { + backButtonText?: string; + createButtonText?: string; + reviewButtonText?: string; + }; +} ``` diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts index ebabb5cad4..23cc3ca3d8 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -45,6 +45,12 @@ export interface TemplateEntityV1beta3 extends Entity { * The type that the Template will create. For example service, website or library. */ type: string; + + /** + * Template specific configuration of the presentation layer. + */ + presentation?: TemplatePresentationV1beta3; + /** * This is a JSONSchema or an array of JSONSchema's which is used to render a form in the frontend * to collect user input and validate it against that schema. This can then be used in the `steps` part below to template @@ -67,6 +73,31 @@ export interface TemplateEntityV1beta3 extends Entity { }; } +/** + * The presentation of the template. + * + * @public + */ +export interface TemplatePresentationV1beta3 extends JsonObject { + /** + * Overrides default buttons' text + */ + buttonLabels?: { + /** + * The text for the button which leads to the previous template page + */ + backButtonText?: string; + /** + * The text for the button which starts the execution of the template + */ + createButtonText?: string; + /** + * The text for the button which opens template's review/summary + */ + reviewButtonText?: string; + }; +} + /** * Step that is part of a Template Entity. * diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index fd34fe6f41..3300aebf46 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -27,6 +27,7 @@ export { isTemplateEntityV1beta3, } from './TemplateEntityV1beta3'; export type { + TemplatePresentationV1beta3, TemplateEntityV1beta3, TemplateEntityStepV1beta3, TemplateParametersV1beta3, diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index a31d9b1d60..3d9361bf18 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -33,6 +33,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; +import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UiSchema } from '@rjsf/utils'; // @alpha (undocumented) @@ -189,6 +190,7 @@ export type StepperProps = { components?: { ReviewStepComponent?: ComponentType; ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element; + backButtonText?: ReactNode; createButtonText?: ReactNode; reviewButtonText?: ReactNode; }; @@ -303,6 +305,7 @@ export const useTemplateParameterSchema: (templateRef: string) => { // @alpha export const useTemplateSchema: (manifest: TemplateParameterSchema) => { steps: ParsedTemplateSchema[]; + presentation?: TemplatePresentationV1beta3 | undefined; }; // @alpha (undocumented) diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index 6acc2f9812..8589078f1e 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -39,6 +39,7 @@ import { StrictRJSFSchema } from '@rjsf/utils'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplatesType } from '@rjsf/utils'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; @@ -482,6 +483,7 @@ export type TemplateGroupFilter = { export type TemplateParameterSchema = { title: string; description?: string; + presentation?: TemplatePresentationV1beta3; steps: Array<{ title: string; description?: string; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 23e97c67e4..9a333944b9 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -81,6 +81,7 @@ export type StepperProps = { components?: { ReviewStepComponent?: ComponentType; ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element; + backButtonText?: ReactNode; createButtonText?: ReactNode; reviewButtonText?: ReactNode; }; @@ -96,11 +97,12 @@ export const Stepper = (stepperProps: StepperProps) => { const { ReviewStateComponent = ReviewState, ReviewStepComponent, + backButtonText = 'Back', createButtonText = 'Create', reviewButtonText = 'Review', } = components; const analytics = useAnalytics(); - const { steps } = useTemplateSchema(props.manifest); + const { presentation, steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [isValidating, setIsValidating] = useState(false); @@ -178,6 +180,13 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; + const backLabel = + presentation?.buttonLabels?.backButtonText ?? backButtonText; + const createLabel = + presentation?.buttonLabels?.createButtonText ?? createButtonText; + const reviewLabel = + presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText; + return ( <> {isValidating && } @@ -213,7 +222,7 @@ export const Stepper = (stepperProps: StepperProps) => { className={styles.backButton} disabled={activeStep < 1 || isValidating} > - Back + {backLabel} @@ -251,7 +260,7 @@ export const Stepper = (stepperProps: StepperProps) => { color="primary" onClick={handleCreate} > - {createButtonText} + {createLabel} diff --git a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts index df70df965c..838f8b1c30 100644 --- a/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; +import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/utils'; import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; @@ -39,7 +40,10 @@ export interface ParsedTemplateSchema { */ export const useTemplateSchema = ( manifest: TemplateParameterSchema, -): { steps: ParsedTemplateSchema[] } => { +): { + steps: ParsedTemplateSchema[]; + presentation?: TemplatePresentationV1beta3; +} => { const featureFlags = useApi(featureFlagsApiRef); const steps = manifest.steps.map(({ title, description, schema }) => ({ title, @@ -76,6 +80,7 @@ export const useTemplateSchema = ( })); return { + presentation: manifest.presentation, steps: returningSteps, }; }; diff --git a/plugins/scaffolder-react/src/types.ts b/plugins/scaffolder-react/src/types.ts index f644fbeb64..c1c26f32bb 100644 --- a/plugins/scaffolder-react/src/types.ts +++ b/plugins/scaffolder-react/src/types.ts @@ -16,6 +16,8 @@ import { JsonObject } from '@backstage/types'; +import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; + /** * The shape of each entry of parameters which gets rendered * as a separate step in the wizard input @@ -25,6 +27,7 @@ import { JsonObject } from '@backstage/types'; export type TemplateParameterSchema = { title: string; description?: string; + presentation?: TemplatePresentationV1beta3; steps: Array<{ title: string; description?: string; diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 5aa217553f..f962bc1620 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -405,11 +405,10 @@ describe('SearchContext', () => { await waitFor(() => { expect(result.current).toEqual(expect.objectContaining(initialState)); + expect(result.current.fetchNextPage).toBeUndefined(); + expect(result.current.fetchPreviousPage).toBeDefined(); }); - expect(result.current.fetchNextPage).toBeUndefined(); - expect(result.current.fetchPreviousPage).toBeDefined(); - await act(async () => { result.current.fetchPreviousPage!(); });