feat: modules and module-editor pages
This commit is contained in:
@ -0,0 +1,68 @@
|
||||
import { FC, useCallback, useMemo } from "react";
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { Center, Flex } from "@mantine/core";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||
import { AttributeSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
attribute: AttributeSchema;
|
||||
};
|
||||
|
||||
const AttributeTableActions: FC<Props> = ({ attribute }) => {
|
||||
const { attributeActions, module } = useModuleEditorContext();
|
||||
const usedAttributeIds = useMemo(
|
||||
() => new Set(module?.attributes.map(a => a.id)),
|
||||
[module]
|
||||
);
|
||||
|
||||
const toggleAttributeInModule = useCallback(
|
||||
(attribute: AttributeSchema) => {
|
||||
if (usedAttributeIds.has(attribute.id)) {
|
||||
attributeActions.removeAttributeFromModule(attribute);
|
||||
} else {
|
||||
attributeActions.addAttributeToModule(attribute);
|
||||
}
|
||||
},
|
||||
[usedAttributeIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Flex gap={"xs"}>
|
||||
<ActionIconWithTip
|
||||
onClick={() => attributeActions.onUpdate(attribute)}
|
||||
disabled={attribute.isBuiltIn}
|
||||
tipLabel={"Редактировать"}>
|
||||
<IconEdit />
|
||||
</ActionIconWithTip>
|
||||
<ActionIconWithTip
|
||||
onClick={() => attributeActions.onDelete(attribute)}
|
||||
disabled={attribute.isBuiltIn}
|
||||
tipLabel={"Удалить"}>
|
||||
<IconTrash />
|
||||
</ActionIconWithTip>
|
||||
<ActionIconWithTip
|
||||
onClick={() => toggleAttributeInModule(attribute)}
|
||||
tipLabel={
|
||||
usedAttributeIds.has(attribute.id)
|
||||
? "Удалить из модуля"
|
||||
: "Добавить в модуль"
|
||||
}>
|
||||
{usedAttributeIds.has(attribute.id) ? (
|
||||
<IconX />
|
||||
) : (
|
||||
<IconArrowRight />
|
||||
)}
|
||||
</ActionIconWithTip>
|
||||
</Flex>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeTableActions;
|
||||
@ -0,0 +1,21 @@
|
||||
import useAttributeTypesList from "@/app/module-editor/[moduleId]/hooks/useAttributeTypesList";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||
import { AttributeTypeSchema } from "@/lib/client";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<AttributeTypeSchema>, "data">;
|
||||
|
||||
const AttributeTypeSelect = (props: Props) => {
|
||||
const { attributeTypes } = useAttributeTypesList();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
label={"Тип атрибута"}
|
||||
data={attributeTypes}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeTypeSelect;
|
||||
@ -0,0 +1,34 @@
|
||||
import { Box, Center, Text } from "@mantine/core";
|
||||
import useAttributesTableColumns from "@/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||
|
||||
const AttributesTable = () => {
|
||||
const { attributes } = useModuleEditorContext();
|
||||
const columns = useAttributesTableColumns();
|
||||
|
||||
if (attributes.length === 0) {
|
||||
return (
|
||||
<Center my={"md"}>
|
||||
<Text>Нет атрибутов</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
h="100%"
|
||||
style={{ overflow: "auto" }}>
|
||||
<BaseTable
|
||||
withTableBorder
|
||||
columns={columns}
|
||||
records={attributes}
|
||||
verticalSpacing={"md"}
|
||||
groups={undefined}
|
||||
styles={{ table: { width: "100%" } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributesTable;
|
||||
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { DataTableColumn } from "mantine-datatable";
|
||||
import { Center } from "@mantine/core";
|
||||
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { AttributeSchema } from "@/lib/client";
|
||||
|
||||
const useAttributesTableColumns = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
title: "Название",
|
||||
accessor: "label",
|
||||
},
|
||||
{
|
||||
title: "Тип",
|
||||
accessor: "type.name",
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: <Center>Действия</Center>,
|
||||
width: "0%",
|
||||
render: attribute => (
|
||||
<AttributeTableActions attribute={attribute} />
|
||||
),
|
||||
},
|
||||
] as DataTableColumn<AttributeSchema>[],
|
||||
[isMobile]
|
||||
);
|
||||
};
|
||||
|
||||
export default useAttributesTableColumns;
|
||||
@ -0,0 +1,18 @@
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||
|
||||
const CreateAttributeButton = () => {
|
||||
const {
|
||||
attributeActions: { onCreate },
|
||||
} = useModuleEditorContext();
|
||||
|
||||
return (
|
||||
<InlineButton
|
||||
onClick={onCreate}
|
||||
w={"100%"}>
|
||||
Создать атрибут
|
||||
</InlineButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAttributeButton;
|
||||
@ -0,0 +1,90 @@
|
||||
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { UpdateAttributeSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<Partial<UpdateAttributeSchema>>;
|
||||
};
|
||||
|
||||
const DefaultAttributeValueInput = ({ form }: Props) => {
|
||||
const type = form.values.type?.type;
|
||||
const label = "Значение по умолчанию";
|
||||
const inputName = "defaultValue";
|
||||
|
||||
const value = form.getValues().defaultValue?.value;
|
||||
|
||||
if (type === "bool") {
|
||||
return (
|
||||
<Checkbox
|
||||
label={label}
|
||||
{...form.getInputProps(inputName, { type: "checkbox" })}
|
||||
checked={value as boolean}
|
||||
onChange={e =>
|
||||
form.setFieldValue("defaultValue", {
|
||||
value: e.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (type === "date") {
|
||||
return (
|
||||
<DatePickerInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={
|
||||
form.values.defaultValue?.value
|
||||
? new Date(String(form.values.defaultValue.value))
|
||||
: null
|
||||
}
|
||||
onChange={value =>
|
||||
form.setFieldValue("defaultValue", { value })
|
||||
}
|
||||
clearable
|
||||
locale={"ru"}
|
||||
valueFormat={"DD.MM.YYYY"}
|
||||
/>
|
||||
);
|
||||
} else if (type === "datetime") {
|
||||
return (
|
||||
<DateTimePicker
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={
|
||||
form.values.defaultValue?.value
|
||||
? new Date(String(form.values.defaultValue.value))
|
||||
: null
|
||||
}
|
||||
onChange={value =>
|
||||
form.setFieldValue("defaultValue", { value })
|
||||
}
|
||||
clearable
|
||||
locale={"ru"}
|
||||
valueFormat={"DD.MM.YYYY HH:mm"}
|
||||
/>
|
||||
);
|
||||
} else if (type === "str") {
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
/>
|
||||
);
|
||||
} else if (type === "int" || type === "float") {
|
||||
return (
|
||||
<NumberInput
|
||||
allowDecimal={type === "float"}
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={Number(form.values.defaultValue?.value)}
|
||||
onChange={value =>
|
||||
form.setFieldValue("defaultValue", { value: Number(value) })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default DefaultAttributeValueInput;
|
||||
@ -0,0 +1,68 @@
|
||||
import { FC } from "react";
|
||||
import { IconEdit, IconX } from "@tabler/icons-react";
|
||||
import { Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import styles from "@/app/module-editor/[moduleId]/ModuleEditor.module.css";
|
||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||
import { ModuleAttributeSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
attribute: ModuleAttributeSchema;
|
||||
};
|
||||
|
||||
const ModuleAttribute: FC<Props> = ({ attribute }) => {
|
||||
const {
|
||||
attributeActions: { removeAttributeFromModule, onEditAttributeLabel },
|
||||
} = useModuleEditorContext();
|
||||
|
||||
const getAttrLabelText = () => {
|
||||
const order = 6;
|
||||
|
||||
if (attribute.label === attribute.originalLabel) {
|
||||
return <Title order={order}>Название: {attribute.label}</Title>;
|
||||
}
|
||||
return (
|
||||
<Group gap={"xs"}>
|
||||
<Title order={order}>Название: {attribute.label}</Title>{" "}
|
||||
<Text>(Ориг. {attribute.originalLabel})</Text>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
flex={1}
|
||||
className={styles["module-attr-container"]}>
|
||||
<Group
|
||||
w={"100%"}
|
||||
h={"100%"}
|
||||
justify={"space-between"}
|
||||
align={"center"}>
|
||||
<Stack gap={7}>
|
||||
<>{getAttrLabelText()}</>
|
||||
<Text>Тип: {attribute.type.name}</Text>
|
||||
</Stack>
|
||||
<Group
|
||||
justify={"end"}
|
||||
wrap={"nowrap"}
|
||||
w={"100%"}
|
||||
gap={"xs"}>
|
||||
<ActionIconWithTip
|
||||
tipLabel={
|
||||
"Редактировать название (только для данного модуля)"
|
||||
}
|
||||
onClick={() => onEditAttributeLabel(attribute)}>
|
||||
<IconEdit />
|
||||
</ActionIconWithTip>
|
||||
<ActionIconWithTip
|
||||
tipLabel={"Удалить из модуля"}
|
||||
onClick={() => removeAttributeFromModule(attribute)}>
|
||||
<IconX />
|
||||
</ActionIconWithTip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleAttribute;
|
||||
@ -0,0 +1,41 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Flex } from "@mantine/core";
|
||||
import ModuleAttribute from "@/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
|
||||
|
||||
const ModuleAttributesEditor = () => {
|
||||
const { module } = useModuleEditorContext();
|
||||
|
||||
const getAttributesRows = () => {
|
||||
if (!module?.attributes) return [];
|
||||
|
||||
const rows: ReactNode[] = [];
|
||||
for (let i = 0; i < module.attributes.length; i += 2) {
|
||||
const rightIdx = i + 1;
|
||||
|
||||
rows.push(
|
||||
<FormFlexRow key={i}>
|
||||
<ModuleAttribute attribute={module.attributes[i]} />
|
||||
{rightIdx < module.attributes.length && (
|
||||
<ModuleAttribute
|
||||
attribute={module.attributes[rightIdx]}
|
||||
/>
|
||||
)}
|
||||
</FormFlexRow>
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={"xs"}
|
||||
direction={"column"}>
|
||||
{getAttributesRows()}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleAttributesEditor;
|
||||
@ -0,0 +1,52 @@
|
||||
import { Button, Flex, Group, Textarea, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import { ModuleInfo } from "../../../hooks/useModulesActions";
|
||||
|
||||
const ModuleCommonInfoEditor = () => {
|
||||
const {
|
||||
module,
|
||||
moduleActions: { updateCommonInfo },
|
||||
} = useModuleEditorContext();
|
||||
|
||||
const form = useForm<ModuleInfo>({
|
||||
initialValues: module ?? {
|
||||
label: "",
|
||||
description: "",
|
||||
},
|
||||
validate: {
|
||||
label: label => !label && "Введите название модуля",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: ModuleInfo) => {
|
||||
updateCommonInfo(values, form.resetDirty);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Flex
|
||||
gap={"xs"}
|
||||
direction={"column"}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("label")}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Описание"}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
variant={"default"}
|
||||
disabled={!form.isDirty()}
|
||||
type={"submit"}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleCommonInfoEditor;
|
||||
@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Fieldset, Flex, Stack } from "@mantine/core";
|
||||
import AttributesTable from "@/app/module-editor/[moduleId]/components/shared/AttributesTable/AttributesTable";
|
||||
import CreateAttributeButton from "@/app/module-editor/[moduleId]/components/shared/CreateAttributeButton/CreateAttributeButton";
|
||||
import ModuleAttributesEditor from "@/app/module-editor/[moduleId]/components/shared/ModuleAttributesEditor/ModuleAttributesEditor";
|
||||
import ModuleCommonInfoEditor from "@/app/module-editor/[moduleId]/components/shared/ModuleCommonInfoEditor/ModuleCommonInfoEditor";
|
||||
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||
|
||||
const PageBody = () => {
|
||||
const { module } = useModuleEditorContext();
|
||||
|
||||
return (
|
||||
<Stack h={"100%"}>
|
||||
<PageBlock
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
fullScreenMobile>
|
||||
<Flex
|
||||
w={"100%"}
|
||||
h={"100%"}
|
||||
flex={3}
|
||||
style={{ overflow: "auto" }}
|
||||
gap={"md"}>
|
||||
<Fieldset
|
||||
flex={1}
|
||||
legend={"Атрибуты"}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
h={"100%"}
|
||||
gap={"xs"}>
|
||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||
<AttributesTable />
|
||||
</Box>
|
||||
<CreateAttributeButton />
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
<Flex
|
||||
gap={"xs"}
|
||||
flex={2}
|
||||
direction={"column"}>
|
||||
<Fieldset
|
||||
flex={1}
|
||||
legend={"Общие данные модуля"}>
|
||||
{module && <ModuleCommonInfoEditor />}
|
||||
</Fieldset>
|
||||
<Fieldset
|
||||
flex={3}
|
||||
legend={"Атрибуты модуля"}>
|
||||
<ModuleAttributesEditor />
|
||||
</Fieldset>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageBlock>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBody;
|
||||
Reference in New Issue
Block a user