Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
+184
@@ -0,0 +1,184 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
|
||||
import JsonPropertyForm from "./JsonPropertyForm";
|
||||
import TrashIcon from "~/components/ui/icons/TrashIcon";
|
||||
import { JsonPropertyDto } from "~/modules/jsonProperties/dtos/JsonPropertyTypeDto";
|
||||
import OrderListButtons from "~/components/ui/sort/OrderListButtons";
|
||||
import JsonPropertyTypeIcon from "./JsonPropertyTypeIcon";
|
||||
import PencilIcon from "~/components/ui/icons/PencilIcon";
|
||||
import NewFieldIcon from "~/components/ui/icons/NewFieldIcon";
|
||||
import { useSubmit } from "react-router";
|
||||
import ConfirmModal, { RefConfirmModal } from "~/components/ui/modals/ConfirmModal";
|
||||
|
||||
interface Props {
|
||||
items: JsonPropertyDto[];
|
||||
}
|
||||
export default function JsonPropertiesTable({ items }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const submit = useSubmit();
|
||||
|
||||
const confirmDelete = useRef<RefConfirmModal>(null);
|
||||
|
||||
const [selectedProperty, setSelectedProperty] = useState<JsonPropertyDto | null | undefined>(undefined);
|
||||
|
||||
function onDelete(item: JsonPropertyDto) {
|
||||
confirmDelete.current?.setValue(item);
|
||||
confirmDelete.current?.show(t("shared.confirmDelete"), t("shared.delete"), t("shared.cancel"), t("shared.warningCannotUndo"));
|
||||
}
|
||||
|
||||
function onDeleteConfirmed(item: JsonPropertyDto) {
|
||||
const form = new FormData();
|
||||
form.set("action", "properties-delete");
|
||||
form.set("name", item.name);
|
||||
submit(form, {
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* <div className="flex justify-between space-x-2">
|
||||
<label className="text-xs font-medium">{t("models.jsonProperty.plural")}</label>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-secondary hover:bg-secondary hover:text-secondary-foreground hover:border-border text-muted-foreground rounded-md border border-transparent p-1 px-2 text-sm font-medium"
|
||||
onClick={() => setSelectedProperty(null)}
|
||||
>
|
||||
{t("shared.add")}
|
||||
</button>
|
||||
</div> */}
|
||||
{items.map((item, idx) => {
|
||||
return <input key={idx} type="hidden" name="properties[]" value={JSON.stringify(item)} readOnly hidden />;
|
||||
})}
|
||||
|
||||
<div className="space-y-1">
|
||||
{items.map((item, idx) => {
|
||||
return (
|
||||
<div key={item.name} className="border-border bg-background rounded-md border px-4 py-1 shadow-2xs">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="flex items-center space-x-2 truncate">
|
||||
<div className=" flex items-center space-x-3 truncate">
|
||||
<div className="hidden shrink-0 sm:flex">
|
||||
<OrderListButtons
|
||||
index={idx}
|
||||
items={items.map((f, idx) => ({ ...f, id: f.name, order: idx }))}
|
||||
editable={true}
|
||||
actionName="properties-sort"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<JsonPropertyTypeIcon type={item.type} className="text-muted-foreground h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-foreground flex items-center space-x-2 truncate text-sm">
|
||||
<div className="flex items-baseline space-x-1 truncate">
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{item.title}
|
||||
{item.required && <span className="text-red-500">*</span>}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{item.name}
|
||||
{["select", "multiselect"].includes(item.type) && "[]"}
|
||||
{["range-date", "range-number"].includes(item.type) && ` (${t("models.jsonProperty.badges.range")})`}
|
||||
</div>
|
||||
</div>
|
||||
{/* {item.type === PropertyType.FORMULA && <div className="truncate italic text-muted-foreground">({item.formula})</div>} */}
|
||||
{["select", "multiselect"].includes(item.type) && (
|
||||
<div className="text-muted-foreground truncate text-xs">
|
||||
{t("shared.options")}: [{!item.options || item.options.length === 0 ? "" : item.options?.map((f) => f.value).join(", ")}]
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 space-x-1">
|
||||
<div className="flex items-center space-x-1 truncate p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedProperty(item)}
|
||||
className="hover:bg-secondary/90 focus:bg-secondary/90 group flex items-center rounded-md border border-transparent p-2 focus:outline-hidden focus:ring-2 focus:ring-gray-400 focus:ring-offset-1"
|
||||
// onClick={() => update(idx, item)}
|
||||
>
|
||||
<PencilIcon className="group-hover:text-muted-foreground h-4 w-4 text-gray-300" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-secondary/90 focus:bg-secondary/90 group flex items-center rounded-md border border-transparent p-2 focus:outline-hidden focus:ring-2 focus:ring-gray-400 focus:ring-offset-1"
|
||||
onClick={() => onDelete(item)}
|
||||
>
|
||||
<TrashIcon className="group-hover:text-muted-foreground h-4 w-4 text-gray-300" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedProperty(null)}
|
||||
className="focus:ring-ring border-border hover:border-border relative block w-full rounded-lg border-2 border-dashed px-12 py-3 text-center focus:outline-hidden focus:ring-2 focus:ring-offset-2"
|
||||
>
|
||||
<NewFieldIcon className="text-muted-foreground mx-auto h-4" />
|
||||
<span className="mt-1 block text-xs font-medium">{t("models.jsonProperty.actions.add")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <TableSimple
|
||||
items={items}
|
||||
emptyState={{ title: t("shared.noCustomProperties"), description: "" }}
|
||||
actions={[
|
||||
{
|
||||
title: t("shared.edit"),
|
||||
onClick: (_, item) => setSelectedProperty(item),
|
||||
},
|
||||
{
|
||||
title: <TrashIcon className="text-muted-foreground group-hover:text-foreground h-4 w-4" />,
|
||||
onClick: (_, item) => onChange(items.filter((i) => i !== item)),
|
||||
},
|
||||
]}
|
||||
headers={[
|
||||
{
|
||||
name: "type",
|
||||
title: t("models.jsonProperty.fields.type"),
|
||||
value: (item) => <JsonPropertyTypeIcon type={item.type} className="h-4 w-4 text-muted-foreground" />,
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
title: t("models.jsonProperty.fields.name"),
|
||||
className: "w-full",
|
||||
value: (item) => (
|
||||
<div className="flex flex-col">
|
||||
<div className="font-medium">
|
||||
{item.title} <span className="text-muted-foreground text-xs">({item.name})</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "required",
|
||||
title: t("shared.required"),
|
||||
value: (item) => (item.required ? <CheckIcon className="text-muted-foreground h-4 w-4" /> : <XIcon className="text-muted-foreground h-4 w-4" />),
|
||||
},
|
||||
]}
|
||||
/> */}
|
||||
|
||||
<SlideOverWideEmpty
|
||||
title={selectedProperty === null ? t("shared.new") : t("shared.edit")}
|
||||
open={selectedProperty !== undefined}
|
||||
onClose={() => setSelectedProperty(undefined)}
|
||||
size="md"
|
||||
>
|
||||
{selectedProperty !== undefined && <JsonPropertyForm item={selectedProperty} onClose={() => setSelectedProperty(undefined)} />}
|
||||
</SlideOverWideEmpty>
|
||||
|
||||
<ConfirmModal ref={confirmDelete} destructive onYes={onDeleteConfirmed} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import InputText, { RefInputText } from "~/components/ui/input/InputText";
|
||||
import StringUtils from "~/utils/shared/StringUtils";
|
||||
import TableSimple from "~/components/ui/tables/TableSimple";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
|
||||
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
|
||||
import InputSelect from "~/components/ui/input/InputSelect";
|
||||
import { Form, useFetcher } from "react-router";
|
||||
import { JsonPropertyDto, JsonPropertyType, JsonPropertyTypes } from "~/modules/jsonProperties/dtos/JsonPropertyTypeDto";
|
||||
import InputCheckbox from "~/components/ui/input/InputCheckbox";
|
||||
// import {toast} from "sonner";
|
||||
import toast from "react-hot-toast";
|
||||
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import TrashIcon from "~/components/ui/icons/TrashIcon";
|
||||
import PencilIcon from "~/components/ui/icons/PencilIcon";
|
||||
import ConfirmModal, { RefConfirmModal } from "~/components/ui/modals/ConfirmModal";
|
||||
|
||||
interface Props {
|
||||
item: JsonPropertyDto | null;
|
||||
// onUpdate?: (item: JsonPropertyDto) => void;
|
||||
// onAdd?: (item: JsonPropertyDto) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
export default function JsonPropertyForm({ item, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const fetcher = useFetcher<{ error?: string; success?: string }>();
|
||||
|
||||
const confirmDelete = useRef<RefConfirmModal>(null);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<{ value: string; name: string } | null>();
|
||||
|
||||
const [name, setName] = useState<string>(item ? item.name : "");
|
||||
const [title, setTitle] = useState<string>(item ? item.title : "");
|
||||
const [type, setType] = useState<JsonPropertyType>(item ? (item.type as any) : "string");
|
||||
const [options, setOptions] = useState<{ value: string; name: string }[]>(item?.options || []);
|
||||
const [isRequired, setIsRequired] = useState<boolean>(item ? item.required : false);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.success) {
|
||||
toast.success(fetcher.data.success);
|
||||
onClose();
|
||||
} else if (fetcher.data?.error) {
|
||||
toast.error(fetcher.data.error);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetcher.data]);
|
||||
|
||||
const mainInput = useRef<RefInputText>(null);
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
mainInput.current?.input.current?.focus();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) {
|
||||
if (title.includes(".")) {
|
||||
const keys = title.split(".");
|
||||
setName(StringUtils.toCamelCase(keys[keys.length - 1].toLowerCase()));
|
||||
} else {
|
||||
setName(StringUtils.toCamelCase(title.toLowerCase()));
|
||||
}
|
||||
}
|
||||
}, [item, title, type]);
|
||||
|
||||
function onAccept() {
|
||||
if (item === null) {
|
||||
const form = new FormData();
|
||||
form.append("action", "properties-add");
|
||||
form.set(
|
||||
"property",
|
||||
JSON.stringify({
|
||||
name,
|
||||
title,
|
||||
type,
|
||||
required: isRequired,
|
||||
options: type === "select" || type === "multiselect" ? options : null,
|
||||
})
|
||||
);
|
||||
fetcher.submit(form, {
|
||||
method: "post",
|
||||
});
|
||||
// if (onAdd) {
|
||||
// onAdd({
|
||||
// name,
|
||||
// title,
|
||||
// type,
|
||||
// required: isRequired,
|
||||
// options: type === "select" || type === "multiselect" ? options : null,
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
const form = new FormData();
|
||||
form.append("action", "properties-edit");
|
||||
form.set("name", item.name);
|
||||
form.set(
|
||||
"property",
|
||||
JSON.stringify({
|
||||
...item,
|
||||
name,
|
||||
title,
|
||||
type,
|
||||
options: type === "select" || type === "multiselect" ? options : null,
|
||||
required: isRequired,
|
||||
})
|
||||
);
|
||||
fetcher.submit(form, {
|
||||
method: "post",
|
||||
});
|
||||
|
||||
// if (onUpdate) {
|
||||
// onUpdate({
|
||||
// ...item,
|
||||
// name,
|
||||
// title,
|
||||
// type,
|
||||
// options: type === "select" || type === "multiselect" ? options : null,
|
||||
// required: isRequired,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
onAccept();
|
||||
}
|
||||
|
||||
function onDelete(item: JsonPropertyDto) {
|
||||
confirmDelete.current?.setValue(item);
|
||||
confirmDelete.current?.show(t("shared.confirmDelete"), t("shared.delete"), t("shared.cancel"), t("shared.warningCannotUndo"));
|
||||
}
|
||||
|
||||
function onDeleteConfirmed(item: JsonPropertyDto) {
|
||||
const form = new FormData();
|
||||
form.set("action", "properties-delete");
|
||||
form.set("name", item.name);
|
||||
fetcher.submit(form, {
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<div className="mb-4 space-y-2">
|
||||
<div>
|
||||
<InputText ref={mainInput} name="title" title={t("models.jsonProperty.title")} value={title} setValue={setTitle} required />
|
||||
</div>
|
||||
<div>
|
||||
<InputText name="name" title={t("models.jsonProperty.name")} value={name} setValue={setName} required />
|
||||
</div>
|
||||
<div>
|
||||
<InputSelect
|
||||
name="type"
|
||||
title={t("models.jsonProperty.type")}
|
||||
value={type}
|
||||
setValue={(e) => setType(e as JsonPropertyType)}
|
||||
options={JsonPropertyTypes.map((property) => ({
|
||||
name: t(`models.jsonProperty.types.${property.value}`),
|
||||
value: property.value,
|
||||
}))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{(type === "select" || type === "multiselect") && (
|
||||
<div className="space-y-1">
|
||||
{/* Caption for an options table, not a single form control. */}
|
||||
<div className="text-xs font-medium">{t("models.jsonProperty.options")}</div>
|
||||
<div>
|
||||
<TableSimple
|
||||
items={options}
|
||||
actions={[
|
||||
{
|
||||
title: t("shared.edit"),
|
||||
onClick: (_, i) => setSelectedOption(i),
|
||||
renderTitle: (_) => <PencilIcon className="group-hover:text-muted-foreground h-4 w-4 text-gray-300" />,
|
||||
},
|
||||
{
|
||||
title: t("shared.remove"),
|
||||
onClick: (_, i) => setOptions((prev) => prev.filter((o) => o.value !== i.value)),
|
||||
renderTitle: (_) => <TrashIcon className="text-muted-foreground group-hover:text-foreground h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
headers={[
|
||||
{
|
||||
name: "name",
|
||||
title: t("shared.name"),
|
||||
value: (item) => item.name,
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
title: t("shared.value"),
|
||||
className: "w-full",
|
||||
value: (item) => item.value,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Button type="button" variant="outline" onClick={() => setSelectedOption(null)}>
|
||||
{t("shared.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<InputCheckbox name="required" title={t("models.jsonProperty.required")} value={isRequired} setValue={setIsRequired} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border mt-3 flex justify-between border-t pt-3">
|
||||
<div>
|
||||
{item && (
|
||||
<ButtonSecondary destructive onClick={() => onDelete(item)}>
|
||||
{t("shared.delete")}
|
||||
</ButtonSecondary>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<ButtonSecondary onClick={onClose}>{t("shared.back")}</ButtonSecondary>
|
||||
<ButtonPrimary type="submit">{t("shared.accept")}</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<SlideOverWideEmpty
|
||||
title={selectedOption === null ? t("shared.new") : selectedOption ? t("shared.edit") : ""}
|
||||
open={selectedOption !== undefined}
|
||||
onClose={() => setSelectedOption(undefined)}
|
||||
size="sm"
|
||||
>
|
||||
{selectedOption !== undefined && (
|
||||
<PropertyOptionForm
|
||||
item={selectedOption}
|
||||
onClose={() => setSelectedOption(undefined)}
|
||||
onUpdate={(option) => {
|
||||
if (selectedOption) {
|
||||
// onChange(items.map((item) => (item === selectedProperty ? property : item)));
|
||||
setOptions(options.map((o) => (o === selectedOption ? option : o)));
|
||||
} else {
|
||||
setOptions([...options, option]);
|
||||
}
|
||||
setSelectedOption(undefined);
|
||||
}}
|
||||
onAdd={(option) => {
|
||||
setOptions([...options, option]);
|
||||
setSelectedOption(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SlideOverWideEmpty>
|
||||
|
||||
<ConfirmModal ref={confirmDelete} destructive onYes={onDeleteConfirmed} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PropertyOptionForm({
|
||||
item,
|
||||
onUpdate,
|
||||
onAdd,
|
||||
onClose,
|
||||
}: {
|
||||
item: { name: string; value: string } | null;
|
||||
onUpdate: (option: { name: string; value: string }) => void;
|
||||
onAdd: (option: { name: string; value: string }) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [name, setName] = useState<string>(item ? item.name : "");
|
||||
const [value, setValue] = useState<string>(item ? item.value : "");
|
||||
|
||||
const mainInput = useRef<RefInputText>(null);
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
mainInput.current?.input.current?.focus();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) {
|
||||
if (name.includes(".")) {
|
||||
const keys = name.split(".");
|
||||
setValue(StringUtils.toCamelCase(keys[keys.length - 1].toLowerCase()));
|
||||
} else {
|
||||
setValue(StringUtils.toCamelCase(name.toLowerCase()));
|
||||
}
|
||||
}
|
||||
}, [item, name]);
|
||||
|
||||
function onAccept() {
|
||||
if (item === null) {
|
||||
onAdd({
|
||||
name,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
onUpdate({
|
||||
...item,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
onAccept();
|
||||
}
|
||||
return (
|
||||
<Form onSubmit={onSubmit} className="space-y-2">
|
||||
<div>
|
||||
<InputText ref={mainInput} name="name" title={t("shared.name")} value={name} setValue={setName} required />
|
||||
</div>
|
||||
<div>
|
||||
<InputText name="value" title={t("shared.value")} value={value} setValue={setValue} required />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<ButtonSecondary onClick={onClose}>{t("shared.back")}</ButtonSecondary>
|
||||
</div>
|
||||
<ButtonPrimary type="submit">{t("shared.accept")}</ButtonPrimary>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { PropertyType } from "~/application/enums/entities/PropertyType";
|
||||
import PropertyBadge from "~/components/entities/properties/PropertyBadge";
|
||||
import { JsonPropertyType } from "../dtos/JsonPropertyTypeDto";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileTextIcon } from "~/components/ui/icons/vendor";
|
||||
|
||||
export default function JsonPropertyTypeIcon({ type, className }: { type: JsonPropertyType; className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
switch (type) {
|
||||
case "string":
|
||||
return <PropertyBadge type={PropertyType.TEXT} className={className} />;
|
||||
case "number":
|
||||
return <PropertyBadge type={PropertyType.NUMBER} className={className} />;
|
||||
case "boolean":
|
||||
return <PropertyBadge type={PropertyType.BOOLEAN} className={className} />;
|
||||
case "date":
|
||||
return <PropertyBadge type={PropertyType.DATE} className={className} />;
|
||||
case "image":
|
||||
return <PropertyBadge type={PropertyType.MEDIA} className={className} />;
|
||||
case "select":
|
||||
return <PropertyBadge type={PropertyType.SELECT} className={className} />;
|
||||
case "multiselect":
|
||||
return <PropertyBadge type={PropertyType.MULTI_SELECT} className={className} />;
|
||||
case "wysiwyg":
|
||||
case "monaco":
|
||||
case "content":
|
||||
return <FileTextIcon className={className} />;
|
||||
}
|
||||
return <div className="font-medium">{t("models.jsonProperty.types." + type)}</div>;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import DateCell from "~/components/ui/dates/DateCell";
|
||||
import { JsonValue } from "../dtos/JsonPropertiesValuesDto";
|
||||
import { JsonPropertyDto } from "../dtos/JsonPropertyTypeDto";
|
||||
import CheckIcon from "~/components/ui/icons/CheckIcon";
|
||||
import XIcon from "~/components/ui/icons/XIcon";
|
||||
|
||||
export default function JsonPropertyValueCell({ property, value }: { property: JsonPropertyDto; value: JsonValue | undefined }) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
switch (property.type) {
|
||||
case "string":
|
||||
return value as string;
|
||||
case "number":
|
||||
return value as number;
|
||||
case "boolean":
|
||||
const booleanValue = value === undefined ? undefined : value === "true" || value === true || value === 1 || value === "1";
|
||||
if (value === undefined || value === null) {
|
||||
return null;
|
||||
}
|
||||
if (booleanValue) {
|
||||
return <CheckIcon className="h-4 w-4" />;
|
||||
} else {
|
||||
return <XIcon className="h-4 w-4" />;
|
||||
}
|
||||
case "image":
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return <img src={value as string} alt="" className="h-8 w-8 object-cover" />;
|
||||
case "date":
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const date = new Date(value as string);
|
||||
return <div>{<DateCell date={date} />}</div>;
|
||||
} catch (e) {
|
||||
return <div className="text-muted-foreground">{value?.toString()}</div>;
|
||||
}
|
||||
case "select":
|
||||
if (!property.options) {
|
||||
return value as string;
|
||||
} else {
|
||||
const option = property.options.find((o) => o.value === value);
|
||||
return option?.name || (value as string);
|
||||
}
|
||||
case "multiselect":
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
if (!property.options) {
|
||||
return <div className="text-muted-foreground">{value.join(", ")}</div>;
|
||||
} else {
|
||||
const values = value.map((v) => {
|
||||
const option = property.options?.find((o) => o.value === v);
|
||||
return option?.name || v;
|
||||
});
|
||||
return values.join(", ");
|
||||
}
|
||||
default:
|
||||
return value as string;
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
import InputText from "~/components/ui/input/InputText";
|
||||
import { JsonPropertyDto } from "../dtos/JsonPropertyTypeDto";
|
||||
import { JsonPropertiesValuesDto, JsonValue } from "../dtos/JsonPropertiesValuesDto";
|
||||
import InputNumber from "~/components/ui/input/InputNumber";
|
||||
import InputCheckbox from "~/components/ui/input/InputCheckbox";
|
||||
import InputDate from "~/components/ui/input/InputDate";
|
||||
import InputImage from "~/components/ui/input/InputImage";
|
||||
import InputSelect from "~/components/ui/input/InputSelect";
|
||||
import InputCombobox from "~/components/ui/input/InputCombobox";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Fragment, lazy, Suspense, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import InputGroup from "~/components/ui/forms/InputGroup";
|
||||
|
||||
const NovelEditor = lazy(() => import("~/modules/novel/ui/editor"));
|
||||
|
||||
export default function JsonPropertyValuesInput({
|
||||
prefix = "attributes",
|
||||
properties,
|
||||
attributes,
|
||||
}: {
|
||||
prefix?: string;
|
||||
properties: JsonPropertyDto[] | JsonValue | null;
|
||||
attributes: JsonPropertiesValuesDto | null;
|
||||
}) {
|
||||
let propertiesObj = properties as JsonPropertyDto[] | null;
|
||||
if (!propertiesObj) {
|
||||
return null;
|
||||
}
|
||||
const groups: { name: string; properties: JsonPropertyDto[] }[] = [];
|
||||
propertiesObj.forEach((property) => {
|
||||
let group = groups.find((g) => g.name === property.group);
|
||||
if (!group) {
|
||||
group = { name: property.group || "", properties: [] };
|
||||
groups.push(group);
|
||||
}
|
||||
group.properties.push(property);
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
{groups.map((group, idx) => {
|
||||
return (
|
||||
<Fragment key={idx}>
|
||||
{group.name ? (
|
||||
<InputGroup title={group.name} className="space-y-2">
|
||||
<GroupInputs prefix={prefix} properties={group.properties} attributes={attributes} />
|
||||
</InputGroup>
|
||||
) : (
|
||||
<GroupInputs prefix={prefix} properties={group.properties} attributes={attributes} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupInputs({ prefix, properties, attributes }: { prefix: string; properties: JsonPropertyDto[]; attributes: JsonPropertiesValuesDto | null }) {
|
||||
let propertiesObj = properties as JsonPropertyDto[] | null;
|
||||
return (
|
||||
<Fragment>
|
||||
{propertiesObj?.map((property) => {
|
||||
return (
|
||||
<div key={property.name}>
|
||||
<JsonPropertyInput prefix={prefix} property={property} attributes={attributes} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function JsonPropertyInput({ prefix, property, attributes }: { prefix: string; property: JsonPropertyDto; attributes: JsonPropertiesValuesDto | null }) {
|
||||
const { t } = useTranslation();
|
||||
let value: JsonValue | undefined = attributes ? attributes[property.name] : undefined;
|
||||
|
||||
switch (property.type) {
|
||||
case "string":
|
||||
let stringValue = value === undefined ? undefined : (value as string);
|
||||
const defaultStringValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (stringValue === undefined && defaultStringValue !== undefined) {
|
||||
stringValue = defaultStringValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputText name={`${prefix}[${property.name}]`} title={t(property.title)} defaultValue={stringValue} required={property.required} />
|
||||
</div>
|
||||
);
|
||||
case "number": {
|
||||
let numberValue = value === undefined ? undefined : (value as number);
|
||||
const defaultNumberValue = property.defaultValue === undefined ? undefined : (property.defaultValue as number);
|
||||
if (numberValue === undefined && defaultNumberValue !== undefined) {
|
||||
numberValue = defaultNumberValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputNumber name={`${prefix}[${property.name}]`} title={t(property.title)} defaultValue={numberValue} required={property.required} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "boolean": {
|
||||
let booleanValue = value === undefined ? undefined : value === "true" || value === true || value === 1 || value === "1";
|
||||
const defaultBooleanValue =
|
||||
property.defaultValue === undefined
|
||||
? undefined
|
||||
: property.defaultValue === "true" || property.defaultValue === true || property.defaultValue === 1 || property.defaultValue === "1";
|
||||
if (booleanValue === undefined && defaultBooleanValue !== undefined) {
|
||||
booleanValue = defaultBooleanValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputCheckbox name={`${prefix}[${property.name}]`} title={t(property.title)} value={booleanValue} required={property.required} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "date": {
|
||||
let dateValue = !value ? undefined : new Date(value as string);
|
||||
const defaultDateValue = property.defaultValue === undefined ? undefined : new Date(property.defaultValue as string);
|
||||
if (dateValue === undefined && defaultDateValue !== undefined) {
|
||||
dateValue = defaultDateValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputDate name={`${prefix}[${property.name}]`} title={t(property.title)} defaultValue={dateValue} required={property.required} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "image": {
|
||||
let imageValue = value === undefined ? undefined : (value as string);
|
||||
const defaultImageValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (imageValue === undefined && defaultImageValue !== undefined) {
|
||||
imageValue = defaultImageValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputImage name={`${prefix}[${property.name}]`} title={t(property.title)} defaultValue={imageValue} required={property.required} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "select": {
|
||||
let stringValue = value === undefined ? undefined : (value as string);
|
||||
const defaultStringValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (stringValue === undefined && defaultStringValue !== undefined) {
|
||||
stringValue = defaultStringValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputSelect
|
||||
name={`${prefix}[${property.name}]`}
|
||||
title={t(property.title)}
|
||||
defaultValue={stringValue}
|
||||
required={property.required}
|
||||
options={property.options?.filter((f) => f.value) || []}
|
||||
placeholder={`${t("shared.select")}...`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "multiselect": {
|
||||
let arrValue = value === undefined ? [] : (value as Array<string>);
|
||||
const defaultArrValue = property.defaultValue === undefined ? [] : (property.defaultValue as Array<string>);
|
||||
if (!value && defaultArrValue.length > 0) {
|
||||
arrValue = defaultArrValue;
|
||||
}
|
||||
return <JsonMultiSelectInput prefix={prefix} property={property} initial={arrValue} />;
|
||||
}
|
||||
case "wysiwyg": {
|
||||
let stringValue = value === undefined ? undefined : (value as string);
|
||||
const defaultStringValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (stringValue === undefined && defaultStringValue !== undefined) {
|
||||
stringValue = defaultStringValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputText
|
||||
name={`${prefix}[${property.name}]`}
|
||||
title={t(property.title)}
|
||||
defaultValue={stringValue}
|
||||
required={property.required}
|
||||
editor="wysiwyg"
|
||||
editorSize="md"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "monaco": {
|
||||
let stringValue = value === undefined ? undefined : (value as string);
|
||||
const defaultStringValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (stringValue === undefined && defaultStringValue !== undefined) {
|
||||
stringValue = defaultStringValue;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<InputText
|
||||
name={`${prefix}[${property.name}]`}
|
||||
title={t(property.title)}
|
||||
defaultValue={stringValue}
|
||||
required={property.required}
|
||||
editor="monaco"
|
||||
editorLanguage="markdown"
|
||||
editorSize="md"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "content": {
|
||||
let stringValue = value === undefined ? undefined : (value as string);
|
||||
const defaultStringValue = property.defaultValue === undefined ? undefined : (property.defaultValue as string);
|
||||
if (stringValue === undefined && defaultStringValue !== undefined) {
|
||||
stringValue = defaultStringValue;
|
||||
}
|
||||
return <ContentForm prefix={prefix} property={property} value={stringValue} />;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function JsonMultiSelectInput({ prefix, property, initial }: { prefix: string; property: JsonPropertyDto; initial: string[] }) {
|
||||
const { t } = useTranslation();
|
||||
const [actualValue, setActualValue] = useState<(string | number)[]>(initial);
|
||||
return (
|
||||
<div>
|
||||
{actualValue?.map((item, idx) => {
|
||||
return <input key={idx} type="hidden" name={`${prefix}[${property.name}][]`} value={item} />;
|
||||
})}
|
||||
<InputCombobox
|
||||
title={t(property.title)}
|
||||
value={actualValue}
|
||||
onChange={setActualValue}
|
||||
required={property.required}
|
||||
options={property.options?.filter((f) => f.value) || []}
|
||||
withSearch={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentForm({ prefix, property, value }: { prefix: string; property: JsonPropertyDto; value: string | undefined }) {
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = useState(value);
|
||||
const [contentType, setContentType] = useState("wysiwyg");
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between space-x-2">
|
||||
{/* <label className="text-sm font-medium text-muted-foreground">Article</label> */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<button type="button" onClick={() => setContentType("wysiwyg")} className="text-muted-foreground text-xs hover:underline">
|
||||
<div className={clsx(contentType === "wysiwyg" ? "font-bold" : "")}>{t("components.editor.modes.wysiwyg")}</div>
|
||||
</button>
|
||||
<div>•</div>
|
||||
<button type="button" onClick={() => setContentType("markdown")} className="text-muted-foreground text-xs hover:underline">
|
||||
<div className={clsx(contentType === "markdown" ? "font-bold" : "")}>{t("components.editor.modes.markdown")}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input name="contentType" value={contentType} readOnly hidden />
|
||||
{contentType === "wysiwyg" ? (
|
||||
<div className="h-[calc(100vh-320px)] overflow-y-auto">
|
||||
<input type="hidden" name="content" value={content} hidden readOnly />
|
||||
<Suspense fallback={null}>
|
||||
<NovelEditor content={content} onChange={(e) => setContent(e.html ?? "")} usingLocalStorage={false} />
|
||||
</Suspense>
|
||||
</div>
|
||||
) : contentType === "markdown" ? (
|
||||
<InputText
|
||||
className="col-span-12 h-[calc(100vh-320px)] overflow-y-auto"
|
||||
rows={6}
|
||||
editor="monaco"
|
||||
editorLanguage="markdown"
|
||||
editorTheme="vs-dark"
|
||||
editorFontSize={14}
|
||||
name="content"
|
||||
value={content}
|
||||
setValue={(e) => setContent(e.toString())}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const sourcePath = path.resolve(process.cwd(), "apps/web/app/modules/jsonProperties/components/JsonPropertiesTable.tsx");
|
||||
|
||||
describe("json properties table i18n", () => {
|
||||
it("uses semantic translation keys for property badges", () => {
|
||||
const source = fs.readFileSync(sourcePath, "utf8");
|
||||
|
||||
expect(source).toContain("models.jsonProperty.badges.range");
|
||||
expect(source).toContain("models.jsonProperty.fields.type");
|
||||
expect(source).toContain("models.jsonProperty.fields.name");
|
||||
expect(source).not.toContain('" (range)"');
|
||||
expect(source).not.toContain('title: "Type"');
|
||||
expect(source).not.toContain('title: "Name"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user