From 5b062153206f6ab9392f3d501f58c01f77fa65ab Mon Sep 17 00:00:00 2001 From: Tuval Simha Date: Sun, 18 Aug 2024 19:38:36 +0300 Subject: [PATCH] (4) Drop v2: Create Operation Modal (#5315) --- .../laboratory/create-operation-modal.tsx | 326 +++++++++++------- .../create-operation-modal.stories.tsx | 97 ++++++ 2 files changed, 297 insertions(+), 126 deletions(-) create mode 100644 packages/web/app/src/stories/create-operation-modal.stories.tsx diff --git a/packages/web/app/src/components/target/laboratory/create-operation-modal.tsx b/packages/web/app/src/components/target/laboratory/create-operation-modal.tsx index 1f8049584..a5ca31cc7 100644 --- a/packages/web/app/src/components/target/laboratory/create-operation-modal.tsx +++ b/packages/web/app/src/components/target/laboratory/create-operation-modal.tsx @@ -1,14 +1,31 @@ import { ReactElement } from 'react'; -import { useFormik } from 'formik'; +import { useForm, UseFormReturn } from 'react-hook-form'; import { useMutation } from 'urql'; -import * as Yup from 'yup'; +import { z } from 'zod'; import { Button } from '@/components/ui/button'; -import { Heading } from '@/components/ui/heading'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'; -import { Input, Modal } from '@/components/v2'; +import { useToast } from '@/components/ui/use-toast'; import { graphql } from '@/gql'; +import { DocumentCollection } from '@/gql/graphql'; import { useCollections } from '@/pages/target-laboratory'; import { useEditorContext } from '@graphiql/react'; +import { zodResolver } from '@hookform/resolvers/zod'; const CreateOperationMutation = graphql(` mutation CreateOperation( @@ -50,6 +67,24 @@ const CreateOperationMutation = graphql(` export type CreateOperationMutationType = typeof CreateOperationMutation; +const createOperationModalFormSchema = z.object({ + name: z + .string({ + required_error: 'Operation name is required', + }) + .min(3, { + message: 'Operation name must be at least 3 characters long', + }) + .max(50, { + message: 'Operation name must be less than 50 characters long', + }), + collectionId: z.string({ + required_error: 'Collection is required', + }), +}); + +export type CreateOperationModalFormValues = z.infer; + export function CreateOperationModal(props: { isOpen: boolean; close: () => void; @@ -58,6 +93,7 @@ export function CreateOperationModal(props: { projectId: string; targetId: string; }): ReactElement { + const { toast } = useToast(); const { isOpen, close, onSaveSuccess } = props; const [mutationCreate, mutateCreate] = useMutation(CreateOperationMutation); @@ -66,137 +102,175 @@ export function CreateOperationModal(props: { projectId: props.projectId, targetId: props.targetId, }); - const { queryEditor, variableEditor, headerEditor } = useEditorContext({ nonNull: true, }); - const { - handleSubmit, - values, - handleChange, - handleBlur, - errors, - isValid, - touched, - isSubmitting, - resetForm, - setFieldValue, - } = useFormik({ - initialValues: { + const form = useForm({ + mode: 'onChange', + resolver: zodResolver(createOperationModalFormSchema), + defaultValues: { name: '', collectionId: '', }, - validationSchema: Yup.object().shape({ - name: Yup.string().min(3).required(), - collectionId: Yup.string().required('Collection is a required field'), - }), - async onSubmit(values) { - const response = await mutateCreate({ - selector: { - target: props.targetId, - organization: props.organizationId, - project: props.projectId, - }, - input: { - name: values.name, - collectionId: values.collectionId, - query: queryEditor?.getValue() ?? '', - variables: variableEditor?.getValue(), - headers: headerEditor?.getValue(), - }, - }); - const result = response.data; - const error = response.error || response.data?.createOperationInDocumentCollection.error; - - if (!error) { - onSaveSuccess?.(result?.createOperationInDocumentCollection.ok?.operation.id); - resetForm(); - close(); - } - }, }); + async function onSubmit(values: CreateOperationModalFormValues) { + if (mutationCreate.error) { + form.setError('name', { + message: mutationCreate.error.message, + }); + } + const response = await mutateCreate({ + selector: { + target: props.targetId, + organization: props.organizationId, + project: props.projectId, + }, + input: { + name: values.name, + collectionId: values.collectionId, + query: queryEditor?.getValue() ?? '', + variables: variableEditor?.getValue(), + headers: headerEditor?.getValue(), + }, + }); + const result = response.data; + const error = response.error || response.data?.createOperationInDocumentCollection.error; + + if (!error) { + onSaveSuccess?.(result?.createOperationInDocumentCollection.ok?.operation.id); + form.reset(); + close(); + } else { + toast({ + title: 'Could not create operation', + description: error.message, + variant: 'destructive', + }); + } + } + return ( - - {!fetching && ( -
- Create Operation - -
- - - {touched.name && errors.name && ( -
{errors.name}
- )} -
- -
- - - {touched.collectionId && errors.collectionId && ( -
{errors.collectionId}
- )} -
- - {mutationCreate.error && ( -
{mutationCreate.error.message}
- )} - -
- - -
-
- )} -
+ + ); +} + +type DocumentCollectionWithOutOperations = Omit< + DocumentCollection, + 'createdBy' | 'createdAt' | 'updatedAt' | 'operations' | 'pageInfo' +>; + +export function CreateOperationModalContent(props: { + isOpen: boolean; + close: () => void; + onSubmit: (values: CreateOperationModalFormValues) => void; + organizationId: string; + projectId: string; + form: UseFormReturn; + targetId: string; + fetching: boolean; + collections: DocumentCollectionWithOutOperations[]; +}): ReactElement { + return ( + + + {!props.fetching && ( +
+ + + Create Operation + +
+ { + return ( + + Operation Name + + + + + + ); + }} + /> + { + return ( + + Collection Description + + + + + + ); + }} + /> +
+ + + + +
+ + )} +
+
); } diff --git a/packages/web/app/src/stories/create-operation-modal.stories.tsx b/packages/web/app/src/stories/create-operation-modal.stories.tsx new file mode 100644 index 000000000..fa3a26f7f --- /dev/null +++ b/packages/web/app/src/stories/create-operation-modal.stories.tsx @@ -0,0 +1,97 @@ +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { + CreateOperationModalContent, + CreateOperationModalFormValues, +} from '@/components/target/laboratory/create-operation-modal'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Meta, StoryFn, StoryObj } from '@storybook/react'; + +const createOperationModalFormSchema = z.object({ + name: z + .string({ + required_error: 'Operation name is required', + }) + .min(2, { + message: 'Operation name must be at least 2 characters long', + }) + .max(50, { + message: 'Operation name must be less than 50 characters long', + }), + collectionId: z.string({ + required_error: 'Collection is required', + }), +}); + +const meta: Meta = { + title: 'Modals/Create Operation Modal', + component: CreateOperationModalContent, + argTypes: { + isOpen: { + control: { + type: 'boolean', + }, + }, + fetching: { + control: { + type: 'boolean', + }, + }, + close: { + action: 'close', + }, + collections: { + control: { + type: 'object', + }, + }, + onSubmit: { + action: 'onSubmit', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const Template: StoryFn = args => { + const form = useForm({ + mode: 'onChange', + resolver: zodResolver(createOperationModalFormSchema), + defaultValues: { + name: '', + collectionId: '', + }, + }); + + return ; +}; + +export const Default: Story = Template.bind({}); +Default.args = { + isOpen: true, + close: () => {}, + collections: [ + { + id: '1', + name: 'Collection 1', + __typename: 'DocumentCollection', + description: 'Collection 1 description', + }, + { + id: '2', + name: 'Collection 2', + __typename: 'DocumentCollection', + description: 'Collection 2 description', + }, + { + id: '3', + name: 'Collection 3', + __typename: 'DocumentCollection', + description: 'Collection 3 description', + }, + ], + fetching: false, + onSubmit: () => {}, +};