feat: modules and module-editor pages
This commit is contained in:
12
src/app/module-editor/[moduleId]/ModuleEditor.module.css
Normal file
12
src/app/module-editor/[moduleId]/ModuleEditor.module.css
Normal file
@ -0,0 +1,12 @@
|
||||
.container {
|
||||
padding: var(--mantine-spacing-sm);
|
||||
border: dashed 1px var(--mantine-color-default-border);
|
||||
border-radius: var(--mantine-radius-lg);
|
||||
}
|
||||
|
||||
.module-attr-container {
|
||||
@mixin light {
|
||||
background-color: var(--color-light-gray-blue);
|
||||
border: 1px dashed var(--color-light-aqua);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import useAttributesActions, {
|
||||
AttributesActions,
|
||||
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
|
||||
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
|
||||
import useModulesActions, {
|
||||
ModulesActions,
|
||||
} from "@/app/module-editor/[moduleId]/hooks/useModulesActions";
|
||||
import { AttributeSchema, ModuleWithAttributesSchema } from "@/lib/client";
|
||||
import { getModuleWithAttributesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||
|
||||
type ModuleEditorContextState = {
|
||||
module?: ModuleWithAttributesSchema;
|
||||
refetchModule: () => void;
|
||||
attributes: AttributeSchema[];
|
||||
refetchAttributes: () => void;
|
||||
attributeActions: AttributesActions;
|
||||
moduleActions: ModulesActions;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
moduleId: number;
|
||||
};
|
||||
|
||||
const useModuleEditorContextState = ({
|
||||
moduleId,
|
||||
}: Props): ModuleEditorContextState => {
|
||||
const { data, refetch: refetchModule } = useQuery(
|
||||
getModuleWithAttributesOptions({ path: { pk: moduleId } })
|
||||
);
|
||||
const module = data?.entity;
|
||||
|
||||
const { attributes, refetch: refetchAttributes } = useAttributesList();
|
||||
|
||||
useEffect(() => {
|
||||
if (module?.isBuiltIn) {
|
||||
redirect("modules");
|
||||
}
|
||||
}, [module]);
|
||||
|
||||
const attributeActions = useAttributesActions({
|
||||
module,
|
||||
refetchModule,
|
||||
refetchAttributes,
|
||||
});
|
||||
|
||||
const moduleActions = useModulesActions({
|
||||
module,
|
||||
refetchModule,
|
||||
});
|
||||
|
||||
return {
|
||||
module,
|
||||
refetchModule,
|
||||
attributes,
|
||||
refetchAttributes,
|
||||
attributeActions,
|
||||
moduleActions,
|
||||
};
|
||||
};
|
||||
|
||||
export const [ModuleEditorContextProvider, useModuleEditorContext] =
|
||||
makeContext<ModuleEditorContextState, Props>(
|
||||
useModuleEditorContextState,
|
||||
"ModuleEditor"
|
||||
);
|
||||
@ -0,0 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getAttributeTypesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
const useAttributeTypesList = () => {
|
||||
const { data, refetch } = useQuery(getAttributeTypesOptions());
|
||||
|
||||
return {
|
||||
attributeTypes: data?.items ?? [],
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAttributeTypesList;
|
||||
214
src/app/module-editor/[moduleId]/hooks/useAttributesActions.tsx
Normal file
214
src/app/module-editor/[moduleId]/hooks/useAttributesActions.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
AttributeSchema,
|
||||
HttpValidationError,
|
||||
ModuleSchemaOutput,
|
||||
} from "@/lib/client";
|
||||
import {
|
||||
addAttributeToModuleMutation,
|
||||
createAttributeMutation,
|
||||
deleteAttributeMutation,
|
||||
removeAttributeFromModuleMutation,
|
||||
updateAttributeLabelMutation,
|
||||
updateAttributeMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
export type AttributesActions = {
|
||||
addAttributeToModule: (attribute: AttributeSchema) => void;
|
||||
removeAttributeFromModule: (attribute: AttributeSchema) => void;
|
||||
onEditAttributeLabel: (attribute: AttributeSchema) => void;
|
||||
onCreate: () => void;
|
||||
onUpdate: (attribute: AttributeSchema) => void;
|
||||
onDelete: (attribute: AttributeSchema) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
module?: ModuleSchemaOutput;
|
||||
refetchModule: () => void;
|
||||
refetchAttributes: () => void;
|
||||
};
|
||||
|
||||
const useAttributesActions = ({
|
||||
module,
|
||||
refetchModule,
|
||||
refetchAttributes,
|
||||
}: Props): AttributesActions => {
|
||||
const onError = (error: AxiosError<HttpValidationError>) => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const addAttrToModuleMutation = useMutation({
|
||||
...addAttributeToModuleMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const removeAttrFromModuleMutation = useMutation({
|
||||
...removeAttributeFromModuleMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const toggleAttributeInModule = (
|
||||
attribute: AttributeSchema,
|
||||
isAdding: boolean
|
||||
) => {
|
||||
if (!module) return;
|
||||
const mutation = isAdding
|
||||
? addAttrToModuleMutation
|
||||
: removeAttrFromModuleMutation;
|
||||
|
||||
mutation.mutate(
|
||||
{
|
||||
body: {
|
||||
moduleId: module.id,
|
||||
attributeId: attribute.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: ({ message }) => {
|
||||
notifications.success({ message });
|
||||
refetchModule();
|
||||
refetchAttributes();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const addAttributeToModule = (attribute: AttributeSchema) =>
|
||||
toggleAttributeInModule(attribute, true);
|
||||
|
||||
const removeAttributeFromModule = (attribute: AttributeSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление атрибута из модуля",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить атрибут "{attribute.label}"
|
||||
из модуля?
|
||||
</Text>
|
||||
),
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => toggleAttributeInModule(attribute, false),
|
||||
});
|
||||
};
|
||||
|
||||
const updateAttributeLabel = useMutation({
|
||||
...updateAttributeLabelMutation(),
|
||||
onError,
|
||||
onSuccess: refetchModule,
|
||||
});
|
||||
|
||||
const onEditAttributeLabel = (attribute: AttributeSchema) => {
|
||||
if (!module) return;
|
||||
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Редактирование имени атрибута в модуле",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onChange: values =>
|
||||
updateAttributeLabel.mutate({
|
||||
body: {
|
||||
moduleId: module.id,
|
||||
attributeId: attribute.id,
|
||||
label: values.name,
|
||||
},
|
||||
}),
|
||||
value: { name: attribute.label },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createAttribute = useMutation({
|
||||
...createAttributeMutation(),
|
||||
onError,
|
||||
onSuccess: refetchAttributes,
|
||||
});
|
||||
|
||||
const onCreate = () => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeEditorModal",
|
||||
title: "Создание атрибута",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onCreate: values =>
|
||||
createAttribute.mutate({
|
||||
body: {
|
||||
entity: values,
|
||||
},
|
||||
}),
|
||||
isEditing: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateAttribute = useMutation({
|
||||
...updateAttributeMutation(),
|
||||
onError,
|
||||
onSuccess: () => {
|
||||
refetchAttributes();
|
||||
refetchModule();
|
||||
},
|
||||
});
|
||||
|
||||
const onUpdate = (attribute: AttributeSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeEditorModal",
|
||||
title: "Редактирование атрибута",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onChange: values =>
|
||||
updateAttribute.mutate({
|
||||
path: {
|
||||
pk: attribute.id,
|
||||
},
|
||||
body: {
|
||||
entity: values,
|
||||
},
|
||||
}),
|
||||
entity: attribute,
|
||||
isEditing: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttribute = useMutation({
|
||||
...deleteAttributeMutation(),
|
||||
onError,
|
||||
onSuccess: () => {
|
||||
refetchModule();
|
||||
refetchAttributes();
|
||||
},
|
||||
});
|
||||
|
||||
const onDelete = (attribute: AttributeSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление атрибута",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить атрибут "{attribute.label}"?
|
||||
</Text>
|
||||
),
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () =>
|
||||
deleteAttribute.mutate({ path: { pk: attribute.id } }),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
addAttributeToModule,
|
||||
removeAttributeFromModule,
|
||||
onEditAttributeLabel,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAttributesActions;
|
||||
32
src/app/module-editor/[moduleId]/hooks/useAttributesList.ts
Normal file
32
src/app/module-editor/[moduleId]/hooks/useAttributesList.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AttributeSchema } from "@/lib/client";
|
||||
import {
|
||||
getAttributesOptions,
|
||||
getProjectsQueryKey,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
const useAttributesList = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data, refetch } = useQuery(getAttributesOptions());
|
||||
|
||||
const queryKey = getProjectsQueryKey();
|
||||
|
||||
const setAttributes = (attributes: AttributeSchema[]) => {
|
||||
queryClient.setQueryData(
|
||||
queryKey,
|
||||
(old: { items: AttributeSchema[] }) => ({
|
||||
...old,
|
||||
items: attributes,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
attributes: data?.items ?? [],
|
||||
setAttributes,
|
||||
refetch,
|
||||
queryKey,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAttributesList;
|
||||
51
src/app/module-editor/[moduleId]/hooks/useModulesActions.tsx
Normal file
51
src/app/module-editor/[moduleId]/hooks/useModulesActions.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ModuleSchemaOutput } from "@/lib/client";
|
||||
import { updateModuleMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
export type ModuleInfo = {
|
||||
label: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
module?: ModuleSchemaOutput;
|
||||
refetchModule: () => void;
|
||||
};
|
||||
|
||||
export type ModulesActions = {
|
||||
updateCommonInfo: (values: ModuleInfo, onSuccess?: () => void) => void;
|
||||
};
|
||||
|
||||
const useModulesActions = ({
|
||||
module,
|
||||
refetchModule,
|
||||
}: Props): ModulesActions => {
|
||||
const updateCommonInfoMutation = useMutation(updateModuleMutation());
|
||||
|
||||
const updateCommonInfo = (values: ModuleInfo, onSuccess?: () => void) => {
|
||||
if (!module) return;
|
||||
|
||||
updateCommonInfoMutation.mutate(
|
||||
{
|
||||
path: {
|
||||
pk: module.id,
|
||||
},
|
||||
body: {
|
||||
entity: values,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchModule();
|
||||
onSuccess && onSuccess();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
updateCommonInfo,
|
||||
};
|
||||
};
|
||||
|
||||
export default useModulesActions;
|
||||
138
src/app/module-editor/[moduleId]/modals/AttributeEditorModal.tsx
Normal file
138
src/app/module-editor/[moduleId]/modals/AttributeEditorModal.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Checkbox, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import AttributeTypeSelect from "@/app/module-editor/[moduleId]/components/shared/AttributeTypeSelect/AttributeTypeSelect";
|
||||
import DefaultAttributeValueInput from "@/app/module-editor/[moduleId]/components/shared/DefaultAttributeValueInput/DefaultAttributeValueInput";
|
||||
import {
|
||||
AttributeSchema,
|
||||
CreateAttributeSchema,
|
||||
UpdateAttributeSchema,
|
||||
} from "@/lib/client";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
||||
|
||||
type Props = CreateEditFormProps<
|
||||
AttributeSchema,
|
||||
CreateAttributeSchema,
|
||||
UpdateAttributeSchema
|
||||
>;
|
||||
|
||||
const AttributeEditorModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const [isInitial, setIsInitial] = useState(true);
|
||||
const [isNullableInputShown, setIsNullableInputShown] = useState(true);
|
||||
const [copyTypeId, setCopyTypeId] = useState<number>();
|
||||
|
||||
const form = useForm<Partial<UpdateAttributeSchema>>({
|
||||
initialValues: innerProps.isEditing
|
||||
? innerProps.entity
|
||||
: ({
|
||||
label: "",
|
||||
name: "",
|
||||
typeId: undefined,
|
||||
type: undefined,
|
||||
isApplicableToGroup: false,
|
||||
isShownOnDashboard: false,
|
||||
isHighlightIfExpired: false,
|
||||
isNullable: false,
|
||||
defaultValue: null,
|
||||
description: "",
|
||||
} as Partial<CreateAttributeSchema>),
|
||||
validate: {
|
||||
label: label => !label?.trim() && "Название не заполнено",
|
||||
type: type => !type && "Тип атрибута не выбран",
|
||||
defaultValue: (defaultValue, values) => {
|
||||
if (defaultValue === null && !values.isNullable) {
|
||||
return "Укажите значение по умолчанию или разрешите пустое значение.";
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const type = form.values.type?.type;
|
||||
setIsNullableInputShown(type !== "bool");
|
||||
|
||||
if (!isInitial) {
|
||||
if (type === "bool") {
|
||||
form.setFieldValue("isNullable", false);
|
||||
form.setFieldValue("defaultValue", { value: false });
|
||||
} else {
|
||||
form.setFieldValue("defaultValue", null);
|
||||
}
|
||||
}
|
||||
setIsInitial(false);
|
||||
setCopyTypeId(form.values.type?.id);
|
||||
}, [form.values.type?.id]);
|
||||
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...innerProps}
|
||||
closeOnSubmit
|
||||
form={form}
|
||||
onClose={() => context.closeContextModal(id)}>
|
||||
<Stack gap={"md"}>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={"Название"}
|
||||
{...form.getInputProps("label")}
|
||||
/>
|
||||
<AttributeTypeSelect
|
||||
withAsterisk
|
||||
disabled={innerProps.isEditing}
|
||||
{...form.getInputProps("type")}
|
||||
onChange={type => {
|
||||
form.setFieldValue("type", type);
|
||||
form.setFieldValue("typeId", type.id);
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Значение синхронизировано в группе"}
|
||||
{...form.getInputProps("isApplicableToGroup", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Значение выводится на дашборде"}
|
||||
{...form.getInputProps("isShownOnDashboard", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
{(form.values.type?.type === "datetime" ||
|
||||
form.values.type?.type === "date") && (
|
||||
<Checkbox
|
||||
label={"Подсветка, если просрочено"}
|
||||
{...form.getInputProps("isHighlightIfExpired", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{isNullableInputShown && (
|
||||
<Checkbox
|
||||
label={"Может быть пустым"}
|
||||
{...form.getInputProps("isNullable", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{form.values.type && copyTypeId === form.values.type.id && (
|
||||
<DefaultAttributeValueInput form={form} />
|
||||
)}
|
||||
<Textarea
|
||||
label={"Описание"}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeEditorModal;
|
||||
39
src/app/module-editor/[moduleId]/page.tsx
Normal file
39
src/app/module-editor/[moduleId]/page.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Suspense } from "react";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Center, Loader } from "@mantine/core";
|
||||
import PageBody from "@/app/module-editor/[moduleId]/components/shared/PageBody/PageBody";
|
||||
import { ModuleEditorContextProvider } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||
|
||||
type Params = {
|
||||
params: Promise<{
|
||||
moduleId: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default async function ModuleEditorPage({ params }: Params) {
|
||||
const { moduleId } = await params;
|
||||
|
||||
const id = Number(moduleId);
|
||||
if (isNaN(id)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Некорректный ID модуля. ID должен быть числом." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center h="50vh">
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
}>
|
||||
<PageContainer>
|
||||
<ModuleEditorContextProvider moduleId={id}>
|
||||
<PageBody />
|
||||
</ModuleEditorContextProvider>
|
||||
</PageContainer>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user