gitscope
← marzneshin / v0.1.2 / app/dashboard/src/pages/services/dialog/index.tsx
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/pages/services/dialog/index.tsx · TypeScript
Blame Raw
  1
  2import {
  3  Alert,
  4  AlertIcon,
  5  Flex,
  6  Button,
  7  Tooltip,
  8  HStack,
  9  Spinner,
 10  ModalBody,
 11  ModalCloseButton,
 12  ModalContent,
 13  Modal,
 14  VStack,
 15  useToast,
 16  ModalOverlay,
 17  IconButton,
 18} from '@chakra-ui/react';
 19import { zodResolver } from '@hookform/resolvers/zod';
 20import { FC, useEffect, useState } from 'react';
 21import { FormProvider, useForm } from 'react-hook-form';
 22import { useTranslation } from 'react-i18next';
 23import {
 24  AddIcon,
 25  DialogModalHeader,
 26  DialogModalFooter,
 27  DeleteIcon,
 28  EditIcon
 29} from 'components/dialog';
 30import { getDefaultValues } from './default-values';
 31import { schema, FormType } from './form-schema';
 32import { InboundsField } from './inbounds-field';
 33import { NameField } from './name-field';
 34import { InboundType, Service, ServiceCreate } from 'types';
 35import { fetchInbounds, useInbounds, useServices } from 'stores';
 36import { useQuery } from 'react-query';
 37import { queryIds } from 'constants/query-ids';
 38
 39const formatService = (service: Service): FormType => {
 40  const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => {
 41    return (typeof inbound !== 'number') ? inbound.id : inbound;
 42  });
 43  return {
 44    ...service,
 45    inbounds,
 46  };
 47};
 48
 49export type ServiceDialogProps = {};
 50
 51export const ServiceDialog: FC<ServiceDialogProps> = () => {
 52  const {
 53    editingService,
 54    isCreatingNewService,
 55    onCreateService,
 56    editService,
 57    onEditingService,
 58    createService,
 59    onDeletingService,
 60  } = useServices();
 61  const { refetchInbounds } = useInbounds();
 62  const {
 63    data: inbounds,
 64  } = useQuery(queryIds.inbounds, () => fetchInbounds());
 65  const isEditing = !!editingService;
 66  const isOpen = isCreatingNewService || isEditing;
 67  const [loading, setLoading] = useState(false);
 68  const [error, setError] = useState<string | null>('');
 69  const toast = useToast();
 70  const { t } = useTranslation();
 71
 72  const form = useForm<FormType>({
 73    defaultValues: getDefaultValues(),
 74    resolver: zodResolver(schema),
 75  });
 76
 77  useEffect(() => {
 78    if (isOpen) {
 79      refetchInbounds();
 80      form.reset();
 81    }
 82  }, [isOpen]);
 83
 84
 85  useEffect(() => {
 86    if (editingService) {
 87      form.reset(formatService(editingService));
 88    }
 89  }, [editingService]);
 90
 91  const submit = async (values: FormType) => {
 92    setLoading(true);
 93    const methods = { edited: editService, created: createService };
 94    const method = isEditing ? 'edited' : 'created';
 95    setError(null);
 96    const { inbounds, name } = values;
 97
 98    let body: ServiceCreate = { name, inbounds, id: editingService?.id };
 99
100    await methods[method](body)
101      .then(() => {
102        toast({
103          title: t(
104            isEditing ? 'serviceDialog.serviceEdited' : 'serviceDialog.serviceCreated',
105            { name: values.name }
106          ),
107          status: 'success',
108          isClosable: true,
109          position: 'top',
110          duration: 3000,
111        });
112        onClose();
113      })
114      .catch((err) => {
115        if (err?.response?.status === 409 || err?.response?.status === 400)
116          setError(err?.response?._data?.detail);
117        if (err?.response?.status === 422) {
118          Object.keys(err.response._data.detail).forEach((key) => {
119            setError(err?.response._data.detail[key] as string);
120            form.setError(key as 'name' | 'inbounds', { type: 'custom', message: err.response._data.detail[key], });
121          });
122        }
123      })
124      .finally(() => {
125        setLoading(false);
126      });
127  };
128
129  const onClose = () => {
130    form.reset(getDefaultValues());
131    onCreateService(false);
132    onEditingService(null);
133    setError(null);
134  };
135
136  const disabled = loading;
137
138  return (
139    <Modal isOpen={isOpen} onClose={onClose} size="lg">
140      <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
141      <FormProvider {...form} formState={form.formState}>
142        <ModalContent mx="3">
143          <form onSubmit={form.handleSubmit(submit)}>
144            <DialogModalHeader HeaderIcon={isEditing ? EditIcon : AddIcon} title={isEditing ? t('serviceDialog.editServiceTitle') : t('serviceDialog.createNewService')} />
145            <ModalCloseButton mt={3} disabled={disabled} />
146            <ModalBody>
147              <VStack justifyContent="space-between">
148                <Flex
149                  flexDirection="column"
150                  gridAutoRows="min-content"
151                  w="full"
152                >
153                  <NameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
154                  <InboundsField t={t} inbounds={inbounds || []} form={form} />
155                </Flex>
156                {error && (
157                  <Alert
158                    status="error"
159                    display={{ base: 'none', md: 'flex' }}
160                  >
161                    <AlertIcon />
162                    {error}
163                  </Alert>
164                )}
165              </VStack>
166              {error && (
167                <Alert
168                  mt="3"
169                  status="error"
170                  display={{ base: 'flex', md: 'none' }}
171                >
172                  <AlertIcon />
173                  {error}
174                </Alert>
175              )}
176            </ModalBody>
177            <DialogModalFooter>
178              <HStack
179                justifyContent="flex-start"
180                w={{
181                  base: 'full',
182                  sm: 'unset',
183                }}
184              >
185                {isEditing && editingService !== null && (
186                  <>
187                    <Tooltip label={t('delete')} placement="top">
188                      <IconButton
189                        aria-label="Delete"
190                        size="sm"
191                        onClick={() => {
192                          onDeletingService(editingService);
193                          onClose();
194                        }}
195                      >
196                        <DeleteIcon />
197                      </IconButton>
198                    </Tooltip>
199                  </>
200                )}
201              </HStack>
202              <HStack
203                w="full"
204                maxW={{ md: '50%', base: 'full' }}
205                justify="end"
206              >
207                <Button
208                  type="submit"
209                  size="sm"
210                  px="8"
211                  colorScheme="primary"
212                  leftIcon={loading ? <Spinner size="xs" /> : undefined}
213                  disabled={false}
214                >
215                  {isEditing ? t('serviceDialog.editService') : t('createService')}
216                </Button>
217              </HStack>
218            </DialogModalFooter>
219          </form>
220        </ModalContent>
221      </FormProvider>
222    </Modal >
223  );
224};