feat(scaffolder): disable the submit button on creating
close #29054 Signed-off-by: JounQin <admin@1stg.me>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
---
|
||||
|
||||
Disable the submit button on creating
|
||||
@@ -15,17 +15,16 @@
|
||||
*/
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import type { RJSFValidationError } from '@rjsf/utils';
|
||||
import { act, fireEvent, waitFor } from '@testing-library/react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { LayoutTemplate } from '../../../layouts';
|
||||
import { SecretsContextProvider } from '../../../secrets';
|
||||
import { TemplateParameterSchema } from '../../../types';
|
||||
import { Stepper } from './Stepper';
|
||||
|
||||
import type { RJSFValidationError } from '@rjsf/utils';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
|
||||
describe('Stepper', () => {
|
||||
it('should render the step titles for each step of the manifest', async () => {
|
||||
const manifest: TemplateParameterSchema = {
|
||||
@@ -762,7 +761,10 @@ describe('Stepper', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const onCreate = jest.fn();
|
||||
// `onCreate` must be async to mock the submit button disabled behavior
|
||||
const onCreate = jest.fn(
|
||||
() => new Promise<void>(resolve => setTimeout(resolve, 0)),
|
||||
);
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<SecretsContextProvider>
|
||||
@@ -783,10 +785,18 @@ describe('Stepper', () => {
|
||||
fireEvent.click(getByRole('button', { name: 'Review' }));
|
||||
});
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Create' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByRole('button', { name: 'Create' })).toBeDisabled(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByRole('button', { name: 'Create' }));
|
||||
});
|
||||
|
||||
expect(onCreate).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(onCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
thing: { repoOrg: 'backstage' },
|
||||
|
||||
@@ -115,7 +115,7 @@ export type StepperProps = {
|
||||
*/
|
||||
export const Stepper = (stepperProps: StepperProps) => {
|
||||
const { t } = useTranslationRef(scaffolderReactTranslationRef);
|
||||
const { layouts = [], components = {}, ...props } = stepperProps;
|
||||
const { layouts = [], components = {}, onCreate, ...props } = stepperProps;
|
||||
const {
|
||||
ReviewStateComponent = ReviewState,
|
||||
ReviewStepComponent,
|
||||
@@ -220,10 +220,17 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
|
||||
const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
props.onCreate(stepsState);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
setIsCreating(true);
|
||||
analytics.captureEvent('click', `${createLabel}`);
|
||||
}, [props, stepsState, analytics, createLabel]);
|
||||
try {
|
||||
await onCreate(stepsState);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [analytics, createLabel, onCreate, stepsState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -318,6 +325,7 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
{backLabel}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isCreating}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreate}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
|
||||
|
||||
const workflowOnCreate = useCallback(
|
||||
async (formState: Record<string, JsonValue>) => {
|
||||
onCreate(formState);
|
||||
await onCreate(formState);
|
||||
|
||||
const name =
|
||||
typeof formState.name === 'string' ? formState.name : undefined;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import {
|
||||
@@ -98,29 +98,41 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
|
||||
return data?.metadata.annotations?.[ANNOTATION_EDIT_URL];
|
||||
}, [templateRef, catalogApi]);
|
||||
|
||||
const onCreate = async (initialValues: Record<string, JsonValue>) => {
|
||||
if (isCreating) {
|
||||
return;
|
||||
}
|
||||
const onCreate = useCallback(
|
||||
async (initialValues: Record<string, JsonValue>) => {
|
||||
if (isCreating) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
setIsCreating(true);
|
||||
|
||||
const { formState: values, secrets } = await decorators.run({
|
||||
formState: initialValues,
|
||||
secrets: contextSecrets,
|
||||
const { formState: values, secrets } = await decorators.run({
|
||||
formState: initialValues,
|
||||
secrets: contextSecrets,
|
||||
manifest,
|
||||
});
|
||||
|
||||
const { taskId } = await scaffolderApi.scaffold({
|
||||
templateRef,
|
||||
values,
|
||||
secrets,
|
||||
});
|
||||
|
||||
navigate(taskRoute({ taskId }));
|
||||
},
|
||||
[
|
||||
contextSecrets,
|
||||
decorators,
|
||||
isCreating,
|
||||
manifest,
|
||||
});
|
||||
|
||||
const { taskId } = await scaffolderApi.scaffold({
|
||||
navigate,
|
||||
scaffolderApi,
|
||||
taskRoute,
|
||||
templateRef,
|
||||
values,
|
||||
secrets,
|
||||
});
|
||||
],
|
||||
);
|
||||
|
||||
navigate(taskRoute({ taskId }));
|
||||
};
|
||||
|
||||
const onError = () => <Navigate to={rootRef()} />;
|
||||
const onError = useCallback(() => <Navigate to={rootRef()} />, [rootRef]);
|
||||
|
||||
return (
|
||||
<AnalyticsContext attributes={{ entityRef: templateRef }}>
|
||||
|
||||
Reference in New Issue
Block a user