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