CustomExpiryModal.tsx164 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import dayjs from 'dayjs' |
| 3 | import { SubmitHandler, useForm, useWatch } from 'react-hook-form' |
| 4 | import { |
| 5 | Button, |
| 6 | Form, |
| 7 | FormControl, |
| 8 | FormField, |
| 9 | Input, |
| 10 | Modal, |
| 11 | Select, |
| 12 | SelectContent, |
| 13 | SelectItem, |
| 14 | SelectTrigger, |
| 15 | SelectValue, |
| 16 | } from 'ui' |
| 17 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 18 | import * as z from 'zod' |
| 19 | |
| 20 | import { useCopyUrl } from './useCopyUrl' |
| 21 | import { DATETIME_FORMAT } from '@/lib/constants' |
| 22 | import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer' |
| 23 | |
| 24 | const unitMap = { |
| 25 | days: 3600 * 24, |
| 26 | weeks: 3600 * 24 * 7, |
| 27 | months: 3600 * 24 * 30, |
| 28 | years: 3600 * 24 * 365, |
| 29 | } as const |
| 30 | |
| 31 | const formSchema = z.object({ |
| 32 | expiresIn: z.preprocess( |
| 33 | (val) => (val ? val : undefined), |
| 34 | z.coerce |
| 35 | .number({ required_error: 'Required', invalid_type_error: 'Required' }) |
| 36 | .positive('Expiry duration must be greater than 0') |
| 37 | ), |
| 38 | units: z.enum(['days', 'weeks', 'months', 'years']), |
| 39 | }) |
| 40 | |
| 41 | type FormSchema = z.infer<typeof formSchema> |
| 42 | |
| 43 | const formId = 'storage-custom-expiry-form' |
| 44 | |
| 45 | export const CustomExpiryModal = () => { |
| 46 | const { onCopyUrl } = useCopyUrl() |
| 47 | const snap = useStorageExplorerStateSnapshot() |
| 48 | const { selectedFileCustomExpiry, setSelectedFileCustomExpiry } = snap |
| 49 | |
| 50 | const visible = selectedFileCustomExpiry !== undefined |
| 51 | const onClose = () => setSelectedFileCustomExpiry(undefined) |
| 52 | |
| 53 | const form = useForm<FormSchema>({ |
| 54 | resolver: zodResolver(formSchema as any), |
| 55 | defaultValues: { expiresIn: 0, units: 'days' }, |
| 56 | }) |
| 57 | |
| 58 | const handleClose = () => { |
| 59 | onClose() |
| 60 | form.reset() |
| 61 | } |
| 62 | |
| 63 | const { isDirty, isSubmitting, isValid } = form.formState |
| 64 | const handleSubmit: SubmitHandler<FormSchema> = async (values) => { |
| 65 | await onCopyUrl(selectedFileCustomExpiry!.path!, values.expiresIn * unitMap[values.units]) |
| 66 | handleClose() |
| 67 | } |
| 68 | |
| 69 | const [expiresIn, units] = useWatch({ |
| 70 | name: ['expiresIn', 'units'], |
| 71 | control: form.control, |
| 72 | }) |
| 73 | |
| 74 | return ( |
| 75 | <Modal |
| 76 | hideFooter |
| 77 | size="small" |
| 78 | header="Custom expiry for signed URL" |
| 79 | visible={visible} |
| 80 | alignFooter="right" |
| 81 | confirmText="Get URL" |
| 82 | onCancel={handleClose} |
| 83 | > |
| 84 | <Form {...form}> |
| 85 | <Modal.Content> |
| 86 | <p className="text-sm text-foreground-light mb-4"> |
| 87 | Enter the duration for which the URL will be valid for: |
| 88 | </p> |
| 89 | <form |
| 90 | id={formId} |
| 91 | onSubmit={form.handleSubmit(handleSubmit)} |
| 92 | noValidate |
| 93 | className="flex items-start space-x-2" |
| 94 | > |
| 95 | <div className="grow"> |
| 96 | <FormField |
| 97 | control={form.control} |
| 98 | name="expiresIn" |
| 99 | render={({ field }) => ( |
| 100 | <FormItemLayout layout="vertical" label="Duration"> |
| 101 | <FormControl> |
| 102 | <Input |
| 103 | {...field} |
| 104 | type="number" |
| 105 | onChange={(e) => { |
| 106 | field.onChange( |
| 107 | isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber |
| 108 | ) |
| 109 | }} |
| 110 | /> |
| 111 | </FormControl> |
| 112 | </FormItemLayout> |
| 113 | )} |
| 114 | /> |
| 115 | </div> |
| 116 | <div> |
| 117 | <FormField |
| 118 | control={form.control} |
| 119 | name="units" |
| 120 | render={({ field }) => ( |
| 121 | <FormItemLayout layout="vertical" label="Units"> |
| 122 | <FormControl> |
| 123 | <Select value={field.value} onValueChange={field.onChange}> |
| 124 | <SelectTrigger> |
| 125 | <SelectValue aria-label="Units" placeholder="Select an option" /> |
| 126 | </SelectTrigger> |
| 127 | <SelectContent> |
| 128 | <SelectItem value="days">days</SelectItem> |
| 129 | <SelectItem value="weeks">weeks</SelectItem> |
| 130 | <SelectItem value="months">months</SelectItem> |
| 131 | <SelectItem value="years">years</SelectItem> |
| 132 | </SelectContent> |
| 133 | </Select> |
| 134 | </FormControl> |
| 135 | </FormItemLayout> |
| 136 | )} |
| 137 | /> |
| 138 | </div> |
| 139 | </form> |
| 140 | {isDirty && isValid && ( |
| 141 | <p className="text-sm text-foreground-light mt-2"> |
| 142 | URL will expire on {dayjs().add(expiresIn, units).format(DATETIME_FORMAT)} |
| 143 | </p> |
| 144 | )} |
| 145 | </Modal.Content> |
| 146 | <Modal.Separator /> |
| 147 | <Modal.Content className="flex items-center justify-end space-x-2"> |
| 148 | <Button type="default" onClick={handleClose}> |
| 149 | Cancel |
| 150 | </Button> |
| 151 | <Button |
| 152 | form={formId} |
| 153 | disabled={!isDirty || isSubmitting} |
| 154 | loading={isSubmitting} |
| 155 | htmlType="submit" |
| 156 | type="primary" |
| 157 | > |
| 158 | Get signed URL |
| 159 | </Button> |
| 160 | </Modal.Content> |
| 161 | </Form> |
| 162 | </Modal> |
| 163 | ) |
| 164 | } |