Compare commits
1 Commits
main
...
pragmatic-
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c2a3a2af |
@ -1,25 +0,0 @@
|
|||||||
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");
|
|
||||||
15
package.json
15
package.json
@ -7,10 +7,16 @@
|
|||||||
"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 & sudo npx tsc fix-client.ts && mv -f ./fix-client.js ./fix-client.cjs && sudo node ./fix-client.cjs",
|
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client",
|
||||||
"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": {
|
||||||
|
"@atlaskit/avatar": "^25.4.2",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.7",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-live-region": "^1.3.1",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.7",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@ -27,23 +33,23 @@
|
|||||||
"@tabler/icons-react": "^3.34.0",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"@types/react-dom": "19.1.2",
|
||||||
"axios": "1.12.0",
|
"axios": "1.12.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"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.18",
|
"dayjs": "^1.11.15",
|
||||||
"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",
|
||||||
"lexorank": "^1.0.5",
|
"lexorank": "^1.0.5",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"mantine-contextmenu": "^8.2.0",
|
|
||||||
"mantine-datatable": "^8.2.0",
|
"mantine-datatable": "^8.2.0",
|
||||||
"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.2.0",
|
"react-dom": "19.1.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,7 +75,6 @@
|
|||||||
"@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,14 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RefObject, useMemo, useRef } from "react";
|
import { useMemo } from "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";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
import BuiltInLinkData from "@/types/BuiltInLinkData";
|
|
||||||
|
|
||||||
const PageBody = () => {
|
const PageBody = () => {
|
||||||
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
|
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
|
||||||
@ -22,24 +20,6 @@ const PageBody = () => {
|
|||||||
[modulesSet]
|
[modulesSet]
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
|
||||||
{
|
|
||||||
icon: IconList,
|
|
||||||
label: "Атрибуты",
|
|
||||||
href: "/attributes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconApps,
|
|
||||||
label: "Модули",
|
|
||||||
href: "/modules",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconTag,
|
|
||||||
label: "Теги",
|
|
||||||
href: "/tags",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBlock fullScreenMobile>
|
<PageBlock fullScreenMobile>
|
||||||
<Stack p={"xs"}>
|
<Stack p={"xs"}>
|
||||||
@ -53,10 +33,7 @@ const PageBody = () => {
|
|||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
type={"container"}
|
type={"container"}
|
||||||
cols={2}>
|
cols={2}>
|
||||||
{[
|
{filteredMobileButtonsData.map((data, index) => (
|
||||||
...commonActionsData.current,
|
|
||||||
...filteredMobileButtonsData,
|
|
||||||
].map((data, index) => (
|
|
||||||
<Action
|
<Action
|
||||||
linkData={data}
|
linkData={data}
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"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");
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"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"
|
|
||||||
);
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
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: () => "Удаление опции",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
enum AttributePageView {
|
|
||||||
ATTRIBUTES,
|
|
||||||
SELECTS,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AttributePageView;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import { IconCheckbox, IconDotsVertical, IconTrash } from "@tabler/icons-react";
|
|
||||||
import { Box, Menu } from "@mantine/core";
|
|
||||||
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
|
||||||
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDelete: () => void;
|
|
||||||
startDealsSelecting: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const GroupMenu: FC<Props> = ({ onDelete, startDealsSelecting }) => {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Target>
|
|
||||||
<Box
|
|
||||||
px={"md"}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
|
||||||
<ThemeIcon size={"sm"}>
|
|
||||||
<IconDotsVertical />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Box>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={onDelete}
|
|
||||||
icon={<IconTrash />}
|
|
||||||
label={"Удалить группу"}
|
|
||||||
/>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={startDealsSelecting}
|
|
||||||
icon={<IconCheckbox />}
|
|
||||||
label={"Добавить/удалить сделки"}
|
|
||||||
/>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupMenu;
|
|
||||||
@ -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, Title } from "@mantine/core";
|
import { Box, Group, Stack, Text } 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>
|
||||||
<Title order={6}>{selectedProject?.name}</Title>
|
<Text>{selectedProject?.name}</Text>
|
||||||
<Box
|
<Box
|
||||||
p={"md"}
|
p={"md"}
|
||||||
onClick={openBoardsEditorDrawer}>
|
onClick={openBoardsEditorDrawer}>
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
.create-button {
|
.create-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: max-content;
|
min-height: max-content;
|
||||||
border: 1px dashed;
|
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: var(--color-light-white-blue);
|
background-color: var(--color-light-white-blue);
|
||||||
border-color: lightblue;
|
|
||||||
}
|
}
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-7);
|
background-color: var(--mantine-color-dark-7);
|
||||||
border-color: var(--mantine-color-dark-5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +1,12 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
flex: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px dashed;
|
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: var(--color-light-white-blue);
|
background-color: var(--color-light-white-blue);
|
||||||
border-color: lightblue;
|
|
||||||
}
|
}
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-7);
|
background-color: var(--mantine-color-dark-7);
|
||||||
border-color: var(--mantine-color-dark-5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-selected {
|
|
||||||
border: 2px dashed !important;
|
|
||||||
@mixin light {
|
|
||||||
border-color: dodgerblue !important;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
border-color: dodgerblue !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-mainly-selected {
|
|
||||||
border: 2px solid;
|
|
||||||
@mixin light {
|
|
||||||
border-color: dodgerblue !important;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
border-color: dodgerblue !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-in-group {
|
|
||||||
padding: 0;
|
|
||||||
border: 1px dashed;
|
|
||||||
@mixin light {
|
|
||||||
background-color: var(--color-light-aqua);
|
|
||||||
border-color: lightblue;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-6);
|
|
||||||
border-color: var(--mantine-color-dark-5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +1,21 @@
|
|||||||
import { IconCategoryPlus } from "@tabler/icons-react";
|
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
|
||||||
import classNames from "classnames";
|
|
||||||
import { useContextMenu } from "mantine-contextmenu";
|
|
||||||
import { Box, Card, Group, Stack, Text } from "@mantine/core";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
import { ModuleNames } from "@/modules/modules";
|
import { ModuleNames } from "@/modules/modules";
|
||||||
import styles from "./DealCard.module.css";
|
import styles from "./DealCard.module.css";
|
||||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
deal: DealSchema;
|
deal: DealSchema;
|
||||||
isInGroup?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DealCard = ({ deal, isInGroup = false }: Props) => {
|
const DealCard = ({ deal }: Props) => {
|
||||||
const { selectedProject, modulesSet } = useProjectsContext();
|
const { selectedProject, modulesSet } = useProjectsContext();
|
||||||
const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext();
|
const { dealsCrud, refetchDeals } = useDealsContext();
|
||||||
const { openDrawer } = useDrawersContext();
|
const { openDrawer } = useDrawersContext();
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (groupDealsSelection.isDealsSelecting) {
|
|
||||||
groupDealsSelection.toggleDeal(deal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
openDrawer({
|
openDrawer({
|
||||||
key: "dealEditorDrawer",
|
key: "dealEditorDrawer",
|
||||||
props: {
|
props: {
|
||||||
@ -40,38 +28,10 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { showContextMenu } = useContextMenu();
|
|
||||||
|
|
||||||
const dealContextMenu =
|
|
||||||
deal.group || isMobile
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
key: "startGroupForming",
|
|
||||||
onClick: () =>
|
|
||||||
groupDealsSelection.startSelectingWithDeal(deal.id),
|
|
||||||
title: "Создать группу",
|
|
||||||
icon: <IconCategoryPlus />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getSelectedStyles = () => {
|
|
||||||
if (groupDealsSelection.selectedBaseDealId === deal.id) {
|
|
||||||
return styles["container-mainly-selected"];
|
|
||||||
}
|
|
||||||
if (groupDealsSelection.selectedDealIds.has(deal.id)) {
|
|
||||||
return styles["container-selected"];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={styles.container}>
|
||||||
getSelectedStyles(),
|
|
||||||
isInGroup ? styles["container-in-group"] : styles.container
|
|
||||||
)}
|
|
||||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
|
||||||
<Group
|
<Group
|
||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
wrap={"nowrap"}
|
wrap={"nowrap"}
|
||||||
@ -101,7 +61,10 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{!deal.group && <DealTags dealId={deal.id} tags={deal.tags} />}
|
<Group gap={"xs"}>
|
||||||
|
<Pill className={styles["first-tag"]}>Срочно</Pill>
|
||||||
|
<Pill className={styles["second-tag"]}>Бесплатно</Pill>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { IconGripHorizontal } from "@tabler/icons-react";
|
||||||
|
import { Flex, rem, TextInput, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||||
|
import FulfillmentGroupInfo from "@/app/deals/components/shared/DealGroupCard/components/FulfillmentGroupInfo";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
import { ModuleNames } from "@/modules/modules";
|
||||||
|
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: GroupWithDealsSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealGroupCard: FC<Props> = ({ group }) => {
|
||||||
|
const theme = useMantineColorScheme();
|
||||||
|
const [name, setName] = useState<string>(group.name ?? "");
|
||||||
|
const [debouncedName] = useDebouncedValue(name, 200);
|
||||||
|
const { modulesSet } = useProjectsContext();
|
||||||
|
const isServicesAndProductsIncluded = modulesSet.has(
|
||||||
|
ModuleNames.FULFILLMENT_BASE
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateName = () => {
|
||||||
|
if (debouncedName === group.name) return;
|
||||||
|
CardGroupService.updateCardGroup({
|
||||||
|
requestBody: {
|
||||||
|
data: {
|
||||||
|
...group,
|
||||||
|
name: debouncedName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) return;
|
||||||
|
setName(group.name || "");
|
||||||
|
notifications.guess(response.ok, { message: response.message });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateName();
|
||||||
|
}, [debouncedName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
border: "dashed var(--item-border-size) var(--mantine-color-default-border)",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
}}
|
||||||
|
p={rem(5)}
|
||||||
|
py={"xs"}
|
||||||
|
bg={
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? "var(--mantine-color-dark-5)"
|
||||||
|
: "var(--mantine-color-gray-1)"
|
||||||
|
}
|
||||||
|
gap={"xs"}
|
||||||
|
direction={"column"}>
|
||||||
|
<Flex
|
||||||
|
justify={"space-between"}
|
||||||
|
align={"center"}
|
||||||
|
gap={"xs"}
|
||||||
|
px={"xs"}>
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
onChange={event => setName(event.currentTarget.value)}
|
||||||
|
variant={"unstyled"}
|
||||||
|
/>
|
||||||
|
<IconGripHorizontal />
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={"xs"}>
|
||||||
|
{group.deals?.map(deal => (
|
||||||
|
<DealCard
|
||||||
|
key={deal.id}
|
||||||
|
deal={deal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
{isServicesAndProductsIncluded && (
|
||||||
|
<FulfillmentGroupInfo group={group} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealGroupCard;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { Flex, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { DealGroupSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: DealGroupSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FulfillmentGroupInfo: FC<Props> = ({ group }) => {
|
||||||
|
const theme = useMantineColorScheme();
|
||||||
|
|
||||||
|
const totalPrice = useMemo(
|
||||||
|
() =>
|
||||||
|
group.deals?.reduce((acc, deal) => acc + (deal.totalPrice ?? 0), 0),
|
||||||
|
[group.deals]
|
||||||
|
);
|
||||||
|
const totalProducts = useMemo(
|
||||||
|
() =>
|
||||||
|
group.deals?.reduce(
|
||||||
|
(acc, deal) => acc + (deal.productsQuantity ?? 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
[group.deals]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
p={"xs"}
|
||||||
|
direction={"column"}
|
||||||
|
bg={
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? "var(--mantine-color-dark-6)"
|
||||||
|
: "var(--mantine-color-gray-2)"
|
||||||
|
}
|
||||||
|
style={{ borderRadius: "0.5rem" }}>
|
||||||
|
<Text
|
||||||
|
c={"gray.6"}
|
||||||
|
size={"xs"}>
|
||||||
|
Сумма: {totalPrice?.toLocaleString("ru-RU")} руб.
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
c={"gray.6"}
|
||||||
|
size={"xs"}>
|
||||||
|
Всего товаров: {totalProducts?.toLocaleString("ru-RU")}{" "}
|
||||||
|
шт.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FulfillmentGroupInfo;
|
||||||
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
.group-container {
|
|
||||||
border: 1px dashed;
|
|
||||||
@mixin light {
|
|
||||||
background-color: var(--color-light-white-blue);
|
|
||||||
border-color: lightblue;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-7);
|
|
||||||
border-color: var(--mantine-color-dark-5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-group {
|
|
||||||
border: 2px solid;
|
|
||||||
@mixin light {
|
|
||||||
border-color: dodgerblue;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
border-color: dodgerblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import React, { FC, useEffect, useState } from "react";
|
|
||||||
import { IconCheckbox, IconTrash } from "@tabler/icons-react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { useContextMenu } from "mantine-contextmenu";
|
|
||||||
import { Flex, Stack, TextInput } from "@mantine/core";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
|
||||||
import GroupMenu from "@/app/deals/components/mobile/GroupMenu/GroupMenu";
|
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
|
||||||
import styles from "./DealsGroup.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
group: GroupWithDealsSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DealsGroup: FC<Props> = ({ group }) => {
|
|
||||||
const [groupName, setGroupName] = useState(group.name ?? "");
|
|
||||||
const [debouncedGroupName] = useDebouncedValue(groupName, 600);
|
|
||||||
const {
|
|
||||||
groupsCrud,
|
|
||||||
groupDealsSelection: {
|
|
||||||
startSelectingWithExistingGroup,
|
|
||||||
selectedGroupId,
|
|
||||||
},
|
|
||||||
} = useDealsContext();
|
|
||||||
const { showContextMenu } = useContextMenu();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (debouncedGroupName === group.name) return;
|
|
||||||
groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
|
|
||||||
}, [debouncedGroupName]);
|
|
||||||
|
|
||||||
const dealContextMenu = isMobile
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
key: "delete",
|
|
||||||
onClick: () => groupsCrud.onDelete(group.id),
|
|
||||||
title: "Удалить группу",
|
|
||||||
icon: <IconTrash />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "startDealsSelecting",
|
|
||||||
onClick: () => startSelectingWithExistingGroup(group),
|
|
||||||
title: "Добавить/удалить сделки",
|
|
||||||
icon: <IconCheckbox />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
className={classNames(
|
|
||||||
styles["group-container"],
|
|
||||||
selectedGroupId === group.id && styles["selected-group"]
|
|
||||||
)}
|
|
||||||
gap={"xs"}
|
|
||||||
bdrs={"lg"}
|
|
||||||
p={"xs"}
|
|
||||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
|
||||||
<Flex
|
|
||||||
mx={"xs"}
|
|
||||||
align={"center"}
|
|
||||||
w={"100%"}>
|
|
||||||
<TextInput
|
|
||||||
value={groupName}
|
|
||||||
onChange={e => setGroupName(e.target.value)}
|
|
||||||
variant={"unstyled"}
|
|
||||||
onKeyDown={e => e.stopPropagation()}
|
|
||||||
flex={1}
|
|
||||||
/>
|
|
||||||
{isMobile && (
|
|
||||||
<GroupMenu
|
|
||||||
startDealsSelecting={() =>
|
|
||||||
startSelectingWithExistingGroup(group)
|
|
||||||
}
|
|
||||||
onDelete={() => groupsCrud.onDelete(group.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
{group.items.map(deal => (
|
|
||||||
<DealCard
|
|
||||||
deal={deal}
|
|
||||||
isInGroup
|
|
||||||
key={deal.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{group.items.length > 0 && (
|
|
||||||
<DealTags
|
|
||||||
groupId={group.id}
|
|
||||||
tags={group.items[0].tags}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealsGroup;
|
|
||||||
@ -1,92 +1,67 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||||
import DealsGroup from "@/app/deals/components/shared/DealsGroup/DealsGroup";
|
|
||||||
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
||||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
import DndFunnel from "@/components/dnd-pragmatic/DndFunnel/DndFunnel";
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
|
|
||||||
const Funnel: FC = () => {
|
const Funnel: FC = () => {
|
||||||
const { selectedBoard } = useBoardsContext();
|
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
||||||
const { dealsWithoutGroup, groupsWithDeals } = useDealsContext();
|
const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
|
||||||
const isMobile = useIsMobile();
|
useDealsContext();
|
||||||
|
|
||||||
const { sortedStatuses, handleDragOver, handleDragEnd, swiperRef } =
|
const updateStatus = (statusId: number, lexorank: string) => {
|
||||||
useDealsAndStatusesDnd();
|
setStatuses(
|
||||||
|
statuses.map(status =>
|
||||||
|
status.id === statusId ? { ...status, lexorank } : status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
statusesCrud.onUpdate(statusId, { lexorank });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeal = (dealId: number, lexorank: string, statusId: number) => {
|
||||||
|
const status = statuses.find(s => s.id === statusId);
|
||||||
|
if (!status) return;
|
||||||
|
setDeals(
|
||||||
|
deals.map(deal =>
|
||||||
|
deal.id === dealId ? { ...deal, lexorank, status } : deal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
dealsCrud.onUpdate(dealId, { lexorank, statusId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FunnelDnd<StatusSchema, DealSchema, GroupWithDealsSchema>
|
<DndFunnel
|
||||||
containers={sortedStatuses}
|
columns={statuses}
|
||||||
itemsAndGroups={sortByLexorank([
|
updateColumn={updateStatus}
|
||||||
...dealsWithoutGroup,
|
items={dealsWithoutGroup}
|
||||||
...groupsWithDeals,
|
groups={groupsWithDeals}
|
||||||
])}
|
updateItem={updateDeal}
|
||||||
onDragOver={handleDragOver}
|
getColumnItemsGroups={statusId =>
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
swiperRef={swiperRef}
|
|
||||||
getItemsByContainer={(status: StatusSchema) =>
|
|
||||||
sortByLexorank([
|
sortByLexorank([
|
||||||
...dealsWithoutGroup.filter(
|
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
||||||
deal => deal.status.id === status.id
|
|
||||||
),
|
|
||||||
...groupsWithDeals.filter(
|
...groupsWithDeals.filter(
|
||||||
group => group.items[0].status.id === status.id
|
g =>
|
||||||
|
g.items.length > 0 &&
|
||||||
|
g.items[0].status.id === statusId
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
renderContainer={(
|
renderColumnHeader={status => (
|
||||||
status: StatusSchema,
|
<StatusColumnHeader status={status} />
|
||||||
funnelColumnComponent: ReactNode,
|
|
||||||
renderDraggable,
|
|
||||||
index
|
|
||||||
) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
renderHeader={renderDraggable}
|
|
||||||
createFormEnabled={index === 0}>
|
|
||||||
{funnelColumnComponent}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
)}
|
||||||
renderContainerHeader={status => (
|
renderItem={deal => (
|
||||||
<StatusColumnHeader
|
|
||||||
status={status}
|
|
||||||
isDragging={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={(deal: DealSchema) => (
|
|
||||||
<DealCard
|
<DealCard
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
deal={deal}
|
deal={deal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderGroup={(group: GroupWithDealsSchema) => (
|
renderGroup={group => <Box flex={1}>{group.name}</Box>}
|
||||||
<DealsGroup
|
|
||||||
key={`${group.id}group`}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
renderHeader={() => (
|
|
||||||
<StatusColumnHeader
|
|
||||||
status={status}
|
|
||||||
isDragging
|
|
||||||
/>
|
|
||||||
)}>
|
|
||||||
{children}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
|
||||||
disabledColumns={isMobile}
|
|
||||||
isCreatingContainerEnabled={!!selectedBoard}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
.shadow {
|
|
||||||
@mixin light {
|
|
||||||
box-shadow: var(--light-shadow);
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-7);
|
|
||||||
box-shadow: var(--dark-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Affix, Flex, Stack, Title, Transition } from "@mantine/core";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
import styles from "./GroupDealsSelectionAffix.module.css";
|
|
||||||
|
|
||||||
const GroupDealsSelectionAffix = () => {
|
|
||||||
const {
|
|
||||||
groupDealsSelection: {
|
|
||||||
finishDealsSelecting,
|
|
||||||
cancelDealsSelecting,
|
|
||||||
isDealsSelecting,
|
|
||||||
},
|
|
||||||
} = useDealsContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Affix position={{ bottom: 35, right: 35 }}>
|
|
||||||
<Transition
|
|
||||||
transition="slide-up"
|
|
||||||
mounted={isDealsSelecting}>
|
|
||||||
{transitionStyles => (
|
|
||||||
<Stack
|
|
||||||
bdrs={"xl"}
|
|
||||||
bd={"1px solid var(--mantine-color-default-border"}
|
|
||||||
className={styles.shadow}
|
|
||||||
p={"md"}
|
|
||||||
gap={"md"}
|
|
||||||
style={transitionStyles}>
|
|
||||||
<Title
|
|
||||||
order={5}
|
|
||||||
ta={"center"}>
|
|
||||||
Выбор сделок для группы
|
|
||||||
</Title>
|
|
||||||
<Flex gap={"xs"}>
|
|
||||||
<InlineButton onClick={cancelDealsSelecting}>
|
|
||||||
Отмена
|
|
||||||
</InlineButton>
|
|
||||||
<InlineButton
|
|
||||||
variant={"filled"}
|
|
||||||
onClick={finishDealsSelecting}>
|
|
||||||
Сохранить
|
|
||||||
</InlineButton>
|
|
||||||
</Flex>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Affix>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupDealsSelectionAffix;
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box } from "@mantine/core";
|
import { Flex } from "@mantine/core";
|
||||||
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
||||||
import {
|
import {
|
||||||
BoardView,
|
BoardView,
|
||||||
@ -10,12 +10,10 @@ 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();
|
||||||
|
|
||||||
@ -36,7 +34,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, projectId: selectedProject?.id };
|
return { boardId: selectedBoard?.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -48,7 +46,11 @@ const PageBody = () => {
|
|||||||
<PageBlock
|
<PageBlock
|
||||||
fullScreenMobile
|
fullScreenMobile
|
||||||
style={{ flex: 1 }}>
|
style={{ flex: 1 }}>
|
||||||
<Box h={"100%"}>{getViewContent()}</Box>
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
h={"100%"}>
|
||||||
|
{getViewContent()}
|
||||||
|
</Flex>
|
||||||
</PageBlock>
|
</PageBlock>
|
||||||
</DealsContextProvider>
|
</DealsContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,20 +2,17 @@ import React, { FC } from "react";
|
|||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Text } from "@mantine/core";
|
||||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
isDragging: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
const StatusColumnHeader: FC<Props> = ({ status }) => {
|
||||||
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
||||||
const { selectedBoard } = useBoardsContext();
|
const { selectedBoard } = useBoardsContext();
|
||||||
const { groupDealsSelection } = useDealsContext();
|
|
||||||
|
|
||||||
const handleSave = (value: string) => {
|
const handleSave = (value: string) => {
|
||||||
const newValue = value.trim();
|
const newValue = value.trim();
|
||||||
@ -30,6 +27,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
|||||||
p={"sm"}
|
p={"sm"}
|
||||||
wrap={"nowrap"}
|
wrap={"nowrap"}
|
||||||
mb={"xs"}
|
mb={"xs"}
|
||||||
|
w={"100%"}
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `solid ${status.color} 3px`,
|
borderBottom: `solid ${status.color} 3px`,
|
||||||
}}>
|
}}>
|
||||||
@ -44,14 +42,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
|||||||
}}
|
}}
|
||||||
getChildren={startEditing => (
|
getChildren={startEditing => (
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text>{status.name}</Text>
|
||||||
style={{
|
|
||||||
cursor: "grab",
|
|
||||||
userSelect: "none",
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
}}>
|
|
||||||
{status.name}
|
|
||||||
</Text>
|
|
||||||
<StatusMenu
|
<StatusMenu
|
||||||
board={selectedBoard}
|
board={selectedBoard}
|
||||||
status={status}
|
status={status}
|
||||||
@ -61,9 +52,6 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
|||||||
}
|
}
|
||||||
refetchStatuses={refetchStatuses}
|
refetchStatuses={refetchStatuses}
|
||||||
onDeleteStatus={statusesCrud.onDelete}
|
onDeleteStatus={statusesCrud.onDelete}
|
||||||
startDealsSelecting={
|
|
||||||
groupDealsSelection.startSelecting
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import {
|
import {
|
||||||
IconCheckbox,
|
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconExchange,
|
IconExchange,
|
||||||
@ -22,7 +21,6 @@ type Props = {
|
|||||||
onStatusColorChange: (color: string) => void;
|
onStatusColorChange: (color: string) => void;
|
||||||
board: BoardSchema | null;
|
board: BoardSchema | null;
|
||||||
onDeleteStatus: (status: StatusSchema) => void;
|
onDeleteStatus: (status: StatusSchema) => void;
|
||||||
startDealsSelecting?: () => void;
|
|
||||||
refetchStatuses?: () => void;
|
refetchStatuses?: () => void;
|
||||||
withChangeOrderButton?: boolean;
|
withChangeOrderButton?: boolean;
|
||||||
};
|
};
|
||||||
@ -33,7 +31,6 @@ const StatusMenu: FC<Props> = ({
|
|||||||
onStatusColorChange,
|
onStatusColorChange,
|
||||||
board,
|
board,
|
||||||
onDeleteStatus,
|
onDeleteStatus,
|
||||||
startDealsSelecting,
|
|
||||||
refetchStatuses,
|
refetchStatuses,
|
||||||
withChangeOrderButton = true,
|
withChangeOrderButton = true,
|
||||||
}) => {
|
}) => {
|
||||||
@ -99,13 +96,6 @@ const StatusMenu: FC<Props> = ({
|
|||||||
label={"Изменить порядок"}
|
label={"Изменить порядок"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isMobile && startDealsSelecting && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={startDealsSelecting}
|
|
||||||
icon={<IconCheckbox />}
|
|
||||||
label={"Создать группу сделок"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { Space } from "@mantine/core";
|
import { Space } from "@mantine/core";
|
||||||
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
|
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
|
||||||
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
||||||
import GroupDealsSelectionAffix from "@/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix";
|
|
||||||
|
|
||||||
export const BoardView = () => (
|
export const BoardView = () => (
|
||||||
<>
|
<>
|
||||||
<MainBlockHeader />
|
<MainBlockHeader />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Funnel />
|
<Funnel />
|
||||||
<GroupDealsSelectionAffix />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import React from "react";
|
||||||
import { UseFormReturnType } from "@mantine/form";
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
|
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
|
||||||
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
||||||
import useGroupDealsSelection, {
|
|
||||||
GroupDealsSelection,
|
|
||||||
} from "@/app/deals/hooks/useGroupDealsSelection";
|
|
||||||
import useDealGroupCrud, { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
|
|
||||||
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
|
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
|
||||||
import useDealsList from "@/hooks/lists/useDealsList";
|
import useDealsList from "@/hooks/lists/useDealsList";
|
||||||
import { SortingForm } from "@/hooks/utils/useSorting";
|
import { SortingForm } from "@/hooks/utils/useSorting";
|
||||||
@ -23,14 +19,12 @@ type DealsContextState = {
|
|||||||
groupsWithDeals: GroupWithDealsSchema[];
|
groupsWithDeals: GroupWithDealsSchema[];
|
||||||
refetchDeals: () => void;
|
refetchDeals: () => void;
|
||||||
dealsCrud: DealsCrud;
|
dealsCrud: DealsCrud;
|
||||||
groupsCrud: GroupsCrud;
|
|
||||||
paginationInfo?: PaginationInfoSchema;
|
paginationInfo?: PaginationInfoSchema;
|
||||||
page: number;
|
page: number;
|
||||||
setPage: Dispatch<SetStateAction<number>>;
|
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||||
dealsFiltersForm: UseFormReturnType<DealsFiltersForm>;
|
dealsFiltersForm: UseFormReturnType<DealsFiltersForm>;
|
||||||
isChangedFilters: boolean;
|
isChangedFilters: boolean;
|
||||||
sortingForm: UseFormReturnType<SortingForm>;
|
sortingForm: UseFormReturnType<SortingForm>;
|
||||||
groupDealsSelection: GroupDealsSelection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -54,25 +48,18 @@ const useDealsContextState = ({
|
|||||||
|
|
||||||
const dealsCrud = useDealsCrud({
|
const dealsCrud = useDealsCrud({
|
||||||
...dealsListObjects,
|
...dealsListObjects,
|
||||||
projectId,
|
|
||||||
boardId,
|
boardId,
|
||||||
statuses,
|
statuses,
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupsCrud = useDealGroupCrud();
|
|
||||||
|
|
||||||
const groupDealsSelection = useGroupDealsSelection({ groupsCrud });
|
|
||||||
|
|
||||||
const { dealsWithoutGroup, groupsWithDeals } =
|
const { dealsWithoutGroup, groupsWithDeals } =
|
||||||
useDealsAndGroups(dealsListObjects);
|
useDealsAndGroups(dealsListObjects);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dealsListObjects,
|
...dealsListObjects,
|
||||||
dealsCrud,
|
|
||||||
groupsCrud,
|
|
||||||
dealsWithoutGroup,
|
dealsWithoutGroup,
|
||||||
groupsWithDeals,
|
groupsWithDeals,
|
||||||
groupDealsSelection,
|
dealsCrud,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,22 +10,16 @@ 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 {
|
const { projects, refetch: refetchProjects, queryKey } = useProjectsList();
|
||||||
projects,
|
|
||||||
refetch: refetchProjects,
|
|
||||||
queryKey,
|
|
||||||
isLoading,
|
|
||||||
} = useProjectsList();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -40,7 +34,10 @@ 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]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -57,7 +54,6 @@ 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={"50%"}
|
size={"100%"}
|
||||||
position={"left"}
|
position={"right"}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
removeScrollProps={{ allowPinchZoom: true }}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
@ -30,6 +30,7 @@ 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, Stack, Title } from "@mantine/core";
|
import { Box, Center, Divider, Group, Text } 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>
|
||||||
<Title order={6}>{project.name}</Title>
|
<Text>{project.name}</Text>
|
||||||
</Center>
|
</Center>
|
||||||
<Box p={"lg"} />
|
<Box p={"lg"} />
|
||||||
</Group>
|
</Group>
|
||||||
@ -58,9 +58,8 @@ 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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
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,9 +1,8 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import { IconBlocks, IconEdit, IconHistory } from "@tabler/icons-react";
|
import { 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";
|
||||||
@ -21,6 +20,23 @@ 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}
|
||||||
@ -39,14 +55,7 @@ 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.modules) {
|
for (const module of props.project.builtInModules) {
|
||||||
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 (
|
||||||
@ -66,24 +75,7 @@ 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.modules) {
|
for (const module of props.project.builtInModules) {
|
||||||
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 (
|
||||||
@ -92,14 +84,7 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const tabPanel = (
|
tabPanels.push(getTabPanel(tab.key, tab.tab(props)));
|
||||||
<DealEditorTabPanel
|
|
||||||
key={tab.key}
|
|
||||||
value={tab.key}>
|
|
||||||
{tab.tab(props)}
|
|
||||||
</DealEditorTabPanel>
|
|
||||||
);
|
|
||||||
tabPanels.push(tabPanel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,12 +105,8 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
{getModuleTabs()}
|
{getModuleTabs()}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<DealEditorTabPanel value={"general"}>
|
{getTabPanel("general", <GeneralTab {...props} />)}
|
||||||
<GeneralTab {...props} />
|
{getTabPanel("history", <DealStatusHistoryTab {...props} />)}
|
||||||
</DealEditorTabPanel>
|
|
||||||
<DealEditorTabPanel value={"history"}>
|
|
||||||
<DealStatusHistoryTab {...props} />
|
|
||||||
</DealEditorTabPanel>
|
|
||||||
{getModuleTabPanels()}
|
{getModuleTabPanels()}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Drawer } from "@mantine/core";
|
import { Drawer } from "@mantine/core";
|
||||||
|
import ProjectEditorBody from "@/app/deals/drawers/ProjectEditorDrawer/components/ProjectEditorBody";
|
||||||
import { DrawerProps } from "@/drawers/types";
|
import { DrawerProps } from "@/drawers/types";
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
import { ProjectSchema } from "@/lib/client";
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import ProjectEditorBody from "@/drawers/common/ProjectEditorDrawer/components/ProjectEditorBody";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: ProjectSchema;
|
value: ProjectSchema;
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { IconBlocks, IconEdit, IconTags } from "@tabler/icons-react";
|
import { IconBlocks, IconEdit } from "@tabler/icons-react";
|
||||||
import { Tabs } from "@mantine/core";
|
import { Tabs } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
GeneralTab,
|
GeneralTab,
|
||||||
ModulesTab,
|
ModulesTab,
|
||||||
} from "@/drawers/common/ProjectEditorDrawer/tabs";
|
} from "@/app/deals/drawers/ProjectEditorDrawer/tabs";
|
||||||
import TagsTab from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/TagsTab";
|
|
||||||
import { ProjectSchema } from "@/lib/client";
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import styles from "../ProjectEditorDrawer.module.css";
|
import styles from "../ProjectEditorDrawer.module.css";
|
||||||
|
|
||||||
@ -31,21 +30,13 @@ const ProjectEditorBody: FC<Props> = props => {
|
|||||||
leftSection={<IconBlocks />}>
|
leftSection={<IconBlocks />}>
|
||||||
Модули
|
Модули
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
|
||||||
value={"tags"}
|
|
||||||
leftSection={<IconTags />}>
|
|
||||||
Теги
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Panel value={"general"}>
|
<Tabs.Panel value="general">
|
||||||
<GeneralTab {...props} />
|
<GeneralTab {...props} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value={"modules"}>
|
<Tabs.Panel value="modules">
|
||||||
<ModulesTab {...props} />
|
<ModulesTab {...props} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value={"tags"}>
|
|
||||||
<TagsTab {...props} />
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
3
src/app/deals/drawers/ProjectEditorDrawer/index.ts
Normal file
3
src/app/deals/drawers/ProjectEditorDrawer/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import ProjectEditorDrawer from "@/app/deals/drawers/ProjectEditorDrawer/ProjectEditorDrawer";
|
||||||
|
|
||||||
|
export default ProjectEditorDrawer;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Stack, TextInput } from "@mantine/core";
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
|
import Footer from "@/app/deals/drawers/ProjectEditorDrawer/tabs/GeneralTab/components/Footer";
|
||||||
import { ProjectSchema } from "@/lib/client";
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import Footer from "./components/Footer";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: ProjectSchema;
|
value: ProjectSchema;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Button, Stack } from "@mantine/core";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import resolveDependencies from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
|
import resolveDependencies from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
|
||||||
import { ProjectSchema } from "@/lib/client";
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import ModulesTable from "./components/ModulesTable";
|
import ModulesTable from "./components/ModulesTable";
|
||||||
|
|
||||||
@ -16,10 +16,12 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (values: ProjectSchema) => {
|
const onSubmit = (values: ProjectSchema) => {
|
||||||
const modulesWithDependencies = resolveDependencies(values.modules);
|
const modulesWithDependencies = resolveDependencies(
|
||||||
|
values.builtInModules
|
||||||
|
);
|
||||||
const updatedValues = {
|
const updatedValues = {
|
||||||
...values,
|
...values,
|
||||||
modules: modulesWithDependencies,
|
builtInModules: modulesWithDependencies,
|
||||||
};
|
};
|
||||||
form.setValues(updatedValues);
|
form.setValues(updatedValues);
|
||||||
form.resetDirty();
|
form.resetDirty();
|
||||||
@ -30,9 +32,9 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
|
|||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<Stack p={"md"}>
|
<Stack p={"md"}>
|
||||||
<ModulesTable
|
<ModulesTable
|
||||||
selectedRecords={form.values.modules}
|
selectedRecords={form.values.builtInModules}
|
||||||
onSelectedRecordsChange={modules =>
|
onSelectedRecordsChange={modules =>
|
||||||
form.setFieldValue("modules", modules)
|
form.setFieldValue("builtInModules", modules)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -1,24 +1,24 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Divider, Stack } from "@mantine/core";
|
import { Divider, Stack } from "@mantine/core";
|
||||||
|
import useModulesTableColumns from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
||||||
import useModulesTableColumns from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
|
import useBuiltInModulesList from "@/hooks/lists/useBuiltInModulesList";
|
||||||
import useModulesList from "@/hooks/lists/useModulesList";
|
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
||||||
import { ModuleSchemaOutput } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedRecords: ModuleSchemaOutput[];
|
selectedRecords: BuiltInModuleSchemaOutput[];
|
||||||
onSelectedRecordsChange: (records: ModuleSchemaOutput[]) => void;
|
onSelectedRecordsChange: (records: BuiltInModuleSchemaOutput[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModulesTable: FC<Props> = props => {
|
const ModulesTable: FC<Props> = props => {
|
||||||
const columns = useModulesTableColumns();
|
const columns = useModulesTableColumns();
|
||||||
const { modules } = useModulesList();
|
const { builtInModules } = useBuiltInModulesList();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Divider />
|
<Divider />
|
||||||
<BaseTable
|
<BaseTable
|
||||||
records={modules}
|
records={builtInModules}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
verticalSpacing={"md"}
|
verticalSpacing={"md"}
|
||||||
allRecordsSelectionCheckboxProps={{
|
allRecordsSelectionCheckboxProps={{
|
||||||
@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
import { DataTableColumn } from "mantine-datatable";
|
||||||
import { em, Group, Text, Tooltip } from "@mantine/core";
|
import { em, Group, Text, Tooltip } from "@mantine/core";
|
||||||
import { ModuleSchemaOutput } from "@/lib/client";
|
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
||||||
|
|
||||||
const useModulesTableColumns = () => {
|
const useModulesTableColumns = () => {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
@ -38,7 +38,7 @@ const useModulesTableColumns = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
] as DataTableColumn<ModuleSchemaOutput>[],
|
] as DataTableColumn<BuiltInModuleSchemaOutput>[],
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import { ModuleSchemaOutput } from "@/lib/client";
|
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
||||||
|
|
||||||
const resolveDependencies = (
|
const resolveDependencies = (
|
||||||
modules: ModuleSchemaOutput[]
|
modules: BuiltInModuleSchemaOutput[]
|
||||||
): ModuleSchemaOutput[] => {
|
): BuiltInModuleSchemaOutput[] => {
|
||||||
const resolved = new Set<number>();
|
const resolved = new Set<number>();
|
||||||
const result: ModuleSchemaOutput[] = [];
|
const result: BuiltInModuleSchemaOutput[] = [];
|
||||||
|
|
||||||
const addModule = (module: ModuleSchemaOutput) => {
|
const addModule = (module: BuiltInModuleSchemaOutput) => {
|
||||||
if (resolved.has(module.id)) return;
|
if (resolved.has(module.id)) return;
|
||||||
resolved.add(module.id);
|
resolved.add(module.id);
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Center, Divider, Drawer, Stack, Title } from "@mantine/core";
|
import { Center, Divider, Drawer, Text } 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,20 +30,19 @@ const ProjectsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
gap: "xs",
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Stack gap={"xs"}>
|
<Center>
|
||||||
<Center p={"xs"}>
|
<Text>Проекты</Text>
|
||||||
<Title order={6}>Проекты</Title>
|
</Center>
|
||||||
</Center>
|
<Divider />
|
||||||
<Divider />
|
<ProjectsContextProvider>
|
||||||
<ProjectsContextProvider>
|
<ProjectsDrawerBody
|
||||||
<ProjectsDrawerBody
|
onSelect={onSelect}
|
||||||
onSelect={onSelect}
|
onClose={onClose}
|
||||||
onClose={onClose}
|
/>
|
||||||
/>
|
</ProjectsContextProvider>
|
||||||
</ProjectsContextProvider>
|
|
||||||
</Stack>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,7 +34,9 @@ const ProjectMobile: FC<Props> = ({ project, onSelect, closeDrawer }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
p={"xs"}
|
w={"100%"}
|
||||||
|
pl={"xs"}
|
||||||
|
py={"xs"}
|
||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
wrap={"nowrap"}
|
wrap={"nowrap"}
|
||||||
className={styles.project}
|
className={styles.project}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ 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 { Divider, Stack } from "@mantine/core";
|
import { 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,7 +25,6 @@ const ProjectsDrawerBody: FC<Props> = ({ onSelect, onClose }) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
|
||||||
<CreateProjectButton />
|
<CreateProjectButton />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -40,9 +40,7 @@ const StatusMobile: FC<Props> = ({ status, board }) => {
|
|||||||
board={board}
|
board={board}
|
||||||
onDeleteStatus={statusesCrud.onDelete}
|
onDeleteStatus={statusesCrud.onDelete}
|
||||||
handleEdit={startEditing}
|
handleEdit={startEditing}
|
||||||
onStatusColorChange={color =>
|
onStatusColorChange={color => statusesCrud.onUpdate(status.id, { color })}
|
||||||
statusesCrud.onUpdate(status.id, { color })
|
|
||||||
}
|
|
||||||
withChangeOrderButton={false}
|
withChangeOrderButton={false}
|
||||||
/>
|
/>
|
||||||
</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, Stack, Title } from "@mantine/core";
|
import { Box, Center, Divider, Group, Text } 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>
|
||||||
<Title order={6}>{board.name}</Title>
|
<Text>{board.name}</Text>
|
||||||
</Center>
|
</Center>
|
||||||
<Box p={"lg"} />
|
<Box p={"lg"} />
|
||||||
</Group>
|
</Group>
|
||||||
@ -60,9 +60,8 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
|
|||||||
dragHandleStyle={{ width: "auto" }}
|
dragHandleStyle={{ width: "auto" }}
|
||||||
vertical
|
vertical
|
||||||
/>
|
/>
|
||||||
{statuses.length > 0 && <Divider />}
|
|
||||||
<CreateStatusButton />
|
<CreateStatusButton />
|
||||||
</Stack>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -25,16 +25,15 @@ const useDealsAndGroups = ({ deals }: Props) => {
|
|||||||
groupData.items.push(deal);
|
groupData.items.push(deal);
|
||||||
groupsWithDealMap.set(deal.group.id, groupData);
|
groupsWithDealMap.set(deal.group.id, groupData);
|
||||||
} else {
|
} else {
|
||||||
groupsWithDealMap.set(deal.group.id, {
|
groupsWithDealMap.set(deal.group.id, { ...deal.group, items: [] });
|
||||||
...deal.group,
|
|
||||||
items: [deal],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortByLexorank(groupsWithDealMap.values().toArray());
|
return sortByLexorank(groupsWithDealMap.values().toArray());
|
||||||
}, [deals]);
|
}, [deals]);
|
||||||
|
|
||||||
|
console.log(groupsWithDeals);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dealsWithoutGroup,
|
dealsWithoutGroup,
|
||||||
groupsWithDeals,
|
groupsWithDeals,
|
||||||
|
|||||||
@ -1,441 +0,0 @@
|
|||||||
import { RefObject, useMemo, useRef } from "react";
|
|
||||||
import { DragOverEvent, Over } from "@dnd-kit/core";
|
|
||||||
import { SwiperRef } from "swiper/swiper-react";
|
|
||||||
import { useDebouncedCallback } from "@mantine/hooks";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
|
||||||
import isItemGroup from "@/app/deals/utils/isItemGroup";
|
|
||||||
import {
|
|
||||||
getContainerId,
|
|
||||||
isContainerId,
|
|
||||||
} from "@/components/dnd/FunnelDnd/utils/columnId";
|
|
||||||
import {
|
|
||||||
getGroupId,
|
|
||||||
isGroupId,
|
|
||||||
} from "@/components/dnd/FunnelDnd/utils/groupId";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { StatusSchema } from "@/lib/client";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
|
||||||
|
|
||||||
type ReturnType = {
|
|
||||||
sortedStatuses: StatusSchema[];
|
|
||||||
handleDragOver: ({ active, over }: DragOverEvent) => void;
|
|
||||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
|
||||||
swiperRef: RefObject<SwiperRef | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
|
||||||
const swiperRef = useRef<SwiperRef>(null);
|
|
||||||
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
|
||||||
const {
|
|
||||||
deals,
|
|
||||||
dealsWithoutGroup,
|
|
||||||
groupsWithDeals,
|
|
||||||
setDeals,
|
|
||||||
dealsCrud,
|
|
||||||
groupsCrud,
|
|
||||||
} = useDealsContext();
|
|
||||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const {
|
|
||||||
getNewRankForSameStatus,
|
|
||||||
getNewRankForAnotherStatus,
|
|
||||||
getNewStatusRank,
|
|
||||||
} = useGetNewRank();
|
|
||||||
|
|
||||||
const debouncedSetStatuses = useDebouncedCallback(setStatuses, 200);
|
|
||||||
const debouncedSetDeals = useDebouncedCallback(setDeals, 200);
|
|
||||||
|
|
||||||
const getStatusByDealId = (dealId: number) => {
|
|
||||||
const deal = dealsWithoutGroup.find(deal => deal.id === dealId);
|
|
||||||
if (!deal) return;
|
|
||||||
return statuses.find(status => status.id === deal.status.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusByGroupId = (groupId: number) => {
|
|
||||||
const group = groupsWithDeals.find(group => group.id === groupId);
|
|
||||||
if (!group || group.items.length === 0) return;
|
|
||||||
return statuses.find(status => status.id === group.items[0].status.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusById = (statusId: number) => {
|
|
||||||
return statuses.find(status => status.id === statusId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusDealsAndGroups = (statusId: number) =>
|
|
||||||
sortByLexorank([
|
|
||||||
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
|
||||||
...groupsWithDeals.filter(
|
|
||||||
g => g.items.length > 0 && g.items[0].status.id === statusId
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const swipeSliderDuringDrag = (activeId: number | string, over: Over) => {
|
|
||||||
let activeStatus: StatusSchema | undefined;
|
|
||||||
if (typeof activeId === "string") {
|
|
||||||
activeStatus = getStatusByGroupId(getGroupId(activeId));
|
|
||||||
} else {
|
|
||||||
activeStatus = getStatusByDealId(Number(activeId));
|
|
||||||
}
|
|
||||||
const swiperActiveStatus =
|
|
||||||
statuses[swiperRef.current?.swiper.activeIndex ?? 0];
|
|
||||||
if (swiperActiveStatus.id !== activeStatus?.id) return;
|
|
||||||
|
|
||||||
const activeStatusLexorank = activeStatus?.lexorank;
|
|
||||||
let overStatusLexorank: string | undefined;
|
|
||||||
|
|
||||||
if (typeof over.id === "string") {
|
|
||||||
if (isContainerId(over.id)) {
|
|
||||||
const overStatusId = getContainerId(over.id);
|
|
||||||
overStatusLexorank = statuses.find(
|
|
||||||
s => s.id === overStatusId
|
|
||||||
)?.lexorank;
|
|
||||||
} else {
|
|
||||||
const overGroupId = getGroupId(over.id);
|
|
||||||
overStatusLexorank = getStatusByGroupId(overGroupId)?.lexorank;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!activeStatusLexorank ||
|
|
||||||
!overStatusLexorank ||
|
|
||||||
!swiperRef.current?.swiper
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const activeIndex = sortedStatuses.findIndex(
|
|
||||||
s => s.lexorank === activeStatusLexorank
|
|
||||||
);
|
|
||||||
const overIndex = sortedStatuses.findIndex(
|
|
||||||
s => s.lexorank === overStatusLexorank
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeIndex > overIndex) {
|
|
||||||
swiperRef.current.swiper.slidePrev();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeIndex < overIndex) {
|
|
||||||
swiperRef.current.swiper.slideNext();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = ({ active, over }: DragOverEvent) => {
|
|
||||||
if (!over) return;
|
|
||||||
const activeId = active.id as string | number;
|
|
||||||
|
|
||||||
if (isMobile && (typeof activeId !== "string" || isGroupId(activeId))) {
|
|
||||||
swipeSliderDuringDrag(activeId, over);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof activeId !== "string") {
|
|
||||||
handleDealDragOver(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isContainerId(activeId)) {
|
|
||||||
handleColumnDragOver(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleGroupDragOver(activeId, over);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDealDragOver = (activeId: string | number, over: Over) => {
|
|
||||||
const activeDealId = Number(activeId);
|
|
||||||
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
|
||||||
if (!activeStatusId) return;
|
|
||||||
|
|
||||||
const { overStatus, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
activeDealId,
|
|
||||||
undefined,
|
|
||||||
activeStatusId
|
|
||||||
);
|
|
||||||
if (!overStatus) return;
|
|
||||||
|
|
||||||
debouncedSetDeals(
|
|
||||||
deals.map(deal =>
|
|
||||||
deal.id === activeDealId
|
|
||||||
? {
|
|
||||||
...deal,
|
|
||||||
status: overStatus,
|
|
||||||
lexorank: newLexorank || deal.lexorank,
|
|
||||||
}
|
|
||||||
: deal
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGroupDragOver = (activeId: string, over: Over) => {
|
|
||||||
const activeGroupId = getGroupId(activeId);
|
|
||||||
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
|
|
||||||
if (!activeStatusId) return;
|
|
||||||
|
|
||||||
const { overStatus, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
undefined,
|
|
||||||
activeGroupId,
|
|
||||||
activeStatusId
|
|
||||||
);
|
|
||||||
if (!overStatus) return;
|
|
||||||
|
|
||||||
debouncedSetDeals(
|
|
||||||
deals.map(deal =>
|
|
||||||
deal.group && deal.group.id === activeGroupId
|
|
||||||
? {
|
|
||||||
...deal,
|
|
||||||
status: overStatus,
|
|
||||||
group: {
|
|
||||||
...deal.group,
|
|
||||||
lexorank: newLexorank || deal.group.lexorank,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: deal
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnDragOver = (activeId: string, over: Over) => {
|
|
||||||
const activeStatusId = getContainerId(activeId);
|
|
||||||
let overStatusId: number;
|
|
||||||
|
|
||||||
if (typeof over.id === "string") {
|
|
||||||
if (isContainerId(over.id)) {
|
|
||||||
overStatusId = getContainerId(over.id);
|
|
||||||
} else {
|
|
||||||
const status = getStatusByGroupId(getGroupId(over.id));
|
|
||||||
if (!status) return;
|
|
||||||
overStatusId = status.id;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const deal = dealsWithoutGroup.find(deal => deal.id === over.id);
|
|
||||||
if (!deal) return;
|
|
||||||
overStatusId = deal.status.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overStatusId || activeStatusId === overStatusId) return;
|
|
||||||
|
|
||||||
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
|
||||||
if (!newRank) return;
|
|
||||||
|
|
||||||
debouncedSetStatuses(
|
|
||||||
statuses.map(status =>
|
|
||||||
status.id === activeStatusId
|
|
||||||
? { ...status, lexorank: newRank }
|
|
||||||
: status
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDropTarget = (
|
|
||||||
overId: string | number,
|
|
||||||
activeDealId: number | undefined,
|
|
||||||
activeGroupId: number | undefined,
|
|
||||||
activeStatusId: number,
|
|
||||||
isOnDragEnd: boolean = false
|
|
||||||
): { overStatus?: StatusSchema; newLexorank?: string } => {
|
|
||||||
if (typeof overId === "string") {
|
|
||||||
if (isContainerId(overId)) {
|
|
||||||
return getStatusDropTarget(overId);
|
|
||||||
}
|
|
||||||
if (isGroupId(overId)) {
|
|
||||||
return getGroupDropTarget(
|
|
||||||
overId,
|
|
||||||
activeGroupId,
|
|
||||||
activeStatusId,
|
|
||||||
isOnDragEnd
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDealDropTarget(
|
|
||||||
Number(overId),
|
|
||||||
activeDealId,
|
|
||||||
activeStatusId,
|
|
||||||
isOnDragEnd
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusDropTarget = (overId: string) => ({
|
|
||||||
overStatus: getStatusById(getContainerId(overId)),
|
|
||||||
newLexorank: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDealDropTarget = (
|
|
||||||
overId: number,
|
|
||||||
activeDealId: number | undefined,
|
|
||||||
activeStatusId: number,
|
|
||||||
isOnDragEnd: boolean = false
|
|
||||||
) => {
|
|
||||||
const overDealId = Number(overId);
|
|
||||||
const overStatus = getStatusByDealId(overDealId);
|
|
||||||
|
|
||||||
if (!overStatus || (!isOnDragEnd && activeDealId === overDealId)) {
|
|
||||||
return { overStatus: undefined, newLexorank: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusItems = getStatusDealsAndGroups(overStatus.id);
|
|
||||||
const overDealIndex = statusItems.findIndex(
|
|
||||||
deal => !isItemGroup(deal) && deal.id === overDealId
|
|
||||||
);
|
|
||||||
const activeDealIndex = statusItems.findIndex(
|
|
||||||
deal => !isItemGroup(deal) && deal.id === activeDealId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeStatusId === overStatus.id) {
|
|
||||||
const newLexorank = getNewRankForSameStatus(
|
|
||||||
statusItems,
|
|
||||||
overDealIndex,
|
|
||||||
activeDealIndex
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLexorank = getNewRankForAnotherStatus(
|
|
||||||
statusItems,
|
|
||||||
overDealIndex
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGroupDropTarget = (
|
|
||||||
overId: string,
|
|
||||||
activeGroupId: number | undefined,
|
|
||||||
activeStatusId: number,
|
|
||||||
isOnDragEnd: boolean = false
|
|
||||||
) => {
|
|
||||||
const overGroupId = getGroupId(overId);
|
|
||||||
const overStatus = getStatusByGroupId(overGroupId);
|
|
||||||
|
|
||||||
if (!overStatus || (!isOnDragEnd && activeGroupId === overGroupId)) {
|
|
||||||
return { overStatus: undefined, newLexorank: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusItems = getStatusDealsAndGroups(overStatus.id);
|
|
||||||
const overGroupIndex = statusItems.findIndex(
|
|
||||||
group => isItemGroup(group) && group.id === overGroupId
|
|
||||||
);
|
|
||||||
const activeGroupIndex = statusItems.findIndex(
|
|
||||||
group => isItemGroup(group) && group.id === activeGroupId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeStatusId === overStatus.id) {
|
|
||||||
const newLexorank = getNewRankForSameStatus(
|
|
||||||
statusItems,
|
|
||||||
overGroupIndex,
|
|
||||||
activeGroupIndex
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLexorank = getNewRankForAnotherStatus(
|
|
||||||
statusItems,
|
|
||||||
overGroupIndex
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
|
||||||
if (!over) return;
|
|
||||||
|
|
||||||
const activeId: string | number = active.id;
|
|
||||||
|
|
||||||
if (typeof activeId !== "string") {
|
|
||||||
handleDealDragEnd(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isContainerId(activeId)) {
|
|
||||||
handleStatusColumnDragEnd(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleGroupDragEnd(activeId, over);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
|
|
||||||
const activeStatusId = getContainerId(activeId);
|
|
||||||
let overStatusId: number;
|
|
||||||
|
|
||||||
if (typeof over.id === "string" && isContainerId(over.id)) {
|
|
||||||
overStatusId = getContainerId(over.id);
|
|
||||||
} else {
|
|
||||||
const deal = dealsWithoutGroup.find(
|
|
||||||
deal => deal.status.id === over.id
|
|
||||||
);
|
|
||||||
if (!deal) return;
|
|
||||||
overStatusId = deal.status.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overStatusId) return;
|
|
||||||
|
|
||||||
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
|
||||||
if (!newRank) return;
|
|
||||||
|
|
||||||
onStatusDragEnd?.(activeStatusId, newRank);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
|
||||||
statusesCrud.onUpdate(statusId, { lexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
|
||||||
const activeDealId = Number(activeId);
|
|
||||||
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
|
||||||
if (!activeStatusId) return;
|
|
||||||
|
|
||||||
const { overStatus, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
activeDealId,
|
|
||||||
undefined,
|
|
||||||
activeStatusId,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (!overStatus) return;
|
|
||||||
|
|
||||||
onDealDragEnd(activeDealId, overStatus.id, newLexorank);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDealDragEnd = (
|
|
||||||
dealId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => {
|
|
||||||
dealsCrud.onUpdate(dealId, { statusId, lexorank, name: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGroupDragEnd = (activeId: string, over: Over) => {
|
|
||||||
const activeGroupId = getGroupId(activeId);
|
|
||||||
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
|
|
||||||
if (!activeStatusId) return;
|
|
||||||
|
|
||||||
const { overStatus, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
undefined,
|
|
||||||
activeGroupId,
|
|
||||||
activeStatusId,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (!overStatus) return;
|
|
||||||
|
|
||||||
onGroupDragEnd(activeGroupId, overStatus.id, newLexorank);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGroupDragEnd = (
|
|
||||||
groupId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => {
|
|
||||||
groupsCrud.onUpdate(groupId, { statusId, lexorank, name: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
swiperRef,
|
|
||||||
sortedStatuses,
|
|
||||||
handleDragOver,
|
|
||||||
handleDragEnd,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDealsAndStatusesDnd;
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
import { LexoRank } from "lexorank";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import {
|
|
||||||
BaseDraggable,
|
|
||||||
BaseGroupDraggable,
|
|
||||||
} from "@/components/dnd/types/types";
|
|
||||||
import { getNewLexorank } from "@/utils/lexorank/generation";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
|
||||||
|
|
||||||
type NewRankGetters<
|
|
||||||
TItem extends BaseDraggable,
|
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
> = {
|
|
||||||
getNewRankForSameStatus: (
|
|
||||||
statusItemsAndGroups: (TItem | TGroup)[],
|
|
||||||
overItemOrGroupIndex: number,
|
|
||||||
activeItemOrGroupIndex: number
|
|
||||||
) => string;
|
|
||||||
getNewRankForAnotherStatus: (
|
|
||||||
statusItemsAndGroups: (TItem | TGroup)[],
|
|
||||||
overItemOrGroupIndex: number
|
|
||||||
) => string;
|
|
||||||
getNewStatusRank: (
|
|
||||||
activeStatusId: number,
|
|
||||||
overStatusId: number
|
|
||||||
) => string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGetNewRank = <
|
|
||||||
TItem extends BaseDraggable,
|
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
>(): NewRankGetters<TItem, TGroup> => {
|
|
||||||
const { statuses } = useStatusesContext();
|
|
||||||
|
|
||||||
const getNewRankForSameStatus = (
|
|
||||||
statusItemsAndGroups: (TItem | TGroup)[],
|
|
||||||
overItemOrGroupIndex: number,
|
|
||||||
activeItemOrGroupIndex: number
|
|
||||||
): string => {
|
|
||||||
const [leftIndex, rightIndex] =
|
|
||||||
overItemOrGroupIndex < activeItemOrGroupIndex
|
|
||||||
? [overItemOrGroupIndex - 1, overItemOrGroupIndex]
|
|
||||||
: [overItemOrGroupIndex, overItemOrGroupIndex + 1];
|
|
||||||
|
|
||||||
const leftLexorank =
|
|
||||||
leftIndex >= 0
|
|
||||||
? LexoRank.parse(statusItemsAndGroups[leftIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
const rightLexorank =
|
|
||||||
rightIndex < statusItemsAndGroups.length
|
|
||||||
? LexoRank.parse(statusItemsAndGroups[rightIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNewRankForAnotherStatus = (
|
|
||||||
statusItemsAndGroups: (TItem | TGroup)[],
|
|
||||||
overItemOrGroupIndex: number
|
|
||||||
): string => {
|
|
||||||
const leftLexorank =
|
|
||||||
overItemOrGroupIndex > 0
|
|
||||||
? LexoRank.parse(
|
|
||||||
statusItemsAndGroups[overItemOrGroupIndex - 1].lexorank
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const rightLexorank = LexoRank.parse(
|
|
||||||
statusItemsAndGroups[overItemOrGroupIndex].lexorank
|
|
||||||
);
|
|
||||||
|
|
||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNewStatusRank = (
|
|
||||||
activeStatusId: number,
|
|
||||||
overStatusId: number
|
|
||||||
): string | null => {
|
|
||||||
const sortedStatusList = sortByLexorank(statuses);
|
|
||||||
const overIndex = sortedStatusList.findIndex(
|
|
||||||
s => s.id === overStatusId
|
|
||||||
);
|
|
||||||
const activeIndex = sortedStatusList.findIndex(
|
|
||||||
s => s.id === activeStatusId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (overIndex === -1 || activeIndex === -1) return null;
|
|
||||||
|
|
||||||
const [leftIndex, rightIndex] =
|
|
||||||
overIndex < activeIndex
|
|
||||||
? [overIndex - 1, overIndex]
|
|
||||||
: [overIndex, overIndex + 1];
|
|
||||||
|
|
||||||
const leftLexorank =
|
|
||||||
leftIndex >= 0
|
|
||||||
? LexoRank.parse(statuses[leftIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
const rightLexorank =
|
|
||||||
rightIndex < statuses.length
|
|
||||||
? LexoRank.parse(statuses[rightIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getNewRankForSameStatus,
|
|
||||||
getNewRankForAnotherStatus,
|
|
||||||
getNewStatusRank,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useGetNewRank;
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
|
||||||
import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
|
||||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
groupsCrud: GroupsCrud;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GroupDealsSelection = {
|
|
||||||
isDealsSelecting: boolean;
|
|
||||||
selectedBaseDealId: number | null;
|
|
||||||
startSelectingWithDeal: (dealId: number) => void;
|
|
||||||
selectedGroupId: number | null;
|
|
||||||
startSelectingWithExistingGroup: (group: GroupWithDealsSchema) => void;
|
|
||||||
startSelecting: () => void;
|
|
||||||
selectedDealIds: Set<number>;
|
|
||||||
setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>;
|
|
||||||
toggleDeal: (deal: DealSchema) => void;
|
|
||||||
finishDealsSelecting: () => void;
|
|
||||||
cancelDealsSelecting: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
|
|
||||||
const [selectedDealIds, setSelectedDealIds] = useState<Set<number>>(
|
|
||||||
new Set()
|
|
||||||
);
|
|
||||||
const [isDealsSelecting, setIsDealsSelecting] = useState<boolean>(false);
|
|
||||||
const [selectedBaseDealId, setSelectedBaseDealId] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const toggleDeal = (deal: DealSchema) => {
|
|
||||||
if (selectedBaseDealId === deal.id) return;
|
|
||||||
|
|
||||||
if (selectedDealIds.has(deal.id)) {
|
|
||||||
selectedDealIds.delete(deal.id);
|
|
||||||
} else {
|
|
||||||
if (!selectedBaseDealId && !selectedGroupId) {
|
|
||||||
if (deal.group) return;
|
|
||||||
setSelectedBaseDealId(deal.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedDealIds.add(deal.id);
|
|
||||||
}
|
|
||||||
setSelectedDealIds(new Set(selectedDealIds));
|
|
||||||
};
|
|
||||||
|
|
||||||
const finishDealsSelecting = () => {
|
|
||||||
if (selectedBaseDealId) {
|
|
||||||
groupsCrud.onCreate(
|
|
||||||
selectedBaseDealId,
|
|
||||||
selectedDealIds.values().toArray()
|
|
||||||
);
|
|
||||||
setSelectedBaseDealId(null);
|
|
||||||
} else if (selectedGroupId) {
|
|
||||||
groupsCrud.onUpdateDealsInGroup(
|
|
||||||
selectedGroupId,
|
|
||||||
selectedDealIds.values().toArray()
|
|
||||||
);
|
|
||||||
setSelectedGroupId(null);
|
|
||||||
}
|
|
||||||
setIsDealsSelecting(false);
|
|
||||||
setSelectedDealIds(new Set());
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelDealsSelecting = () => {
|
|
||||||
setSelectedDealIds(new Set());
|
|
||||||
setSelectedBaseDealId(null);
|
|
||||||
setSelectedGroupId(null);
|
|
||||||
setIsDealsSelecting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For editing group
|
|
||||||
const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => {
|
|
||||||
setSelectedDealIds(new Set(group.items.map(item => item.id)));
|
|
||||||
setSelectedBaseDealId(null);
|
|
||||||
setSelectedGroupId(group.id);
|
|
||||||
setIsDealsSelecting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For creating group on desktop
|
|
||||||
const startSelectingWithDeal = (dealId: number) => {
|
|
||||||
setSelectedDealIds(new Set([dealId]));
|
|
||||||
setSelectedBaseDealId(dealId);
|
|
||||||
setSelectedGroupId(null);
|
|
||||||
setIsDealsSelecting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For creating group on mobile
|
|
||||||
const startSelecting = () => {
|
|
||||||
setSelectedDealIds(new Set());
|
|
||||||
setSelectedBaseDealId(null);
|
|
||||||
setSelectedGroupId(null);
|
|
||||||
setIsDealsSelecting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDealsSelecting,
|
|
||||||
selectedBaseDealId,
|
|
||||||
startSelectingWithDeal,
|
|
||||||
selectedGroupId,
|
|
||||||
startSelectingWithExistingGroup,
|
|
||||||
startSelecting,
|
|
||||||
selectedDealIds,
|
|
||||||
setSelectedDealIds,
|
|
||||||
toggleDeal,
|
|
||||||
finishDealsSelecting,
|
|
||||||
cancelDealsSelecting,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useGroupDealsSelection;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
"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,19 +17,14 @@ import { combineProviders } from "@/utils/combineProviders";
|
|||||||
|
|
||||||
async function prefetchData() {
|
async function prefetchData() {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
const projectsData = await queryClient.fetchQuery(getProjectsOptions());
|
||||||
|
|
||||||
try {
|
const firstProjectId = projectsData.items?.[0]?.id;
|
||||||
const projectsData = await queryClient.fetchQuery(getProjectsOptions());
|
if (!firstProjectId) return queryClient;
|
||||||
|
|
||||||
const firstProjectId = projectsData.items?.[0]?.id;
|
await queryClient.prefetchQuery(
|
||||||
if (!firstProjectId) return queryClient;
|
getBoardsOptions({ path: { projectId: firstProjectId } })
|
||||||
|
);
|
||||||
await queryClient.prefetchQuery(
|
|
||||||
getBoardsOptions({ path: { projectId: firstProjectId } })
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Prefetch failed, continuing without data:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryClient;
|
return queryClient;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import {
|
|
||||||
BaseDraggable,
|
|
||||||
BaseGroupDraggable,
|
|
||||||
} from "@/components/dnd/types/types";
|
|
||||||
|
|
||||||
const isItemGroup = <
|
|
||||||
TItem extends BaseDraggable,
|
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
>(
|
|
||||||
item: TItem | TGroup
|
|
||||||
): boolean => {
|
|
||||||
return "items" in item;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default isItemGroup;
|
|
||||||
6
src/app/deals/utils/statusId.ts
Normal file
6
src/app/deals/utils/statusId.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const STATUS_POSTFIX = "-status";
|
||||||
|
|
||||||
|
export const isStatusId = (rawId: string) => rawId.endsWith(STATUS_POSTFIX);
|
||||||
|
|
||||||
|
export const getStatusId = (rawId: string) =>
|
||||||
|
Number(rawId.replace(STATUS_POSTFIX, ""));
|
||||||
@ -2,7 +2,6 @@ import "@mantine/core/styles.css";
|
|||||||
import "mantine-datatable/styles.layer.css";
|
import "mantine-datatable/styles.layer.css";
|
||||||
import "@mantine/notifications/styles.css";
|
import "@mantine/notifications/styles.css";
|
||||||
import "@mantine/dates/styles.css";
|
import "@mantine/dates/styles.css";
|
||||||
import "mantine-contextmenu/styles.css";
|
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
import "swiper/css/scrollbar";
|
import "swiper/css/scrollbar";
|
||||||
@ -15,8 +14,6 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { theme } from "@/theme";
|
import { theme } from "@/theme";
|
||||||
import "@/app/global.css";
|
import "@/app/global.css";
|
||||||
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";
|
||||||
@ -66,44 +63,40 @@ export default function RootLayout({ children }: Props) {
|
|||||||
<MantineProvider
|
<MantineProvider
|
||||||
theme={theme}
|
theme={theme}
|
||||||
defaultColorScheme={"auto"}>
|
defaultColorScheme={"auto"}>
|
||||||
<ContextMenuProvider>
|
<ReactQueryProvider>
|
||||||
<ReactQueryProvider>
|
<ReduxProvider>
|
||||||
<ReduxProvider>
|
<ModalsProvider
|
||||||
<ModalsProvider
|
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||||
labels={{ confirm: "Да", cancel: "Нет" }}
|
modals={modals}>
|
||||||
modals={modals}>
|
<DrawersContextProvider>
|
||||||
<DatesProvider settings={{ locale: "ru" }}>
|
<ProjectsContextProvider>
|
||||||
<DrawersContextProvider>
|
<AppShell
|
||||||
<ProjectsContextProvider>
|
layout={"alt"}
|
||||||
<AppShell
|
withBorder={false}
|
||||||
layout={"alt"}
|
navbar={{
|
||||||
withBorder={false}
|
width: 220,
|
||||||
navbar={{
|
breakpoint: "sm",
|
||||||
width: 220,
|
collapsed: {
|
||||||
breakpoint: "sm",
|
desktop: false,
|
||||||
collapsed: {
|
mobile: true,
|
||||||
desktop: false,
|
},
|
||||||
mobile: true,
|
}}>
|
||||||
},
|
<AppShellNavbarWrapper>
|
||||||
}}>
|
<Navbar />
|
||||||
<AppShellNavbarWrapper>
|
</AppShellNavbarWrapper>
|
||||||
<Navbar />
|
<AppShellMainWrapper>
|
||||||
</AppShellNavbarWrapper>
|
{children}
|
||||||
<AppShellMainWrapper>
|
</AppShellMainWrapper>
|
||||||
{children}
|
<AppShellFooterWrapper>
|
||||||
</AppShellMainWrapper>
|
<Footer />
|
||||||
<AppShellFooterWrapper>
|
</AppShellFooterWrapper>
|
||||||
<Footer />
|
</AppShell>
|
||||||
</AppShellFooterWrapper>
|
</ProjectsContextProvider>
|
||||||
</AppShell>
|
</DrawersContextProvider>
|
||||||
</ProjectsContextProvider>
|
</ModalsProvider>
|
||||||
</DrawersContextProvider>
|
</ReduxProvider>
|
||||||
</DatesProvider>
|
<Notifications position="bottom-right" />
|
||||||
</ModalsProvider>
|
</ReactQueryProvider>
|
||||||
</ReduxProvider>
|
|
||||||
<Notifications position="bottom-right" />
|
|
||||||
</ReactQueryProvider>
|
|
||||||
</ContextMenuProvider>
|
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
"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;
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
"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"
|
|
||||||
);
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user