mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 09:08:34 +00:00
(5) Drop V2: Edit Operation Modal (#5316)
This commit is contained in:
parent
1a058fd455
commit
c153264c45
2 changed files with 232 additions and 75 deletions
|
|
@ -1,11 +1,27 @@
|
|||
import { ReactElement, useMemo } 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 { Input, Modal } from '@/components/v2';
|
||||
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 { useToast } from '@/components/ui/use-toast';
|
||||
import { graphql } from '@/gql';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCollections } from '../../../pages/target-laboratory';
|
||||
|
||||
const UpdateOperationNameMutation = graphql(`
|
||||
|
|
@ -30,6 +46,24 @@ const UpdateOperationNameMutation = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
const editOperationModalFormSchema = 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 EditOperationModalFormValues = z.infer<typeof editOperationModalFormSchema>;
|
||||
|
||||
export const EditOperationModal = (props: {
|
||||
operationId: string;
|
||||
close: () => void;
|
||||
|
|
@ -37,6 +71,7 @@ export const EditOperationModal = (props: {
|
|||
projectId: string;
|
||||
targetId: string;
|
||||
}): ReactElement => {
|
||||
const { toast } = useToast();
|
||||
const [updateOperationNameState, mutate] = useMutation(UpdateOperationNameMutation);
|
||||
const { collections } = useCollections({
|
||||
organizationId: props.organizationId,
|
||||
|
|
@ -55,81 +90,119 @@ export const EditOperationModal = (props: {
|
|||
return [null, null] as const;
|
||||
}, [collections]);
|
||||
|
||||
const { handleSubmit, values, handleChange, handleBlur, errors, isValid, touched, isSubmitting } =
|
||||
useFormik({
|
||||
initialValues: {
|
||||
name: operation?.name ?? '',
|
||||
collectionId: collection?.id ?? '',
|
||||
},
|
||||
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 mutate({
|
||||
selector: {
|
||||
target: props.targetId,
|
||||
organization: props.organizationId,
|
||||
project: props.projectId,
|
||||
},
|
||||
input: {
|
||||
collectionId: values.collectionId,
|
||||
operationId: props.operationId,
|
||||
name: values.name,
|
||||
},
|
||||
});
|
||||
const error = response.error || response.data?.updateOperationInDocumentCollection?.error;
|
||||
const form = useForm<EditOperationModalFormValues>({
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(editOperationModalFormSchema),
|
||||
defaultValues: {
|
||||
name: operation?.name || '',
|
||||
collectionId: collection?.id || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
props.close();
|
||||
}
|
||||
async function onSubmit(values: EditOperationModalFormValues) {
|
||||
const response = await mutate({
|
||||
selector: {
|
||||
target: props.targetId,
|
||||
organization: props.organizationId,
|
||||
project: props.projectId,
|
||||
},
|
||||
input: {
|
||||
collectionId: values.collectionId,
|
||||
operationId: props.operationId,
|
||||
name: values.name,
|
||||
},
|
||||
});
|
||||
const error = response.error || response.data?.updateOperationInDocumentCollection?.error;
|
||||
|
||||
if (!error) {
|
||||
props.close();
|
||||
toast({
|
||||
title: 'Operation Updated',
|
||||
description: 'Operation has been updated successfully',
|
||||
variant: 'default',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open onOpenChange={props.close} className="flex flex-col items-center gap-5">
|
||||
{!updateOperationNameState.fetching && (
|
||||
<form className="flex w-full flex-col gap-8" onSubmit={handleSubmit}>
|
||||
<Heading className="text-center">Edit 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>
|
||||
|
||||
{updateOperationNameState.error && (
|
||||
<div className="text-sm text-red-500">{updateOperationNameState.error.message}</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button type="button" size="lg" onClick={props.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
variant="primary"
|
||||
disabled={isSubmitting || !isValid}
|
||||
data-cy="confirm"
|
||||
>
|
||||
Update Operation
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
<EditOperationModalContent
|
||||
fetching={updateOperationNameState.fetching}
|
||||
close={props.close}
|
||||
isOpen
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditOperationModalContent = (props: {
|
||||
fetching: boolean;
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
form: UseFormReturn<EditOperationModalFormValues>;
|
||||
onSubmit: (values: EditOperationModalFormValues) => void;
|
||||
opreationId?: string;
|
||||
}): 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>Edit 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 autoComplete="off" {...field} placeholder="Your Operation Name" />
|
||||
</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"
|
||||
>
|
||||
Update Operation
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
EditOperationModalContent,
|
||||
EditOperationModalFormValues,
|
||||
} from '@/components/target/laboratory/edit-operation-modal';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Meta, StoryFn, StoryObj } from '@storybook/react';
|
||||
|
||||
const editOperationModalFormSchema = 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 EditOperationModalContent> = {
|
||||
title: 'Modals/Edit Operation Modal',
|
||||
component: EditOperationModalContent,
|
||||
argTypes: {
|
||||
isOpen: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
opreationId: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
fetching: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
close: {
|
||||
action: 'close',
|
||||
},
|
||||
onSubmit: {
|
||||
action: 'onSubmit',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EditOperationModalContent>;
|
||||
|
||||
const Template: StoryFn<typeof EditOperationModalContent> = args => {
|
||||
const form = useForm<EditOperationModalFormValues>({
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(editOperationModalFormSchema),
|
||||
defaultValues: {
|
||||
name: args.opreationId,
|
||||
collectionId: args.opreationId,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
name: args.opreationId,
|
||||
collectionId: args.opreationId,
|
||||
});
|
||||
}, [args.opreationId]);
|
||||
|
||||
return <EditOperationModalContent {...args} form={form} />;
|
||||
};
|
||||
|
||||
export const Default: Story = Template.bind({});
|
||||
Default.args = {
|
||||
close: () => {},
|
||||
isOpen: true,
|
||||
fetching: false,
|
||||
};
|
||||
Loading…
Reference in a new issue