diff --git a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js index 46e491a9a5cbf489abb3690e1a158b7f7c0366f8..aa2509b72f5a80133d1ed15e665cc4f7e9337a72 100644 --- a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js +++ b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js @@ -771,6 +771,9 @@ describe("scenarios > dashboard", () => { modal().within(() => { cy.findByLabelText("Name").type(NEW_COLLECTION); cy.findByText("Create").click(); + cy.findByText("New dashboard"); + cy.findByTestId("select-button").should("have.text", NEW_COLLECTION); + cy.findByText("Create").click(); }); saveDashboard(); closeNavigationSidebar(); diff --git a/e2e/test/scenarios/question/new.cy.spec.js b/e2e/test/scenarios/question/new.cy.spec.js index 683b81f5f6d23a27729db192e4604d37e6968e60..c17d04a6054966e239fc48cedf9023cb5312ec71 100644 --- a/e2e/test/scenarios/question/new.cy.spec.js +++ b/e2e/test/scenarios/question/new.cy.spec.js @@ -301,6 +301,9 @@ describe("scenarios > question > new", () => { modal().within(() => { cy.findByLabelText("Name").type(NEW_COLLECTION); cy.findByText("Create").click(); + cy.findByText("Save new question"); + cy.findByTestId("select-button").should("have.text", NEW_COLLECTION); + cy.findByText("Save").click(); }); cy.get("header").findByText(NEW_COLLECTION); }); diff --git a/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.styled.tsx b/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.styled.tsx index 033b2e6db9c2d70343f52d3ec7c06be997fffef7..d05328a09983426659499f3523cd0f89d8ab1de2 100644 --- a/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.styled.tsx +++ b/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.styled.tsx @@ -10,7 +10,6 @@ export const PopoverItemPicker = styled(ItemPicker)<{ width: number }>` padding: 1rem; overflow: auto; `; - -export const NewButton = styled(Button)` +export const NewCollectionButton = styled(Button)` margin-top: 0.5rem; `; diff --git a/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.tsx b/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.tsx index 70555753af82ba1744bee544020b822f47079c2d..2ba2a17b3c74d6a3044d5fd290c656a8ebefe5f1 100644 --- a/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.tsx +++ b/frontend/src/metabase/collections/containers/FormCollectionPicker/FormCollectionPicker.tsx @@ -6,7 +6,7 @@ import { HTMLAttributes, } from "react"; import { t } from "ttag"; -import { useField } from "formik"; +import { useField, useFormikContext } from "formik"; import { useUniqueId } from "metabase/hooks/use-unique-id"; @@ -16,6 +16,10 @@ import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/Tipp import CollectionName from "metabase/containers/CollectionName"; import SnippetCollectionName from "metabase/containers/SnippetCollectionName"; +import type { + OnClickNewCollection, + Values, +} from "metabase/containers/CreateCollectionOnTheGo"; import Collections from "metabase/entities/collections"; import SnippetCollections from "metabase/entities/snippet-collections"; @@ -24,12 +28,10 @@ import { isValidCollectionId } from "metabase/collections/utils"; import type { CollectionId } from "metabase-types/api"; -import { ButtonProps } from "metabase/core/components/Button"; -import Tooltip from "metabase/core/components/Tooltip"; import { PopoverItemPicker, MIN_POPOVER_WIDTH, - NewButton, + NewCollectionButton, } from "./FormCollectionPicker.styled"; export interface FormCollectionPickerProps @@ -38,8 +40,10 @@ export interface FormCollectionPickerProps title?: string; placeholder?: string; type?: "collections" | "snippet-collections"; + canCreateNew?: boolean; initialOpenCollectionId?: CollectionId; onOpenCollectionChange?: (collectionId: CollectionId) => void; + onClickNewCollection?: OnClickNewCollection; } function ItemName({ @@ -56,22 +60,6 @@ function ItemName({ ); } -export const NewCollectionButton = (props: ButtonProps) => { - const button = ( - <NewButton light icon="add" {...props}> - {t`New collection`} - </NewButton> - ); - // button has to be wrapped in a span when disabled or the tooltip doesn’t show - return props.disabled === true ? ( - <Tooltip tooltip={t`You must first fix the required fields above.`}> - <span>{button}</span> - </Tooltip> - ) : ( - button - ); -}; - function FormCollectionPicker({ className, style, @@ -79,9 +67,10 @@ function FormCollectionPicker({ title, placeholder = t`Select a collection`, type = "collections", + canCreateNew = false, initialOpenCollectionId, onOpenCollectionChange, - children, + onClickNewCollection, }: FormCollectionPickerProps) { const id = useUniqueId(); const [{ value }, { error, touched }, { setValue }] = useField(name); @@ -118,6 +107,10 @@ function FormCollectionPicker({ [id, value, type, title, placeholder, error, touched, className, style], ); + const { values } = useFormikContext<Values>(); + const [openCollectionId, setOpenCollectionId] = + useState<CollectionId>("root"); + const renderContent = useCallback( ({ closePopover }) => { // Search API doesn't support collection namespaces yet @@ -137,9 +130,20 @@ function FormCollectionPicker({ showSearch={hasSearch} width={width} initialOpenCollectionId={initialOpenCollectionId} - onOpenCollectionChange={onOpenCollectionChange} + onOpenCollectionChange={(id: CollectionId) => { + onOpenCollectionChange?.(id); + setOpenCollectionId(id); + }} > - {children} + {canCreateNew && ( + <NewCollectionButton + light + icon="add" + onClick={() => onClickNewCollection?.(values, openCollectionId)} + > + {t`New collection`} + </NewCollectionButton> + )} </PopoverItemPicker> ); }, @@ -148,9 +152,12 @@ function FormCollectionPicker({ type, width, setValue, - children, initialOpenCollectionId, + openCollectionId, + values, onOpenCollectionChange, + canCreateNew, + onClickNewCollection, ], ); diff --git a/frontend/src/metabase/containers/CreateCollectionOnTheGo.tsx b/frontend/src/metabase/containers/CreateCollectionOnTheGo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cef77a5e7abb7695f90a74565d4155e2829b6821 --- /dev/null +++ b/frontend/src/metabase/containers/CreateCollectionOnTheGo.tsx @@ -0,0 +1,57 @@ +import { useState, useCallback, ReactElement } from "react"; +import type { FormikValues } from "formik"; +import { Collection, CollectionId } from "metabase-types/api"; +import CreateCollectionModal from "metabase/collections/containers/CreateCollectionModal"; + +export interface Values extends FormikValues { + collection_id: CollectionId; +} + +interface State { + enabled: boolean; + resumedValues?: Values; + openCollectionId?: CollectionId; +} + +export type OnClickNewCollection = ( + resumedValues: Values, + openCollectionId: CollectionId, +) => void; + +type RenderChildFn = ( + resumedValues: Values | undefined, + onClickNewCollection: OnClickNewCollection, +) => ReactElement; + +export function CreateCollectionOnTheGo({ + children, +}: { + children: RenderChildFn; +}) { + const [state, setState] = useState<State>({ + enabled: false, + }); + const { enabled, openCollectionId, resumedValues } = state; + + const onClickNewCollection = useCallback<OnClickNewCollection>( + (resumedValues, openCollectionId) => + setState({ ...state, enabled: true, resumedValues, openCollectionId }), + [state, setState], + ); + + return enabled ? ( + <CreateCollectionModal + collectionId={openCollectionId} + onClose={() => setState({ ...state, enabled: false })} + onCreate={(collection: Collection) => { + setState({ + ...state, + resumedValues: { ...resumedValues, collection_id: collection.id }, + enabled: false, + }); + }} + /> + ) : ( + children(resumedValues, onClickNewCollection) + ); +} diff --git a/frontend/src/metabase/containers/SaveQuestionModal.tsx b/frontend/src/metabase/containers/SaveQuestionModal.tsx index e6ca1834b8f8486211b2b7e898077d3b5b9312a7..41fbec4c443e327f9836a8d2ecdca3cd90c983b3 100644 --- a/frontend/src/metabase/containers/SaveQuestionModal.tsx +++ b/frontend/src/metabase/containers/SaveQuestionModal.tsx @@ -1,14 +1,15 @@ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { t } from "ttag"; import * as Yup from "yup"; import ModalContent from "metabase/components/ModalContent"; import FormProvider from "metabase/core/components/FormProvider/FormProvider"; -import FormCollectionPicker, { - NewCollectionButton, -} from "metabase/collections/containers/FormCollectionPicker/FormCollectionPicker"; -import CreateCollectionModal from "metabase/collections/containers/CreateCollectionModal"; +import FormCollectionPicker from "metabase/collections/containers/FormCollectionPicker/FormCollectionPicker"; +import { + CreateCollectionOnTheGo, + OnClickNewCollection, +} from "metabase/containers/CreateCollectionOnTheGo"; import Form from "metabase/core/components/Form"; import FormInput from "metabase/core/components/FormInput"; import FormFooter from "metabase/core/components/FormFooter"; @@ -18,7 +19,7 @@ import Button from "metabase/core/components/Button"; import FormSubmitButton from "metabase/core/components/FormSubmitButton"; import FormRadio from "metabase/core/components/FormRadio"; import { canonicalCollectionId } from "metabase/collections/utils"; -import { Collection, CollectionId } from "metabase-types/api"; +import { CollectionId } from "metabase-types/api"; import * as Errors from "metabase/core/utils/errors"; import { getIsSavedQuestionChanged } from "metabase/query_builder/selectors"; import { useSelector } from "metabase/lib/redux"; @@ -55,6 +56,7 @@ interface SaveQuestionModalProps { onClose: () => void; multiStep?: boolean; initialCollectionId?: number; + onClickNewCollection?: OnClickNewCollection; } interface FormValues { @@ -79,6 +81,7 @@ export const SaveQuestionModal = ({ onClose, multiStep, initialCollectionId, + onClickNewCollection, }: SaveQuestionModalProps) => { const handleOverwrite = useCallback( async (originalQuestion: Question, details: FormValues) => { @@ -131,10 +134,6 @@ export const SaveQuestionModal = ({ [originalQuestion, handleOverwrite, handleCreate], ); - const [creatingNewCollection, setCreatingNewCollection] = useState(false); - const [openCollectionId, setOpenCollectionId] = useState<CollectionId>(); - const [stagedValues, setStagedValues] = useState<FormValues | null>(null); - const isReadonly = originalQuestion != null && !originalQuestion.canWrite(); const initialValues: FormValues = { @@ -150,7 +149,6 @@ export const SaveQuestionModal = ({ originalQuestion.canWrite() ? "overwrite" : "create", - ...stagedValues, }; const questionType = question.isDataset() ? "model" : "question"; @@ -175,88 +173,73 @@ export const SaveQuestionModal = ({ ? t`What is the name of your question?` : t`What is the name of your model?`; - if (creatingNewCollection && stagedValues) { - return ( - <CreateCollectionModal - collectionId={openCollectionId} - onClose={() => setCreatingNewCollection(false)} - onCreate={(collection: Collection) => { - handleSubmit({ ...stagedValues, collection_id: collection.id }); - }} - /> - ); - } - return ( - <ModalContent id="SaveQuestionModal" title={title} onClose={onClose}> - <FormProvider - initialValues={initialValues} - onSubmit={handleSubmit} - validationSchema={SAVE_QUESTION_SCHEMA} - enableReinitialize - > - {({ values, isValid }) => ( - <Form> - {showSaveType && ( - <FormRadio - name="saveType" - title={t`Replace or save as new?`} - options={[ - { - name: t`Replace original question, "${originalQuestion?.displayName()}"`, - value: "overwrite", - }, - { name: t`Save as new question`, value: "create" }, - ]} - vertical - /> - )} - <TransitionGroup> - {values.saveType === "create" && ( - <CSSTransition - classNames="saveQuestionModalFields" - timeout={{ - enter: 500, - exit: 500, - }} - > - <div className="saveQuestionModalFields"> - <FormInput - autoFocus - name="name" - title={t`Name`} - placeholder={nameInputPlaceholder} - /> - <FormTextArea - name="description" - title={t`Description`} - placeholder={t`It's optional but oh, so helpful`} - /> - <FormCollectionPicker - onOpenCollectionChange={setOpenCollectionId} - name="collection_id" - title={t`Which collection should this go in?`} + <CreateCollectionOnTheGo> + {(resumedValues, onClickNewCollection) => ( + <ModalContent id="SaveQuestionModal" title={title} onClose={onClose}> + <FormProvider + initialValues={{ ...initialValues, ...resumedValues }} + onSubmit={handleSubmit} + validationSchema={SAVE_QUESTION_SCHEMA} + enableReinitialize + > + {({ values }) => ( + <Form> + {showSaveType && ( + <FormRadio + name="saveType" + title={t`Replace or save as new?`} + options={[ + { + name: t`Replace original question, "${originalQuestion?.displayName()}"`, + value: "overwrite", + }, + { name: t`Save as new question`, value: "create" }, + ]} + vertical + /> + )} + <TransitionGroup> + {values.saveType === "create" && ( + <CSSTransition + classNames="saveQuestionModalFields" + timeout={{ + enter: 500, + exit: 500, + }} > - <NewCollectionButton - disabled={!isValid} - onClick={() => { - setCreatingNewCollection(true); - setStagedValues(values); - }} - /> - </FormCollectionPicker> - </div> - </CSSTransition> - )} - </TransitionGroup> - <FormFooter> - <FormErrorMessage inline /> - <Button type="button" onClick={onClose}>{t`Cancel`}</Button> - <FormSubmitButton title={t`Save`} primary /> - </FormFooter> - </Form> - )} - </FormProvider> - </ModalContent> + <div className="saveQuestionModalFields"> + <FormInput + autoFocus + name="name" + title={t`Name`} + placeholder={nameInputPlaceholder} + /> + <FormTextArea + name="description" + title={t`Description`} + placeholder={t`It's optional but oh, so helpful`} + /> + <FormCollectionPicker + name="collection_id" + title={t`Which collection should this go in?`} + canCreateNew={true} + onClickNewCollection={onClickNewCollection} + /> + </div> + </CSSTransition> + )} + </TransitionGroup> + <FormFooter> + <FormErrorMessage inline /> + <Button type="button" onClick={onClose}>{t`Cancel`}</Button> + <FormSubmitButton title={t`Save`} primary /> + </FormFooter> + </Form> + )} + </FormProvider> + </ModalContent> + )} + </CreateCollectionOnTheGo> ); }; diff --git a/frontend/src/metabase/containers/SaveQuestionModal.unit.spec.tsx b/frontend/src/metabase/containers/SaveQuestionModal.unit.spec.tsx index 750bb3aa482163ace5005b8fe1b3cc4f1200c7fa..063dbecee628234a17bc94cc0081b36ac3aaf350 100644 --- a/frontend/src/metabase/containers/SaveQuestionModal.unit.spec.tsx +++ b/frontend/src/metabase/containers/SaveQuestionModal.unit.spec.tsx @@ -666,7 +666,6 @@ describe("SaveQuestionModal", () => { }); describe("new collection modal", () => { - const nameField = () => screen.getByRole("textbox", { name: /name/i }); const collDropdown = () => screen.getByTestId("select-button"); const newCollBtn = () => screen.getByRole("button", { @@ -683,16 +682,10 @@ describe("SaveQuestionModal", () => { userEvent.click(collDropdown()); await waitFor(() => expect(newCollBtn()).toBeInTheDocument()); }); - it("should not be accessible if the dashboard form is invalid", async () => { - await setup(getQuestion()); - userEvent.clear(nameField()); - userEvent.click(collDropdown()); - await waitFor(() => expect(newCollBtn()).toBeDisabled()); - }); it("should open new collection modal and return to dashboard modal when clicking close", async () => { await setup(getQuestion()); userEvent.click(collDropdown()); - await waitFor(() => expect(newCollBtn()).toBeEnabled()); + await waitFor(() => expect(newCollBtn()).toBeInTheDocument()); userEvent.click(newCollBtn()); await waitFor(() => expect(collModalTitle()).toBeInTheDocument()); userEvent.click(cancelBtn()); diff --git a/frontend/src/metabase/dashboard/containers/CreateDashboardForm.tsx b/frontend/src/metabase/dashboard/containers/CreateDashboardForm.tsx index 9028588d42dc2b7cc2a911e71f1931035063b80d..494d3e8c640f6c82ecbff0471da51817f9f9ff58 100644 --- a/frontend/src/metabase/dashboard/containers/CreateDashboardForm.tsx +++ b/frontend/src/metabase/dashboard/containers/CreateDashboardForm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { t } from "ttag"; import _ from "underscore"; import * as Yup from "yup"; @@ -14,14 +14,14 @@ import FormTextArea from "metabase/core/components/FormTextArea"; import FormSubmitButton from "metabase/core/components/FormSubmitButton"; import FormErrorMessage from "metabase/core/components/FormErrorMessage"; +import { OnClickNewCollection } from "metabase/containers/CreateCollectionOnTheGo"; + import * as Errors from "metabase/core/utils/errors"; import Collections from "metabase/entities/collections"; import Dashboards from "metabase/entities/dashboards"; -import FormCollectionPicker, { - NewCollectionButton, -} from "metabase/collections/containers/FormCollectionPicker/FormCollectionPicker"; +import FormCollectionPicker from "metabase/collections/containers/FormCollectionPicker/FormCollectionPicker"; import type { CollectionId, Dashboard } from "metabase-types/api"; import type { State } from "metabase-types/store"; @@ -41,17 +41,11 @@ export interface CreateDashboardProperties { collection_id: CollectionId; } -export interface StagedDashboard { - values: CreateDashboardProperties; - handleCreate: (values: CreateDashboardProperties) => void; - openCollectionId: CollectionId | undefined; -} - export interface CreateDashboardFormOwnProps { collectionId?: CollectionId | null; // can be used by `getInitialCollectionId` onCreate?: (dashboard: Dashboard) => void; onCancel?: () => void; - saveToNewCollection?: (stagedDash: StagedDashboard) => void; + onClickNewCollection?: OnClickNewCollection; initialValues?: CreateDashboardProperties | null; } @@ -87,7 +81,7 @@ function CreateDashboardForm({ handleCreateDashboard, onCreate, onCancel, - saveToNewCollection, + onClickNewCollection, initialValues, }: Props) { const computedInitialValues = useMemo( @@ -108,15 +102,13 @@ function CreateDashboardForm({ [handleCreateDashboard, onCreate], ); - const [openCollectionId, setOpenCollectionId] = useState<CollectionId>(); - return ( <FormProvider initialValues={computedInitialValues} validationSchema={DASHBOARD_SCHEMA} onSubmit={handleCreate} > - {({ dirty, isValid, values }) => ( + {() => ( <Form> <FormInput name="name" @@ -131,27 +123,17 @@ function CreateDashboardForm({ nullable /> <FormCollectionPicker - onOpenCollectionChange={setOpenCollectionId} name="collection_id" title={t`Which collection should this go in?`} - > - <NewCollectionButton - disabled={!isValid} - onClick={() => - saveToNewCollection?.({ - values, - handleCreate, - openCollectionId, - }) - } - /> - </FormCollectionPicker> + canCreateNew={true} + onClickNewCollection={onClickNewCollection} + /> <FormFooter> <FormErrorMessage inline /> {!!onCancel && ( <Button type="button" onClick={onCancel}>{t`Cancel`}</Button> )} - <FormSubmitButton title={t`Create`} disabled={!dirty} primary /> + <FormSubmitButton title={t`Create`} primary /> </FormFooter> </Form> )} diff --git a/frontend/src/metabase/dashboard/containers/CreateDashboardModal.tsx b/frontend/src/metabase/dashboard/containers/CreateDashboardModal.tsx index c87a410d34a6e8c948aa677a61c3127ae63e6827..f937b79815d31f9df6369f0d6765359b678e1531 100644 --- a/frontend/src/metabase/dashboard/containers/CreateDashboardModal.tsx +++ b/frontend/src/metabase/dashboard/containers/CreateDashboardModal.tsx @@ -1,20 +1,19 @@ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { t } from "ttag"; import { connect } from "react-redux"; import { push } from "react-router-redux"; import type { LocationDescriptor } from "history"; +import { CreateCollectionOnTheGo } from "metabase/containers/CreateCollectionOnTheGo"; import ModalContent from "metabase/components/ModalContent"; import * as Urls from "metabase/lib/urls"; -import type { Dashboard, Collection, CollectionId } from "metabase-types/api"; +import type { Dashboard } from "metabase-types/api"; import type { State } from "metabase-types/store"; -import CreateCollectionModal from "metabase/collections/containers/CreateCollectionModal"; import CreateDashboardForm, { CreateDashboardFormOwnProps, - StagedDashboard, } from "./CreateDashboardForm"; interface CreateDashboardModalOwnProps @@ -50,39 +49,20 @@ function CreateDashboardModal({ [onCreate, onChangeLocation, onClose], ); - const [creatingNewCollection, setCreatingNewCollection] = useState(false); - const [openCollectionId, setOpenCollectionId] = useState<CollectionId>(); - const [stagedDashboard, setStagedDashboard] = - useState<StagedDashboard | null>(null); - const saveToNewCollection = (s: StagedDashboard) => { - setCreatingNewCollection(true); - setOpenCollectionId(s.openCollectionId); - setStagedDashboard(s); - }; - - if (creatingNewCollection && stagedDashboard) { - return ( - <CreateCollectionModal - collectionId={openCollectionId} - onClose={() => setCreatingNewCollection(false)} - onCreate={(collection: Collection) => { - const { values, handleCreate } = stagedDashboard; - handleCreate({ ...values, collection_id: collection.id }); - }} - /> - ); - } - return ( - <ModalContent title={t`New dashboard`} onClose={onClose}> - <CreateDashboardForm - {...props} - onCreate={handleCreate} - onCancel={onClose} - saveToNewCollection={saveToNewCollection} - initialValues={stagedDashboard?.values} - /> - </ModalContent> + <CreateCollectionOnTheGo> + {(resumedValues, onClickNewCollection) => ( + <ModalContent title={t`New dashboard`} onClose={onClose}> + <CreateDashboardForm + {...props} + onCreate={handleCreate} + onCancel={onClose} + onClickNewCollection={onClickNewCollection} + initialValues={resumedValues} + /> + </ModalContent> + )} + </CreateCollectionOnTheGo> ); } diff --git a/frontend/src/metabase/dashboard/containers/CreateDashboardModal.unit.spec.tsx b/frontend/src/metabase/dashboard/containers/CreateDashboardModal.unit.spec.tsx index f7cb8114f943d3738eb37b65ef669b2d5501c42a..71d569da493342b6905d03c6ebeafb7f3e194078 100644 --- a/frontend/src/metabase/dashboard/containers/CreateDashboardModal.unit.spec.tsx +++ b/frontend/src/metabase/dashboard/containers/CreateDashboardModal.unit.spec.tsx @@ -138,17 +138,12 @@ describe("CreateDashboardModal", () => { userEvent.click(collDropdown()); await waitFor(() => expect(newCollBtn()).toBeInTheDocument()); }); - it("should not be accessible if the dashboard form is invalid", async () => { - setup(); - userEvent.click(collDropdown()); - await waitFor(() => expect(newCollBtn()).toBeDisabled()); - }); it("should open new collection modal and return to dashboard modal when clicking close", async () => { setup(); const name = "my dashboard"; userEvent.type(nameField(), name); userEvent.click(collDropdown()); - await waitFor(() => expect(newCollBtn()).toBeEnabled()); + await waitFor(() => expect(newCollBtn()).toBeInTheDocument()); userEvent.click(newCollBtn()); await waitFor(() => expect(collModalTitle()).toBeInTheDocument()); userEvent.click(cancelBtn());