diff --git a/src/components/Settings/Common/Config/ConfigActions.tsx b/src/components/Settings/Common/Config/ConfigActions.tsx index 2cce6ac20..700ccd70d 100644 --- a/src/components/Settings/Common/Config/ConfigActions.tsx +++ b/src/components/Settings/Common/Config/ConfigActions.tsx @@ -10,23 +10,28 @@ import { import { useDisclosure } from '@mantine/hooks'; import { openConfirmModal } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; -import { IconAlertTriangle, IconCheck, IconCopy, IconDownload, IconTrash } from '@tabler/icons-react'; +import { + IconAlertTriangle, + IconCheck, + IconCopy, + IconDownload, + IconTrash, + IconX, +} from '@tabler/icons-react'; import fileDownload from 'js-file-download'; import { Trans, useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; -import { useDeleteConfigMutation } from '../../../../tools/config/mutations/useDeleteConfigMutation'; import Tip from '../../../layout/Tip'; import { CreateConfigCopyModal } from './CreateCopyModal'; +import { api } from '~/utils/api'; export default function ConfigActions() { - const router = useRouter(); const { t } = useTranslation(['settings/general/config-changer', 'settings/common', 'common']); const [createCopyModalOpened, createCopyModal] = useDisclosure(false); const { config } = useConfigContext(); - const { removeConfig } = useConfigStore(); - const { mutateAsync } = useDeleteConfigMutation(config?.configProperties.name ?? 'default'); + const { mutateAsync } = useDeleteConfigMutation(); if (!config) return null; @@ -61,28 +66,9 @@ export default function ConfigActions() { }, zIndex: 201, onConfirm: async () => { - const response = await mutateAsync(); - - if (response.error) { - showNotification({ - title: t('buttons.delete.notifications.deleteFailedDefaultConfig.title'), - message: t('buttons.delete.notifications.deleteFailedDefaultConfig.message'), - }); - return; - } - - showNotification({ - title: t('buttons.delete.notifications.deleted.title'), - icon: , - color: 'green', - autoClose: 1500, - radius: 'md', - message: t('buttons.delete.notifications.deleted.message'), + const response = await mutateAsync({ + name: config?.configProperties.name ?? 'default', }); - - removeConfig(config?.configProperties.name ?? 'default'); - - router.push('/'); }, }); }; @@ -124,6 +110,49 @@ export default function ConfigActions() { ); } +const useDeleteConfigMutation = () => { + const { t } = useTranslation(['settings/general/config-changer']); + const router = useRouter(); + const { removeConfig } = useConfigStore(); + + return api.config.delete.useMutation({ + onError(error) { + if (error.data?.code === 'FORBIDDEN') { + showNotification({ + title: t('buttons.delete.notifications.deleteFailedDefaultConfig.title'), + icon: , + color: 'red', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleteFailedDefaultConfig.message'), + }); + } + showNotification({ + title: t('buttons.delete.notifications.deleteFailed.title'), + icon: , + color: 'red', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleteFailed.message'), + }); + }, + onSuccess(data, variables) { + showNotification({ + title: t('buttons.delete.notifications.deleted.title'), + icon: , + color: 'green', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleted.message'), + }); + + removeConfig(variables.name); + + router.push('/'); + }, + }); +}; + const useStyles = createStyles(() => ({ actionIcon: { width: 'auto', diff --git a/src/server/api/routers/config.ts b/src/server/api/routers/config.ts index d695f7e7f..5e9e24d1a 100644 --- a/src/server/api/routers/config.ts +++ b/src/server/api/routers/config.ts @@ -1,4 +1,8 @@ import fs from 'fs'; +import path from 'path'; +import Consola from 'consola'; +import { z } from 'zod'; +import { TRPCError } from '@trpc/server'; import { createTRPCRouter, publicProcedure } from '../trpc'; export const configRouter = createTRPCRouter({ @@ -9,4 +13,47 @@ export const configRouter = createTRPCRouter({ // Strip the .json extension from the file name return files.map((file) => file.replace('.json', '')); }), + delete: publicProcedure + .input( + z.object({ + name: z.string(), + }) + ) + .mutation(async ({ input }) => { + if (input.name.toLowerCase() === 'default') { + Consola.error("Rejected config deletion because default configuration can't be deleted"); + throw new TRPCError({ + code: 'FORBIDDEN', + message: "Default config can't be deleted", + }); + } + + // Loop over all the files in the /data/configs directory + // Get all the configs in the /data/configs folder + // All the files that end in ".json" + const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); + // Match one file if the configProperties.name is the same as the slug + const matchedFile = files.find((file) => { + const config = JSON.parse(fs.readFileSync(path.join('data/configs', file), 'utf8')); + return config.configProperties.name === input.name; + }); + + // If the target is not in the list of files, return an error + if (!matchedFile) { + Consola.error( + `Rejected config deletion request because config name '${input.name}' was not included in present configurations` + ); + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Target not found', + }); + } + + // Delete the file + fs.unlinkSync(path.join('data/configs', matchedFile)); + Consola.info(`Successfully deleted configuration '${input.name}' from your file system`); + return { + message: 'Configuration deleted with success', + }; + }), }); diff --git a/src/tools/config/mutations/useDeleteConfigMutation.tsx b/src/tools/config/mutations/useDeleteConfigMutation.tsx deleted file mode 100644 index 2350869b5..000000000 --- a/src/tools/config/mutations/useDeleteConfigMutation.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { showNotification } from '@mantine/notifications'; -import { IconX } from '@tabler/icons-react'; -import { useMutation } from '@tanstack/react-query'; -import { useTranslation } from 'next-i18next'; - -export const useDeleteConfigMutation = (configName: string) => { - const { t } = useTranslation(['settings/general/config-changer']); - - return useMutation({ - mutationKey: ['configs/delete', { configName }], - mutationFn: () => fetchDeletion(configName), - onError() { - showNotification({ - title: t('buttons.delete.notifications.deleteFailed.title'), - icon: , - color: 'red', - autoClose: 1500, - radius: 'md', - message: t('buttons.delete.notifications.deleteFailed.message'), - }); - }, - }); -}; - -const fetchDeletion = async (configName: string) => - (await fetch(`/api/configs/${configName}`, { method: 'DELETE' })).json();