// * Packages import { ChangeEvent, DragEvent, useRef, useState } from "react"; import { JsonEditor, IconReplacements, LocalisedStrings, } from "json-edit-react"; import { CheckIcon, ChevronDownIcon, CopyIcon, PencilIcon, PlusIcon, Trash2Icon, UploadIcon, XIcon, } from "lucide-react"; import z from "zod"; // * Components shadcn import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; // * Other import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch"; import InputError from "@/back/Components/InputError"; import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import { cn } from "@/lib/utils"; import str from "@/hooks/use-string"; import useTrans from "@/hooks/use-translations"; type JsonPrimitive = string | number | boolean | null; type JsonObject = { [key: string]: JsonPrimitive | JsonObject | JsonObject[]; }; type ImportConfigJsonProps = { // eslint-disable-next-line no-unused-vars onConfirm: (data: JsonObject[], overwrite: boolean) => void; }; // Zod schemas. const jsonObjectSchema: z.ZodType = z.lazy(() => z.record( z.string(), z.union([ z.string(), z.number(), z.boolean(), z.null(), jsonObjectSchema, z.array(jsonObjectSchema), ]), ), ); const jsonArraySchema = z.array(jsonObjectSchema); export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) { // Json content. const [jsonText, setJsonText] = useState([]); // Modale state. const [isOpen, setIsOpen] = useState(false); // Event state. const [isDragging, setIsDragging] = useState(false); // Overwrite state. const [overwrite, setOverwrite] = useState(false); // Local errors. const [globalErrors, setGlobalErrors] = useState>( {}, ); const [editorErrors, setEditorErrors] = useState>( {}, ); // Input file reference. const inputRef = useRef(null); // Maximum file size accepted (2MB). const maxSize = 2 * 1024 * 1024; /** * Get translation of specific key. * * @param key string * @return string */ const useGetTranslation = (key: string): string => str(useTrans(key)).ucFirst().value(); const transJsonEditorMaxSize = useTrans( "back.json_editor.validation.max_size", { params: { value: "2MB" }, }, ); const transJsonEditorMimes = useTrans("back.json_editor.validation.mimes", { params: { values: ".json" }, }); const transJsonEditorInvalidFileJson = useTrans( "back.json_editor.validation.invalid_file_json", ); const transInvalidContentJson = useTrans( "back.json_editor.validation.invalid_content_json", ); // Custom icons. const icons: IconReplacements = { add: , edit: , delete: , copy: , ok: , cancel: , chevron: , }; // Custom translations. const translations: LocalisedStrings = { ITEM_SINGLE: useGetTranslation("back.json_editor.item_single"), ITEMS_MULTIPLE: useGetTranslation("back.json_editor.item_multiple"), KEY_NEW: useGetTranslation("back.json_editor.key_new"), KEY_SELECT: useGetTranslation("back.json_editor.key_select"), NO_KEY_OPTIONS: useGetTranslation("back.json_editor.no_key_options"), ERROR_KEY_EXISTS: useGetTranslation( "back.json_editor.error_key_exists", ), ERROR_INVALID_JSON: useGetTranslation( "back.json_editor.error_invalid_json", ), ERROR_UPDATE: useGetTranslation("back.json_editor.error_update"), ERROR_DELETE: useGetTranslation("back.json_editor.error_delete"), ERROR_ADD: useGetTranslation("back.json_editor.error_add"), DEFAULT_STRING: useGetTranslation("back.json_editor.default_string"), DEFAULT_NEW_KEY: useGetTranslation("back.json_editor.default_new_key"), SHOW_LESS: useGetTranslation("back.json_editor.show_less"), EMPTY_STRING: useGetTranslation("back.json_editor.empty_string"), TOOLTIP_COPY: useGetTranslation("back.json_editor.copy"), TOOLTIP_EDIT: useGetTranslation("back.json_editor.edit"), TOOLTIP_DELETE: useGetTranslation("back.json_editor.delete"), TOOLTIP_ADD: useGetTranslation("back.json_editor.add"), }; /** * Simulates click on input file. * * @return void */ const openFilePicker = (): void => { inputRef.current?.click(); }; /** * Set json from content file. * * @param file File * @return Promise */ const processFile = async (file: File): Promise => { if (file.size > maxSize) { setGlobalErrors((prev) => ({ ...prev, "global-error": transJsonEditorMaxSize, })); return; } if (!file.name.endsWith(".json")) { setGlobalErrors((prev) => ({ ...prev, "global-error": transJsonEditorMimes, })); return; } try { const text = await file.text(); const parsed = JSON.parse(text); // Validate json with zod. jsonArraySchema.parse(parsed); setJsonText(parsed); setGlobalErrors({}); setIsOpen(true); } catch { setGlobalErrors((prev) => ({ ...prev, "global-error": transJsonEditorInvalidFileJson, })); } }; /** * Reset component. * * @return void */ const resetComponent = (): void => { setIsOpen(false); setTimeout(() => { setJsonText([]); setGlobalErrors({}); setEditorErrors({}); setOverwrite(false); }, 200); }; /** * Action when changing file. * * @param event * @return void */ const handleFileChange = (e: ChangeEvent): void => { const file = e.target.files?.[0]; if (file) processFile(file); }; /** * Action when droping file. * * @param event * @return void */ const handleDrop = (event: DragEvent): void => { event.preventDefault(); setIsDragging(false); const file = event.dataTransfer.files?.[0]; if (file) processFile(file); }; /** * Confirm the json. * * @return void */ const handleConfirm = (): void => { try { // Validate json with zod. jsonArraySchema.parse(jsonText); onConfirm(jsonText, overwrite); resetComponent(); } catch { setEditorErrors((prev) => ({ ...prev, "editor-error": transInvalidContentJson, })); } }; return ( <> {/* Dropzone */} {/* Modal */} {useGetTranslation("back.json_editor.modal_title")} {useGetTranslation( "back.json_editor.modal_description", )}
{/* JSON editor */} setJsonText(val as JsonObject[])} icons={icons} translations={translations} showIconTooltips collapseAnimationTime={200} maxWidth="100%" searchFilter="all" />
setOverwrite(value)} />
); }