Migrate custom fields to new schema factory function;
standardize field descriptions to prefer ui:description and present consistently, utilizing ScaffolderField component where possible. Signed-off-by: Matt Benson <gudnabrsam@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Migrate custom fields to new schema factory function;
|
||||
standardize field descriptions to prefer ui:description and present consistently,
|
||||
utilizing ScaffolderField component where possible.
|
||||
@@ -289,6 +289,8 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'fields.entityPicker.description': 'An entity from the catalog';
|
||||
readonly 'fields.entityTagsPicker.title': 'Tags';
|
||||
readonly 'fields.entityTagsPicker.description': "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters";
|
||||
readonly 'fields.multiEntityPicker.title': 'Entity';
|
||||
readonly 'fields.multiEntityPicker.description': 'An entity from the catalog';
|
||||
readonly 'fields.myGroupsPicker.title': 'Entity';
|
||||
readonly 'fields.myGroupsPicker.description': 'An entity from the catalog';
|
||||
readonly 'fields.ownedEntityPicker.title': 'Entity';
|
||||
|
||||
@@ -71,7 +71,7 @@ export type CustomFieldValidator<TReturnFieldData> =
|
||||
// @public
|
||||
export const EntityNamePickerFieldExtension: FieldExtensionComponent_2<
|
||||
string,
|
||||
{}
|
||||
any
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -104,7 +104,7 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityPickerFieldSchema: FieldSchema<
|
||||
export const EntityPickerFieldSchema: FieldSchema_2<
|
||||
string,
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
@@ -133,8 +133,9 @@ export const EntityPickerFieldSchema: FieldSchema<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type EntityPickerUiOptions =
|
||||
typeof EntityPickerFieldSchema.uiOptionsType;
|
||||
export type EntityPickerUiOptions = NonNullable<
|
||||
(typeof EntityPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const EntityTagsPickerFieldExtension: FieldExtensionComponent_2<
|
||||
@@ -147,7 +148,7 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent_2<
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityTagsPickerFieldSchema: FieldSchema<
|
||||
export const EntityTagsPickerFieldSchema: FieldSchema_2<
|
||||
string[],
|
||||
{
|
||||
helperText?: string | undefined;
|
||||
@@ -157,8 +158,9 @@ export const EntityTagsPickerFieldSchema: FieldSchema<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type EntityTagsPickerUiOptions =
|
||||
typeof EntityTagsPickerFieldSchema.uiOptionsType;
|
||||
export type EntityTagsPickerUiOptions = NonNullable<
|
||||
(typeof EntityTagsPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type FieldExtensionComponent<_TReturnValue, _TInputProps> =
|
||||
@@ -233,27 +235,19 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
// @public
|
||||
export const MyGroupsPickerFieldExtension: FieldExtensionComponent_2<
|
||||
string,
|
||||
{
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
any
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const MyGroupsPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
>;
|
||||
export const MyGroupsPickerFieldSchema: FieldSchema_2<string, any>;
|
||||
|
||||
// @public
|
||||
export const MyGroupsPickerSchema: CustomFieldExtensionSchema_2;
|
||||
|
||||
// @public
|
||||
export type MyGroupsPickerUiOptions =
|
||||
typeof MyGroupsPickerFieldSchema.uiOptionsType;
|
||||
export type MyGroupsPickerUiOptions = NonNullable<
|
||||
(typeof MyGroupsPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
@@ -285,7 +279,7 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const OwnedEntityPickerFieldSchema: FieldSchema<
|
||||
export const OwnedEntityPickerFieldSchema: FieldSchema_2<
|
||||
string,
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
@@ -314,8 +308,9 @@ export const OwnedEntityPickerFieldSchema: FieldSchema<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type OwnedEntityPickerUiOptions =
|
||||
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
|
||||
export type OwnedEntityPickerUiOptions = NonNullable<
|
||||
(typeof OwnedEntityPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
|
||||
@@ -346,7 +341,7 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const OwnerPickerFieldSchema: FieldSchema<
|
||||
export const OwnerPickerFieldSchema: FieldSchema_2<
|
||||
string,
|
||||
{
|
||||
defaultNamespace?: string | false | undefined;
|
||||
@@ -374,7 +369,9 @@ export const OwnerPickerFieldSchema: FieldSchema<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType;
|
||||
export type OwnerPickerUiOptions = NonNullable<
|
||||
(typeof OwnerPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2<
|
||||
@@ -462,8 +459,9 @@ export const RepoUrlPickerFieldSchema: FieldSchema_2<
|
||||
>;
|
||||
|
||||
// @public @deprecated
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerFieldSchema.uiOptionsType;
|
||||
export type RepoUrlPickerUiOptions = NonNullable<
|
||||
(typeof RepoUrlPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export { ReviewStepProps };
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
const EntityNamePickerFieldSchema = makeFieldSchemaFromZod(z.string());
|
||||
const EntityNamePickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
});
|
||||
|
||||
export const EntityNamePickerSchema = EntityNamePickerFieldSchema.schema;
|
||||
|
||||
export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.type;
|
||||
export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.TProps;
|
||||
|
||||
@@ -28,6 +28,8 @@ import { EntityPickerProps } from './schema';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
@@ -891,4 +893,128 @@ describe('<EntityPicker />', () => {
|
||||
expect(onChange).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EntityPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'EntityPicker description from schema',
|
||||
fromUiSchema: 'EntityPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string; default?: string };
|
||||
|
||||
beforeEach(() => {
|
||||
const RealWrapper = Wrapper;
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
description.default = t('fields.entityPicker.description');
|
||||
return <RealWrapper>{children}</RealWrapper>;
|
||||
};
|
||||
});
|
||||
it('presents default description', async () => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(getByText(description.default!)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
'ui:description': description.fromUiSchema,
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
return (
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
rawDescription={description}
|
||||
rawDescription={uiSchema['ui:description'] ?? description}
|
||||
required={required}
|
||||
disabled={isDisabled}
|
||||
errors={errors}
|
||||
|
||||
@@ -13,57 +13,58 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { z as zod } from 'zod';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const entityQueryFilterExpressionSchema = z.record(
|
||||
z
|
||||
export const entityQueryFilterExpressionSchema = zod.record(
|
||||
zod
|
||||
.string()
|
||||
.or(z.object({ exists: z.boolean().optional() }))
|
||||
.or(z.array(z.string())),
|
||||
.or(zod.object({ exists: zod.boolean().optional() }))
|
||||
.or(zod.array(zod.string())),
|
||||
);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
|
||||
),
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
export const EntityPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
|
||||
),
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
@@ -71,14 +72,15 @@ export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityPickerUiOptions =
|
||||
typeof EntityPickerFieldSchema.uiOptionsType;
|
||||
export type EntityPickerUiOptions = NonNullable<
|
||||
(typeof EntityPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type EntityPickerProps = typeof EntityPickerFieldSchema.type;
|
||||
export type EntityPickerProps = typeof EntityPickerFieldSchema.TProps;
|
||||
|
||||
export const EntityPickerSchema = EntityPickerFieldSchema.schema;
|
||||
|
||||
export type EntityPickerFilterQuery = z.TypeOf<
|
||||
export type EntityPickerFilterQuery = zod.TypeOf<
|
||||
typeof entityQueryFilterExpressionSchema
|
||||
>;
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
import { EntityTagsPicker } from './EntityTagsPicker';
|
||||
import { EntityTagsPickerProps } from './schema';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
});
|
||||
|
||||
describe('<EntityTagsPicker />', () => {
|
||||
const entities: Entity[] = [
|
||||
makeEntity('Group', 'default', 'team-a'),
|
||||
makeEntity('Group', 'default', 'squad-b'),
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const schema = { type: 'array', items: { type: 'string' } };
|
||||
const uiSchema: EntityTagsPickerProps['uiSchema'] = {
|
||||
'ui:options': {},
|
||||
};
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
|
||||
let props: FieldProps<string[]>;
|
||||
|
||||
const catalogApi = catalogApiMock.mock({
|
||||
getEntities: jest.fn(async () => ({ items: entities })),
|
||||
});
|
||||
|
||||
let Wrapper: ComponentType<PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('EntityTagsPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'EntityTagsPicker description from schema',
|
||||
fromUiSchema: 'EntityTagsPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string; default?: string };
|
||||
|
||||
beforeEach(() => {
|
||||
const RealWrapper = Wrapper;
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
description.default = t('fields.entityTagsPicker.description');
|
||||
return <RealWrapper>{children}</RealWrapper>;
|
||||
};
|
||||
});
|
||||
it('presents default description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityTagsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(getByText(description.default!)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityTagsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema: {
|
||||
...uiSchema,
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityTagsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,12 +20,12 @@ import { GetEntityFacetsRequest } from '@backstage/catalog-client';
|
||||
import { makeValidator } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { EntityTagsPickerProps } from './schema';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export { EntityTagsPickerSchema } from './schema';
|
||||
|
||||
@@ -36,7 +36,19 @@ export { EntityTagsPickerSchema } from './schema';
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
|
||||
const { formData, onChange, uiSchema } = props;
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const {
|
||||
formData,
|
||||
onChange,
|
||||
schema: {
|
||||
title = t('fields.entityTagsPicker.title'),
|
||||
description = t('fields.entityTagsPicker.description'),
|
||||
},
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
required,
|
||||
errors,
|
||||
} = props;
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
@@ -47,8 +59,6 @@ export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
|
||||
const helperText = uiSchema['ui:options']?.helperText;
|
||||
const isDisabled = uiSchema?.['ui:disabled'] ?? false;
|
||||
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
const { loading, value: existingTags } = useAsync(async () => {
|
||||
const facet = 'metadata.tags';
|
||||
const tagsRequest: GetEntityFacetsRequest = { facets: [facet] };
|
||||
@@ -97,7 +107,13 @@ export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
|
||||
useEffectOnce(() => onChange(formData || []));
|
||||
|
||||
return (
|
||||
<FormControl margin="normal">
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
rawDescription={helperText ?? uiSchema['ui:description'] ?? description}
|
||||
required={required}
|
||||
disabled={isDisabled}
|
||||
errors={errors}
|
||||
>
|
||||
<Autocomplete
|
||||
multiple
|
||||
freeSolo
|
||||
@@ -115,15 +131,14 @@ export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('fields.entityTagsPicker.title')}
|
||||
label={title}
|
||||
disabled={isDisabled}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
error={inputError}
|
||||
helperText={helperText ?? t('fields.entityTagsPicker.description')}
|
||||
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</ScaffolderField>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,30 +13,35 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.array(z.string()),
|
||||
z.object({
|
||||
kinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('List of kinds of entities to derive tags from'),
|
||||
showCounts: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to show usage counts per tag'),
|
||||
helperText: z.string().optional().describe('Helper text to display'),
|
||||
}),
|
||||
);
|
||||
export const EntityTagsPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.array(z.string()),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
kinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('List of kinds of entities to derive tags from'),
|
||||
showCounts: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to show usage counts per tag'),
|
||||
helperText: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Helper text to display; DEPRECATED, simply use ui:description',
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export const EntityTagsPickerSchema = EntityTagsPickerFieldSchema.schema;
|
||||
|
||||
export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type;
|
||||
export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.TProps;
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
@@ -44,5 +49,6 @@ export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type;
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityTagsPickerUiOptions =
|
||||
typeof EntityTagsPickerFieldSchema.uiOptionsType;
|
||||
export type EntityTagsPickerUiOptions = NonNullable<
|
||||
(typeof EntityTagsPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
+101
-1
@@ -30,6 +30,8 @@ import { MultiEntityPickerProps } from './schema';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
@@ -43,7 +45,7 @@ describe('<MultiEntityPicker />', () => {
|
||||
makeEntity('Group', 'default', 'squad-b'),
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const schema = {};
|
||||
const schema = { type: 'array', items: { type: 'string' } };
|
||||
const required = false;
|
||||
let uiSchema: MultiEntityPickerProps['uiSchema'];
|
||||
const rawErrors: string[] = [];
|
||||
@@ -847,4 +849,102 @@ describe('<MultiEntityPicker />', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MultiEntityPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'MultiEntityPicker description from schema',
|
||||
fromUiSchema: 'MultiEntityPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string; default?: string };
|
||||
|
||||
beforeEach(() => {
|
||||
const RealWrapper = Wrapper;
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
description.default = t('fields.multiEntityPicker.description');
|
||||
return <RealWrapper>{children}</RealWrapper>;
|
||||
};
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
it('presents default description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(getByText(description.default!)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema: {
|
||||
...uiSchema,
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
EntityRefPresentationSnapshot,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Autocomplete, {
|
||||
AutocompleteChangeReason,
|
||||
} from '@material-ui/lab/Autocomplete';
|
||||
@@ -44,6 +43,9 @@ import {
|
||||
MultiEntityPickerFilterQuery,
|
||||
} from './schema';
|
||||
import { VirtualizedListbox } from '../VirtualizedListbox';
|
||||
import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
export { MultiEntityPickerSchema } from './schema';
|
||||
|
||||
@@ -52,14 +54,19 @@ export { MultiEntityPickerSchema } from './schema';
|
||||
* field extension.
|
||||
*/
|
||||
export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const {
|
||||
onChange,
|
||||
schema: { title = 'Entity', description = 'An entity from the catalog' },
|
||||
schema: {
|
||||
title = t('fields.multiEntityPicker.title'),
|
||||
description = t('fields.multiEntityPicker.description'),
|
||||
},
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
idSchema,
|
||||
errors,
|
||||
} = props;
|
||||
|
||||
const catalogFilter = buildCatalogFilter(uiSchema);
|
||||
@@ -144,10 +151,12 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
|
||||
}, [entities, onChange, required, allowArbitraryValues]);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
rawDescription={uiSchema['ui:description'] ?? description}
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
disabled={isDisabled}
|
||||
errors={errors}
|
||||
>
|
||||
<Autocomplete
|
||||
multiple
|
||||
@@ -182,7 +191,6 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
|
||||
label={title}
|
||||
disabled={isDisabled}
|
||||
margin="dense"
|
||||
helperText={description}
|
||||
FormHelperTextProps={{
|
||||
margin: 'dense',
|
||||
style: { marginLeft: 0 },
|
||||
@@ -197,7 +205,7 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
|
||||
)}
|
||||
ListboxComponent={VirtualizedListbox}
|
||||
/>
|
||||
</FormControl>
|
||||
</ScaffolderField>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,55 +13,57 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { z as zod } from 'zod';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
export const entityQueryFilterExpressionSchema = z.record(
|
||||
z
|
||||
export const entityQueryFilterExpressionSchema = zod.record(
|
||||
zod
|
||||
.string()
|
||||
.or(z.object({ exists: z.boolean().optional() }))
|
||||
.or(z.array(z.string())),
|
||||
.or(zod.object({ exists: zod.boolean().optional() }))
|
||||
.or(zod.array(zod.string())),
|
||||
);
|
||||
|
||||
export const MultiEntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.array(z.string()),
|
||||
z.object({
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
export const MultiEntityPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.array(z.string()),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `EntityPicker` field extension.
|
||||
*/
|
||||
export type MultiEntityPickerUiOptions =
|
||||
typeof MultiEntityPickerFieldSchema.uiOptionsType;
|
||||
export type MultiEntityPickerUiOptions = NonNullable<
|
||||
(typeof MultiEntityPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type MultiEntityPickerProps = typeof MultiEntityPickerFieldSchema.type;
|
||||
export type MultiEntityPickerProps = typeof MultiEntityPickerFieldSchema.TProps;
|
||||
|
||||
export const MultiEntityPickerSchema = MultiEntityPickerFieldSchema.schema;
|
||||
|
||||
export type MultiEntityPickerFilterQuery = z.TypeOf<
|
||||
export type MultiEntityPickerFilterQuery = zod.TypeOf<
|
||||
typeof entityQueryFilterExpressionSchema
|
||||
>;
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const mockIdentityApi = mockApis.identity({
|
||||
userEntityRef: 'user:default/bob',
|
||||
@@ -99,6 +102,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema: {},
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
await renderInTestApp(
|
||||
@@ -175,6 +179,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema: {},
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { queryByText, getByRole } = await renderInTestApp(
|
||||
@@ -236,6 +241,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema: {},
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
@@ -299,6 +305,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema: {},
|
||||
formData: 'group:default/group1',
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
@@ -327,4 +334,99 @@ describe('<MyGroupsPicker />', () => {
|
||||
|
||||
expect(inputFieldValue).toEqual(userGroups[0].metadata.title);
|
||||
});
|
||||
|
||||
describe('MyGroupsPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'MyGroupsPicker description from schema',
|
||||
fromUiSchema: 'MyGroupsPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string; default?: string };
|
||||
|
||||
let Wrapper: ComponentType<PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
description.default = t('fields.myGroupsPicker.description');
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[identityApiRef, mockIdentityApi],
|
||||
[catalogApiRef, catalogApi],
|
||||
[errorApiRef, mockErrorApi],
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
it('presents default description', async () => {
|
||||
const props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema: {},
|
||||
formData: 'group:default/group1',
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MyGroupsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(getByText(description.default!)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
const props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema: {},
|
||||
formData: 'group:default/group1',
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MyGroupsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
const props = {
|
||||
onChange,
|
||||
schema: {
|
||||
...schema,
|
||||
description: description.fromSchema,
|
||||
},
|
||||
required: true,
|
||||
uiSchema: {
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
formData: 'group:default/group1',
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MyGroupsPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(queryByText(description.default!)).toBe(null);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import { MyGroupsPickerProps, MyGroupsPickerSchema } from './schema';
|
||||
import Autocomplete, {
|
||||
createFilterOptions,
|
||||
@@ -38,6 +37,7 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { VirtualizedListbox } from '../VirtualizedListbox';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export { MyGroupsPickerSchema };
|
||||
|
||||
@@ -52,12 +52,15 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
|
||||
rawErrors,
|
||||
onChange,
|
||||
formData,
|
||||
uiSchema,
|
||||
errors,
|
||||
} = props;
|
||||
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const entityPresentationApi = useApi(entityPresentationApiRef);
|
||||
const isDisabled = uiSchema?.['ui:disabled'] ?? false;
|
||||
|
||||
const { value: groups, loading } = useAsync(async () => {
|
||||
const { userEntityRef } = await identityApi.getBackstageIdentity();
|
||||
@@ -108,10 +111,12 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
|
||||
}, [groups, onChange, selectedEntity, required]);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
rawDescription={uiSchema['ui:description'] ?? description}
|
||||
required={required}
|
||||
error={rawErrors?.length > 0}
|
||||
disabled={isDisabled}
|
||||
errors={errors}
|
||||
>
|
||||
<Autocomplete
|
||||
disabled={required && groups?.catalogEntities.length === 1}
|
||||
@@ -130,7 +135,6 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
|
||||
{...params}
|
||||
label={title}
|
||||
margin="dense"
|
||||
helperText={description}
|
||||
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
|
||||
variant="outlined"
|
||||
required={required}
|
||||
@@ -145,6 +149,6 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
|
||||
})}
|
||||
ListboxComponent={VirtualizedListbox}
|
||||
/>
|
||||
</FormControl>
|
||||
</ScaffolderField>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,42 +14,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* Field schema for the MyGroupsPicker.
|
||||
* @public
|
||||
*/
|
||||
|
||||
export const MyGroupsPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
title: z.string().default('Group').describe('Group'),
|
||||
description: z
|
||||
.string()
|
||||
.default('A group you are part of')
|
||||
.describe('The group to which the entity belongs'),
|
||||
}),
|
||||
);
|
||||
export const MyGroupsPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* UI options for the MyGroupsPicker.
|
||||
* @public
|
||||
*/
|
||||
|
||||
export type MyGroupsPickerUiOptions =
|
||||
typeof MyGroupsPickerFieldSchema.uiOptionsType;
|
||||
export type MyGroupsPickerUiOptions = NonNullable<
|
||||
(typeof MyGroupsPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
/**
|
||||
* Props for the MyGroupsPicker.
|
||||
* @public
|
||||
*/
|
||||
|
||||
export type MyGroupsPickerProps = typeof MyGroupsPickerFieldSchema.type;
|
||||
export type MyGroupsPickerProps = typeof MyGroupsPickerFieldSchema.TProps;
|
||||
|
||||
/**
|
||||
* Schema for the MyGroupsPicker.
|
||||
* @public
|
||||
*/
|
||||
|
||||
export const MyGroupsPickerSchema = MyGroupsPickerFieldSchema.schema;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { OwnedEntityPickerProps } from './schema';
|
||||
import { EntityPickerProps } from '../EntityPicker/schema';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export { OwnedEntityPickerSchema } from './schema';
|
||||
|
||||
@@ -52,22 +53,30 @@ export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => {
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<Autocomplete
|
||||
loading={loading}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={title}
|
||||
margin="dense"
|
||||
helperText={description}
|
||||
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
|
||||
variant="outlined"
|
||||
required={required}
|
||||
InputProps={params.InputProps}
|
||||
/>
|
||||
)}
|
||||
options={[]}
|
||||
/>
|
||||
<ScaffolderField
|
||||
rawDescription={uiSchema['ui:description'] ?? description}
|
||||
required={required}
|
||||
disabled={uiSchema['ui:disabled']}
|
||||
>
|
||||
<Autocomplete
|
||||
loading={loading}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={title}
|
||||
margin="dense"
|
||||
FormHelperTextProps={{
|
||||
margin: 'dense',
|
||||
style: { marginLeft: 0 },
|
||||
}}
|
||||
variant="outlined"
|
||||
required={required}
|
||||
InputProps={params.InputProps}
|
||||
/>
|
||||
)}
|
||||
options={[]}
|
||||
/>
|
||||
</ScaffolderField>
|
||||
);
|
||||
|
||||
const entityPickerUISchema = buildEntityPickerUISchema(
|
||||
|
||||
@@ -13,45 +13,45 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
|
||||
),
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
export const OwnedEntityPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
|
||||
),
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The default entity kind. Options of this kind will not be prefixed.',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
@@ -59,9 +59,10 @@ export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OwnedEntityPickerUiOptions =
|
||||
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
|
||||
export type OwnedEntityPickerUiOptions = NonNullable<
|
||||
(typeof OwnedEntityPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.type;
|
||||
export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.TProps;
|
||||
|
||||
export const OwnedEntityPickerSchema = OwnedEntityPickerFieldSchema.schema;
|
||||
|
||||
@@ -13,43 +13,43 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.default(['Group', 'User'])
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from. Defaults to Group and User',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
export const OwnerPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.default(['Group', 'User'])
|
||||
.optional()
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from. Defaults to Group and User',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to allow arbitrary user input. Defaults to true'),
|
||||
defaultNamespace: z
|
||||
.union([z.string(), z.literal(false)])
|
||||
.optional()
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
@@ -57,7 +57,9 @@ export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType;
|
||||
export type OwnerPickerUiOptions = NonNullable<
|
||||
(typeof OwnerPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type OwnerPickerProps = typeof OwnerPickerFieldSchema.type;
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ import {
|
||||
scaffolderApiRef,
|
||||
useTemplateSecrets,
|
||||
ScaffolderRJSFField,
|
||||
ScaffolderRJSFFormProps as FormProps,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { act, fireEvent, screen } from '@testing-library/react';
|
||||
import { RepoBranchPicker } from './RepoBranchPicker';
|
||||
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
describe('RepoBranchPicker', () => {
|
||||
const mockIntegrationsApi: Partial<ScmIntegrationsApi> = {
|
||||
@@ -333,4 +335,104 @@ describe('RepoBranchPicker', () => {
|
||||
expect(getByText('abc123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RepoBranchPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'RepoBranchPicker description from schema',
|
||||
fromUiSchema: 'RepoBranchPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string };
|
||||
|
||||
let Wrapper: ComponentType<PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[scmIntegrationsApiRef, mockIntegrationsApi],
|
||||
[scmAuthApiRef, {}],
|
||||
[scaffolderApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<SecretsContextProvider>{children}</SecretsContextProvider>
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
it('omits description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string' },
|
||||
uiSchema: { 'ui:field': 'RepoBranchPicker' },
|
||||
fields: {
|
||||
RepoBranchPicker: RepoBranchPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string', description: description.fromSchema },
|
||||
uiSchema: { 'ui:field': 'RepoBranchPicker' },
|
||||
fields: {
|
||||
RepoBranchPicker: RepoBranchPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container, getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(1);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string', description: description.fromSchema },
|
||||
uiSchema: {
|
||||
'ui:field': 'RepoBranchPicker',
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
fields: {
|
||||
RepoBranchPicker: RepoBranchPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container, getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(1);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import { RepoBranchPickerState } from './types';
|
||||
import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker';
|
||||
import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker';
|
||||
import { GitHubRepoBranchPicker } from './GitHubRepoBranchPicker';
|
||||
import { MarkdownContent } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* The underlying component that is rendered in the form for the `RepoBranchPicker`
|
||||
@@ -164,6 +165,8 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const description = uiSchema['ui:description'] ?? schema.description;
|
||||
|
||||
return (
|
||||
<>
|
||||
{schema.title && (
|
||||
@@ -172,8 +175,10 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => {
|
||||
<Divider />
|
||||
</Box>
|
||||
)}
|
||||
{schema.description && (
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
{description && (
|
||||
<Typography variant="body1">
|
||||
<MarkdownContent content={description} />
|
||||
</Typography>
|
||||
)}
|
||||
{renderRepoBranchPicker()}
|
||||
</>
|
||||
|
||||
@@ -13,58 +13,58 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { makeFieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const RepoBranchPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
requestUserCredentials: z
|
||||
.object({
|
||||
secretsKey: z
|
||||
.string()
|
||||
.describe(
|
||||
'Key used within the template secrets context to store the credential',
|
||||
),
|
||||
additionalScopes: z
|
||||
.object({
|
||||
gitea: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Gitea scopes to request'),
|
||||
gerrit: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Gerrit scopes to request'),
|
||||
github: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional GitHub scopes to request'),
|
||||
gitlab: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional GitLab scopes to request'),
|
||||
bitbucket: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional BitBucket scopes to request'),
|
||||
azure: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Azure scopes to request'),
|
||||
})
|
||||
.optional()
|
||||
.describe('Additional permission scopes to request'),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
'If defined will request user credentials to auth against the given SCM platform',
|
||||
),
|
||||
}),
|
||||
);
|
||||
export const RepoBranchPickerFieldSchema = makeFieldSchema({
|
||||
output: z => z.string(),
|
||||
uiOptions: z =>
|
||||
z.object({
|
||||
requestUserCredentials: z
|
||||
.object({
|
||||
secretsKey: z
|
||||
.string()
|
||||
.describe(
|
||||
'Key used within the template secrets context to store the credential',
|
||||
),
|
||||
additionalScopes: z
|
||||
.object({
|
||||
gitea: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Gitea scopes to request'),
|
||||
gerrit: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Gerrit scopes to request'),
|
||||
github: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional GitHub scopes to request'),
|
||||
gitlab: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional GitLab scopes to request'),
|
||||
bitbucket: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional BitBucket scopes to request'),
|
||||
azure: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Azure scopes to request'),
|
||||
})
|
||||
.optional()
|
||||
.describe('Additional permission scopes to request'),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
'If defined will request user credentials to auth against the given SCM platform',
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
@@ -72,10 +72,11 @@ export const RepoBranchPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RepoBranchPickerUiOptions =
|
||||
typeof RepoBranchPickerFieldSchema.uiOptionsType;
|
||||
export type RepoBranchPickerUiOptions = NonNullable<
|
||||
(typeof RepoBranchPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type;
|
||||
export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.TProps;
|
||||
|
||||
// This has been duplicated from /plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts
|
||||
// NOTE: There is a bug with this failing validation in the custom field explorer due
|
||||
|
||||
@@ -30,9 +30,11 @@ import {
|
||||
ScaffolderApi,
|
||||
useTemplateSecrets,
|
||||
ScaffolderRJSFField,
|
||||
ScaffolderRJSFFormProps as FormProps,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
describe('RepoUrlPicker', () => {
|
||||
const mockScaffolderApi: Partial<ScaffolderApi> = {
|
||||
@@ -417,4 +419,106 @@ describe('RepoUrlPicker', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RepoUrlPicker description', () => {
|
||||
const description = {
|
||||
fromSchema: 'RepoUrlPicker description from schema',
|
||||
fromUiSchema: 'RepoUrlPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string };
|
||||
|
||||
let Wrapper: ComponentType<PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapper = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[scmIntegrationsApiRef, mockIntegrationsApi],
|
||||
[scmAuthApiRef, {}],
|
||||
[scaffolderApiRef, mockScaffolderApi],
|
||||
]}
|
||||
>
|
||||
<SecretsContextProvider>{children}</SecretsContextProvider>
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
it('omits description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string' },
|
||||
uiSchema: { 'ui:field': 'RepoUrlPicker' },
|
||||
fields: {
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string', description: description.fromSchema },
|
||||
uiSchema: {
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
},
|
||||
fields: {
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container, getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(1);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: { type: 'string', description: description.fromSchema },
|
||||
uiSchema: {
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
fields: {
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
},
|
||||
formContext: {
|
||||
formData: {},
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { container, getByText, queryByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<Form {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(
|
||||
container.getElementsByClassName('MuiTypography-body1'),
|
||||
).toHaveLength(1);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
|
||||
import { RepoUrlPickerFieldSchema } from './schema';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
|
||||
import { MarkdownContent } from '@backstage/core-components';
|
||||
|
||||
export { RepoUrlPickerSchema } from './schema';
|
||||
|
||||
@@ -166,6 +167,9 @@ export const RepoUrlPicker = (
|
||||
|
||||
const hostType =
|
||||
(state.host && integrationApi.byHost(state.host)?.type) ?? null;
|
||||
|
||||
const description = uiSchema['ui:description'] ?? schema.description;
|
||||
|
||||
return (
|
||||
<>
|
||||
{schema.title && (
|
||||
@@ -174,8 +178,10 @@ export const RepoUrlPicker = (
|
||||
<Divider />
|
||||
</Box>
|
||||
)}
|
||||
{schema.description && (
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
{description && (
|
||||
<Typography variant="body1">
|
||||
<MarkdownContent content={description} />
|
||||
</Typography>
|
||||
)}
|
||||
<RepoUrlPickerHost
|
||||
host={state.host}
|
||||
|
||||
@@ -94,10 +94,11 @@ export const RepoUrlPickerFieldSchema = makeFieldSchema({
|
||||
* @public
|
||||
* @deprecated this will be removed as it's no longer used
|
||||
*/
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerFieldSchema.uiOptionsType;
|
||||
export type RepoUrlPickerUiOptions = NonNullable<
|
||||
(typeof RepoUrlPickerFieldSchema.TProps.uiSchema)['ui:options']
|
||||
>;
|
||||
|
||||
export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type;
|
||||
export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.TProps;
|
||||
|
||||
// This has been duplicated to /plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts
|
||||
// NOTE: There is a bug with this failing validation in the custom field explorer due
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { SecretInput } from './SecretInput';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ScaffolderRJSFFormProps as FormProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||
@@ -73,4 +74,92 @@ describe('<SecretInput />', () => {
|
||||
{ timeout: 500 },
|
||||
);
|
||||
});
|
||||
|
||||
describe('SecretInput description', () => {
|
||||
const description = {
|
||||
fromSchema: 'MyGroupsPicker description from schema',
|
||||
fromUiSchema: 'MyGroupsPicker description from uiSchema',
|
||||
} as { fromSchema: string; fromUiSchema: string };
|
||||
|
||||
it('omits description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: {
|
||||
properties: { myKey: { type: 'string', title: 'secret' } },
|
||||
},
|
||||
uiSchema: {
|
||||
myKey: {
|
||||
'ui:field': 'Secret',
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
Secret: SecretInput,
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { queryByText } = await renderInTestApp(
|
||||
<SecretsContextProvider>
|
||||
<Form {...props} />
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>,
|
||||
);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents schema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: {
|
||||
properties: { myKey: { type: 'string', title: 'secret' } },
|
||||
description: description.fromSchema,
|
||||
},
|
||||
uiSchema: {
|
||||
myKey: {
|
||||
'ui:field': 'Secret',
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
Secret: SecretInput,
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<SecretsContextProvider>
|
||||
<Form {...props} />
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>,
|
||||
);
|
||||
expect(getByText(description.fromSchema)).toBeInTheDocument();
|
||||
expect(queryByText(description.fromUiSchema)).toBe(null);
|
||||
});
|
||||
|
||||
it('presents uiSchema description', async () => {
|
||||
const props = {
|
||||
validator,
|
||||
schema: {
|
||||
properties: { myKey: { type: 'string', title: 'secret' } },
|
||||
description: description.fromSchema,
|
||||
},
|
||||
uiSchema: {
|
||||
myKey: {
|
||||
'ui:field': 'Secret',
|
||||
},
|
||||
'ui:description': description.fromUiSchema,
|
||||
},
|
||||
fields: {
|
||||
Secret: SecretInput,
|
||||
},
|
||||
} as unknown as FormProps<any>;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<SecretsContextProvider>
|
||||
<Form {...props} />
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>,
|
||||
);
|
||||
expect(queryByText(description.fromSchema)).toBe(null);
|
||||
expect(getByText(description.fromUiSchema)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,12 +26,13 @@ export const SecretInput = (props: ScaffolderRJSFFieldProps) => {
|
||||
disabled,
|
||||
errors,
|
||||
required,
|
||||
uiSchema,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
rawDescription={description}
|
||||
rawDescription={uiSchema['ui:description'] ?? description}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
required={required}
|
||||
|
||||
@@ -51,6 +51,10 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
description:
|
||||
"Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters",
|
||||
},
|
||||
multiEntityPicker: {
|
||||
title: 'Entity',
|
||||
description: 'An entity from the catalog',
|
||||
},
|
||||
myGroupsPicker: {
|
||||
title: 'Entity',
|
||||
description: 'An entity from the catalog',
|
||||
|
||||
Reference in New Issue
Block a user