- Updated translation utility to improve consistency and reduce redundancy. - Removed deprecated useTrans hook and related code. - Introduced a new Zustand store for managing translations and locale settings. - Cleaned up unused hooks and files related to localization.
438 lines
12 KiB
TypeScript
438 lines
12 KiB
TypeScript
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<JsonObject> = 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<JsonObject[]>([]);
|
|
|
|
// Modale state.
|
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
// Event state.
|
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
|
// Overwrite state.
|
|
const [overwrite, setOverwrite] = useState<boolean>(false);
|
|
|
|
// Local errors.
|
|
const [globalErrors, setGlobalErrors] = useState<Record<string, string>>({});
|
|
const [editorErrors, setEditorErrors] = useState<Record<string, string>>({});
|
|
|
|
// Input file reference.
|
|
const inputRef = useRef<HTMLInputElement>(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: <PlusIcon className="w-4 h-4" />,
|
|
edit: <PencilIcon className="w-4 h-4" />,
|
|
delete: <Trash2Icon className="w-4 h-4" />,
|
|
copy: <CopyIcon className="w-4 h-4" />,
|
|
ok: <CheckIcon className="w-4 h-4" />,
|
|
cancel: <XIcon className="w-4 h-4" />,
|
|
chevron: <ChevronDownIcon className="cursor-pointer w-4 h-4" />,
|
|
};
|
|
|
|
// 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<void>
|
|
*/
|
|
const processFile = async (file: File): Promise<void> => {
|
|
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<HTMLInputElement>): void => {
|
|
const file = event.target.files?.[0];
|
|
if (file) void processFile(file);
|
|
};
|
|
|
|
/**
|
|
* Action when droping file.
|
|
*
|
|
* @param event
|
|
* @return void
|
|
*/
|
|
const handleDrop = (event: DragEvent<HTMLDivElement>): 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 */}
|
|
<Button
|
|
variant="ghost"
|
|
type="button"
|
|
className={cn(
|
|
"flex border border-dashed overflow-hidden whitespace-normal [&_svg]:!pointer-events-auto [&_svg]:!size-auto [&_svg]:!shrink text-muted-foreground w-full h-36",
|
|
{ "text-accent-foreground bg-accent": isDragging },
|
|
{
|
|
"border-destructive mb-1.5": Object.values(globalErrors).length,
|
|
},
|
|
)}
|
|
onClick={openFilePicker}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
}}
|
|
onDragLeave={() => {
|
|
setIsDragging(false);
|
|
}}
|
|
// @ts-expect-error function return are not compatible for ts
|
|
onDrop={handleDrop}
|
|
>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept=".json,application/json"
|
|
className="hidden"
|
|
onChange={handleFileChange}
|
|
/>
|
|
<div className="flex flex-col justify-center items-center">
|
|
<UploadIcon />
|
|
<p className="text-center break-normal w-full mt-2">
|
|
{trans("uwil-dynamic-pages::trans.json_editor.select-file")}
|
|
</p>
|
|
<small>
|
|
{str(
|
|
trans("uwil-dynamic-pages::trans.json_editor.allowed_format", {
|
|
params: { format: ".json" },
|
|
}),
|
|
)
|
|
.ucFirst()
|
|
.value()}
|
|
.{" "}
|
|
{str(
|
|
trans("uwil-dynamic-pages::trans.json_editor.rules_weight", {
|
|
params: { size: "2MB" },
|
|
}),
|
|
)
|
|
.ucFirst()
|
|
.value()}
|
|
</small>
|
|
</div>
|
|
</Button>
|
|
<InputError message={globalErrors["global-error"]} />
|
|
{/* Modal */}
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogContent className="max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.modal_title",
|
|
)}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.modal_description",
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="rounded-md w-full max-h-[350px] overflow-scroll border">
|
|
{/* JSON editor */}
|
|
<JsonEditor
|
|
data={jsonText}
|
|
setData={(val) => {
|
|
setJsonText(val as JsonObject[]);
|
|
}}
|
|
icons={icons}
|
|
translations={translations}
|
|
showIconTooltips
|
|
collapseAnimationTime={200}
|
|
maxWidth="100%"
|
|
searchFilter="all"
|
|
theme={jsonEditorTheme}
|
|
/>
|
|
</div>
|
|
<InputError message={editorErrors["editor-error"]} />
|
|
<DialogFooter className="sm:justify-between">
|
|
<SimpleSwitch
|
|
label={useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.overwrite",
|
|
)}
|
|
name="overwrite"
|
|
checked={overwrite}
|
|
setData={(_, value) => {
|
|
setOverwrite(value);
|
|
}}
|
|
/>
|
|
<div className="flex justify-center items-center gap-1.5">
|
|
<BtnOptionnalLinkWithTooltip
|
|
content={useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.cancel",
|
|
)}
|
|
tooltipContent={useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.tooltip_cancel",
|
|
)}
|
|
type="button"
|
|
variant="secondary"
|
|
size="default"
|
|
onClick={resetComponent}
|
|
/>
|
|
<BtnOptionnalLinkWithTooltip
|
|
content={useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.validate",
|
|
)}
|
|
tooltipContent={useGetTranslation(
|
|
"uwil-dynamic-pages::trans.json_editor.tooltip_validate",
|
|
)}
|
|
type="button"
|
|
size="default"
|
|
onClick={handleConfirm}
|
|
/>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|