Compare commits
43 Commits
8cc11bca67
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 44b89245f0 | |||
| b0eab6cce7 | |||
| 1a1f584b81 | |||
| cc910c8495 | |||
| d3270a3532 | |||
| 38ae35795b | |||
| 3b1aca2df1 | |||
| 4d3264a92f | |||
| 33dd1e1c0f | |||
| 311210394f | |||
| 03be3903cb | |||
| 8020561da6 | |||
| a383f218f1 | |||
| fd5e878c29 | |||
| e8ffafa6c5 | |||
| 95cc7f6259 | |||
| 69bf7848e1 | |||
| 4370684c58 | |||
| 2948189291 | |||
| ee90ebe0f0 | |||
| 418e4b6b63 | |||
| 3355c73b8b | |||
| 07736ee374 | |||
| 42f1c75603 | |||
| 4342ab17ed | |||
| e9c9f6b65d | |||
| d38454f3af | |||
| 0c2fca2c23 | |||
| 9207bd915d | |||
| da769fa2c0 | |||
| 4cc6360bb4 | |||
| 8019fa566c | |||
| ea6a6df371 | |||
| 3575b9f34a | |||
| 71c9d08b8c | |||
| d4c0eac4a0 | |||
| e39df47520 | |||
| d9da3d1bc5 | |||
| 5b754865cf | |||
| 2bdbebc453 | |||
| 57a7ab0871 | |||
| 4d5723bc72 | |||
| 82f08b4f83 |
25
fix-client.ts
Normal file
25
fix-client.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const zodPath = "src/lib/client/zod.gen.ts";
|
||||||
|
let content = fs.readFileSync(zodPath, "utf8");
|
||||||
|
// Replace only for the upload schema
|
||||||
|
const zodTarget = "upload_file: z.string";
|
||||||
|
while (content.includes(zodTarget)) {
|
||||||
|
content = content.replace(zodTarget, "upload_file: z.any");
|
||||||
|
}
|
||||||
|
fs.writeFileSync(zodPath, content);
|
||||||
|
console.log("✅ Fixed zod schema for upload_file");
|
||||||
|
|
||||||
|
const utilsPath = "src/lib/client/client/utils.ts";
|
||||||
|
content = fs.readFileSync(utilsPath, "utf8");
|
||||||
|
|
||||||
|
const utilsTarget = "@ts-expect-error";
|
||||||
|
while (content.includes(utilsTarget)) {
|
||||||
|
content = content.replace(utilsTarget, "@ts-ignore");
|
||||||
|
}
|
||||||
|
content = content.replace(
|
||||||
|
"...(mergedHeaders[key] ?? []),",
|
||||||
|
"...(mergedHeaders[key] ?? []) as any,"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(utilsPath, content);
|
||||||
|
console.log("✅ Fixed utils.ts");
|
||||||
@ -7,7 +7,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client",
|
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client & sudo npx tsc fix-client.ts && mv -f ./fix-client.js ./fix-client.cjs && sudo node ./fix-client.cjs",
|
||||||
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
|
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"dayjs": "^1.11.15",
|
"dayjs": "^1.11.18",
|
||||||
"framer-motion": "^12.23.7",
|
"framer-motion": "^12.23.7",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
@ -43,7 +43,7 @@
|
|||||||
"next": "15.4.7",
|
"next": "15.4.7",
|
||||||
"phone": "^3.1.67",
|
"phone": "^3.1.67",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
@ -69,6 +69,7 @@
|
|||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.11",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"@types/react-slick": "^0",
|
"@types/react-slick": "^0",
|
||||||
"@types/redux-persist": "^4.3.1",
|
"@types/redux-persist": "^4.3.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RefObject, useMemo, useRef } from "react";
|
import { RefObject, useMemo, useRef } from "react";
|
||||||
import { IconTag } from "@tabler/icons-react";
|
import { IconApps, IconList, IconTag } from "@tabler/icons-react";
|
||||||
import { SimpleGrid, Stack } from "@mantine/core";
|
import { SimpleGrid, Stack } from "@mantine/core";
|
||||||
import Action from "@/app/actions/components/Action/Action";
|
import Action from "@/app/actions/components/Action/Action";
|
||||||
import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
|
import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
|
||||||
@ -23,6 +23,16 @@ const PageBody = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
||||||
|
{
|
||||||
|
icon: IconList,
|
||||||
|
label: "Атрибуты",
|
||||||
|
href: "/attributes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IconApps,
|
||||||
|
label: "Модули",
|
||||||
|
href: "/modules",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: IconTag,
|
icon: IconTag,
|
||||||
label: "Теги",
|
label: "Теги",
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const mobileButtonsData: LinkData[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconFileBarcode,
|
icon: IconFileBarcode,
|
||||||
label: "Шаблоны штрихкодов",
|
label: "Шаблоны ШК",
|
||||||
href: "/barcode-templates",
|
href: "/barcode-templates",
|
||||||
moduleName: ModuleNames.FULFILLMENT_BASE,
|
moduleName: ModuleNames.FULFILLMENT_BASE,
|
||||||
},
|
},
|
||||||
|
|||||||
27
src/app/attributes/components/AttrViewSegmentedControl.tsx
Normal file
27
src/app/attributes/components/AttrViewSegmentedControl.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import AttributePageView from "@/app/attributes/types/view";
|
||||||
|
import BaseSegmentedControl, {
|
||||||
|
BaseSegmentedControlProps,
|
||||||
|
} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl";
|
||||||
|
|
||||||
|
type Props = Omit<BaseSegmentedControlProps<AttributePageView>, "data">;
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
label: "Аттрибуты",
|
||||||
|
value: AttributePageView.ATTRIBUTES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Справочники",
|
||||||
|
value: AttributePageView.SELECTS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AttrViewSegmentedControl: FC<Props> = props => (
|
||||||
|
<BaseSegmentedControl
|
||||||
|
data={data}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AttrViewSegmentedControl;
|
||||||
87
src/app/attributes/components/AttributesHeader.tsx
Normal file
87
src/app/attributes/components/AttributesHeader.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Dispatch, FC, SetStateAction, useMemo } from "react";
|
||||||
|
import { Divider, Flex, Group, TextInput } from "@mantine/core";
|
||||||
|
import AttrViewSegmentedControl from "@/app/attributes/components/AttrViewSegmentedControl";
|
||||||
|
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
||||||
|
import AttributePageView from "@/app/attributes/types/view";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
view: AttributePageView;
|
||||||
|
setView: Dispatch<SetStateAction<AttributePageView>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributesHeader: FC<Props> = ({ view, setView }) => {
|
||||||
|
const {
|
||||||
|
attributesActions,
|
||||||
|
selectsActions,
|
||||||
|
attrSearch,
|
||||||
|
setAttrSearch,
|
||||||
|
selectSearch,
|
||||||
|
setSelectSearch,
|
||||||
|
} = useAttributesContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const attributeActions = useMemo(
|
||||||
|
() => (
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
<InlineButton
|
||||||
|
onClick={attributesActions.onCreate}
|
||||||
|
w={isMobile ? "100%" : "auto"}>
|
||||||
|
Создать атрибут
|
||||||
|
</InlineButton>
|
||||||
|
<TextInput
|
||||||
|
value={attrSearch}
|
||||||
|
onChange={e => setAttrSearch(e.currentTarget.value)}
|
||||||
|
w={isMobile ? "100%" : "auto"}
|
||||||
|
placeholder={"Поиск..."}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
[isMobile, attrSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectActions = useMemo(
|
||||||
|
() => (
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
<InlineButton
|
||||||
|
onClick={selectsActions.onCreate}
|
||||||
|
w={isMobile ? "100%" : "auto"}>
|
||||||
|
Создать справочник
|
||||||
|
</InlineButton>
|
||||||
|
<TextInput
|
||||||
|
value={selectSearch}
|
||||||
|
onChange={e => setSelectSearch(e.currentTarget.value)}
|
||||||
|
w={isMobile ? "100%" : "auto"}
|
||||||
|
placeholder={"Поиск..."}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
[isMobile, selectSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
wrap={"nowrap"}
|
||||||
|
gap={"xs"}
|
||||||
|
align={isMobile ? "unset" : "center"}
|
||||||
|
mt={isMobile ? "xs" : ""}
|
||||||
|
mx={isMobile ? "xs" : ""}
|
||||||
|
direction={isMobile ? "column-reverse" : "row"}
|
||||||
|
justify={"space-between"}>
|
||||||
|
{view === AttributePageView.ATTRIBUTES
|
||||||
|
? attributeActions
|
||||||
|
: selectActions}
|
||||||
|
{isMobile && <Divider />}
|
||||||
|
<AttrViewSegmentedControl
|
||||||
|
value={view}
|
||||||
|
onChange={setView}
|
||||||
|
styles={{ root: { margin: 0, padding: 0 } }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributesHeader;
|
||||||
40
src/app/attributes/components/AttributesTable.tsx
Normal file
40
src/app/attributes/components/AttributesTable.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { IconMoodSad } from "@tabler/icons-react";
|
||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
||||||
|
import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const AttributesTable: FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { attributes } = useAttributesContext();
|
||||||
|
const columns = useAttributesTableColumns();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
withTableBorder
|
||||||
|
columns={columns}
|
||||||
|
records={attributes}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
emptyState={
|
||||||
|
<Group mt={attributes.length === 0 ? "xl" : 0}>
|
||||||
|
<Text>Нет атрибутов</Text>
|
||||||
|
<IconMoodSad />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
groups={undefined}
|
||||||
|
styles={{
|
||||||
|
table: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
header: { zIndex: 1 },
|
||||||
|
}}
|
||||||
|
mx={isMobile ? "xs" : 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributesTable;
|
||||||
42
src/app/attributes/components/PageBody.tsx
Normal file
42
src/app/attributes/components/PageBody.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import AttributesHeader from "@/app/attributes/components/AttributesHeader";
|
||||||
|
import AttributesTable from "@/app/attributes/components/AttributesTable";
|
||||||
|
import SelectsTable from "@/app/attributes/components/SelectsTable";
|
||||||
|
import AttributePageView from "@/app/attributes/types/view";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
|
||||||
|
const PageBody = () => {
|
||||||
|
const [view, setView] = useState<AttributePageView>(
|
||||||
|
AttributePageView.ATTRIBUTES
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageBlock
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
fullScreenMobile>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "var(--mantine-spacing-md)",
|
||||||
|
}}>
|
||||||
|
<AttributesHeader
|
||||||
|
view={view}
|
||||||
|
setView={setView}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, overflow: "auto" }}>
|
||||||
|
{view === AttributePageView.ATTRIBUTES ? (
|
||||||
|
<AttributesTable />
|
||||||
|
) : (
|
||||||
|
<SelectsTable />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
36
src/app/attributes/components/SelectsTable.tsx
Normal file
36
src/app/attributes/components/SelectsTable.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { IconMoodSad } from "@tabler/icons-react";
|
||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
||||||
|
import useSelectsTableColumns from "@/app/attributes/hooks/useSelectsTableColumns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const SelectsTable = () => {
|
||||||
|
const { selects } = useAttributesContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const columns = useSelectsTableColumns();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
withTableBorder
|
||||||
|
columns={columns}
|
||||||
|
records={selects}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
emptyState={
|
||||||
|
<Group mt={selects.length === 0 ? "xl" : 0}>
|
||||||
|
<Text>Нет справочников</Text>
|
||||||
|
<IconMoodSad />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
groups={undefined}
|
||||||
|
styles={{
|
||||||
|
table: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
mx={isMobile ? "xs" : 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectsTable;
|
||||||
62
src/app/attributes/contexts/AttributesContext.tsx
Normal file
62
src/app/attributes/contexts/AttributesContext.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes";
|
||||||
|
import useSelectsActions, {
|
||||||
|
SelectsActions,
|
||||||
|
} from "@/app/attributes/hooks/useSelectsActions";
|
||||||
|
import useAttributesActions, {
|
||||||
|
AttributesActions,
|
||||||
|
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
|
||||||
|
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
|
||||||
|
import useAttrSelectsList from "@/hooks/lists/useAttrSelectsList";
|
||||||
|
import { AttributeSchema, AttrSelectSchema } from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import useFilteredSelects from "@/app/attributes/hooks/useFilteredSelects";
|
||||||
|
|
||||||
|
type AttributesContextState = {
|
||||||
|
attributes: AttributeSchema[];
|
||||||
|
attributesActions: AttributesActions;
|
||||||
|
attrSearch: string;
|
||||||
|
setAttrSearch: Dispatch<SetStateAction<string>>;
|
||||||
|
selects: AttrSelectSchema[];
|
||||||
|
selectsActions: SelectsActions;
|
||||||
|
selectSearch: string;
|
||||||
|
setSelectSearch: Dispatch<SetStateAction<string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAttributesContextState = (): AttributesContextState => {
|
||||||
|
const { attributes, refetch: refetchAttributes } = useAttributesList();
|
||||||
|
const attributesActions = useAttributesActions({
|
||||||
|
refetchAttributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
search: attrSearch,
|
||||||
|
setSearch: setAttrSearch,
|
||||||
|
filteredAttributes,
|
||||||
|
} = useFilteredAttributes({ attributes });
|
||||||
|
|
||||||
|
const { selects, queryKey } = useAttrSelectsList();
|
||||||
|
const selectsActions = useSelectsActions({ queryKey });
|
||||||
|
|
||||||
|
const {
|
||||||
|
search: selectSearch,
|
||||||
|
setSearch: setSelectSearch,
|
||||||
|
filteredSelects,
|
||||||
|
} = useFilteredSelects({ selects });
|
||||||
|
|
||||||
|
return {
|
||||||
|
attributes: filteredAttributes,
|
||||||
|
attributesActions,
|
||||||
|
attrSearch,
|
||||||
|
setAttrSearch,
|
||||||
|
selects: filteredSelects,
|
||||||
|
selectsActions,
|
||||||
|
selectSearch,
|
||||||
|
setSelectSearch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [AttributesContextProvider, useAttributesContext] =
|
||||||
|
makeContext<AttributesContextState>(useAttributesContextState, "Attribute");
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Drawer } from "@mantine/core";
|
||||||
|
import EditorBody from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/EditorBody";
|
||||||
|
import { SelectEditorContextProvider } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
||||||
|
import { DrawerProps } from "@/drawers/types";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { AttrSelectSchema, UpdateAttrSelectSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
select: AttrSelectSchema;
|
||||||
|
onSelectChange: (
|
||||||
|
values: UpdateAttrSelectSchema,
|
||||||
|
onSuccess: () => void
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttrSelectEditorDrawer: FC<DrawerProps<Props>> = ({
|
||||||
|
onClose,
|
||||||
|
opened,
|
||||||
|
props,
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
size={isMobile ? "100%" : "30%"}
|
||||||
|
title={"Редактирование справочника"}
|
||||||
|
position={"left"}
|
||||||
|
onClose={onClose}
|
||||||
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
|
withCloseButton
|
||||||
|
opened={opened}
|
||||||
|
trapFocus={false}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<SelectEditorContextProvider {...props}>
|
||||||
|
<EditorBody />
|
||||||
|
</SelectEditorContextProvider>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttrSelectEditorDrawer;
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Button, Flex, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
||||||
|
import { UpdateAttrSelectSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const CommonInfoEditor = () => {
|
||||||
|
const { select, onSelectChange } = useSelectEditorContext();
|
||||||
|
|
||||||
|
const form = useForm<UpdateAttrSelectSchema>({
|
||||||
|
initialValues: select || {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: name => !name && "Введите название",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.onSubmit(values => onSelectChange(values))}>
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
<TextInput
|
||||||
|
label={"Название справочника"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
data-autofocus
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant={"default"}
|
||||||
|
type={"submit"}>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommonInfoEditor;
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
import { Flex, TextInput } from "@mantine/core";
|
||||||
|
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
||||||
|
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
|
||||||
|
const CreateOptionButton = () => {
|
||||||
|
const {
|
||||||
|
optionsActions: {
|
||||||
|
isCreatingOption,
|
||||||
|
createOptionForm,
|
||||||
|
onStartCreating,
|
||||||
|
onFinishCreating,
|
||||||
|
},
|
||||||
|
} = useSelectEditorContext();
|
||||||
|
|
||||||
|
if (!isCreatingOption) {
|
||||||
|
return (
|
||||||
|
<Flex flex={1}>
|
||||||
|
<InlineButton
|
||||||
|
fullWidth
|
||||||
|
onClick={onStartCreating}>
|
||||||
|
Добавить опцию
|
||||||
|
</InlineButton>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
flex={1}>
|
||||||
|
<TextInput
|
||||||
|
{...createOptionForm.getInputProps("name")}
|
||||||
|
flex={1}
|
||||||
|
placeholder={"Название"}
|
||||||
|
/>
|
||||||
|
<ActionIconWithTip
|
||||||
|
tipLabel={"Сохранить"}
|
||||||
|
onClick={onFinishCreating}>
|
||||||
|
<IconCheck />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateOptionButton;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Divider, Flex } from "@mantine/core";
|
||||||
|
import CommonInfoEditor from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CommonInfoEditor";
|
||||||
|
import CreateOptionButton from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CreateOptionButton";
|
||||||
|
import OptionsTable from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionsTable";
|
||||||
|
|
||||||
|
const EditorBody = () => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
<CommonInfoEditor />
|
||||||
|
<Divider
|
||||||
|
label={"Опции"}
|
||||||
|
my={"xs"}
|
||||||
|
/>
|
||||||
|
<CreateOptionButton />
|
||||||
|
<OptionsTable />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditorBody;
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { IconCheck, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
|
import { Divider, Flex, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
||||||
|
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||||
|
import { AttrOptionSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
option: AttrOptionSchema;
|
||||||
|
renderDraggable?: (item: AttrOptionSchema) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionTableRow: FC<Props> = ({ option, renderDraggable }) => {
|
||||||
|
const {
|
||||||
|
optionsActions: {
|
||||||
|
onStartEditing,
|
||||||
|
onFinishEditing,
|
||||||
|
onDelete,
|
||||||
|
editingOptionsData,
|
||||||
|
setEditingOptionsData,
|
||||||
|
},
|
||||||
|
} = useSelectEditorContext();
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
optionId: number
|
||||||
|
) => {
|
||||||
|
setEditingOptionsData(prev => {
|
||||||
|
prev.set(optionId, e.currentTarget.value);
|
||||||
|
return new Map(prev);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={"xs"}
|
||||||
|
mt={"xs"}>
|
||||||
|
<Group
|
||||||
|
wrap={"nowrap"}
|
||||||
|
justify={"space-between"}>
|
||||||
|
<Group wrap={"nowrap"}>
|
||||||
|
{renderDraggable && renderDraggable(option)}
|
||||||
|
{editingOptionsData.has(option.id) ? (
|
||||||
|
<TextInput
|
||||||
|
value={editingOptionsData.get(option.id)}
|
||||||
|
onChange={e => onChange(e, option.id)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
option.name
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Flex
|
||||||
|
justify={"center"}
|
||||||
|
gap={"xs"}>
|
||||||
|
{editingOptionsData.has(option.id) ? (
|
||||||
|
<ActionIconWithTip
|
||||||
|
onClick={() => onFinishEditing(option)}
|
||||||
|
tipLabel={"Сохранить"}>
|
||||||
|
<IconCheck />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
) : (
|
||||||
|
<ActionIconWithTip
|
||||||
|
onClick={() => onStartEditing(option)}
|
||||||
|
tipLabel={"Редактировать"}>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ActionIconWithTip
|
||||||
|
color={"red"}
|
||||||
|
onClick={() => onDelete(option)}
|
||||||
|
tipLabel={"Удалить"}>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
</Flex>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OptionTableRow;
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IconGripVertical } from "@tabler/icons-react";
|
||||||
|
import { Box, Divider, Stack } from "@mantine/core";
|
||||||
|
import OptionTableRow from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionTableRow";
|
||||||
|
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
||||||
|
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||||
|
|
||||||
|
const OptionsTable = () => {
|
||||||
|
const { options } = useSelectEditorContext();
|
||||||
|
const { onDragEnd } = useSelectEditorContext();
|
||||||
|
|
||||||
|
const renderDraggable = () => (
|
||||||
|
<Box p={"xs"}>
|
||||||
|
<IconGripVertical />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Divider />
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={options}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
renderItem={(item, renderDraggable) => (
|
||||||
|
<OptionTableRow
|
||||||
|
option={item}
|
||||||
|
renderDraggable={renderDraggable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderDraggable={renderDraggable}
|
||||||
|
dragHandleStyle={{ width: "auto" }}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OptionsTable;
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import useAttrOptionsList from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsList";
|
||||||
|
import {
|
||||||
|
AttrOptionSchema,
|
||||||
|
AttrSelectSchema,
|
||||||
|
UpdateAttrSelectSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
import useOptionsActions, { OptionsActions } from "../hooks/useOptionsActions";
|
||||||
|
|
||||||
|
type SelectEditorContextState = {
|
||||||
|
select: AttrSelectSchema;
|
||||||
|
onSelectChange: (values: UpdateAttrSelectSchema) => void;
|
||||||
|
options: AttrOptionSchema[];
|
||||||
|
optionsActions: OptionsActions;
|
||||||
|
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
select: AttrSelectSchema;
|
||||||
|
onSelectChange: (
|
||||||
|
values: UpdateAttrSelectSchema,
|
||||||
|
onSuccess: () => void
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectEditorContextState = ({
|
||||||
|
select,
|
||||||
|
onSelectChange,
|
||||||
|
}: Props): SelectEditorContextState => {
|
||||||
|
const { options, queryKey } = useAttrOptionsList({ selectId: select.id });
|
||||||
|
|
||||||
|
const optionsActions = useOptionsActions({ queryKey, select, options });
|
||||||
|
|
||||||
|
const onSelectChangeWithMsg = (values: UpdateAttrSelectSchema) => {
|
||||||
|
onSelectChange(values, () => {
|
||||||
|
notifications.success({
|
||||||
|
message: "Название справочника сохранено",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||||
|
optionsActions.onUpdate(itemId, { lexorank: newLexorank });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
select,
|
||||||
|
onSelectChange: onSelectChangeWithMsg,
|
||||||
|
options,
|
||||||
|
optionsActions,
|
||||||
|
onDragEnd,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [SelectEditorContextProvider, useSelectEditorContext] =
|
||||||
|
makeContext<SelectEditorContextState, Props>(
|
||||||
|
useSelectEditorContextState,
|
||||||
|
"SelectEditor"
|
||||||
|
);
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
||||||
|
import {
|
||||||
|
AttrOptionSchema,
|
||||||
|
CreateAttrOptionSchema,
|
||||||
|
UpdateAttrOptionSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
createAttrOptionMutation,
|
||||||
|
deleteAttrOptionMutation,
|
||||||
|
updateAttrOptionMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { getNewLexorank } from "@/utils/lexorank/generation";
|
||||||
|
import { getMaxByLexorank } from "@/utils/lexorank/max";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryKey: any[];
|
||||||
|
options: AttrOptionSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AttrOptionsCrud = {
|
||||||
|
onCreate: (
|
||||||
|
data: Partial<CreateAttrOptionSchema>,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => void;
|
||||||
|
onUpdate: (
|
||||||
|
optionId: number,
|
||||||
|
option: UpdateAttrOptionSchema,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => void;
|
||||||
|
onDelete: (option: AttrOptionSchema, onSuccess?: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAttrOptionsCrud = ({
|
||||||
|
queryKey,
|
||||||
|
options,
|
||||||
|
}: Props): AttrOptionsCrud => {
|
||||||
|
return useCrudOperations<
|
||||||
|
AttrOptionSchema,
|
||||||
|
UpdateAttrOptionSchema,
|
||||||
|
CreateAttrOptionSchema
|
||||||
|
>({
|
||||||
|
key: "getAttrOptions",
|
||||||
|
queryKey,
|
||||||
|
mutations: {
|
||||||
|
create: createAttrOptionMutation(),
|
||||||
|
update: updateAttrOptionMutation(),
|
||||||
|
delete: deleteAttrOptionMutation(),
|
||||||
|
},
|
||||||
|
getCreateEntity: data => {
|
||||||
|
const lastOption = getMaxByLexorank(options);
|
||||||
|
const newLexorank = getNewLexorank(
|
||||||
|
lastOption ? LexoRank.parse(lastOption.lexorank) : null
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: data.name!,
|
||||||
|
selectId: data.selectId!,
|
||||||
|
lexorank: newLexorank.toString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getUpdateEntity: (old, update) => ({
|
||||||
|
...old,
|
||||||
|
name: update.name ?? old.name,
|
||||||
|
lexorank: update.lexorank ?? old.lexorank,
|
||||||
|
}),
|
||||||
|
getDeleteConfirmTitle: () => "Удаление опции",
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AttrOptionSchema } from "@/lib/client";
|
||||||
|
import {
|
||||||
|
getAttrOptionsOptions,
|
||||||
|
getAttrOptionsQueryKey,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAttrOptionsList = ({ selectId }: Props) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const options = { path: { selectId } };
|
||||||
|
const { data, refetch } = useQuery(getAttrOptionsOptions(options));
|
||||||
|
|
||||||
|
const queryKey = getAttrOptionsQueryKey(options);
|
||||||
|
|
||||||
|
const setOptions = (options: AttrOptionSchema[]) => {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
queryKey,
|
||||||
|
(old: { items: AttrOptionSchema[] }) => ({
|
||||||
|
...old,
|
||||||
|
items: options,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: sortByLexorank(data?.items ?? []),
|
||||||
|
setOptions,
|
||||||
|
refetch,
|
||||||
|
queryKey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttrOptionsList;
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
AttrOptionSchema,
|
||||||
|
AttrSelectSchema,
|
||||||
|
CreateAttrOptionSchema,
|
||||||
|
UpdateAttrOptionSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
import { useAttrOptionsCrud } from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsCrud";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryKey: any[];
|
||||||
|
select: AttrSelectSchema;
|
||||||
|
options: AttrOptionSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OptionsActions = {
|
||||||
|
isCreatingOption: boolean;
|
||||||
|
createOptionForm: UseFormReturnType<CreateAttrOptionSchema>;
|
||||||
|
onStartCreating: () => void;
|
||||||
|
onFinishCreating: () => void;
|
||||||
|
editingOptionsData: Map<number, string>;
|
||||||
|
setEditingOptionsData: Dispatch<SetStateAction<Map<number, string>>>;
|
||||||
|
onStartEditing: (option: AttrOptionSchema) => void;
|
||||||
|
onFinishEditing: (option: AttrOptionSchema) => void;
|
||||||
|
onUpdate: (optionId: number, data: UpdateAttrOptionSchema) => void;
|
||||||
|
onDelete: (option: AttrOptionSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useOptionsActions = ({ queryKey, select, options }: Props) => {
|
||||||
|
const [isCreatingOption, setIsCreatingOption] = useState<boolean>(false);
|
||||||
|
const [editingOptionsData, setEditingOptionsData] = useState<
|
||||||
|
Map<number, string>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
const createOptionForm = useForm<CreateAttrOptionSchema>({
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
lexorank: "",
|
||||||
|
selectId: select.id,
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: name => !name && "Введите название",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const optionCrud = useAttrOptionsCrud({ queryKey, options });
|
||||||
|
|
||||||
|
const onStartCreating = () => {
|
||||||
|
setIsCreatingOption(true);
|
||||||
|
};
|
||||||
|
const onFinishCreating = () => {
|
||||||
|
if (createOptionForm.validate().hasErrors) return;
|
||||||
|
optionCrud.onCreate(createOptionForm.values, () => {
|
||||||
|
notifications.success({ message: "Опция успешно создана" });
|
||||||
|
createOptionForm.reset();
|
||||||
|
setIsCreatingOption(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartEditing = (option: AttrOptionSchema) => {
|
||||||
|
setEditingOptionsData(prev => {
|
||||||
|
prev.set(option.id, option.name);
|
||||||
|
return new Map(prev);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onFinishEditing = (option: AttrOptionSchema) => {
|
||||||
|
if (!editingOptionsData.has(option.id)) return;
|
||||||
|
|
||||||
|
const newName = editingOptionsData.get(option.id);
|
||||||
|
if (!newName) {
|
||||||
|
notifications.error({ message: "Название не может быть пустым" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
optionCrud.onUpdate(option.id, { ...option, name: newName }, () => {
|
||||||
|
notifications.success({ message: "Опция сохранена" });
|
||||||
|
setEditingOptionsData(prev => {
|
||||||
|
prev.delete(option.id);
|
||||||
|
return new Map(prev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (option: AttrOptionSchema) => {
|
||||||
|
optionCrud.onDelete(option, () =>
|
||||||
|
notifications.success({ message: "Опция удалена" })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = optionCrud.onUpdate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCreatingOption,
|
||||||
|
createOptionForm,
|
||||||
|
onStartCreating,
|
||||||
|
onFinishCreating,
|
||||||
|
editingOptionsData,
|
||||||
|
setEditingOptionsData,
|
||||||
|
onStartEditing,
|
||||||
|
onFinishEditing,
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOptionsActions;
|
||||||
72
src/app/attributes/hooks/useAttributesTableColumns.tsx
Normal file
72
src/app/attributes/hooks/useAttributesTableColumns.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Box, Center } from "@mantine/core";
|
||||||
|
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
|
||||||
|
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
import { useAttributesContext } from "../contexts/AttributesContext";
|
||||||
|
|
||||||
|
const useAttributesTableColumns = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { attributesActions } = useAttributesContext();
|
||||||
|
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Название атрибута",
|
||||||
|
accessor: "label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Тип",
|
||||||
|
accessor: "type.name",
|
||||||
|
render: attr =>
|
||||||
|
attr.type.type === "select"
|
||||||
|
? `Выбор "${attr.label}"`
|
||||||
|
: attr.type.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Значение по умолчанию",
|
||||||
|
accessor: "defaultValue",
|
||||||
|
render: attr => <AttributeDefaultValue attribute={attr} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: isMobile
|
||||||
|
? "Синх. в группе"
|
||||||
|
: "Синхронизировано в группе",
|
||||||
|
accessor: "isApplicableToGroup",
|
||||||
|
render: attr => renderCheck(attr.isApplicableToGroup),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Может быть пустым",
|
||||||
|
accessor: "isNullable",
|
||||||
|
render: attr => renderCheck(attr.isNullable),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Описаниие",
|
||||||
|
accessor: "description",
|
||||||
|
render: attr => <Box>{attr.description}</Box>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: attribute => (
|
||||||
|
<AttributeTableActions
|
||||||
|
attribute={attribute}
|
||||||
|
onUpdate={attributesActions.onUpdate}
|
||||||
|
onDelete={attributesActions.onDelete}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] as DataTableColumn<AttributeSchema>[],
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttributesTableColumns;
|
||||||
29
src/app/attributes/hooks/useFilteredAttributes.ts
Normal file
29
src/app/attributes/hooks/useFilteredAttributes.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributes: AttributeSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFilteredAttributes = ({ attributes }: Props) => {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
const filteredAttributes = useMemo(
|
||||||
|
() =>
|
||||||
|
attributes.filter(
|
||||||
|
attr =>
|
||||||
|
attr.type.name.includes(search) ||
|
||||||
|
attr.label.includes(search) ||
|
||||||
|
attr.description.includes(search)
|
||||||
|
),
|
||||||
|
[attributes, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
filteredAttributes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFilteredAttributes;
|
||||||
23
src/app/attributes/hooks/useFilteredSelects.ts
Normal file
23
src/app/attributes/hooks/useFilteredSelects.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { AttrSelectSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selects: AttrSelectSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFilteredSelects = ({ selects }: Props) => {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
|
const filteredSelects = useMemo(
|
||||||
|
() => selects.filter(s => s.name.includes(search)),
|
||||||
|
[selects, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
filteredSelects,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFilteredSelects;
|
||||||
50
src/app/attributes/hooks/useSelectsActions.tsx
Normal file
50
src/app/attributes/hooks/useSelectsActions.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
import { useAttrSelectsCrud } from "@/hooks/cruds/useSelectsCrud";
|
||||||
|
import { AttrSelectSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryKey: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectsActions = {
|
||||||
|
onCreate: () => void;
|
||||||
|
onUpdate: (select: AttrSelectSchema) => void;
|
||||||
|
onDelete: (select: AttrSelectSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectsActions = (props: Props): SelectsActions => {
|
||||||
|
const attrSelectsCrud = useAttrSelectsCrud(props);
|
||||||
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
|
const onCreate = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "enterNameModal",
|
||||||
|
title: "Создание справочника",
|
||||||
|
innerProps: {
|
||||||
|
onChange: values => attrSelectsCrud.onCreate(values),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = (select: AttrSelectSchema) => {
|
||||||
|
openDrawer({
|
||||||
|
key: "attrSelectEditorDrawer",
|
||||||
|
props: {
|
||||||
|
onSelectChange: (values, onSuccess) =>
|
||||||
|
attrSelectsCrud.onUpdate(select.id, values, onSuccess),
|
||||||
|
select,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = attrSelectsCrud.onDelete;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSelectsActions;
|
||||||
38
src/app/attributes/hooks/useSelectsTableColumns.tsx
Normal file
38
src/app/attributes/hooks/useSelectsTableColumns.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Center } from "@mantine/core";
|
||||||
|
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { AttrSelectSchema } from "@/lib/client";
|
||||||
|
import { useAttributesContext } from "../contexts/AttributesContext";
|
||||||
|
|
||||||
|
const useSelectsTableColumns = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { selectsActions } = useAttributesContext();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Название справочника",
|
||||||
|
accessor: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: select => (
|
||||||
|
<UpdateDeleteTableActions
|
||||||
|
onDelete={() => selectsActions.onDelete(select)}
|
||||||
|
onChange={() => selectsActions.onUpdate(select)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] as DataTableColumn<AttrSelectSchema>[],
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSelectsTableColumns;
|
||||||
22
src/app/attributes/page.tsx
Normal file
22
src/app/attributes/page.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Center, Loader } from "@mantine/core";
|
||||||
|
import PageBody from "@/app/attributes/components/PageBody";
|
||||||
|
import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext";
|
||||||
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
|
|
||||||
|
export default async function AttributesPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Center h="50vh">
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
}>
|
||||||
|
<PageContainer>
|
||||||
|
<AttributesContextProvider>
|
||||||
|
<PageBody />
|
||||||
|
</AttributesContextProvider>
|
||||||
|
</PageContainer>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/app/attributes/types/view.ts
Normal file
6
src/app/attributes/types/view.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
enum AttributePageView {
|
||||||
|
ATTRIBUTES,
|
||||||
|
SELECTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttributePageView;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
||||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
import { Box, Group, Stack, Title } from "@mantine/core";
|
||||||
import Boards from "@/app/deals/components/shared/Boards/Boards";
|
import Boards from "@/app/deals/components/shared/Boards/Boards";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
@ -51,7 +51,7 @@ const MainBlockHeader = () => {
|
|||||||
onClick={openProjectsEditorDrawer}>
|
onClick={openProjectsEditorDrawer}>
|
||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</Box>
|
</Box>
|
||||||
<Text>{selectedProject?.name}</Text>
|
<Title order={6}>{selectedProject?.name}</Title>
|
||||||
<Box
|
<Box
|
||||||
p={"md"}
|
p={"md"}
|
||||||
onClick={openBoardsEditorDrawer}>
|
onClick={openBoardsEditorDrawer}>
|
||||||
|
|||||||
@ -10,10 +10,12 @@ import {
|
|||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { DealsContextProvider } from "@/app/deals/contexts/DealsContext";
|
import { DealsContextProvider } from "@/app/deals/contexts/DealsContext";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import useCreateFirstProject from "@/app/deals/hooks/useCreateFirstProject";
|
||||||
import useView from "@/app/deals/hooks/useView";
|
import useView from "@/app/deals/hooks/useView";
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
|
||||||
const PageBody = () => {
|
const PageBody = () => {
|
||||||
|
useCreateFirstProject();
|
||||||
const { selectedBoard } = useBoardsContext();
|
const { selectedBoard } = useBoardsContext();
|
||||||
const { selectedProject } = useProjectsContext();
|
const { selectedProject } = useProjectsContext();
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ const PageBody = () => {
|
|||||||
if (view === "table") {
|
if (view === "table") {
|
||||||
return { withPagination: true, projectId: selectedProject?.id };
|
return { withPagination: true, projectId: selectedProject?.id };
|
||||||
}
|
}
|
||||||
return { boardId: selectedBoard?.id };
|
return { boardId: selectedBoard?.id, projectId: selectedProject?.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -54,6 +54,7 @@ const useDealsContextState = ({
|
|||||||
|
|
||||||
const dealsCrud = useDealsCrud({
|
const dealsCrud = useDealsCrud({
|
||||||
...dealsListObjects,
|
...dealsListObjects,
|
||||||
|
projectId,
|
||||||
boardId,
|
boardId,
|
||||||
statuses,
|
statuses,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,16 +10,22 @@ import makeContext from "@/lib/contextFactory/contextFactory";
|
|||||||
import { ModuleNames } from "@/modules/modules";
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
|
||||||
type ProjectsContextState = {
|
type ProjectsContextState = {
|
||||||
|
projects: ProjectSchema[];
|
||||||
|
isLoading: boolean;
|
||||||
selectedProject: ProjectSchema | null;
|
selectedProject: ProjectSchema | null;
|
||||||
setSelectedProjectId: (id: number | null) => void;
|
setSelectedProjectId: (id: number | null) => void;
|
||||||
refetchProjects: () => void;
|
refetchProjects: () => void;
|
||||||
projects: ProjectSchema[];
|
|
||||||
projectsCrud: ProjectsCrud;
|
projectsCrud: ProjectsCrud;
|
||||||
modulesSet: Set<ModuleNames>;
|
modulesSet: Set<ModuleNames>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useProjectsContextState = (): ProjectsContextState => {
|
const useProjectsContextState = (): ProjectsContextState => {
|
||||||
const { projects, refetch: refetchProjects, queryKey } = useProjectsList();
|
const {
|
||||||
|
projects,
|
||||||
|
refetch: refetchProjects,
|
||||||
|
queryKey,
|
||||||
|
isLoading,
|
||||||
|
} = useProjectsList();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -34,10 +40,7 @@ const useProjectsContextState = (): ProjectsContextState => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const modulesSet = useMemo(
|
const modulesSet = useMemo(
|
||||||
() =>
|
() => new Set(selectedProject?.modules.map(m => m.key as ModuleNames)),
|
||||||
new Set(
|
|
||||||
selectedProject?.builtInModules.map(m => m.key as ModuleNames)
|
|
||||||
),
|
|
||||||
[selectedProject]
|
[selectedProject]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ const useProjectsContextState = (): ProjectsContextState => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
isLoading,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
refetchProjects,
|
refetchProjects,
|
||||||
setSelectedProjectId: handleSetSelectedProjectId,
|
setSelectedProjectId: handleSetSelectedProjectId,
|
||||||
|
|||||||
@ -18,8 +18,8 @@ const BoardsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
size={"100%"}
|
size={"50%"}
|
||||||
position={"right"}
|
position={"left"}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
@ -30,7 +30,6 @@ const BoardsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "xs",
|
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<BoardsMobileContextProvider project={project}>
|
<BoardsMobileContextProvider project={project}>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||||
import { Box, Center, Divider, Group, Text } from "@mantine/core";
|
import { Box, Center, Divider, Group, Stack, Title } from "@mantine/core";
|
||||||
import BoardMobile from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/BoardMobile";
|
import BoardMobile from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/BoardMobile";
|
||||||
import CreateBoardButton from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/CreateBoardButton";
|
import CreateBoardButton from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/CreateBoardButton";
|
||||||
import { useBoardsMobileContext } from "@/app/deals/drawers/BoardsMobileEditorDrawer/contexts/BoardsMobileContext";
|
import { useBoardsMobileContext } from "@/app/deals/drawers/BoardsMobileEditorDrawer/contexts/BoardsMobileContext";
|
||||||
@ -37,7 +37,7 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack gap={"xs"}>
|
||||||
<Group justify={"space-between"}>
|
<Group justify={"space-between"}>
|
||||||
<Box
|
<Box
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -45,7 +45,7 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</Box>
|
</Box>
|
||||||
<Center>
|
<Center>
|
||||||
<Text>{project.name}</Text>
|
<Title order={6}>{project.name}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
<Box p={"lg"} />
|
<Box p={"lg"} />
|
||||||
</Group>
|
</Group>
|
||||||
@ -58,8 +58,9 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
dragHandleStyle={{ width: "auto" }}
|
dragHandleStyle={{ width: "auto" }}
|
||||||
vertical
|
vertical
|
||||||
/>
|
/>
|
||||||
|
{boards.length > 0 && <Divider />}
|
||||||
<CreateBoardButton onCreateBoard={boardsCrud.onCreate} />
|
<CreateBoardButton onCreateBoard={boardsCrud.onCreate} />
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { omit } from "lodash";
|
||||||
|
import ObjectSelect from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import useAttributeOptionsList from "@/hooks/lists/useAttributeOptionsList";
|
||||||
|
import { AttrOptionSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: number;
|
||||||
|
onChange: (val: any) => void;
|
||||||
|
selectId: number;
|
||||||
|
error?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttrOptionSelect = (props: Props) => {
|
||||||
|
const { options } = useAttributeOptionsList(props);
|
||||||
|
|
||||||
|
const [selectedOption, setSelectedOption] = useState<AttrOptionSchema>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.value) {
|
||||||
|
setSelectedOption(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedOption(options.find(option => option.id === props.value));
|
||||||
|
}, [props.value, options]);
|
||||||
|
|
||||||
|
const restProps = omit(props, ["value, onChange", "selectId"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
label={"Значение"}
|
||||||
|
{...restProps}
|
||||||
|
data={options}
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={option => {
|
||||||
|
setSelectedOption(option);
|
||||||
|
props.onChange(option.id);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setSelectedOption(undefined);
|
||||||
|
props.onChange(null);
|
||||||
|
}}
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttrOptionSelect;
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { CSSProperties, FC, JSX } from "react";
|
||||||
|
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||||
|
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||||
|
import AttrOptionSelect from "@/app/deals/drawers/DealEditorDrawer/components/AttrOptionSelect";
|
||||||
|
import { DealModuleAttributeSchema } from "@/lib/client";
|
||||||
|
import { naiveDateTimeStringToUtc } from "@/utils/datetime";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attrInfo: DealModuleAttributeSchema;
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
error?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeValueInput: FC<Props> = ({
|
||||||
|
attrInfo,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
style,
|
||||||
|
}) => {
|
||||||
|
const commonProps = {
|
||||||
|
label: attrInfo.label,
|
||||||
|
style,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCheckbox = () => (
|
||||||
|
<Checkbox
|
||||||
|
{...commonProps}
|
||||||
|
checked={Boolean(value)}
|
||||||
|
onChange={e => onChange(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDatePicker = () => (
|
||||||
|
<DatePickerInput
|
||||||
|
{...commonProps}
|
||||||
|
value={value ? new Date(String(value)) : null}
|
||||||
|
onChange={val => {
|
||||||
|
onChange(val ? new Date(val) : null);
|
||||||
|
}}
|
||||||
|
clearable
|
||||||
|
locale={"ru"}
|
||||||
|
valueFormat={"DD.MM.YYYY"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDateTimePicker = () => (
|
||||||
|
<DateTimePicker
|
||||||
|
{...commonProps}
|
||||||
|
value={value ? naiveDateTimeStringToUtc(value) : null}
|
||||||
|
onChange={val => {
|
||||||
|
if (!val) return;
|
||||||
|
const localDate = new Date(val.replace(" ", "T"));
|
||||||
|
const utcString = localDate
|
||||||
|
.toISOString()
|
||||||
|
.substring(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
onChange(utcString);
|
||||||
|
}}
|
||||||
|
clearable
|
||||||
|
locale={"ru"}
|
||||||
|
valueFormat={"DD.MM.YYYY HH:mm"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTextInput = () => (
|
||||||
|
<TextInput
|
||||||
|
{...commonProps}
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={e => onChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNumberInput = () => (
|
||||||
|
<NumberInput
|
||||||
|
{...commonProps}
|
||||||
|
allowDecimal={attrInfo.type.type === "float"}
|
||||||
|
value={value ? Number(value) : undefined}
|
||||||
|
onChange={value => onChange(Number(value))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSelect = () => {
|
||||||
|
if (!attrInfo.select?.id) return <></>;
|
||||||
|
return (
|
||||||
|
<AttrOptionSelect
|
||||||
|
{...commonProps}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
selectId={attrInfo.select.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderingFuncMap: Record<string, () => JSX.Element> = {
|
||||||
|
bool: renderCheckbox,
|
||||||
|
date: renderDatePicker,
|
||||||
|
datetime: renderDateTimePicker,
|
||||||
|
str: renderTextInput,
|
||||||
|
int: renderNumberInput,
|
||||||
|
float: renderNumberInput,
|
||||||
|
select: renderSelect,
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = renderingFuncMap[attrInfo.type.type];
|
||||||
|
return render();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeValueInput;
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import { IconEdit, IconHistory } from "@tabler/icons-react";
|
import { IconBlocks, IconEdit, IconHistory } from "@tabler/icons-react";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { Box, Tabs, Text } from "@mantine/core";
|
import { Box, Tabs, Text } from "@mantine/core";
|
||||||
|
import DealEditorTabPanel from "@/app/deals/drawers/DealEditorDrawer/components/DealEditorTabPanel";
|
||||||
import TabsList from "@/app/deals/drawers/DealEditorDrawer/components/TabsList";
|
import TabsList from "@/app/deals/drawers/DealEditorDrawer/components/TabsList";
|
||||||
|
import CustomTab from "@/app/deals/drawers/DealEditorDrawer/tabs/CustomTab/CustomTab";
|
||||||
import DealStatusHistoryTab from "@/app/deals/drawers/DealEditorDrawer/tabs/DealStatusHistoryTab/DealStatusHistoryTab";
|
import DealStatusHistoryTab from "@/app/deals/drawers/DealEditorDrawer/tabs/DealStatusHistoryTab/DealStatusHistoryTab";
|
||||||
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab";
|
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab";
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
@ -20,23 +21,6 @@ type Props = {
|
|||||||
const DealEditorBody: FC<Props> = props => {
|
const DealEditorBody: FC<Props> = props => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const getTabPanel = (value: string, component: ReactNode): ReactNode => (
|
|
||||||
<Tabs.Panel
|
|
||||||
key={value}
|
|
||||||
value={value}>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.2 }}>
|
|
||||||
<Box
|
|
||||||
h={"100%"}
|
|
||||||
w={"100%"}>
|
|
||||||
{component}
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
</Tabs.Panel>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getTab = (key: string, label: string, icon: ReactNode) => (
|
const getTab = (key: string, label: string, icon: ReactNode) => (
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
key={key}
|
key={key}
|
||||||
@ -55,7 +39,14 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
if (!props.project) return [];
|
if (!props.project) return [];
|
||||||
const tabs: ReactNode[] = [];
|
const tabs: ReactNode[] = [];
|
||||||
|
|
||||||
for (const module of props.project.builtInModules) {
|
for (const module of props.project.modules) {
|
||||||
|
if (!module.isBuiltIn) {
|
||||||
|
for (const tab of module.tabs) {
|
||||||
|
tabs.push(getTab(tab.key, tab.label, <IconBlocks />));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const moduleInfo = MODULES[module.key];
|
const moduleInfo = MODULES[module.key];
|
||||||
for (const tab of moduleInfo.tabs) {
|
for (const tab of moduleInfo.tabs) {
|
||||||
if (
|
if (
|
||||||
@ -75,7 +66,24 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
if (!props.project) return [];
|
if (!props.project) return [];
|
||||||
const tabPanels: ReactNode[] = [];
|
const tabPanels: ReactNode[] = [];
|
||||||
|
|
||||||
for (const module of props.project.builtInModules) {
|
for (const module of props.project.modules) {
|
||||||
|
if (!module.isBuiltIn) {
|
||||||
|
const tabPanel = (
|
||||||
|
<DealEditorTabPanel
|
||||||
|
key={module.key}
|
||||||
|
value={module.key}>
|
||||||
|
<CustomTab
|
||||||
|
key={module.key}
|
||||||
|
moduleId={module.id}
|
||||||
|
deal={props.value}
|
||||||
|
/>
|
||||||
|
</DealEditorTabPanel>
|
||||||
|
);
|
||||||
|
|
||||||
|
tabPanels.push(tabPanel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const moduleInfo = MODULES[module.key];
|
const moduleInfo = MODULES[module.key];
|
||||||
for (const tab of moduleInfo.tabs) {
|
for (const tab of moduleInfo.tabs) {
|
||||||
if (
|
if (
|
||||||
@ -84,7 +92,14 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
tabPanels.push(getTabPanel(tab.key, tab.tab(props)));
|
const tabPanel = (
|
||||||
|
<DealEditorTabPanel
|
||||||
|
key={tab.key}
|
||||||
|
value={tab.key}>
|
||||||
|
{tab.tab(props)}
|
||||||
|
</DealEditorTabPanel>
|
||||||
|
);
|
||||||
|
tabPanels.push(tabPanel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,8 +120,12 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
{getModuleTabs()}
|
{getModuleTabs()}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{getTabPanel("general", <GeneralTab {...props} />)}
|
<DealEditorTabPanel value={"general"}>
|
||||||
{getTabPanel("history", <DealStatusHistoryTab {...props} />)}
|
<GeneralTab {...props} />
|
||||||
|
</DealEditorTabPanel>
|
||||||
|
<DealEditorTabPanel value={"history"}>
|
||||||
|
<DealStatusHistoryTab {...props} />
|
||||||
|
</DealEditorTabPanel>
|
||||||
{getModuleTabPanels()}
|
{getModuleTabPanels()}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import React, { FC, PropsWithChildren } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Box, Tabs } from "@mantine/core";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealEditorTabPanel: FC<PropsWithChildren<Props>> = ({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tabs.Panel
|
||||||
|
key={value}
|
||||||
|
value={value}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2 }}>
|
||||||
|
<Box
|
||||||
|
h={"100%"}
|
||||||
|
w={"100%"}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</Tabs.Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealEditorTabPanel;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import AttributeEditor from "@/components/ui/AttributesEditor/AttributesEditor";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
moduleId: number;
|
||||||
|
deal: DealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomTab: FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<AttributeEditor
|
||||||
|
{...props}
|
||||||
|
containerStyle={{
|
||||||
|
paddingBlock: "var(--mantine-spacing-xs)",
|
||||||
|
paddingInline: "var(--mantine-spacing-md)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomTab;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Center, Divider, Drawer, Text } from "@mantine/core";
|
import { Center, Divider, Drawer, Stack, Title } from "@mantine/core";
|
||||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import ProjectsDrawerBody from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/ProjectsDrawerBody";
|
import ProjectsDrawerBody from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/ProjectsDrawerBody";
|
||||||
import { DrawerProps } from "@/drawers/types";
|
import { DrawerProps } from "@/drawers/types";
|
||||||
@ -30,11 +30,11 @@ const ProjectsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "xs",
|
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Center>
|
<Stack gap={"xs"}>
|
||||||
<Text>Проекты</Text>
|
<Center p={"xs"}>
|
||||||
|
<Title order={6}>Проекты</Title>
|
||||||
</Center>
|
</Center>
|
||||||
<Divider />
|
<Divider />
|
||||||
<ProjectsContextProvider>
|
<ProjectsContextProvider>
|
||||||
@ -43,6 +43,7 @@ const ProjectsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
|
</Stack>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,9 +34,7 @@ const ProjectMobile: FC<Props> = ({ project, onSelect, closeDrawer }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
w={"100%"}
|
p={"xs"}
|
||||||
pl={"xs"}
|
|
||||||
py={"xs"}
|
|
||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
wrap={"nowrap"}
|
wrap={"nowrap"}
|
||||||
className={styles.project}
|
className={styles.project}
|
||||||
|
|||||||
@ -30,7 +30,6 @@ const StatusesMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "xs",
|
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<StatusesMobileContextProvider board={board}>
|
<StatusesMobileContextProvider board={board}>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Stack } from "@mantine/core";
|
import { Divider, Stack } from "@mantine/core";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import CreateProjectButton from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/CreateProjectButton";
|
import CreateProjectButton from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/CreateProjectButton";
|
||||||
import ProjectMobile from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/ProjectMobile";
|
import ProjectMobile from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/ProjectMobile";
|
||||||
@ -25,6 +25,7 @@ const ProjectsDrawerBody: FC<Props> = ({ onSelect, onClose }) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Divider />
|
||||||
<CreateProjectButton />
|
<CreateProjectButton />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React, { FC } from "react";
|
|||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
||||||
import { BoardSchema, StatusSchema } from "@/lib/client";
|
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const StatusMobile: FC<Props> = ({ status, board }) => {
|
const StatusMobile: FC<Props> = ({ status, board }) => {
|
||||||
const { statusesCrud } = useStatusesMobileContext();
|
const { statusesCrud } = useStatusesMobileContext();
|
||||||
const { groupDealsSelection } = useDealsContext();
|
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@ -46,7 +44,6 @@ const StatusMobile: FC<Props> = ({ status, board }) => {
|
|||||||
statusesCrud.onUpdate(status.id, { color })
|
statusesCrud.onUpdate(status.id, { color })
|
||||||
}
|
}
|
||||||
withChangeOrderButton={false}
|
withChangeOrderButton={false}
|
||||||
startDealsSelecting={groupDealsSelection.startSelecting}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||||
import { Box, Center, Divider, Group, Text } from "@mantine/core";
|
import { Box, Center, Divider, Group, Stack, Title } from "@mantine/core";
|
||||||
import CreateStatusButton from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/CreateStatusButton";
|
import CreateStatusButton from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/CreateStatusButton";
|
||||||
import StatusMobile from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/StatusMobile";
|
import StatusMobile from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/StatusMobile";
|
||||||
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
||||||
@ -39,7 +39,7 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
statusesCrud.onUpdate(itemId, { lexorank: newLexorank });
|
statusesCrud.onUpdate(itemId, { lexorank: newLexorank });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack gap={"xs"}>
|
||||||
<Group justify={"space-between"}>
|
<Group justify={"space-between"}>
|
||||||
<Box
|
<Box
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -47,7 +47,7 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</Box>
|
</Box>
|
||||||
<Center>
|
<Center>
|
||||||
<Text>{board.name}</Text>
|
<Title order={6}>{board.name}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
<Box p={"lg"} />
|
<Box p={"lg"} />
|
||||||
</Group>
|
</Group>
|
||||||
@ -60,8 +60,9 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
dragHandleStyle={{ width: "auto" }}
|
dragHandleStyle={{ width: "auto" }}
|
||||||
vertical
|
vertical
|
||||||
/>
|
/>
|
||||||
|
{statuses.length > 0 && <Divider />}
|
||||||
<CreateStatusButton />
|
<CreateStatusButton />
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
36
src/app/deals/hooks/useCreateFirstProject.tsx
Normal file
36
src/app/deals/hooks/useCreateFirstProject.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
|
||||||
|
const useCreateFirstProject = () => {
|
||||||
|
const hasOpened = useRef(false);
|
||||||
|
const { projects, projectsCrud, isLoading } = useProjectsContext();
|
||||||
|
const shouldOpen = useMemo(
|
||||||
|
() => !isLoading && projects.length === 0,
|
||||||
|
[isLoading, projects]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldOpen || hasOpened.current) return;
|
||||||
|
|
||||||
|
hasOpened.current = true;
|
||||||
|
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "createFirstProjectModal",
|
||||||
|
withCloseButton: false,
|
||||||
|
closeOnClickOutside: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
overlayProps: {
|
||||||
|
color: "black",
|
||||||
|
blur: 6,
|
||||||
|
backgroundOpacity: 0.45,
|
||||||
|
},
|
||||||
|
innerProps: {
|
||||||
|
onSubmit: values =>
|
||||||
|
projectsCrud.onCreate(values, modals.closeAll),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [shouldOpen]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCreateFirstProject;
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Space,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import Logo from "@/components/ui/Logo/Logo";
|
||||||
|
import { CreateProjectSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSubmit: (values: CreateProjectSchema, onSuccess?: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateFirstProjectModal = ({
|
||||||
|
id,
|
||||||
|
context,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const form = useForm<CreateProjectSchema>({
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: name =>
|
||||||
|
(!name || name.trim().length === 0) && "Введите название",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClose = () => context.closeModal(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(values =>
|
||||||
|
innerProps.onSubmit(values, onClose)
|
||||||
|
)}>
|
||||||
|
<Flex
|
||||||
|
gap={"xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
<Logo title={"Fulfillment & Delivery"} />
|
||||||
|
<Divider />
|
||||||
|
<Space />
|
||||||
|
<Center>
|
||||||
|
<Title order={4}>Создайте свой первый проект</Title>
|
||||||
|
</Center>
|
||||||
|
<Space />
|
||||||
|
<Divider />
|
||||||
|
<Space />
|
||||||
|
<TextInput
|
||||||
|
label={"Название"}
|
||||||
|
placeholder={"Введите название"}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
<Space />
|
||||||
|
<Button
|
||||||
|
variant={"filled"}
|
||||||
|
type={"submit"}>
|
||||||
|
<Text>Сохранить</Text>
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateFirstProjectModal;
|
||||||
@ -17,6 +17,8 @@ import { combineProviders } from "@/utils/combineProviders";
|
|||||||
|
|
||||||
async function prefetchData() {
|
async function prefetchData() {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
try {
|
||||||
const projectsData = await queryClient.fetchQuery(getProjectsOptions());
|
const projectsData = await queryClient.fetchQuery(getProjectsOptions());
|
||||||
|
|
||||||
const firstProjectId = projectsData.items?.[0]?.id;
|
const firstProjectId = projectsData.items?.[0]?.id;
|
||||||
@ -25,6 +27,9 @@ async function prefetchData() {
|
|||||||
await queryClient.prefetchQuery(
|
await queryClient.prefetchQuery(
|
||||||
getBoardsOptions({ path: { projectId: firstProjectId } })
|
getBoardsOptions({ path: { projectId: firstProjectId } })
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Prefetch failed, continuing without data:", e);
|
||||||
|
}
|
||||||
|
|
||||||
return queryClient;
|
return queryClient;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { theme } from "@/theme";
|
import { theme } from "@/theme";
|
||||||
import "@/app/global.css";
|
import "@/app/global.css";
|
||||||
import { ContextMenuProvider } from "mantine-contextmenu";
|
import { ContextMenuProvider } from "mantine-contextmenu";
|
||||||
|
import { DatesProvider } from "@mantine/dates";
|
||||||
import { ModalsProvider } from "@mantine/modals";
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
@ -71,6 +72,7 @@ export default function RootLayout({ children }: Props) {
|
|||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||||
modals={modals}>
|
modals={modals}>
|
||||||
|
<DatesProvider settings={{ locale: "ru" }}>
|
||||||
<DrawersContextProvider>
|
<DrawersContextProvider>
|
||||||
<ProjectsContextProvider>
|
<ProjectsContextProvider>
|
||||||
<AppShell
|
<AppShell
|
||||||
@ -96,6 +98,7 @@ export default function RootLayout({ children }: Props) {
|
|||||||
</AppShell>
|
</AppShell>
|
||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
</DrawersContextProvider>
|
</DrawersContextProvider>
|
||||||
|
</DatesProvider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
<Notifications position="bottom-right" />
|
<Notifications position="bottom-right" />
|
||||||
|
|||||||
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,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
|
||||||
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<AttributeSchema | null>,
|
||||||
|
"data" | "getLabelFn" | "getValueFn"
|
||||||
|
> & {
|
||||||
|
attributesToExclude?: AttributeSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeSelect: FC<Props> = ({ attributesToExclude, ...props }) => {
|
||||||
|
const { attributes } = useAttributesList();
|
||||||
|
|
||||||
|
const availableAttributes = useMemo(() => {
|
||||||
|
const attrIdsToExcludeSet = new Set(
|
||||||
|
attributesToExclude?.map(a => a.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return attributes.filter(a => !attrIdsToExcludeSet.has(a.id));
|
||||||
|
}, [attributes, attributesToExclude]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
searchable
|
||||||
|
placeholder={"Выберите статус"}
|
||||||
|
onClear={() => props.onChange(null)}
|
||||||
|
getLabelFn={(option: AttributeSchema) => option.label}
|
||||||
|
data={availableAttributes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeSelect;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { IconArrowLeft } from "@tabler/icons-react";
|
||||||
|
import { Box, Button, Group, Title } from "@mantine/core";
|
||||||
|
|
||||||
|
const MobileHeader = () => {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={"space-between"}
|
||||||
|
wrap={"nowrap"}>
|
||||||
|
<Button
|
||||||
|
variant={"default"}
|
||||||
|
onClick={() => redirect("/modules")}>
|
||||||
|
<IconArrowLeft />
|
||||||
|
</Button>
|
||||||
|
<Title order={5}>Редактирование модуля</Title>
|
||||||
|
<Box mx={"lg"} />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileHeader;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import { AttrSelectSchema } from "@/lib/client";
|
||||||
|
import useAttributeSelectsList from "./useAttributeSelectsList";
|
||||||
|
|
||||||
|
type Props = Omit<ObjectSelectProps<AttrSelectSchema>, "data" | "getLabelFn">;
|
||||||
|
|
||||||
|
const AttrSelectSelect = (props: Props) => {
|
||||||
|
const { selects } = useAttributeSelectsList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
label={"Объект для выбора"}
|
||||||
|
data={selects}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttrSelectSelect;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getAttrSelectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useAttributeSelectsList = () => {
|
||||||
|
const { data, refetch } = useQuery(getAttrSelectsOptions());
|
||||||
|
|
||||||
|
return {
|
||||||
|
selects: data?.items ?? [],
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttributeSelectsList;
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { IconArrowRight, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
|
import { Center, Flex } from "@mantine/core";
|
||||||
|
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attribute: AttributeSchema;
|
||||||
|
onUpdate: (attribute: AttributeSchema) => void;
|
||||||
|
onDelete: (attribute: AttributeSchema) => void;
|
||||||
|
addAttributeToModule?: (attribute: AttributeSchema) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeTableActions: FC<Props> = ({
|
||||||
|
attribute,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
addAttributeToModule,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Flex gap={"xs"}>
|
||||||
|
<ActionIconWithTip
|
||||||
|
onClick={() => onUpdate(attribute)}
|
||||||
|
disabled={attribute.isBuiltIn}
|
||||||
|
tipLabel={"Редактировать"}>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
<ActionIconWithTip
|
||||||
|
onClick={() => onDelete(attribute)}
|
||||||
|
disabled={attribute.isBuiltIn}
|
||||||
|
tipLabel={"Удалить"}>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
{addAttributeToModule && (
|
||||||
|
<ActionIconWithTip
|
||||||
|
onClick={() => addAttributeToModule(attribute)}
|
||||||
|
tipLabel={"Добавить в модуль"}>
|
||||||
|
<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,40 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
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, module } = useModuleEditorContext();
|
||||||
|
const unusedAttributes = useMemo(() => {
|
||||||
|
const usedAttrIds = new Set(module?.attributes.map(a => a.id));
|
||||||
|
return attributes.filter(a => !usedAttrIds.has(a.id));
|
||||||
|
}, [module, attributes]);
|
||||||
|
|
||||||
|
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={unusedAttributes}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
groups={undefined}
|
||||||
|
styles={{ table: { width: "100%" } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributesTable;
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
"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 { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const useAttributesTableColumns = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { attributeActions } = useModuleEditorContext();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Название",
|
||||||
|
accessor: "label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Тип",
|
||||||
|
accessor: "type.name",
|
||||||
|
render: attr =>
|
||||||
|
attr.type.type === "select"
|
||||||
|
? `Выбор "${attr.select?.name}"`
|
||||||
|
: attr.type.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: <Center>Действия</Center>,
|
||||||
|
width: "0%",
|
||||||
|
render: attribute => (
|
||||||
|
<AttributeTableActions
|
||||||
|
attribute={attribute}
|
||||||
|
{...attributeActions}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] 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,28 @@
|
|||||||
|
import ObjectSelect from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
import useAttributeOptionsList from "@/hooks/lists/useAttributeOptionsList";
|
||||||
|
import { AttrOptionSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: AttrOptionSchema | null;
|
||||||
|
onChange: (val: AttrOptionSchema | null) => void;
|
||||||
|
selectId: number;
|
||||||
|
error?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultAttrOptionSelect = ({ selectId, ...props }: Props) => {
|
||||||
|
const { options } = useAttributeOptionsList({ selectId });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
label={"Значение"}
|
||||||
|
{...props}
|
||||||
|
data={options}
|
||||||
|
onClear={() => props.onChange(null)}
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultAttrOptionSelect;
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||||
|
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
import DefaultAttrOptionSelect from "@/app/module-editor/[moduleId]/components/shared/DefaultAttrOptionSelect/DefaultAttrOptionSelect";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
import { naiveDateTimeStringToUtc } from "@/utils/datetime";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturnType<Partial<AttributeSchema>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultAttributeValueInput = ({ form }: Props) => {
|
||||||
|
const type = form.values.type?.type;
|
||||||
|
const label = "Значение по умолчанию";
|
||||||
|
const inputName = "defaultValue";
|
||||||
|
|
||||||
|
const value = form.getValues().defaultValue;
|
||||||
|
|
||||||
|
if (type === "bool") {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
label={label}
|
||||||
|
{...form.getInputProps(inputName, { type: "checkbox" })}
|
||||||
|
checked={value as boolean}
|
||||||
|
onChange={e =>
|
||||||
|
form.setFieldValue("defaultValue", e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === "date") {
|
||||||
|
return (
|
||||||
|
<DatePickerInput
|
||||||
|
label={label}
|
||||||
|
{...form.getInputProps(inputName)}
|
||||||
|
value={
|
||||||
|
form.values.defaultValue
|
||||||
|
? new Date(String(form.values.defaultValue))
|
||||||
|
: 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
|
||||||
|
? naiveDateTimeStringToUtc(
|
||||||
|
form.values.defaultValue as string
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={val => {
|
||||||
|
if (!val) return;
|
||||||
|
const localDate = new Date(val.replace(" ", "T"));
|
||||||
|
const utcString = localDate
|
||||||
|
.toISOString()
|
||||||
|
.substring(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
form.setFieldValue("defaultValue", utcString);
|
||||||
|
}}
|
||||||
|
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)}
|
||||||
|
onChange={value =>
|
||||||
|
form.setFieldValue("defaultValue", Number(value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === "select") {
|
||||||
|
if (!form.values.select?.id) return <></>;
|
||||||
|
return (
|
||||||
|
<DefaultAttrOptionSelect
|
||||||
|
label={"Значение по умолчанию"}
|
||||||
|
{...form.getInputProps("defaultOption")}
|
||||||
|
value={form.values.defaultOption}
|
||||||
|
onChange={value => {
|
||||||
|
form.setFieldValue("defaultOption", value);
|
||||||
|
form.setFieldValue("defaultOptionId", value?.id);
|
||||||
|
}}
|
||||||
|
selectId={form.values.select?.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultAttributeValueInput;
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { IconEditCircle, 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}{" "}
|
||||||
|
{attribute.select && `"${attribute.select.name}"`}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Group
|
||||||
|
justify={"end"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
w={"100%"}
|
||||||
|
gap={"xs"}>
|
||||||
|
<ActionIconWithTip
|
||||||
|
tipLabel={
|
||||||
|
"Редактировать название (только для данного модуля)"
|
||||||
|
}
|
||||||
|
onClick={() => onEditAttributeLabel(attribute)}>
|
||||||
|
<IconEditCircle />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
<ActionIconWithTip
|
||||||
|
tipLabel={"Удалить из модуля"}
|
||||||
|
onClick={() => removeAttributeFromModule(attribute)}>
|
||||||
|
<IconX />
|
||||||
|
</ActionIconWithTip>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleAttribute;
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Flex } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
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";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const ModuleAttributesEditor = () => {
|
||||||
|
const {
|
||||||
|
module,
|
||||||
|
attributeActions: { addAttributeToModule },
|
||||||
|
} = useModuleEditorContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddAttributeClick = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "addAttributeModal",
|
||||||
|
title: "Добавление атрибута",
|
||||||
|
withCloseButton: true,
|
||||||
|
innerProps: {
|
||||||
|
onSubmit: addAttributeToModule,
|
||||||
|
usedAttributes: module?.attributes ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={isMobile ? "md" : "xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
{isMobile && (
|
||||||
|
<InlineButton onClick={onAddAttributeClick}>
|
||||||
|
Добавить атрибут
|
||||||
|
</InlineButton>
|
||||||
|
)}
|
||||||
|
{getAttributesRows()}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleAttributesEditor;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { Button, Flex, Group, Textarea, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { ModuleInfo } from "../../../hooks/useModulesActions";
|
||||||
|
|
||||||
|
const ModuleCommonInfoEditor = () => {
|
||||||
|
const {
|
||||||
|
module,
|
||||||
|
moduleActions: { updateCommonInfo },
|
||||||
|
} = useModuleEditorContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
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"}
|
||||||
|
w={isMobile ? "100%" : "auto"}
|
||||||
|
disabled={!form.isDirty()}
|
||||||
|
type={"submit"}>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleCommonInfoEditor;
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Box, Divider, 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";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import MobileHeader from "@/app/module-editor/[moduleId]/components/mobile/MobileHeader/MobileHeader";
|
||||||
|
|
||||||
|
const PageBody = () => {
|
||||||
|
const { module } = useModuleEditorContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
if (!module) return;
|
||||||
|
|
||||||
|
const renderMobile = () => (
|
||||||
|
<Flex
|
||||||
|
m={"xs"}
|
||||||
|
gap={"xs"}
|
||||||
|
flex={2}
|
||||||
|
direction={"column"}>
|
||||||
|
<MobileHeader />
|
||||||
|
<Divider />
|
||||||
|
<Fieldset
|
||||||
|
flex={1}
|
||||||
|
legend={"Общие данные модуля"}>
|
||||||
|
{module && <ModuleCommonInfoEditor />}
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset
|
||||||
|
flex={3}
|
||||||
|
legend={"Атрибуты модуля"}>
|
||||||
|
<ModuleAttributesEditor />
|
||||||
|
</Fieldset>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDesktop = () => (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack h={"100%"}>
|
||||||
|
<PageBlock
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
fullScreenMobile>
|
||||||
|
{isMobile ? renderMobile() : renderDesktop()}
|
||||||
|
</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;
|
||||||
110
src/app/module-editor/[moduleId]/hooks/useAttributesActions.tsx
Normal file
110
src/app/module-editor/[moduleId]/hooks/useAttributesActions.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Text } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import useAttributesCrud from "@/app/module-editor/[moduleId]/hooks/useAttributesCrud";
|
||||||
|
import { AttributeSchema, ModuleSchemaOutput } from "@/lib/client";
|
||||||
|
|
||||||
|
export type AttributesActions = {
|
||||||
|
addAttributeToModule: (
|
||||||
|
attribute: AttributeSchema,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => 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 = (props: Props): AttributesActions => {
|
||||||
|
const crud = useAttributesCrud(props);
|
||||||
|
|
||||||
|
const addAttributeToModule = (
|
||||||
|
attribute: AttributeSchema,
|
||||||
|
onSuccess?: () => void
|
||||||
|
) => crud.onToggleAttributeInModule(attribute, true, onSuccess);
|
||||||
|
|
||||||
|
const removeAttributeFromModule = (attribute: AttributeSchema) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: "Удаление атрибута из модуля",
|
||||||
|
children: (
|
||||||
|
<Text>
|
||||||
|
Вы уверены, что хотите удалить атрибут "{attribute.label}"
|
||||||
|
из модуля?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: () => crud.onToggleAttributeInModule(attribute, false),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditAttributeLabel = (attribute: AttributeSchema) => {
|
||||||
|
if (!props.module) return;
|
||||||
|
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "enterNameModal",
|
||||||
|
title: "Редактирование имени атрибута в модуле",
|
||||||
|
withCloseButton: true,
|
||||||
|
innerProps: {
|
||||||
|
onChange: values =>
|
||||||
|
crud.onUpdateLabel(attribute.id, values.name),
|
||||||
|
value: { name: attribute.label },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "attributeEditorModal",
|
||||||
|
title: "Создание атрибута",
|
||||||
|
withCloseButton: true,
|
||||||
|
innerProps: {
|
||||||
|
onCreate: crud.onCreate,
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = (attribute: AttributeSchema) => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "attributeEditorModal",
|
||||||
|
title: "Редактирование атрибута",
|
||||||
|
withCloseButton: true,
|
||||||
|
innerProps: {
|
||||||
|
onChange: values => crud.onUpdate(attribute.id, values),
|
||||||
|
entity: attribute,
|
||||||
|
isEditing: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (attribute: AttributeSchema) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: "Удаление атрибута",
|
||||||
|
children: (
|
||||||
|
<Text>
|
||||||
|
Вы уверены, что хотите удалить атрибут "{attribute.label}"?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: () => crud.onDelete(attribute.id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addAttributeToModule,
|
||||||
|
removeAttributeFromModule,
|
||||||
|
onEditAttributeLabel,
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttributesActions;
|
||||||
181
src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx
Normal file
181
src/app/module-editor/[moduleId]/hooks/useAttributesCrud.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import {
|
||||||
|
AttributeSchema,
|
||||||
|
CreateAttributeSchema,
|
||||||
|
HttpValidationError,
|
||||||
|
ModuleSchemaOutput,
|
||||||
|
UpdateAttributeSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
addAttributeToModuleMutation,
|
||||||
|
createAttributeMutation,
|
||||||
|
deleteAttributeMutation,
|
||||||
|
removeAttributeFromModuleMutation,
|
||||||
|
updateAttributeLabelMutation,
|
||||||
|
updateAttributeMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
module?: ModuleSchemaOutput;
|
||||||
|
refetchModule?: () => void;
|
||||||
|
refetchAttributes: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttributesCrud = {
|
||||||
|
onToggleAttributeInModule: (
|
||||||
|
attribute: AttributeSchema,
|
||||||
|
isAdding: boolean,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
) => void;
|
||||||
|
onUpdateLabel: (attributeId: number, name: string) => void;
|
||||||
|
onCreate: (attribute: CreateAttributeSchema) => void;
|
||||||
|
onUpdate: (attributeId: number, values: UpdateAttributeSchema) => void;
|
||||||
|
onDelete: (attributeId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAttributesCrud = ({
|
||||||
|
module,
|
||||||
|
refetchModule,
|
||||||
|
refetchAttributes,
|
||||||
|
}: Props): AttributesCrud => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
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 removeGetDealModuleAttrQuery = () => {
|
||||||
|
if (!module) return;
|
||||||
|
|
||||||
|
queryClient.removeQueries({
|
||||||
|
predicate: query => {
|
||||||
|
const key = query.queryKey[0] as {
|
||||||
|
_id?: string;
|
||||||
|
path?: { moduleId?: number };
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
key?._id === "getDealModuleAttributes" &&
|
||||||
|
key?.path?.moduleId === module.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleAttributeInModule = (
|
||||||
|
attribute: AttributeSchema,
|
||||||
|
isAdding: boolean,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
) => {
|
||||||
|
if (!module) return;
|
||||||
|
const mutation = isAdding
|
||||||
|
? addAttrToModuleMutation
|
||||||
|
: removeAttrFromModuleMutation;
|
||||||
|
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
path: {
|
||||||
|
moduleId: module.id,
|
||||||
|
attributeId: attribute.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ message }) => {
|
||||||
|
notifications.success({ message });
|
||||||
|
refetchModule && refetchModule();
|
||||||
|
refetchAttributes();
|
||||||
|
removeGetDealModuleAttrQuery();
|
||||||
|
onSuccess && onSuccess();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAttrLabelMutation = useMutation({
|
||||||
|
...updateAttributeLabelMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: refetchModule,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdateLabel = (attributeId: number, name: string) => {
|
||||||
|
if (!module) return;
|
||||||
|
|
||||||
|
updateAttrLabelMutation.mutate({
|
||||||
|
body: {
|
||||||
|
moduleId: module.id,
|
||||||
|
attributeId,
|
||||||
|
label: name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAttrMutation = useMutation({
|
||||||
|
...createAttributeMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: refetchAttributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreate = (values: CreateAttributeSchema) => {
|
||||||
|
createAttrMutation.mutate({
|
||||||
|
body: {
|
||||||
|
entity: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAttrMutation = useMutation({
|
||||||
|
...updateAttributeMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchModule && refetchModule();
|
||||||
|
refetchAttributes();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdate = (attributeId: number, values: UpdateAttributeSchema) => {
|
||||||
|
updateAttrMutation.mutate({
|
||||||
|
path: {
|
||||||
|
pk: attributeId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
entity: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAttrMutation = useMutation({
|
||||||
|
...deleteAttributeMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchModule && refetchModule();
|
||||||
|
refetchAttributes();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDelete = (attributeId: number) =>
|
||||||
|
deleteAttrMutation.mutate({ path: { pk: attributeId } });
|
||||||
|
|
||||||
|
return {
|
||||||
|
onToggleAttributeInModule,
|
||||||
|
onUpdateLabel,
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttributesCrud;
|
||||||
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;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button, Stack } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import AttributeSelect from "@/app/module-editor/[moduleId]/components/mobile/AttributeSelect/AttributeSelect";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSubmit: (attribute: AttributeSchema, onSuccess?: () => void) => void;
|
||||||
|
usedAttributes: AttributeSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddAttrForm = {
|
||||||
|
attribute?: AttributeSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddAttributeModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const form = useForm<AddAttrForm>({
|
||||||
|
validate: {
|
||||||
|
attribute: attribute => !attribute && "Атрибут не выбран",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClose = () => context.closeContextModal(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(values =>
|
||||||
|
innerProps.onSubmit(values.attribute!, onClose)
|
||||||
|
)}>
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<AttributeSelect
|
||||||
|
attributesToExclude={innerProps.usedAttributes}
|
||||||
|
{...form.getInputProps("attribute")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type={"submit"}
|
||||||
|
variant={"default"}>
|
||||||
|
Добавить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddAttributeModal;
|
||||||
147
src/app/module-editor/[moduleId]/modals/AttributeEditorModal.tsx
Normal file
147
src/app/module-editor/[moduleId]/modals/AttributeEditorModal.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"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 AttrSelectSelect from "@/app/module-editor/[moduleId]/components/shared/AttrSelectSelect/AttrSelectSelect";
|
||||||
|
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<any>({
|
||||||
|
initialValues: innerProps.isEditing
|
||||||
|
? innerProps.entity
|
||||||
|
: ({
|
||||||
|
label: "",
|
||||||
|
name: "",
|
||||||
|
typeId: undefined,
|
||||||
|
type: undefined,
|
||||||
|
selectId: undefined,
|
||||||
|
select: undefined,
|
||||||
|
isApplicableToGroup: false,
|
||||||
|
isNullable: false,
|
||||||
|
defaultValue: null,
|
||||||
|
defaultOptionId: null,
|
||||||
|
defaultOption: null,
|
||||||
|
description: "",
|
||||||
|
} as Partial<AttributeSchema>),
|
||||||
|
validate: {
|
||||||
|
label: label => !label?.trim() && "Название не заполнено",
|
||||||
|
type: type => !type && "Тип атрибута не выбран",
|
||||||
|
isNullable: (isNullable, values) => {
|
||||||
|
if (
|
||||||
|
values.defaultValue === null &&
|
||||||
|
values.defaultOption === null &&
|
||||||
|
!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", 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 => {
|
||||||
|
if (type.type !== "select") {
|
||||||
|
form.setFieldValue("select", undefined);
|
||||||
|
form.setFieldValue("selectId", undefined);
|
||||||
|
}
|
||||||
|
form.setFieldValue("type", type);
|
||||||
|
form.setFieldValue("typeId", type.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{form.values.type?.type === "select" && (
|
||||||
|
<AttrSelectSelect
|
||||||
|
withAsterisk
|
||||||
|
searchable
|
||||||
|
disabled={innerProps.isEditing}
|
||||||
|
{...form.getInputProps("select")}
|
||||||
|
onChange={select => {
|
||||||
|
form.setFieldValue("select", select);
|
||||||
|
form.setFieldValue("selectId", select.id);
|
||||||
|
form.setFieldValue("defaultValue", null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Checkbox
|
||||||
|
label={"Значение синхронизировано в группе"}
|
||||||
|
{...form.getInputProps("isApplicableToGroup", {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/app/modules/components/InnerAttributesTable.tsx
Normal file
41
src/app/modules/components/InnerAttributesTable.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Center, Text } from "@mantine/core";
|
||||||
|
import useAttributesInnerTableColumns from "@/app/modules/hooks/useAttributesInnerTableColumns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import { ModuleAttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributes: ModuleAttributeSchema[];
|
||||||
|
moduleId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InnerAttributesTable: FC<Props> = ({ attributes, moduleId }) => {
|
||||||
|
const innerColumns = useAttributesInnerTableColumns();
|
||||||
|
|
||||||
|
if (attributes.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center my={"md"}>
|
||||||
|
<Text>В модуле нет атрибутов</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
key={moduleId}
|
||||||
|
withTableBorder
|
||||||
|
columns={innerColumns}
|
||||||
|
records={attributes}
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
groups={undefined}
|
||||||
|
styles={{
|
||||||
|
table: { width: "100%" },
|
||||||
|
header: { zIndex: 0 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InnerAttributesTable;
|
||||||
30
src/app/modules/components/ModulesHeader.tsx
Normal file
30
src/app/modules/components/ModulesHeader.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import { useModulesContext } from "@/app/modules/contexts/ModulesContext";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
|
||||||
|
const ModulesHeader: FC = () => {
|
||||||
|
const {
|
||||||
|
modulesActions: { onCreate },
|
||||||
|
} = useModulesContext();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
wrap={"nowrap"}
|
||||||
|
justify={"space-between"}
|
||||||
|
mt={isMobile ? "xs" : ""}
|
||||||
|
mx={isMobile ? "xs" : ""}>
|
||||||
|
<InlineButton
|
||||||
|
onClick={onCreate}
|
||||||
|
w={isMobile ? "100%" : ""}>
|
||||||
|
Создать модуль
|
||||||
|
</InlineButton>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModulesHeader;
|
||||||
46
src/app/modules/components/ModulesTable.tsx
Normal file
46
src/app/modules/components/ModulesTable.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { useModulesContext } from "@/app/modules/contexts/ModulesContext";
|
||||||
|
import useModulesTableColumns from "@/app/modules/hooks/useModulesTableColumns";
|
||||||
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import InnerAttributesTable from "@/app/modules/components/InnerAttributesTable";
|
||||||
|
|
||||||
|
const ModulesTable: FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { modules } = useModulesContext();
|
||||||
|
|
||||||
|
const [expandedModuleIds, setExpandedModuleIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const outerColumns = useModulesTableColumns({
|
||||||
|
expandedModuleIds,
|
||||||
|
setExpandedModuleIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTable
|
||||||
|
columns={outerColumns}
|
||||||
|
groups={undefined}
|
||||||
|
records={modules}
|
||||||
|
withTableBorder
|
||||||
|
verticalSpacing={"md"}
|
||||||
|
rowExpansion={{
|
||||||
|
allowMultiple: true,
|
||||||
|
expanded: {
|
||||||
|
recordIds: expandedModuleIds,
|
||||||
|
onRecordIdsChange: setExpandedModuleIds,
|
||||||
|
},
|
||||||
|
content: ({ record }) => (
|
||||||
|
<InnerAttributesTable
|
||||||
|
attributes={record.attributes}
|
||||||
|
moduleId={record.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
mx={isMobile ? "xs" : 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModulesTable;
|
||||||
26
src/app/modules/components/PageBody.tsx
Normal file
26
src/app/modules/components/PageBody.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ModulesTable from "@/app/modules/components/ModulesTable";
|
||||||
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
|
import ModulesHeader from "./ModulesHeader";
|
||||||
|
|
||||||
|
const PageBody = () => (
|
||||||
|
<PageBlock
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
fullScreenMobile>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "var(--mantine-spacing-md)",
|
||||||
|
}}>
|
||||||
|
<ModulesHeader />
|
||||||
|
<div style={{ flex: 1, overflow: "auto" }}>
|
||||||
|
<ModulesTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageBlock>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PageBody;
|
||||||
26
src/app/modules/contexts/ModulesContext.tsx
Normal file
26
src/app/modules/contexts/ModulesContext.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import useModulesWithAttrsList from "@/app/modules/hooks/useModulesWithAttrsList";
|
||||||
|
import { ModuleWithAttributesSchema } from "@/lib/client";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import useModulesActions, { ModulesActions } from "@/app/modules/hooks/useModulesActions";
|
||||||
|
|
||||||
|
type ModulesContextState = {
|
||||||
|
modules: ModuleWithAttributesSchema[];
|
||||||
|
refetchModules: () => void;
|
||||||
|
modulesActions: ModulesActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useModulesContextState = (): ModulesContextState => {
|
||||||
|
const { modules, refetch } = useModulesWithAttrsList();
|
||||||
|
const modulesActions = useModulesActions({ refetchModules: refetch });
|
||||||
|
|
||||||
|
return {
|
||||||
|
modules,
|
||||||
|
refetchModules: refetch,
|
||||||
|
modulesActions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [ModulesContextProvider, useModulesContext] =
|
||||||
|
makeContext<ModulesContextState>(useModulesContextState, "Modules");
|
||||||
59
src/app/modules/hooks/useAttributesInnerTableColumns.tsx
Normal file
59
src/app/modules/hooks/useAttributesInnerTableColumns.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { ModuleAttributeSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
const useAttributesInnerTableColumns = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Название атрибута",
|
||||||
|
accessor: "label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Тип",
|
||||||
|
accessor: "type.name",
|
||||||
|
render: attr =>
|
||||||
|
attr.type.type === "select"
|
||||||
|
? `Выбор "${attr.select?.name}"`
|
||||||
|
: attr.type.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Значение по умолчанию",
|
||||||
|
accessor: "defaultValue",
|
||||||
|
render: attr => <AttributeDefaultValue attribute={attr} />,
|
||||||
|
hidden: isMobile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Синхронизировано в группе",
|
||||||
|
accessor: "isApplicableToGroup",
|
||||||
|
render: attr => renderCheck(attr.isApplicableToGroup),
|
||||||
|
hidden: isMobile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Может быть пустым",
|
||||||
|
accessor: "isNullable",
|
||||||
|
render: attr => renderCheck(attr.isNullable),
|
||||||
|
hidden: isMobile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Описаниие",
|
||||||
|
accessor: "description",
|
||||||
|
render: attr => <Box>{attr.description}</Box>,
|
||||||
|
},
|
||||||
|
] as DataTableColumn<ModuleAttributeSchema>[],
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAttributesInnerTableColumns;
|
||||||
86
src/app/modules/hooks/useModulesActions.tsx
Normal file
86
src/app/modules/hooks/useModulesActions.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { Text } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import { HttpValidationError, ModuleSchemaOutput } from "@/lib/client";
|
||||||
|
import {
|
||||||
|
createModuleMutation,
|
||||||
|
deleteModuleMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
export type ModulesActions = {
|
||||||
|
onCreate: () => void;
|
||||||
|
onUpdate: (module: ModuleSchemaOutput) => void;
|
||||||
|
onDelete: (module: ModuleSchemaOutput) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
refetchModules: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
|
||||||
|
const onError = (error: AxiosError<HttpValidationError>, _: any) => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
...createModuleMutation(),
|
||||||
|
onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreate = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "moduleCreatorModal",
|
||||||
|
title: "Создание модуля",
|
||||||
|
innerProps: {
|
||||||
|
onCreate: (data, onSuccess) =>
|
||||||
|
createMutation.mutate(
|
||||||
|
{ body: { entity: data } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchModules();
|
||||||
|
onSuccess && onSuccess();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = (module: ModuleSchemaOutput) => {
|
||||||
|
redirect(`/module-editor/${module.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
...deleteModuleMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: refetchModules,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDelete = (module: ModuleSchemaOutput) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: "Удаление модуля",
|
||||||
|
children: (
|
||||||
|
<Text>
|
||||||
|
Вы уверены, что хотите удалить модуль "{module.label}"?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: () => deleteMutation.mutate({ path: { pk: module.id } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useModulesActions;
|
||||||
115
src/app/modules/hooks/useModulesTableColumns.tsx
Normal file
115
src/app/modules/hooks/useModulesTableColumns.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronsDown,
|
||||||
|
IconChevronsRight,
|
||||||
|
IconChevronsUp,
|
||||||
|
IconChevronUp,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
|
import { Box, Group, Text, Tooltip } from "@mantine/core";
|
||||||
|
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
||||||
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
|
import { ModuleWithAttributesSchema } from "@/lib/client";
|
||||||
|
import { useModulesContext } from "../contexts/ModulesContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
expandedModuleIds: number[];
|
||||||
|
setExpandedModuleIds: (ids: number[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useModulesTableColumns = ({
|
||||||
|
expandedModuleIds,
|
||||||
|
setExpandedModuleIds,
|
||||||
|
}: Props) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const {
|
||||||
|
modules,
|
||||||
|
modulesActions: { onUpdate, onDelete },
|
||||||
|
} = useModulesContext();
|
||||||
|
|
||||||
|
const onExpandAllClick = () => {
|
||||||
|
if (expandedModuleIds.length !== modules.length) {
|
||||||
|
setExpandedModuleIds(modules.map(c => c.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedModuleIds([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExpandAllIcon = () => {
|
||||||
|
if (expandedModuleIds.length === modules.length)
|
||||||
|
return <IconChevronsUp />;
|
||||||
|
if (expandedModuleIds.length === 0) return <IconChevronsDown />;
|
||||||
|
return <IconChevronsRight />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessor: "",
|
||||||
|
title: (
|
||||||
|
<Group>
|
||||||
|
<Box
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={onExpandAllClick}>
|
||||||
|
{getExpandAllIcon()}
|
||||||
|
</Box>
|
||||||
|
Модуль
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
render: ({ id, label }) => (
|
||||||
|
<Group
|
||||||
|
key={id}
|
||||||
|
wrap={"nowrap"}>
|
||||||
|
{expandedModuleIds.includes(id) ? (
|
||||||
|
<IconChevronUp />
|
||||||
|
) : (
|
||||||
|
<IconChevronDown />
|
||||||
|
)}
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Описание",
|
||||||
|
accessor: "description",
|
||||||
|
hidden: isMobile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Зависит от модулей",
|
||||||
|
accessor: "dependsOn",
|
||||||
|
render: module => (
|
||||||
|
<Text>
|
||||||
|
{module.dependsOn?.map(m => m.label).join(", ")}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
hidden: isMobile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "actions",
|
||||||
|
title: isMobile ? "" : "Действия",
|
||||||
|
sortable: false,
|
||||||
|
textAlign: "center",
|
||||||
|
width: "0%",
|
||||||
|
render: module => (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
module.isBuiltIn
|
||||||
|
? "Нельзя изменять встроенные модули"
|
||||||
|
: null
|
||||||
|
}>
|
||||||
|
<UpdateDeleteTableActions
|
||||||
|
onDelete={() => onDelete(module)}
|
||||||
|
onChange={() => onUpdate(module)}
|
||||||
|
disabled={module.isBuiltIn}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] as DataTableColumn<ModuleWithAttributesSchema>[],
|
||||||
|
[expandedModuleIds, modules, isMobile]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useModulesTableColumns;
|
||||||
13
src/app/modules/hooks/useModulesWithAttrsList.ts
Normal file
13
src/app/modules/hooks/useModulesWithAttrsList.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getModulesWithAttributesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useModulesWithAttrsList = () => {
|
||||||
|
const { data, refetch } = useQuery(getModulesWithAttributesOptions());
|
||||||
|
|
||||||
|
return {
|
||||||
|
modules: data?.items ?? [],
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useModulesWithAttrsList;
|
||||||
54
src/app/modules/modals/ModuleCreatorModal.tsx
Normal file
54
src/app/modules/modals/ModuleCreatorModal.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
import { CreateModuleSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCreate: (data: CreateModuleSchema, onSuccess?: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModuleCreatorModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<Props>) => {
|
||||||
|
const form = useForm<CreateModuleSchema>({
|
||||||
|
initialValues: {
|
||||||
|
label: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
label: label => !label?.trim() && "Название не заполнено",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => context.closeContextModal(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(values =>
|
||||||
|
innerProps.onCreate(values, close)
|
||||||
|
)}>
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
|
label={"Название"}
|
||||||
|
{...form.getInputProps("label")}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label={"Описание"}
|
||||||
|
{...form.getInputProps("description")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type={"submit"}
|
||||||
|
variant={"default"}>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleCreatorModal;
|
||||||
22
src/app/modules/page.tsx
Normal file
22
src/app/modules/page.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Center, Loader } from "@mantine/core";
|
||||||
|
import PageBody from "@/app/modules/components/PageBody";
|
||||||
|
import { ModulesContextProvider } from "@/app/modules/contexts/ModulesContext";
|
||||||
|
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||||
|
|
||||||
|
export default async function ModulesPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Center h="50vh">
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
}>
|
||||||
|
<PageContainer>
|
||||||
|
<ModulesContextProvider>
|
||||||
|
<PageBody />
|
||||||
|
</ModulesContextProvider>
|
||||||
|
</PageContainer>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -71,7 +71,7 @@ export const useProductsTableColumns = ({
|
|||||||
}
|
}
|
||||||
showLabel={"Показать все"}
|
showLabel={"Показать все"}
|
||||||
hideLabel={"Скрыть"}>
|
hideLabel={"Скрыть"}>
|
||||||
{product.barcodes.map(barcode => (
|
{product.barcodes?.map(barcode => (
|
||||||
<List.Item key={barcode}>
|
<List.Item key={barcode}>
|
||||||
{barcode}
|
{barcode}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export type ProductsFiltersForm = {
|
|||||||
|
|
||||||
type ProductsContextState = {
|
type ProductsContextState = {
|
||||||
productsFiltersForm: UseFormReturnType<ProductsFiltersForm>;
|
productsFiltersForm: UseFormReturnType<ProductsFiltersForm>;
|
||||||
|
refetch: () => void;
|
||||||
products: ProductSchema[];
|
products: ProductSchema[];
|
||||||
productsCrud: ProductsCrud;
|
productsCrud: ProductsCrud;
|
||||||
paginationInfo?: PaginationInfoSchema;
|
paginationInfo?: PaginationInfoSchema;
|
||||||
@ -37,7 +38,7 @@ const useProductsContextState = (): ProductsContextState => {
|
|||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
const { products, paginationInfo, queryKey } = useProductsList({
|
const { products, paginationInfo, queryKey, refetch } = useProductsList({
|
||||||
clientId: productsFiltersForm.values.client?.id,
|
clientId: productsFiltersForm.values.client?.id,
|
||||||
searchInput: debouncedSearchInput,
|
searchInput: debouncedSearchInput,
|
||||||
page: productsFiltersForm.values.page,
|
page: productsFiltersForm.values.page,
|
||||||
@ -47,6 +48,7 @@ const useProductsContextState = (): ProductsContextState => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
productsFiltersForm,
|
productsFiltersForm,
|
||||||
|
refetch,
|
||||||
products,
|
products,
|
||||||
productsCrud,
|
productsCrud,
|
||||||
paginationInfo,
|
paginationInfo,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ProductSchema } from "@/lib/client";
|
|||||||
import { notifications } from "@/lib/notifications";
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
const useProductsActions = () => {
|
const useProductsActions = () => {
|
||||||
const { productsCrud, productsFiltersForm } = useProductsContext();
|
const { productsCrud, productsFiltersForm, refetch } = useProductsContext();
|
||||||
|
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
if (!productsFiltersForm.values.client) {
|
if (!productsFiltersForm.values.client) {
|
||||||
@ -19,6 +19,7 @@ const useProductsActions = () => {
|
|||||||
innerProps: {
|
innerProps: {
|
||||||
onCreate: productsCrud.onCreate,
|
onCreate: productsCrud.onCreate,
|
||||||
clientId: productsFiltersForm.values.client.id,
|
clientId: productsFiltersForm.values.client.id,
|
||||||
|
refetchProducts: refetch,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -33,6 +34,7 @@ const useProductsActions = () => {
|
|||||||
onChange: updated => productsCrud.onUpdate(product.id, updated),
|
onChange: updated => productsCrud.onUpdate(product.id, updated),
|
||||||
clientId: product.clientId,
|
clientId: product.clientId,
|
||||||
entity: product,
|
entity: product,
|
||||||
|
refetchProducts: refetch,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { ProductService } from "../../../client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
clientId: number;
|
|
||||||
page?: number;
|
|
||||||
itemsPerPage?: number;
|
|
||||||
searchInput: string;
|
|
||||||
};
|
|
||||||
const useProductsList = (props: Props) => {
|
|
||||||
const { clientId, page, itemsPerPage, searchInput } = props;
|
|
||||||
const { data, refetch, isLoading } = useQuery({
|
|
||||||
queryKey: ["getAllServices", clientId, page, itemsPerPage, searchInput],
|
|
||||||
queryFn: () => ProductService.getProductsByClientId(props),
|
|
||||||
});
|
|
||||||
const products = !data ? [] : data.products;
|
|
||||||
const paginationInfo = data?.paginationInfo;
|
|
||||||
return { products, paginationInfo, refetch, isLoading };
|
|
||||||
};
|
|
||||||
export default useProductsList;
|
|
||||||
@ -47,7 +47,7 @@ const ServicesKitEditorModal = ({
|
|||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
<ServiceTypeSegmentedControl
|
<ServiceTypeSegmentedControl
|
||||||
value={form.values.serviceType?.toString()}
|
value={form.values.serviceType}
|
||||||
onChange={tab => {
|
onChange={tab => {
|
||||||
form.setFieldValue("serviceType", Number(tab));
|
form.setFieldValue("serviceType", Number(tab));
|
||||||
form.setFieldValue("services", []);
|
form.setFieldValue("services", []);
|
||||||
|
|||||||
@ -19,10 +19,14 @@ const PageBody = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DealTagsContextProvider project={selectedProject}>
|
<DealTagsContextProvider project={selectedProject}>
|
||||||
<Stack gap={"md"}>
|
<Stack
|
||||||
|
gap={"md"}
|
||||||
|
h={"100%"}>
|
||||||
<TagsPageHeader project={selectedProject} />
|
<TagsPageHeader project={selectedProject} />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<div style={{ minHeight: "0%" }}>
|
||||||
<TagsTable />
|
<TagsTable />
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</DealTagsContextProvider>
|
</DealTagsContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -39,7 +39,15 @@ const DraggableTableRow: FC<Props> = ({
|
|||||||
rowProps.className,
|
rowProps.className,
|
||||||
classes["draggable-row"]
|
classes["draggable-row"]
|
||||||
)}
|
)}
|
||||||
{...provided.draggableProps}>
|
{...provided.draggableProps}
|
||||||
|
// style={{
|
||||||
|
// ...provided.draggableProps.style,
|
||||||
|
// left: "auto !important",
|
||||||
|
// top: "auto !important",
|
||||||
|
//
|
||||||
|
// // does not work after scroll
|
||||||
|
// }}
|
||||||
|
>
|
||||||
<TableTd maw={isMobile ? 2 : "auto"}>
|
<TableTd maw={isMobile ? 2 : "auto"}>
|
||||||
<Center
|
<Center
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
|
|||||||
@ -29,3 +29,7 @@
|
|||||||
margin-bottom: calc(var(--mantine-spacing-sm) * 2);
|
margin-bottom: calc(var(--mantine-spacing-sm) * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swiper-container :global(.swiper-pagination) {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -133,7 +133,10 @@ const FunnelDnd = <
|
|||||||
}}
|
}}
|
||||||
className={classes["swiper-container"]}
|
className={classes["swiper-container"]}
|
||||||
spaceBetween={15}
|
spaceBetween={15}
|
||||||
style={{ paddingInline: "10vw" }}
|
style={{
|
||||||
|
paddingInline: "10vw",
|
||||||
|
height: "calc(100vh - 188px)",
|
||||||
|
}}
|
||||||
modules={[Pagination]}
|
modules={[Pagination]}
|
||||||
freeMode={{ enabled: false }}
|
freeMode={{ enabled: false }}
|
||||||
pagination={{ enabled: true, clickable: true }}>
|
pagination={{ enabled: true, clickable: true }}>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
IconApps,
|
||||||
IconBox,
|
IconBox,
|
||||||
IconColumns,
|
IconColumns,
|
||||||
IconFileBarcode,
|
IconFileBarcode,
|
||||||
IconLayoutKanban,
|
IconLayoutKanban,
|
||||||
|
IconList,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { ModuleNames } from "@/modules/modules";
|
import { ModuleNames } from "@/modules/modules";
|
||||||
@ -15,6 +17,18 @@ const linksData: LinkData[] = [
|
|||||||
href: "/deals",
|
href: "/deals",
|
||||||
moduleName: undefined,
|
moduleName: undefined,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: IconApps,
|
||||||
|
label: "Модули",
|
||||||
|
href: "/modules",
|
||||||
|
moduleName: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IconList,
|
||||||
|
label: "Атрибуты",
|
||||||
|
href: "/attributes",
|
||||||
|
moduleName: undefined,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: IconUsers,
|
icon: IconUsers,
|
||||||
label: "Клиенты",
|
label: "Клиенты",
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
import { AttributeSchema } from "@/lib/client";
|
||||||
|
import {
|
||||||
|
naiveDateTimeStringToUtc,
|
||||||
|
utcDateTimeToLocalString,
|
||||||
|
utcDateToLocalString,
|
||||||
|
} from "@/utils/datetime";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attribute: AttributeSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeDefaultValue: FC<Props> = ({ attribute }) => {
|
||||||
|
if (!attribute.defaultValue && !attribute.defaultOption) return <>-</>;
|
||||||
|
const value = attribute.defaultValue;
|
||||||
|
|
||||||
|
const type = attribute.type.type;
|
||||||
|
if (type === "datetime") {
|
||||||
|
return utcDateTimeToLocalString(
|
||||||
|
naiveDateTimeStringToUtc(value as string)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === "date") {
|
||||||
|
return utcDateToLocalString(value as string);
|
||||||
|
}
|
||||||
|
if (type === "bool") {
|
||||||
|
return value ? <IconCheck /> : <IconX />;
|
||||||
|
}
|
||||||
|
if (type === "select") {
|
||||||
|
return attribute.defaultOption?.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{value}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeDefaultValue;
|
||||||
169
src/components/ui/AttributesEditor/AttributesEditor.tsx
Normal file
169
src/components/ui/AttributesEditor/AttributesEditor.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Flex, Group } from "@mantine/core";
|
||||||
|
import AttributeValueInput from "@/app/deals/drawers/DealEditorDrawer/components/AttributeValueInput";
|
||||||
|
import useDealAttributeValuesActions from "@/components/ui/AttributesEditor/useDealAttributeValuesActions";
|
||||||
|
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import {
|
||||||
|
DealModuleAttributeSchema,
|
||||||
|
DealSchema,
|
||||||
|
UpdateDealModuleAttributeSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
moduleId: number;
|
||||||
|
deal: DealSchema;
|
||||||
|
containerStyle?: CSSProperties;
|
||||||
|
buttonContainerStyle?: CSSProperties;
|
||||||
|
buttonStyle?: CSSProperties;
|
||||||
|
attributesInTwoColumns?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttrInfo = {
|
||||||
|
value?: any;
|
||||||
|
isApplicableToGroup: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeEditor: FC<Props> = ({
|
||||||
|
moduleId,
|
||||||
|
deal,
|
||||||
|
containerStyle,
|
||||||
|
buttonContainerStyle,
|
||||||
|
buttonStyle,
|
||||||
|
attributesInTwoColumns = true,
|
||||||
|
}) => {
|
||||||
|
const { dealAttributes, updateAttributeValues } =
|
||||||
|
useDealAttributeValuesActions({
|
||||||
|
moduleId,
|
||||||
|
dealId: deal.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [attributeValuesMap, setAttributeValuesMap] = useState<
|
||||||
|
Map<number, AttrInfo | null>
|
||||||
|
>(new Map());
|
||||||
|
const [attributeErrorsMap, setAttributeErrorsMap] = useState<
|
||||||
|
Map<number, string>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const values = new Map<number, AttrInfo | null>();
|
||||||
|
for (const dealAttr of dealAttributes) {
|
||||||
|
values.set(dealAttr.attributeId, {
|
||||||
|
...dealAttr,
|
||||||
|
value: dealAttr.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setAttributeValuesMap(values);
|
||||||
|
}, [dealAttributes]);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
let isErrorFound = false;
|
||||||
|
for (const attr of dealAttributes) {
|
||||||
|
const value = attributeValuesMap.get(attr.attributeId);
|
||||||
|
if (!attr.isNullable && (value === null || value === undefined)) {
|
||||||
|
attributeErrorsMap.set(attr.attributeId, "Обязательное поле");
|
||||||
|
isErrorFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
||||||
|
if (isErrorFound) return;
|
||||||
|
|
||||||
|
const attributeValues: UpdateDealModuleAttributeSchema[] =
|
||||||
|
attributeValuesMap
|
||||||
|
.entries()
|
||||||
|
.map(
|
||||||
|
([attributeId, info]) =>
|
||||||
|
({
|
||||||
|
attributeId,
|
||||||
|
...info,
|
||||||
|
}) as UpdateDealModuleAttributeSchema
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
updateAttributeValues(attributeValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttributeElement = useCallback(
|
||||||
|
(attribute: DealModuleAttributeSchema) => (
|
||||||
|
<AttributeValueInput
|
||||||
|
key={attribute.attributeId}
|
||||||
|
attrInfo={attribute}
|
||||||
|
value={attributeValuesMap.get(attribute.attributeId)?.value}
|
||||||
|
onChange={value => {
|
||||||
|
attributeValuesMap.set(attribute.attributeId, {
|
||||||
|
...attribute,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
setAttributeValuesMap(new Map(attributeValuesMap));
|
||||||
|
attributeErrorsMap.delete(attribute.attributeId);
|
||||||
|
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
||||||
|
}}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={attributeErrorsMap.get(attribute.attributeId)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[attributeValuesMap, attributeErrorsMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributesRows = useMemo(() => {
|
||||||
|
if (!dealAttributes) return [];
|
||||||
|
const boolAttributes = dealAttributes.filter(
|
||||||
|
a => a.type.type === "bool"
|
||||||
|
);
|
||||||
|
const otherAttributes = dealAttributes.filter(
|
||||||
|
a => a.type.type !== "bool"
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (attributesInTwoColumns) {
|
||||||
|
for (let i = 0; i < otherAttributes.length; i += 2) {
|
||||||
|
const rightIdx = i + 1;
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<FormFlexRow key={`row${i}`}>
|
||||||
|
{getAttributeElement(otherAttributes[i])}
|
||||||
|
{rightIdx < otherAttributes.length &&
|
||||||
|
getAttributeElement(otherAttributes[rightIdx])}
|
||||||
|
</FormFlexRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const attr of otherAttributes) {
|
||||||
|
rows.push(getAttributeElement(attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attr of boolAttributes) {
|
||||||
|
rows.push(getAttributeElement(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}, [dealAttributes, getAttributeElement]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={"xs"}
|
||||||
|
style={containerStyle}>
|
||||||
|
{attributesRows}
|
||||||
|
<Group style={buttonContainerStyle}>
|
||||||
|
<InlineButton
|
||||||
|
style={buttonStyle}
|
||||||
|
onClick={onSubmit}>
|
||||||
|
Сохранить
|
||||||
|
</InlineButton>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeEditor;
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import {
|
||||||
|
DealModuleAttributeSchema,
|
||||||
|
HttpValidationError,
|
||||||
|
UpdateDealModuleAttributeSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
getDealModuleAttributesOptions,
|
||||||
|
updateDealModuleAttributesMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dealId: number;
|
||||||
|
moduleId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDealAttributeValuesActions = ({ dealId, moduleId }: Props) => {
|
||||||
|
const { data, refetch: refetchAttributes } = useQuery(
|
||||||
|
getDealModuleAttributesOptions({
|
||||||
|
path: {
|
||||||
|
dealId,
|
||||||
|
moduleId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedAttributes = useMemo(() => {
|
||||||
|
if (!data?.attributes) return [];
|
||||||
|
const sortedAttributes: DealModuleAttributeSchema[] = [];
|
||||||
|
for (const attr of data.attributes) {
|
||||||
|
if (attr.type.type === "bool") sortedAttributes.push(attr);
|
||||||
|
else sortedAttributes.unshift(attr);
|
||||||
|
}
|
||||||
|
return sortedAttributes;
|
||||||
|
}, [data?.attributes]);
|
||||||
|
|
||||||
|
const onError = (error: AxiosError<HttpValidationError>) => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAttributeValuesMutation = useMutation({
|
||||||
|
...updateDealModuleAttributesMutation(),
|
||||||
|
onError,
|
||||||
|
onSuccess: ({ message }) => {
|
||||||
|
notifications.success({ message });
|
||||||
|
refetchAttributes();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAttributeValues = (
|
||||||
|
attributeValues: UpdateDealModuleAttributeSchema[]
|
||||||
|
) => {
|
||||||
|
updateAttributeValuesMutation.mutate({
|
||||||
|
path: {
|
||||||
|
moduleId,
|
||||||
|
dealId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
attributes: attributeValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
dealAttributes: sortedAttributes,
|
||||||
|
updateAttributeValues,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealAttributeValuesActions;
|
||||||
@ -19,6 +19,7 @@ type Props = {
|
|||||||
otherActions?: ActionData[];
|
otherActions?: ActionData[];
|
||||||
dotsForMobile?: boolean;
|
dotsForMobile?: boolean;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UpdateDeleteTableActions: FC<Props> = ({
|
const UpdateDeleteTableActions: FC<Props> = ({
|
||||||
@ -27,6 +28,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
otherActions,
|
otherActions,
|
||||||
style,
|
style,
|
||||||
dotsForMobile = false,
|
dotsForMobile = false,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -45,6 +47,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
icon={<IconEdit />}
|
icon={<IconEdit />}
|
||||||
label={"Редактировать"}
|
label={"Редактировать"}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
{otherActions?.map(
|
{otherActions?.map(
|
||||||
action =>
|
action =>
|
||||||
@ -54,6 +57,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
label={action.label}
|
label={action.label}
|
||||||
key={action.label}
|
key={action.label}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@ -61,6 +65,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
icon={<IconTrash />}
|
icon={<IconTrash />}
|
||||||
label={"Удалить"}
|
label={"Удалить"}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
@ -76,6 +81,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onChange();
|
onChange();
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
tipLabel={"Редактировать"}>
|
tipLabel={"Редактировать"}>
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
</ActionIconWithTip>
|
</ActionIconWithTip>
|
||||||
@ -87,6 +93,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
action.onClick();
|
action.onClick();
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
key={action.label}
|
key={action.label}
|
||||||
tipLabel={action.label}>
|
tipLabel={action.label}>
|
||||||
{action.icon}
|
{action.icon}
|
||||||
@ -99,6 +106,7 @@ const UpdateDeleteTableActions: FC<Props> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete();
|
onDelete();
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
tipLabel={"Удалить"}>
|
tipLabel={"Удалить"}>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
</ActionIconWithTip>
|
</ActionIconWithTip>
|
||||||
|
|||||||
@ -5,16 +5,25 @@ type Props = {
|
|||||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownMenuItem: FC<Props> = ({ icon, label, onClick }) => {
|
const DropdownMenuItem: FC<Props> = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
const onClickWrapper: MouseEventHandler<HTMLButtonElement> = e => {
|
const onClickWrapper: MouseEventHandler<HTMLButtonElement> = e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (disabled) return;
|
||||||
onClick(e);
|
onClick(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Item onClick={onClickWrapper}>
|
<Menu.Item
|
||||||
|
onClick={onClickWrapper}
|
||||||
|
disabled={disabled}>
|
||||||
<Group wrap={"nowrap"}>
|
<Group wrap={"nowrap"}>
|
||||||
{icon}
|
{icon}
|
||||||
<Text>{label}</Text>
|
<Text>{label}</Text>
|
||||||
|
|||||||
@ -5,8 +5,7 @@ type Props = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Logo = ({ title }: Props) => {
|
const Logo = ({ title }: Props) => (
|
||||||
return (
|
|
||||||
<Stack
|
<Stack
|
||||||
align="center"
|
align="center"
|
||||||
gap={0}>
|
gap={0}>
|
||||||
@ -33,7 +32,6 @@ const Logo = ({ title }: Props) => {
|
|||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default Logo;
|
export default Logo;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user