feat: module dependencies

This commit is contained in:
2025-09-18 17:53:26 +04:00
parent a95d05e28b
commit 0805a86335
17 changed files with 153 additions and 119 deletions

View File

@ -1,7 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { isEqual } from "lodash";
import { Button, Stack } from "@mantine/core"; import { Button, Stack } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import resolveDependencies from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import ModulesTable from "./components/ModulesTable"; import ModulesTable from "./components/ModulesTable";
@ -16,8 +16,16 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
}); });
const onSubmit = (values: ProjectSchema) => { const onSubmit = (values: ProjectSchema) => {
onChange(values); const modulesWithDependencies = resolveDependencies(
form.setInitialValues(values); values.builtInModules
);
const updatedValues = {
...values,
builtInModules: modulesWithDependencies,
};
form.setValues(updatedValues);
form.resetDirty();
onChange(updatedValues);
}; };
return ( return (
@ -32,7 +40,7 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
<Button <Button
type={"submit"} type={"submit"}
variant={"default"} variant={"default"}
disabled={isEqual(value, form.values)}> disabled={!form.isDirty()}>
Сохранить Сохранить
</Button> </Button>
</Stack> </Stack>

View File

@ -1,28 +1,29 @@
import { FC, useRef } from "react"; import { FC } from "react";
import { Divider, Stack } from "@mantine/core"; import { Divider, Stack } from "@mantine/core";
import useModulesTableColumns from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns"; import useModulesTableColumns from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable"; import BaseTable from "@/components/ui/BaseTable/BaseTable";
import { BuiltInModuleSchema } from "@/lib/client"; import useBuiltInModulesList from "@/hooks/lists/useBuiltInModulesList";
import { MODULES } from "@/modules/modules"; import { BuiltInModuleSchemaOutput } from "@/lib/client";
type Props = { type Props = {
selectedRecords: BuiltInModuleSchema[]; selectedRecords: BuiltInModuleSchemaOutput[];
onSelectedRecordsChange: (records: BuiltInModuleSchema[]) => void; onSelectedRecordsChange: (records: BuiltInModuleSchemaOutput[]) => void;
}; };
const ModulesTable: FC<Props> = props => { const ModulesTable: FC<Props> = props => {
const columns = useModulesTableColumns(); const columns = useModulesTableColumns();
const modules = useRef( const { builtInModules } = useBuiltInModulesList();
Object.values(MODULES).map(module => module.modelData)
);
return ( return (
<Stack gap={0}> <Stack gap={0}>
<Divider /> <Divider />
<BaseTable <BaseTable
records={modules.current} records={builtInModules}
columns={columns} columns={columns}
verticalSpacing={"md"} verticalSpacing={"md"}
allRecordsSelectionCheckboxProps={{
hidden: true,
}}
{...props} {...props}
/> />
</Stack> </Stack>

View File

@ -1,6 +1,8 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IconInfoCircle } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { BuiltInModuleSchema } from "@/lib/client"; import { em, Group, Text, Tooltip } from "@mantine/core";
import { BuiltInModuleSchemaOutput } from "@/lib/client";
const useModulesTableColumns = () => { const useModulesTableColumns = () => {
return useMemo( return useMemo(
@ -9,14 +11,34 @@ const useModulesTableColumns = () => {
{ {
accessor: "label", accessor: "label",
title: "Название", title: "Название",
width: "30%", width: "20%",
}, },
{ {
title: "Описание", title: "Описание",
accessor: "description", accessor: "description",
width: "70%", width: "50%",
}, },
] as DataTableColumn<BuiltInModuleSchema>[], {
title: (
<Group gap={"sm"}>
<Text>Зависит от модулей</Text>
<Tooltip
label={
"Зависимости автоматически подключатся при сохранении"
}>
<IconInfoCircle size={em(25)} />
</Tooltip>
</Group>
),
accessor: "dependsOn",
width: "30%",
render: module => (
<Text>
{module.dependsOn?.map(m => m.label).join(", ")}
</Text>
),
},
] as DataTableColumn<BuiltInModuleSchemaOutput>[],
[] []
); );
}; };

View File

@ -0,0 +1,22 @@
import { uniqBy } from "lodash";
import { BuiltInModuleSchemaOutput } from "@/lib/client";
const resolveDependencies = (
modules: BuiltInModuleSchemaOutput[]
): BuiltInModuleSchemaOutput[] => {
const resolved = new Set<number>();
const result: BuiltInModuleSchemaOutput[] = [];
const addModule = (module: BuiltInModuleSchemaOutput) => {
if (resolved.has(module.id)) return;
resolved.add(module.id);
module.dependsOn.forEach(addModule);
result.push(module);
};
modules.forEach(addModule);
return uniqBy(result, "id");
};
export default resolveDependencies;

View File

@ -0,0 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { getBuiltInModulesOptions } from "@/lib/client/@tanstack/react-query.gen";
const useBuiltInModulesList = () => {
const { data, refetch } = useQuery(getBuiltInModulesOptions());
return {
builtInModules: data?.items ?? [],
refetch,
};
};
export default useBuiltInModulesList;

View File

@ -1276,7 +1276,7 @@ export const addKitToDealQueryKey = (options: Options<AddKitToDealData>) =>
createQueryKey("addKitToDeal", options); createQueryKey("addKitToDeal", options);
/** /**
* Add Kit To Deal Product * Add Kit To Deal
*/ */
export const addKitToDealOptions = (options: Options<AddKitToDealData>) => { export const addKitToDealOptions = (options: Options<AddKitToDealData>) => {
return queryOptions({ return queryOptions({
@ -1294,7 +1294,7 @@ export const addKitToDealOptions = (options: Options<AddKitToDealData>) => {
}; };
/** /**
* Add Kit To Deal Product * Add Kit To Deal
*/ */
export const addKitToDealMutation = ( export const addKitToDealMutation = (
options?: Partial<Options<AddKitToDealData>> options?: Partial<Options<AddKitToDealData>>

View File

@ -990,7 +990,7 @@ export const updateDealService = <ThrowOnError extends boolean = false>(
}; };
/** /**
* Add Kit To Deal Product * Add Kit To Deal
*/ */
export const addKitToDeal = <ThrowOnError extends boolean = false>( export const addKitToDeal = <ThrowOnError extends boolean = false>(
options: Options<AddKitToDealData, ThrowOnError> options: Options<AddKitToDealData, ThrowOnError>

View File

@ -25,7 +25,7 @@ export type BoardSchema = {
/** /**
* BuiltInModuleSchema * BuiltInModuleSchema
*/ */
export type BuiltInModuleSchema = { export type BuiltInModuleSchemaInput = {
/** /**
* Id * Id
*/ */
@ -46,6 +46,40 @@ export type BuiltInModuleSchema = {
* Description * Description
*/ */
description: string; description: string;
/**
* Dependson
*/
dependsOn: Array<BuiltInModuleSchemaInput>;
};
/**
* BuiltInModuleSchema
*/
export type BuiltInModuleSchemaOutput = {
/**
* Id
*/
id: number;
/**
* Key
*/
key: string;
/**
* Label
*/
label: string;
/**
* Iconname
*/
iconName: string;
/**
* Description
*/
description: string;
/**
* Dependson
*/
dependsOn: Array<BuiltInModuleSchemaOutput>;
}; };
/** /**
@ -690,7 +724,7 @@ export type GetAllBuiltInModulesResponse = {
/** /**
* Items * Items
*/ */
items: Array<BuiltInModuleSchema>; items: Array<BuiltInModuleSchemaOutput>;
}; };
/** /**
@ -938,7 +972,7 @@ export type ProjectSchema = {
/** /**
* Builtinmodules * Builtinmodules
*/ */
builtInModules: Array<BuiltInModuleSchema>; builtInModules: Array<BuiltInModuleSchemaOutput>;
}; };
/** /**
@ -1316,7 +1350,7 @@ export type UpdateProjectSchema = {
/** /**
* Builtinmodules * Builtinmodules
*/ */
builtInModules?: Array<BuiltInModuleSchema>; builtInModules?: Array<BuiltInModuleSchemaInput>;
}; };
/** /**

View File

@ -15,12 +15,37 @@ export const zBoardSchema = z.object({
/** /**
* BuiltInModuleSchema * BuiltInModuleSchema
*/ */
export const zBuiltInModuleSchema = z.object({ export const zBuiltInModuleSchemaInput = z.object({
id: z.int(), id: z.int(),
key: z.string(), key: z.string(),
label: z.string(), label: z.string(),
iconName: z.string(), iconName: z.string(),
description: z.string(), description: z.string(),
get dependsOn() {
return z.array(
z.lazy((): any => {
return zBuiltInModuleSchemaInput;
})
);
},
});
/**
* BuiltInModuleSchema
*/
export const zBuiltInModuleSchemaOutput = z.object({
id: z.int(),
key: z.string(),
label: z.string(),
iconName: z.string(),
description: z.string(),
get dependsOn() {
return z.array(
z.lazy((): any => {
return zBuiltInModuleSchemaOutput;
})
);
},
}); });
/** /**
@ -304,7 +329,7 @@ export const zCreateProjectRequest = z.object({
export const zProjectSchema = z.object({ export const zProjectSchema = z.object({
id: z.int(), id: z.int(),
name: z.string(), name: z.string(),
builtInModules: z.array(zBuiltInModuleSchema), builtInModules: z.array(zBuiltInModuleSchemaOutput),
}); });
/** /**
@ -507,7 +532,7 @@ export const zDeleteStatusResponse = z.object({
* GetAllBuiltInModulesResponse * GetAllBuiltInModulesResponse
*/ */
export const zGetAllBuiltInModulesResponse = z.object({ export const zGetAllBuiltInModulesResponse = z.object({
items: z.array(zBuiltInModuleSchema), items: z.array(zBuiltInModuleSchemaOutput),
}); });
/** /**
@ -772,7 +797,7 @@ export const zUpdateProductServiceResponse = z.object({
*/ */
export const zUpdateProjectSchema = z.object({ export const zUpdateProjectSchema = z.object({
name: z.optional(z.union([z.string(), z.null()])), name: z.optional(z.union([z.string(), z.null()])),
builtInModules: z.optional(z.array(zBuiltInModuleSchema)), builtInModules: z.optional(z.array(zBuiltInModuleSchemaInput)),
}); });
/** /**

View File

@ -11,36 +11,6 @@ const DealServicesTable: FC = () => {
// const isLocked = isDealLocked(deal); // TODO bills // const isLocked = isDealLocked(deal); // TODO bills
// const [currentService, setCurrentService] = useState<
// DealServiceSchema | undefined
// >();
// const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
// const onEmployeeClick = (item: CardServiceSchema) => {
// if (!onChange) return;
// setCurrentService(item);
// setEmployeesModalVisible(true);
// };
// const onEmployeeModalClose = () => {
// setEmployeesModalVisible(false);
// setCurrentService(undefined);
// };
// const getCurrentEmployees = (): UserSchema[] => {
// if (!currentService) return [];
// const item = items.find(
// i => i.service.id === currentService.service.id
// );
// if (!item) return [];
// return item.employees;
// };
// const onEmployeesChange = (items: UserSchema[]) => {
// if (!currentService || !onChange) return;
// debouncedOnChange({
// ...currentService,
// employees: items,
// });
// };
return ( return (
<Flex <Flex
direction={"column"} direction={"column"}

View File

@ -49,11 +49,6 @@ const DealServiceRow: FC<Props> = ({ value, onChange, onDelete }) => {
tipLabel={"Удалить услугу"}> tipLabel={"Удалить услугу"}>
<IconTrash /> <IconTrash />
</ActionIconWithTip> </ActionIconWithTip>
{/*<Tooltip label="Сотрудники">*/}
{/*<ActionIcon onClick={() => onEmployeeClick(service)}>*/}
{/* <IconUsersGroup />*/}
{/*</ActionIcon>*/}
{/*</Tooltip>*/}
<NumberInput <NumberInput
flex={1} flex={1}
suffix={" шт."} suffix={" шт."}

View File

@ -57,11 +57,6 @@ const ProductServicesTable: FC<Props> = ({
onChange, onChange,
}); });
// const [currentService, setCurrentService] = useState<
// ProductServiceSchema | undefined
// >();
// const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onCreateClick = () => { const onCreateClick = () => {
const excludeServiceIds = dealProduct.productServices.map( const excludeServiceIds = dealProduct.productServices.map(
productService => productService.service.id productService => productService.service.id
@ -80,31 +75,6 @@ const ProductServicesTable: FC<Props> = ({
}); });
}; };
// const onEmployeeClick = (item: CardProductServiceSchema) => {
// if (!onChange) return;
// setCurrentService(item);
// setEmployeesModalVisible(true);
// };
// const onEmployeeModalClose = () => {
// setEmployeesModalVisible(false);
// setCurrentService(undefined);
// };
// const getCurrentEmployees = (): UserSchema[] => {
// if (!currentService) return [];
// const item = items.find(
// i => i.service.id === currentService.service.id
// );
// if (!item) return [];
// return item.employees;
// };
// const onEmployeesChange = (items: UserSchema[]) => {
// if (!currentService || !onChange) return;
// onChange({
// ...currentService,
// employees: items,
// });
// };
const isEmptyTable = dealProduct.productServices.length === 0; const isEmptyTable = dealProduct.productServices.length === 0;
return ( return (

View File

@ -46,15 +46,6 @@ const useProductServicesTableColumns = ({
onClick={() => onChange(dealProductService)}> onClick={() => onChange(dealProductService)}>
<IconEdit /> <IconEdit />
</ActionIconWithTip> </ActionIconWithTip>
{/*<Tooltip label="Сотрудники">*/}
{/* <ActionIcon*/}
{/* onClick={() =>*/}
{/* onEmployeeClick(row.original)*/}
{/* }*/}
{/* variant={"default"}>*/}
{/* <IconUsersGroup />*/}
{/* </ActionIcon>*/}
{/*</Tooltip>*/}
</Flex> </Flex>
), ),
}, },

View File

@ -38,7 +38,6 @@ const ProductServiceEditorModal = ({
service: undefined, service: undefined,
serviceId: undefined, serviceId: undefined,
price: undefined, price: undefined,
// employees: [],
isFixedPrice: false, isFixedPrice: false,
}; };

View File

@ -15,13 +15,6 @@ const modules: ModulesType = {
key: "fulfillment_base", key: "fulfillment_base",
icon: <IconBox />, icon: <IconBox />,
}, },
modelData: {
id: 1,
key: "fulfillment_base",
label: "Фулфиллмент",
iconName: "IconBox",
description: "Создание товаров и услуг, их привязка к сделкам",
},
}, },
}; };

View File

@ -20,13 +20,6 @@ const modules: ModulesType = {
key: "{{this.key}}", key: "{{this.key}}",
icon: {{#if this.iconName}}<{{this.iconName}} />{{else}}None{{/if}}, icon: {{#if this.iconName}}<{{this.iconName}} />{{else}}None{{/if}},
}, },
modelData: {
id: {{this.id}},
key: "{{this.key}}",
label: "{{this.label}}",
iconName: "{{this.iconName}}",
description: "{{this.description}}",
},
}, },
{{/each}} {{/each}}
}; };

View File

@ -1,5 +1,4 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { BuiltInModuleSchema } from "@/lib/client";
export type Module = { export type Module = {
renderInfo: { renderInfo: {
@ -7,7 +6,6 @@ export type Module = {
key: string; key: string;
icon: ReactNode; icon: ReactNode;
}; };
modelData: BuiltInModuleSchema;
getTab?: (props: any) => ReactNode; getTab?: (props: any) => ReactNode;
}; };