diff --git a/.changeset/great-bears-work.md b/.changeset/great-bears-work.md new file mode 100644 index 0000000000..572cf1538d --- /dev/null +++ b/.changeset/great-bears-work.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added a `context` parameter to validator functions, letting them have access to +the API holder. + +If you have implemented custom validators and use `createScaffolderFieldExtension`, +your `validation` function can now optionally accept a third parameter, +`context: { apiHolder: ApiHolder }`. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 5da1869fc2..7982ad997e 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -4,6 +4,7 @@ ```ts +import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -24,9 +25,21 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; // @public (undocumented) export function createScaffolderFieldExtension(options: FieldExtensionOptions): Extension<() => null>; +// @public (undocumented) +export type CustomFieldValidator = ((data: T, field: FieldValidation) => void) | ((data: T, field: FieldValidation, context: { + apiHolder: ApiHolder; +}) => void); + // @public (undocumented) export const EntityPickerFieldExtension: () => null; +// @public (undocumented) +export type FieldExtensionOptions = { + name: string; + component: (props: FieldProps) => JSX.Element | null; + validation?: CustomFieldValidator; +}; + // @public (undocumented) export const OwnerPickerFieldExtension: () => null; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index b6cab5b620..e72255f129 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -15,13 +15,13 @@ */ import { JsonObject, JsonValue } from '@backstage/config'; import { LinearProgress } from '@material-ui/core'; -import { FieldValidation, FormValidation, IChangeEvent } from '@rjsf/core'; +import { FormValidation, IChangeEvent } from '@rjsf/core'; import React, { useCallback, useState } from 'react'; import { generatePath, Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; -import { FieldExtensionOptions } from '../../extensions'; +import { CustomFieldValidator, FieldExtensionOptions } from '../../extensions'; import { rootRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; @@ -32,7 +32,13 @@ import { Lifecycle, Page, } from '@backstage/core-components'; -import { errorApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + ApiHolder, + errorApiRef, + useApi, + useApiHolder, + useRouteRef, +} from '@backstage/core-plugin-api'; const useTemplateParameterSchema = (templateName: string) => { const scaffolderApi = useApi(scaffolderApiRef); @@ -54,10 +60,10 @@ function isObject(obj: unknown): obj is JsonObject { export const createValidator = ( rootSchema: JsonObject, - validators: Record< - string, - undefined | ((value: JsonValue, validation: FieldValidation) => void) - >, + validators: Record>, + context: { + apiHolder: ApiHolder; + }, ) => { function validate( schema: JsonObject, @@ -86,7 +92,11 @@ export const createValidator = ( const fieldName = isObject(propSchema) && (propSchema['ui:field'] as string); if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!(propData as JsonValue, propValidation); + validators[fieldName]!( + propData as JsonValue, + propValidation, + context, + ); } } } @@ -103,6 +113,7 @@ export const TemplatePage = ({ }: { customFieldExtensions?: FieldExtensionOptions[]; }) => { + const apiHolder = useApiHolder(); const errorApi = useApi(errorApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); @@ -171,7 +182,11 @@ export const TemplatePage = ({ steps={schema.steps.map(step => { return { ...step, - validate: createValidator(step.schema, customFieldValidators), + validate: createValidator( + step.schema, + customFieldValidators, + { apiHolder }, + ), }; })} /> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts index f0df198507..1ea66e7df9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { repoPickerValidation } from './validation'; import { FieldValidation } from '@rjsf/core'; @@ -22,7 +23,7 @@ describe('RepoPicker Validation', () => { addError: jest.fn(), } as unknown) as FieldValidation); - it('validaties when no repo', () => { + it('validates when no repo', () => { const mockFieldValidation = fieldValidator(); repoPickerValidation('github.com?owner=a', mockFieldValidation); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 827afa3cab..760b9f7890 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { FieldValidation } from '@rjsf/core'; export const repoPickerValidation = ( diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index f53052cb67..131cd105da 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { FieldExtensionOptions } from './types'; +import { CustomFieldValidator, FieldExtensionOptions } from './types'; import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; @@ -46,6 +46,6 @@ attachComponentData( true, ); -export type { FieldExtensionOptions }; +export type { CustomFieldValidator, FieldExtensionOptions }; export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default'; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 8f1c155f28..644897f7f4 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -13,10 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FieldProps } from '@rjsf/core'; +export type CustomFieldValidator = + | ((data: T, field: FieldValidation) => void) + | (( + data: T, + field: FieldValidation, + context: { apiHolder: ApiHolder }, + ) => void); + export type FieldExtensionOptions = { name: string; component: (props: FieldProps) => JSX.Element | null; - validation?: (data: T, field: FieldValidation) => void; + validation?: CustomFieldValidator; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 6025abc067..a034f5f574 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -16,9 +16,11 @@ export { scaffolderApiRef, ScaffolderClient } from './api'; export type { ScaffolderApi } from './api'; -export { - createScaffolderFieldExtension, +export { createScaffolderFieldExtension } from './extensions'; +export type { ScaffolderFieldExtensions, + CustomFieldValidator, + FieldExtensionOptions, } from './extensions'; export { EntityPickerFieldExtension,