(1) Drop V2: Create Collection Modal (#5310)

This commit is contained in:
Tuval Simha 2024-08-18 18:25:23 +03:00 committed by GitHub
parent 4d89c7596e
commit 38aa3ff159
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 263 additions and 126 deletions

View file

@ -1,11 +1,28 @@
import { ReactElement, useEffect } from 'react';
import { useFormik } from 'formik';
import { useForm, UseFormReturn } from 'react-hook-form';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Heading } from '@/components/ui/heading';
import { Callout, Input, Modal } from '@/components/v2';
import {
Dialog,
DialogContent,
DialogDescription,
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 { Callout } from '@/components/v2';
import { graphql } from '@/gql';
import { zodResolver } from '@hookform/resolvers/zod';
const CollectionQuery = graphql(`
query Collection($selector: TargetSelectorInput!, $id: ID!) {
@ -103,6 +120,22 @@ const UpdateCollectionMutation = graphql(`
}
`);
const createCollectionModalFormSchema = z.object({
name: z
.string({
required_error: 'Collection name is required',
})
.min(2, {
message: 'Collection name must be at least 2 characters long',
})
.max(50, {
message: 'Collection name must be at most 50 characters long',
}),
description: z.string().optional(),
});
export type CreateCollectionModalFormValues = z.infer<typeof createCollectionModalFormSchema>;
export function CreateCollectionModal(props: {
isOpen: boolean;
toggleModalOpen: () => void;
@ -128,140 +161,156 @@ export function CreateCollectionModal(props: {
pause: !collectionId,
});
const error = mutationCreate.error || collectionError || mutationUpdate.error;
const errorCombined = mutationCreate.error || collectionError || mutationUpdate.error;
const fetching = loadingCollection;
const form = useForm<CreateCollectionModalFormValues>({
mode: 'onChange',
resolver: zodResolver(createCollectionModalFormSchema),
defaultValues: {
name: '',
description: '',
},
});
useEffect(() => {
if (!collectionId) {
resetForm();
form.reset();
} else if (data) {
const { documentCollection } = data.target!;
if (documentCollection) {
void setValues({
name: documentCollection.name,
description: documentCollection.description || '',
});
void form.setValue('name', documentCollection.name);
void form.setValue('description', documentCollection.description || '');
}
}
}, [data, collectionId]);
const {
handleSubmit,
values,
handleChange,
errors,
touched,
isSubmitting,
setValues,
resetForm,
} = useFormik({
initialValues: {
name: '',
description: '',
},
validationSchema: Yup.object().shape({
name: Yup.string().required(),
description: Yup.string(),
}),
async onSubmit(values) {
const { error } = collectionId
? await mutateUpdate({
selector: {
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: {
collectionId,
name: values.name,
description: values.description,
},
})
: await mutateCreate({
selector: {
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: values,
});
if (!error) {
resetForm();
toggleModalOpen();
}
},
});
async function onSubmit(values: CreateCollectionModalFormValues) {
const { error } = collectionId
? await mutateUpdate({
selector: {
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: {
collectionId,
name: values.name,
description: values.description,
},
})
: await mutateCreate({
selector: {
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: values,
});
if (!error || errorCombined) {
form.reset();
toggleModalOpen();
}
}
return (
<Modal open={isOpen} onOpenChange={toggleModalOpen}>
{!fetching && (
<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<Heading className="text-center">
{collectionId ? 'Update' : 'Create'} Shared Collection
</Heading>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Collection Name
</label>
<Input
data-cy="input.name"
name="name"
placeholder="My Collection"
value={values.name}
onChange={handleChange}
isInvalid={!!(touched.name && errors.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="description">
Collection Description
</label>
<Input
data-cy="input.description"
name="description"
value={values.description}
onChange={handleChange}
isInvalid={!!(touched.description && errors.description)}
/>
{touched.description && errors.description && (
<div className="text-sm text-red-500">{errors.description}</div>
)}
<Callout type="info" className="mt-0">
This collection will be available to everyone in the organization
</Callout>
</div>
{error && <div className="text-sm text-red-500">{error.message}</div>}
<div className="flex w-full gap-2">
<Button
type="button"
size="lg"
className="w-full justify-center"
onClick={toggleModalOpen}
>
Cancel
</Button>
<Button
type="submit"
size="lg"
className="w-full justify-center"
variant="primary"
disabled={isSubmitting}
data-cy="confirm"
>
{collectionId ? 'Update' : 'Add'}
</Button>
</div>
</form>
)}
</Modal>
<CreateCollectionModalContent
isOpen={isOpen}
toggleModalOpen={toggleModalOpen}
onSubmit={onSubmit}
form={form}
collectionId={collectionId}
fetching={fetching}
/>
);
}
export function CreateCollectionModalContent(props: {
isOpen: boolean;
toggleModalOpen: () => void;
onSubmit: (values: CreateCollectionModalFormValues) => void;
form: UseFormReturn<CreateCollectionModalFormValues>;
collectionId?: string;
fetching: boolean;
}) {
return (
<Dialog open={props.isOpen} onOpenChange={props.toggleModalOpen}>
<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>
{props.collectionId ? 'Update' : 'Create'} Shared Collection
</DialogTitle>
<DialogDescription>
{props.collectionId
? 'Update the shared collection name and description'
: 'Create a shared collection that everyone in the organization can access'}
</DialogDescription>
</DialogHeader>
<div className="space-y-8">
<FormField
control={props.form.control}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Collection Name</FormLabel>
<FormControl>
<Input {...field} placeholder="My Collection" />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={props.form.control}
name="description"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Collection Description</FormLabel>
<FormControl>
<Input {...field} placeholder="My Collection" />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<Callout type="info" className="mt-0">
This collection will be available to everyone in the organization
</Callout>
<DialogFooter>
<Button
type="button"
size="lg"
className="w-full justify-center"
onClick={ev => {
ev.preventDefault();
props.toggleModalOpen();
}}
>
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"
>
{props.collectionId ? 'Update' : 'Add'}
</Button>
</DialogFooter>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,88 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
CreateCollectionModalContent,
CreateCollectionModalFormValues,
} from '@/components/target/laboratory/create-collection-modal';
import { zodResolver } from '@hookform/resolvers/zod';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
const createCollectionModalFormSchema = z.object({
name: z
.string({
required_error: 'Collection name is required',
})
.min(2, {
message: 'Collection name must be at least 2 characters long',
})
.max(50, {
message: 'Collection name must be at most 50 characters long',
}),
description: z.string().optional(),
});
const meta: Meta<typeof CreateCollectionModalContent> = {
title: 'Modals/Create Collection Modal',
component: CreateCollectionModalContent,
argTypes: {
collectionId: {
control: {
type: 'text',
},
},
fetching: {
control: {
type: 'boolean',
},
},
isOpen: {
control: {
type: 'boolean',
},
},
},
};
export default meta;
type Story = StoryObj<typeof CreateCollectionModalContent>;
const Template: StoryFn<typeof CreateCollectionModalContent> = args => {
const form = useForm<CreateCollectionModalFormValues>({
mode: 'onChange',
resolver: zodResolver(createCollectionModalFormSchema),
defaultValues: {
name: '',
description: '',
},
});
useEffect(() => {
if (!args.collectionId) {
form.reset();
} else {
void form.setValue('name', args.collectionId);
}
}, [args.collectionId]);
return <CreateCollectionModalContent {...args} form={form} />;
};
export const Create: Story = Template.bind({});
Create.args = {
collectionId: '',
fetching: false,
isOpen: true,
toggleModalOpen: () => console.log('Toggle Modal Open'),
onSubmit: data => console.log('Submit', data),
};
export const Update: Story = Template.bind({});
Update.args = {
collectionId: 'collectionId',
fetching: false,
isOpen: true,
toggleModalOpen: () => console.log('Toggle Modal Open'),
onSubmit: data => console.log('Submit', data),
};