(4) Drop v2: Create Operation Modal (#5315)

This commit is contained in:
Tuval Simha 2024-08-18 19:38:36 +03:00 committed by GitHub
parent b93687441e
commit 5b06215320
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 297 additions and 126 deletions

View file

@ -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<typeof createOperationModalFormSchema>;
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<CreateOperationModalFormValues>({
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 (
<Modal open={isOpen} onOpenChange={close}>
{!fetching && (
<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<Heading className="text-center">Create Operation</Heading>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Operation Name
</label>
<Input
name="name"
placeholder="Your Operation Name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
isInvalid={!!(touched.name && errors.name)}
data-cy="input.name"
/>
{touched.name && errors.name && (
<div className="text-sm text-red-500">{errors.name}</div>
)}
</div>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Which collection would you like to save this operation to?
</label>
<Select
value={values.collectionId}
onValueChange={async v => {
await setFieldValue('collectionId', v);
}}
>
<SelectTrigger>
{collections.find(c => c.id === values.collectionId)?.name || 'Select collection'}
</SelectTrigger>
<SelectContent className="w-[--radix-select-trigger-width]">
{collections.map(c => (
<SelectItem key={c.id} value={c.id}>
{c.name}
<div className="mt-1 line-clamp-1 text-xs opacity-50">{c.description}</div>
</SelectItem>
))}
</SelectContent>
</Select>
{touched.collectionId && errors.collectionId && (
<div className="text-sm text-red-500">{errors.collectionId}</div>
)}
</div>
{mutationCreate.error && (
<div className="text-sm text-red-500">{mutationCreate.error.message}</div>
)}
<div className="flex w-full gap-2">
<Button
type="button"
size="lg"
className="w-full justify-center"
onClick={() => {
resetForm();
close();
}}
>
Cancel
</Button>
<Button
type="submit"
size="lg"
className="w-full justify-center"
variant="primary"
disabled={isSubmitting || !isValid || values.collectionId === ''}
data-cy="confirm"
>
Add Operation
</Button>
</div>
</form>
)}
</Modal>
<CreateOperationModalContent
close={close}
onSubmit={onSubmit}
isOpen={isOpen}
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
fetching={fetching}
form={form}
collections={collections}
/>
);
}
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<CreateOperationModalFormValues>;
targetId: string;
fetching: boolean;
collections: DocumentCollectionWithOutOperations[];
}): ReactElement {
return (
<Dialog open={props.isOpen} onOpenChange={props.close}>
<DialogContent className="container w-4/5 max-w-[600px] md:w-3/5">
{!props.fetching && (
<Form {...props.form}>
<form className="space-y-8" onSubmit={props.form.handleSubmit(props.onSubmit)}>
<DialogHeader>
<DialogTitle>Create Operation</DialogTitle>
</DialogHeader>
<div className="space-y-8">
<FormField
control={props.form.control}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Operation Name</FormLabel>
<FormControl>
<Input {...field} placeholder="Your Operation Name" />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={props.form.control}
name="collectionId"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Collection Description</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={async v => {
await field.onChange(v);
}}
>
<SelectTrigger>
{props.collections.find(c => c.id === field.value)?.name ??
'Select a Collection'}
</SelectTrigger>
<SelectContent className="w-[--radix-select-trigger-width]">
{props.collections.map(c => (
<SelectItem key={c.id} value={c.id}>
{c.name}
<div className="mt-1 line-clamp-1 text-xs opacity-50">
{c.description}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<DialogFooter>
<Button
type="button"
size="lg"
className="w-full justify-center"
onClick={ev => {
ev.preventDefault();
props.close();
props.form.reset();
}}
>
Cancel
</Button>
<Button
type="submit"
size="lg"
className="w-full justify-center"
variant="primary"
disabled={props.form.formState.isSubmitting || !props.form.formState.isValid}
data-cy="confirm"
>
Add Operation
</Button>
</DialogFooter>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);
}

View file

@ -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<typeof CreateOperationModalContent> = {
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<typeof CreateOperationModalContent>;
const Template: StoryFn<typeof CreateOperationModalContent> = args => {
const form = useForm<CreateOperationModalFormValues>({
mode: 'onChange',
resolver: zodResolver(createOperationModalFormSchema),
defaultValues: {
name: '',
collectionId: '',
},
});
return <CreateOperationModalContent {...args} form={form} />;
};
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: () => {},
};