import { JsonEditor, type IconReplacements, type LocalisedStrings, } from "json-edit-react"; import { CheckIcon, ChevronDownIcon, CopyIcon, PencilIcon, PlusIcon, Trash2Icon, UploadIcon, XIcon, } from "lucide-react"; import { useRef, useState, type ChangeEvent, type DragEvent } from "react"; import z from "zod"; import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import InputError from "@/back/Components/InputError"; import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import str from "@/hooks/use-string"; import trans from "@/hooks/use-translations"; import { cn } from "@/lib/utils"; type JsonPrimitive = string | number | boolean | null; interface JsonObject { [key: string]: JsonPrimitive | JsonObject | JsonObject[]; } interface ImportConfigJsonProps { 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(trans(key)).ucFirst().value(); const transJsonEditorMaxSize = trans( "uwil-dynamic-pages::trans.json_editor.validation.max_size", { params: { value: "2MB" }, }, ); const transJsonEditorMimes = trans( "uwil-dynamic-pages::trans.json_editor.validation.mimes", { params: { values: ".json" }, }, ); const transJsonEditorInvalidFileJson = trans( "uwil-dynamic-pages::trans.json_editor.validation.invalid_file_json", ); const transInvalidContentJson = trans( "uwil-dynamic-pages::trans.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( "uwil-dynamic-pages::trans.json_editor.item_single", ), ITEMS_MULTIPLE: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.item_multiple", ), KEY_NEW: useGetTranslation("uwil-dynamic-pages::trans.json_editor.key_new"), KEY_SELECT: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.key_select", ), NO_KEY_OPTIONS: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.no_key_options", ), ERROR_KEY_EXISTS: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.error_key_exists", ), ERROR_INVALID_JSON: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.error_invalid_json", ), ERROR_UPDATE: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.error_update", ), ERROR_DELETE: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.error_delete", ), ERROR_ADD: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.error_add", ), DEFAULT_STRING: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.default_string", ), DEFAULT_NEW_KEY: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.default_new_key", ), SHOW_LESS: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.show_less", ), EMPTY_STRING: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.empty_string", ), TOOLTIP_COPY: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.copy", ), TOOLTIP_EDIT: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.edit", ), TOOLTIP_DELETE: useGetTranslation( "uwil-dynamic-pages::trans.json_editor.delete", ), TOOLTIP_ADD: useGetTranslation("uwil-dynamic-pages::trans.json_editor.add"), }; const hsl = (varName: string) => `hsl(var(${varName}))`; const jsonEditorTheme = { displayName: false, container: { backgroundColor: hsl("--background"), color: hsl("--foreground"), }, property: hsl("--primary"), bracket: hsl("--muted-foreground"), itemCount: hsl("--muted-foreground"), string: hsl("--foreground"), number: hsl("--informative"), boolean: hsl("--preventive"), null: hsl("--destructive"), input: [hsl("--foreground"), { backgroundColor: hsl("--input") }], error: hsl("--destructive-foreground"), iconCollection: hsl("--muted-foreground"), iconEdit: hsl("--muted-foreground"), iconDelete: hsl("--destructive"), iconAdd: hsl("--successive"), iconCopy: hsl("--muted-foreground"), iconOk: hsl("--successive"), iconCancel: hsl("--destructive"), }; /** * 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) as JsonObject[]; // 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 = (event: ChangeEvent): void => { const file = event.target.files?.[0]; if (file) void 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] as File | null; if (file) void 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( "uwil-dynamic-pages::trans.json_editor.modal_title", )} {useGetTranslation( "uwil-dynamic-pages::trans.json_editor.modal_description", )}
{/* JSON editor */} { setJsonText(val as JsonObject[]); }} icons={icons} translations={translations} showIconTooltips collapseAnimationTime={200} maxWidth="100%" searchFilter="all" theme={jsonEditorTheme} />
{ setOverwrite(value); }} />
); }