Compare commits
1 Commits
8cc11bca67
...
pragmatic-
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c2a3a2af |
@ -11,6 +11,12 @@
|
|||||||
"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,6 +33,7 @@
|
|||||||
"@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",
|
||||||
@ -38,7 +45,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RefObject, useMemo, useRef } from "react";
|
import { useMemo } from "react";
|
||||||
import { 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,14 +20,6 @@ const PageBody = () => {
|
|||||||
[modulesSet]
|
[modulesSet]
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
|
||||||
{
|
|
||||||
icon: IconTag,
|
|
||||||
label: "Теги",
|
|
||||||
href: "/tags",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBlock fullScreenMobile>
|
<PageBlock fullScreenMobile>
|
||||||
<Stack p={"xs"}>
|
<Stack p={"xs"}>
|
||||||
@ -43,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}
|
||||||
|
|||||||
@ -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,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,
|
||||||
@ -46,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 = {
|
||||||
@ -58,20 +52,14 @@ const useDealsContextState = ({
|
|||||||
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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
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 useBuiltInModulesList from "@/hooks/lists/useBuiltInModulesList";
|
||||||
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
import { BuiltInModuleSchemaOutput } from "@/lib/client";
|
||||||
|
|
||||||
@ -2,7 +2,6 @@ import React, { FC } from "react";
|
|||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
|
||||||
import { BoardSchema, StatusSchema } from "@/lib/client";
|
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const StatusMobile: FC<Props> = ({ status, board }) => {
|
const StatusMobile: FC<Props> = ({ status, board }) => {
|
||||||
const { statusesCrud } = useStatusesMobileContext();
|
const { statusesCrud } = useStatusesMobileContext();
|
||||||
const { groupDealsSelection } = useDealsContext();
|
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
modals.openContextModal({
|
modals.openContextModal({
|
||||||
@ -42,11 +40,8 @@ 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}
|
||||||
startDealsSelecting={groupDealsSelection.startSelecting}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,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,7 +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 { 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";
|
||||||
@ -65,42 +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>
|
||||||
<DrawersContextProvider>
|
<ProjectsContextProvider>
|
||||||
<ProjectsContextProvider>
|
<AppShell
|
||||||
<AppShell
|
layout={"alt"}
|
||||||
layout={"alt"}
|
withBorder={false}
|
||||||
withBorder={false}
|
navbar={{
|
||||||
navbar={{
|
width: 220,
|
||||||
width: 220,
|
breakpoint: "sm",
|
||||||
breakpoint: "sm",
|
collapsed: {
|
||||||
collapsed: {
|
desktop: false,
|
||||||
desktop: false,
|
mobile: true,
|
||||||
mobile: true,
|
},
|
||||||
},
|
}}>
|
||||||
}}>
|
<AppShellNavbarWrapper>
|
||||||
<AppShellNavbarWrapper>
|
<Navbar />
|
||||||
<Navbar />
|
</AppShellNavbarWrapper>
|
||||||
</AppShellNavbarWrapper>
|
<AppShellMainWrapper>
|
||||||
<AppShellMainWrapper>
|
{children}
|
||||||
{children}
|
</AppShellMainWrapper>
|
||||||
</AppShellMainWrapper>
|
<AppShellFooterWrapper>
|
||||||
<AppShellFooterWrapper>
|
<Footer />
|
||||||
<Footer />
|
</AppShellFooterWrapper>
|
||||||
</AppShellFooterWrapper>
|
</AppShell>
|
||||||
</AppShell>
|
</ProjectsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</DrawersContextProvider>
|
||||||
</DrawersContextProvider>
|
</ModalsProvider>
|
||||||
</ModalsProvider>
|
</ReduxProvider>
|
||||||
</ReduxProvider>
|
<Notifications position="bottom-right" />
|
||||||
<Notifications position="bottom-right" />
|
</ReactQueryProvider>
|
||||||
</ReactQueryProvider>
|
|
||||||
</ContextMenuProvider>
|
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Center, Divider, Stack, Text } from "@mantine/core";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import TagsPageHeader from "@/app/tags/components/TagsPageHeader/TagsPageHeader";
|
|
||||||
import TagsTable from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTable";
|
|
||||||
import { DealTagsContextProvider } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
|
||||||
|
|
||||||
const PageBody = () => {
|
|
||||||
const { selectedProject } = useProjectsContext();
|
|
||||||
|
|
||||||
if (!selectedProject) {
|
|
||||||
return (
|
|
||||||
<Center>
|
|
||||||
<Text>Проект не найден</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DealTagsContextProvider project={selectedProject}>
|
|
||||||
<Stack gap={"md"}>
|
|
||||||
<TagsPageHeader project={selectedProject} />
|
|
||||||
<Divider />
|
|
||||||
<TagsTable />
|
|
||||||
</Stack>
|
|
||||||
</DealTagsContextProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageBody;
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { IconChevronLeft, IconPlus } from "@tabler/icons-react";
|
|
||||||
import { Box, Group, Title } from "@mantine/core";
|
|
||||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
|
||||||
import { ProjectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
project: ProjectSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TagsPageHeader: FC<Props> = ({ project }) => {
|
|
||||||
const { onCreateClick } = useDealTagActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
mx={"xs"}
|
|
||||||
mt={"md"}
|
|
||||||
wrap={"nowrap"}
|
|
||||||
justify={"space-between"}>
|
|
||||||
<Link href={"/actions"}>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Link>
|
|
||||||
<Title order={5}>Теги проекта "{project.name}"</Title>
|
|
||||||
<Box onClick={onCreateClick}>
|
|
||||||
<IconPlus />
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsPageHeader;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Center, Loader } from "@mantine/core";
|
|
||||||
import PageBody from "@/app/tags/components/PageBody/PageBody";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Page for mobiles only
|
|
||||||
*/
|
|
||||||
export default async function TagsPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Center h="50vh">
|
|
||||||
<Loader size="lg" />
|
|
||||||
</Center>
|
|
||||||
}>
|
|
||||||
<PageContainer>
|
|
||||||
<PageBlock fullScreenMobile>
|
|
||||||
<PageBody />
|
|
||||||
</PageBlock>
|
|
||||||
</PageContainer>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
15
src/components/dnd-pragmatic/DndFunnel/DndFunnel.module.css
Normal file
15
src/components/dnd-pragmatic/DndFunnel/DndFunnel.module.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.visible-column {
|
||||||
|
border-radius: var(--mantine-spacing-lg);
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--color-light-aqua);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/components/dnd-pragmatic/DndFunnel/DndFunnel.tsx
Normal file
39
src/components/dnd-pragmatic/DndFunnel/DndFunnel.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import DndBoard from "@/components/dnd-pragmatic/DndFunnel/components/DndBoard";
|
||||||
|
import { DndColumn } from "@/components/dnd-pragmatic/DndFunnel/components/DndColumn";
|
||||||
|
import { DndFunnelContextProvider } from "@/components/dnd-pragmatic/DndFunnel/contexts/DndBoardContext";
|
||||||
|
import {
|
||||||
|
BaseColumnType,
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import FunnelDndProps from "@/components/dnd-pragmatic/DndFunnel/types/FunnelDndProps";
|
||||||
|
|
||||||
|
const DndFunnel = <
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>(
|
||||||
|
props: FunnelDndProps<ColumnType, ItemType, GroupType>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<DndFunnelContextProvider {...props}>
|
||||||
|
<DndBoard>
|
||||||
|
{props.columns.map(column => (
|
||||||
|
<DndColumn
|
||||||
|
column={column}
|
||||||
|
columnItemsAndGroups={props.getColumnItemsGroups(
|
||||||
|
column.id
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
key={column.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DndBoard>
|
||||||
|
</DndFunnelContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DndFunnel;
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import React, { ForwardedRef, forwardRef, ReactNode, type Ref } from "react";
|
||||||
|
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box";
|
||||||
|
import { Flex, xcss } from "@atlaskit/primitives";
|
||||||
|
import { BaseItemType } from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import { CardState } from "@/components/dnd-pragmatic/DndFunnel/types/DndStates";
|
||||||
|
|
||||||
|
const baseStyles = xcss({
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateStyles: {
|
||||||
|
[Key in CardState["type"]]: ReturnType<typeof xcss> | undefined;
|
||||||
|
} = {
|
||||||
|
idle: xcss({
|
||||||
|
cursor: "grab",
|
||||||
|
}),
|
||||||
|
dragging: xcss({
|
||||||
|
opacity: 0.4,
|
||||||
|
}),
|
||||||
|
// no shadow for preview - the platform will add it's own drop shadow
|
||||||
|
preview: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
type CardPrimitiveProps<ItemType extends BaseItemType> = {
|
||||||
|
closestEdge: Edge | null;
|
||||||
|
item: ItemType;
|
||||||
|
renderItem: (item: any) => ReactNode;
|
||||||
|
state: CardState;
|
||||||
|
actionMenuTriggerRef?: Ref<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CardWrapper = forwardRef(
|
||||||
|
<ItemType extends BaseItemType>(
|
||||||
|
{ closestEdge, item, renderItem, state }: CardPrimitiveProps<ItemType>,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
columnGap="space.100"
|
||||||
|
alignItems="center"
|
||||||
|
xcss={[baseStyles, stateStyles[state.type]]}>
|
||||||
|
{renderItem(item)}
|
||||||
|
{closestEdge && <DropIndicator edge={closestEdge} />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CardWrapper;
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import React, { forwardRef, memo, useEffect, type ReactNode } from "react";
|
||||||
|
import { autoScrollWindowForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import { useDndFunnelContext } from "../contexts/DndBoardContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const boardStyles = classNames({
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "space.200",
|
||||||
|
flexDirection: "row",
|
||||||
|
flex: 1,
|
||||||
|
border: "1px white solid",
|
||||||
|
});
|
||||||
|
|
||||||
|
const DndBoard = memo(
|
||||||
|
forwardRef<HTMLDivElement, Props>(({ children }: Props, ref) => {
|
||||||
|
const { instanceId } = useDndFunnelContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return autoScrollWindowForElements({
|
||||||
|
canScroll: ({ source }) =>
|
||||||
|
source.data.instanceId === instanceId,
|
||||||
|
});
|
||||||
|
}, [instanceId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={boardStyles}
|
||||||
|
flex={1}
|
||||||
|
ref={ref}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DndBoard;
|
||||||
146
src/components/dnd-pragmatic/DndFunnel/components/DndCard.tsx
Normal file
146
src/components/dnd-pragmatic/DndFunnel/components/DndCard.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, { memo, ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
attachClosestEdge,
|
||||||
|
extractClosestEdge,
|
||||||
|
type Edge,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import {
|
||||||
|
draggable,
|
||||||
|
dropTargetForElements,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source";
|
||||||
|
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
||||||
|
import { dropTargetForExternal } from "@atlaskit/pragmatic-drag-and-drop/external/adapter";
|
||||||
|
import { Box } from "@atlaskit/primitives";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
import CardWrapper from "@/components/dnd-pragmatic/DndFunnel/components/CardWrapper";
|
||||||
|
import { useDndFunnelContext } from "@/components/dnd-pragmatic/DndFunnel/contexts/DndBoardContext";
|
||||||
|
import { BaseItemType } from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import { CardState } from "../types/DndStates";
|
||||||
|
|
||||||
|
const idleState: CardState = { type: "idle" };
|
||||||
|
const draggingState: CardState = { type: "dragging" };
|
||||||
|
|
||||||
|
type DndCardProps<ItemType extends BaseItemType> = {
|
||||||
|
item: ItemType;
|
||||||
|
renderItem: (item: any) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DndCard = memo(
|
||||||
|
<ItemType extends BaseItemType>({
|
||||||
|
item,
|
||||||
|
renderItem,
|
||||||
|
}: DndCardProps<ItemType>) => {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [closestEdge, setClosestEdge] = useState<Edge | null>(null);
|
||||||
|
const [state, setState] = useState<CardState>(idleState);
|
||||||
|
|
||||||
|
const { instanceId } = useDndFunnelContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
invariant(element);
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
draggable({
|
||||||
|
element,
|
||||||
|
getInitialData: () => ({
|
||||||
|
type: "card",
|
||||||
|
itemId: item.id,
|
||||||
|
instanceId,
|
||||||
|
}),
|
||||||
|
onGenerateDragPreview: ({
|
||||||
|
location,
|
||||||
|
source,
|
||||||
|
nativeSetDragImage,
|
||||||
|
}) => {
|
||||||
|
const rect = source.element.getBoundingClientRect();
|
||||||
|
|
||||||
|
setCustomNativeDragPreview({
|
||||||
|
nativeSetDragImage,
|
||||||
|
getOffset: preserveOffsetOnSource({
|
||||||
|
element,
|
||||||
|
input: location.current.input,
|
||||||
|
}),
|
||||||
|
render({ container }) {
|
||||||
|
setState({ type: "preview", container, rect });
|
||||||
|
return () => setState(draggingState);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart: () => setState(draggingState),
|
||||||
|
onDrop: () => setState(idleState),
|
||||||
|
}),
|
||||||
|
dropTargetForExternal({
|
||||||
|
element,
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element,
|
||||||
|
canDrop: ({ source }) => {
|
||||||
|
return (
|
||||||
|
source.data.instanceId === instanceId &&
|
||||||
|
source.data.type === "card"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getIsSticky: () => true,
|
||||||
|
getData: ({ input, element }) => {
|
||||||
|
const data = { type: "card", itemId: item.id };
|
||||||
|
|
||||||
|
return attachClosestEdge(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
allowedEdges: ["top", "bottom"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragEnter: args => {
|
||||||
|
if (args.source.data.itemId !== item.id) {
|
||||||
|
setClosestEdge(extractClosestEdge(args.self.data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrag: args => {
|
||||||
|
if (args.source.data.itemId !== item.id) {
|
||||||
|
setClosestEdge(extractClosestEdge(args.self.data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragLeave: () => {
|
||||||
|
setClosestEdge(null);
|
||||||
|
},
|
||||||
|
onDrop: () => {
|
||||||
|
setClosestEdge(null);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [instanceId, item.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardWrapper
|
||||||
|
ref={ref}
|
||||||
|
item={item}
|
||||||
|
renderItem={renderItem}
|
||||||
|
state={state}
|
||||||
|
closestEdge={closestEdge}
|
||||||
|
/>
|
||||||
|
{state.type === "preview" &&
|
||||||
|
ReactDOM.createPortal(
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: state.rect.width,
|
||||||
|
height: state.rect.height,
|
||||||
|
}}>
|
||||||
|
<CardWrapper
|
||||||
|
item={item}
|
||||||
|
renderItem={renderItem}
|
||||||
|
state={state}
|
||||||
|
closestEdge={null}
|
||||||
|
/>
|
||||||
|
</Box>,
|
||||||
|
state.container
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
306
src/components/dnd-pragmatic/DndFunnel/components/DndColumn.tsx
Normal file
306
src/components/dnd-pragmatic/DndFunnel/components/DndColumn.tsx
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import React, { memo, ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import { easeInOut } from "@atlaskit/motion/curves";
|
||||||
|
import { durations } from "@atlaskit/motion/durations";
|
||||||
|
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
|
import {
|
||||||
|
attachClosestEdge,
|
||||||
|
extractClosestEdge,
|
||||||
|
type Edge,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import {
|
||||||
|
draggable,
|
||||||
|
dropTargetForElements,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { centerUnderPointer } from "@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer";
|
||||||
|
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
||||||
|
import { Box, Flex, Inline, Stack, xcss } from "@atlaskit/primitives";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
import { Stack as MantineStack, ScrollArea } from "@mantine/core";
|
||||||
|
import DndGroup from "@/components/dnd-pragmatic/DndFunnel/components/DndGroup";
|
||||||
|
import SafariDndColumnPreview from "@/components/dnd-pragmatic/DndFunnel/components/SafariDndColumnPreview";
|
||||||
|
import { useDndFunnelContext } from "@/components/dnd-pragmatic/DndFunnel/contexts/DndBoardContext";
|
||||||
|
import {
|
||||||
|
BaseColumnType,
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import { ColumnState } from "@/components/dnd-pragmatic/DndFunnel/types/DndStates";
|
||||||
|
import { DndCard } from "./DndCard";
|
||||||
|
import styles from "../DndFunnel.module.css";
|
||||||
|
|
||||||
|
const columnStyles = xcss({
|
||||||
|
width: "250px",
|
||||||
|
borderRadius: "radius.xlarge",
|
||||||
|
transition: `background ${durations.medium}ms ${easeInOut}`,
|
||||||
|
position: "relative",
|
||||||
|
// Replace height: "100%" with these:
|
||||||
|
alignSelf: "stretch", // fill parent's height
|
||||||
|
minHeight: "0", // allow it to shrink inside parent flex
|
||||||
|
border: "1px solid red",
|
||||||
|
});
|
||||||
|
|
||||||
|
const stackStyles = xcss({
|
||||||
|
minHeight: "0",
|
||||||
|
border: "1px solid red",
|
||||||
|
flexGrow: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollContainerStyles = xcss({
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardListStyles = xcss({
|
||||||
|
boxSizing: "border-box",
|
||||||
|
minHeight: "100%",
|
||||||
|
padding: "space.100",
|
||||||
|
gap: "space.100",
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnHeaderStyles = xcss({
|
||||||
|
paddingInlineStart: "space.200",
|
||||||
|
paddingInlineEnd: "space.200",
|
||||||
|
paddingBlockStart: "space.100",
|
||||||
|
color: "color.text.subtlest",
|
||||||
|
userSelect: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
// preventing re-renders with stable state objects
|
||||||
|
const idle: ColumnState = { type: "idle" };
|
||||||
|
|
||||||
|
const stateStyles: {
|
||||||
|
[key in ColumnState["type"]]: ReturnType<typeof xcss> | undefined;
|
||||||
|
} = {
|
||||||
|
"idle": xcss({
|
||||||
|
cursor: "grab",
|
||||||
|
}),
|
||||||
|
"is-column-over": undefined,
|
||||||
|
"generate-column-preview": xcss({
|
||||||
|
isolation: "isolate",
|
||||||
|
}),
|
||||||
|
"generate-safari-column-preview": undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDraggingStyles = xcss({
|
||||||
|
opacity: 0.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
column: ColumnType;
|
||||||
|
columnItemsAndGroups: (ItemType | GroupType)[];
|
||||||
|
renderColumnHeader: (column: any) => ReactNode;
|
||||||
|
renderItem: (item: any) => ReactNode;
|
||||||
|
renderGroup: (group: any) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DndColumn = memo(
|
||||||
|
<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>({
|
||||||
|
column,
|
||||||
|
columnItemsAndGroups,
|
||||||
|
renderColumnHeader,
|
||||||
|
renderItem,
|
||||||
|
renderGroup,
|
||||||
|
}: Props<ColumnType, ItemType, GroupType>) => {
|
||||||
|
const columnId = column.id;
|
||||||
|
const columnRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const columnInnerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const headerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [state, setState] = useState<ColumnState>(idle);
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { instanceId } = useDndFunnelContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invariant(columnRef.current);
|
||||||
|
invariant(columnInnerRef.current);
|
||||||
|
invariant(headerRef.current);
|
||||||
|
invariant(scrollableRef.current);
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
draggable({
|
||||||
|
element: columnRef.current,
|
||||||
|
dragHandle: headerRef.current,
|
||||||
|
getInitialData: () => ({
|
||||||
|
columnId,
|
||||||
|
type: "column",
|
||||||
|
instanceId,
|
||||||
|
}),
|
||||||
|
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
||||||
|
const isSafari: boolean =
|
||||||
|
navigator.userAgent.includes("AppleWebKit") &&
|
||||||
|
!navigator.userAgent.includes("Chrome");
|
||||||
|
|
||||||
|
if (!isSafari) {
|
||||||
|
setState({ type: "generate-column-preview" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCustomNativeDragPreview({
|
||||||
|
getOffset: centerUnderPointer,
|
||||||
|
render: ({ container }) => {
|
||||||
|
setState({
|
||||||
|
type: "generate-safari-column-preview",
|
||||||
|
container,
|
||||||
|
});
|
||||||
|
return () => setState(idle);
|
||||||
|
},
|
||||||
|
nativeSetDragImage,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragStart: () => {
|
||||||
|
setIsDragging(true);
|
||||||
|
},
|
||||||
|
onDrop() {
|
||||||
|
setState(idle);
|
||||||
|
setIsDragging(false);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element: columnInnerRef.current,
|
||||||
|
getData: () => ({ columnId }),
|
||||||
|
canDrop: ({ source }) => {
|
||||||
|
return (
|
||||||
|
source.data.instanceId === instanceId &&
|
||||||
|
source.data.type === "card"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getIsSticky: () => true,
|
||||||
|
onDragLeave: () => setState(idle),
|
||||||
|
onDrop: () => setState(idle),
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element: columnRef.current,
|
||||||
|
canDrop: ({ source }) => {
|
||||||
|
return (
|
||||||
|
source.data.instanceId === instanceId &&
|
||||||
|
source.data.type === "column"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getIsSticky: () => true,
|
||||||
|
getData: ({ input, element }) => {
|
||||||
|
const data = {
|
||||||
|
columnId,
|
||||||
|
};
|
||||||
|
return attachClosestEdge(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
allowedEdges: ["left", "right"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragEnter: args => {
|
||||||
|
setState({
|
||||||
|
type: "is-column-over",
|
||||||
|
closestEdge: extractClosestEdge(args.self.data),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDrag: args => {
|
||||||
|
// skip react re-render if edge is not changing
|
||||||
|
setState(current => {
|
||||||
|
const closestEdge: Edge | null = extractClosestEdge(
|
||||||
|
args.self.data
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
current.type === "is-column-over" &&
|
||||||
|
current.closestEdge === closestEdge
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "is-column-over",
|
||||||
|
closestEdge,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragLeave: () => setState(idle),
|
||||||
|
onDrop: () => setState(idle),
|
||||||
|
}),
|
||||||
|
autoScrollForElements({
|
||||||
|
element: scrollableRef.current,
|
||||||
|
canScroll: ({ source }) =>
|
||||||
|
source.data.instanceId === instanceId &&
|
||||||
|
source.data.type === "card",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [columnId, instanceId]);
|
||||||
|
|
||||||
|
const renderItemsAndGroups = () =>
|
||||||
|
columnItemsAndGroups.map(groupOrItem =>
|
||||||
|
"items" in groupOrItem ? (
|
||||||
|
<DndGroup
|
||||||
|
renderGroup={renderGroup}
|
||||||
|
group={groupOrItem}
|
||||||
|
key={`${groupOrItem.id}group`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DndCard
|
||||||
|
item={groupOrItem}
|
||||||
|
renderItem={renderItem}
|
||||||
|
key={groupOrItem.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
ref={columnRef}
|
||||||
|
direction={"column"}
|
||||||
|
xcss={[columnStyles, stateStyles[state.type]]}>
|
||||||
|
<Stack
|
||||||
|
xcss={stackStyles}
|
||||||
|
ref={columnInnerRef}>
|
||||||
|
<Stack
|
||||||
|
xcss={[
|
||||||
|
stackStyles,
|
||||||
|
isDragging ? isDraggingStyles : undefined,
|
||||||
|
]}>
|
||||||
|
<MantineStack className={styles["visible-column"]}>
|
||||||
|
<ScrollArea scrollbars={"y"}>
|
||||||
|
<Inline
|
||||||
|
xcss={columnHeaderStyles}
|
||||||
|
ref={headerRef}
|
||||||
|
spread="space-between"
|
||||||
|
alignBlock="center">
|
||||||
|
{renderColumnHeader(column)}
|
||||||
|
</Inline>
|
||||||
|
<Box
|
||||||
|
xcss={scrollContainerStyles}
|
||||||
|
ref={scrollableRef}>
|
||||||
|
<Stack
|
||||||
|
xcss={cardListStyles}
|
||||||
|
space="space.100">
|
||||||
|
{renderItemsAndGroups()}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
</MantineStack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
{state.type === "is-column-over" && state.closestEdge && (
|
||||||
|
<DropIndicator edge={state.closestEdge} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{state.type === "generate-safari-column-preview"
|
||||||
|
? createPortal(
|
||||||
|
<SafariDndColumnPreview
|
||||||
|
column={column}
|
||||||
|
renderColumnHeader={renderColumnHeader}
|
||||||
|
/>,
|
||||||
|
state.container
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
119
src/components/dnd-pragmatic/DndFunnel/components/DndGroup.tsx
Normal file
119
src/components/dnd-pragmatic/DndFunnel/components/DndGroup.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import React, { memo, ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import FocusRing from "@atlaskit/focus-ring";
|
||||||
|
import { attachClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import {
|
||||||
|
draggable,
|
||||||
|
dropTargetForElements,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { centerUnderPointer } from "@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer";
|
||||||
|
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
import { useDndFunnelContext } from "@/components/dnd-pragmatic/DndFunnel/contexts/DndBoardContext";
|
||||||
|
import {
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import { GroupState } from "@/components/dnd-pragmatic/DndFunnel/types/DndStates";
|
||||||
|
|
||||||
|
const idleState: GroupState = { type: "idle" };
|
||||||
|
const draggingState: GroupState = { type: "dragging" };
|
||||||
|
const isCardOver: GroupState = { type: "is-card-over" };
|
||||||
|
const isGroupOver: GroupState = { type: "is-group-over" };
|
||||||
|
|
||||||
|
type Props<
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
group: GroupType;
|
||||||
|
renderGroup: (group: any) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DndGroup = memo(
|
||||||
|
<ItemType extends BaseItemType, GroupType extends BaseGroupType<ItemType>>({
|
||||||
|
group,
|
||||||
|
renderGroup,
|
||||||
|
}: Props<ItemType, GroupType>) => {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { instanceId } = useDndFunnelContext();
|
||||||
|
|
||||||
|
const [state, setState] = useState<GroupState>(idleState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
invariant(element);
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
draggable({
|
||||||
|
element,
|
||||||
|
getInitialData: () => ({
|
||||||
|
groupId: group.id,
|
||||||
|
type: "group",
|
||||||
|
instanceId,
|
||||||
|
}),
|
||||||
|
onGenerateDragPreview: ({ source, nativeSetDragImage }) => {
|
||||||
|
const rect = source.element.getBoundingClientRect();
|
||||||
|
|
||||||
|
setCustomNativeDragPreview({
|
||||||
|
nativeSetDragImage,
|
||||||
|
getOffset: centerUnderPointer,
|
||||||
|
render({ container }) {
|
||||||
|
setState({ type: "preview", container, rect });
|
||||||
|
return () => setState(draggingState);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragStart: () => setState(draggingState),
|
||||||
|
onDrop: () => setState(idleState),
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element,
|
||||||
|
getData: ({ input, element }) => {
|
||||||
|
const data = {
|
||||||
|
groupId,
|
||||||
|
};
|
||||||
|
return attachClosestEdge(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
allowedEdges: ["top", "bottom"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
canDrop: ({ source }) =>
|
||||||
|
source.data.instanceId === instanceId &&
|
||||||
|
source.data.type === "group" &&
|
||||||
|
source.data.id !== group.id,
|
||||||
|
onDragEnter: ({ element }) =>
|
||||||
|
setState(
|
||||||
|
element.type === "card" ? isCardOver : isGroupOver
|
||||||
|
),
|
||||||
|
onDragLeave: () => setState(idleState),
|
||||||
|
onDragStart: () => setState(isCardOver),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [group]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: state.type === "is-card-over" && "1px solid red",
|
||||||
|
}}>
|
||||||
|
<FocusRing isInset>
|
||||||
|
<div ref={ref}>
|
||||||
|
{renderGroup(group)}
|
||||||
|
{(state.type === "is-card-over" ||
|
||||||
|
state.type === "is-group-over") &&
|
||||||
|
state.closestEdge && (
|
||||||
|
<DropIndicator
|
||||||
|
edge={state.closestEdge}
|
||||||
|
gap={token("space.200", "0")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FocusRing>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DndGroup;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Box, xcss } from "@atlaskit/primitives";
|
||||||
|
import { BaseColumnType } from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
|
||||||
|
const safariPreviewStyles = xcss({
|
||||||
|
width: "250px",
|
||||||
|
backgroundColor: "elevation.surface.sunken",
|
||||||
|
borderRadius: "radius.small",
|
||||||
|
padding: "space.200",
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnHeaderStyles = xcss({
|
||||||
|
paddingInlineStart: "space.200",
|
||||||
|
paddingInlineEnd: "space.200",
|
||||||
|
paddingBlockStart: "space.100",
|
||||||
|
color: "color.text.subtlest",
|
||||||
|
userSelect: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props<ColumnType extends BaseColumnType> = {
|
||||||
|
column: ColumnType;
|
||||||
|
renderColumnHeader: (column: ColumnType) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SafariDndColumnPreview = <ColumnType extends BaseColumnType>({
|
||||||
|
column,
|
||||||
|
renderColumnHeader,
|
||||||
|
}: Props<ColumnType>) => {
|
||||||
|
return (
|
||||||
|
<Box xcss={[columnHeaderStyles, safariPreviewStyles]}>
|
||||||
|
{renderColumnHeader(column)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SafariDndColumnPreview;
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { SwiperRef } from "swiper/swiper-react";
|
||||||
|
import useResolveDrop from "@/components/dnd-pragmatic/DndFunnel/hooks/useResolveDrop";
|
||||||
|
import {
|
||||||
|
BaseColumnType,
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
import FunnelDndProps from "@/components/dnd-pragmatic/DndFunnel/types/FunnelDndProps";
|
||||||
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
|
|
||||||
|
export type DndFunnelContextState<
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
instanceId: symbol;
|
||||||
|
mixedItemsAndGroups: Array<GroupType | ItemType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFunnelContextState = <
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>({
|
||||||
|
columns,
|
||||||
|
updateColumn,
|
||||||
|
items,
|
||||||
|
updateItem,
|
||||||
|
getColumnItemsGroups,
|
||||||
|
groups,
|
||||||
|
}: FunnelDndProps<ColumnType, ItemType, GroupType>): DndFunnelContextState<
|
||||||
|
ItemType,
|
||||||
|
GroupType
|
||||||
|
> => {
|
||||||
|
const swiperRef = useRef<SwiperRef>(null);
|
||||||
|
const sortedColumns = useMemo(() => sortByLexorank(columns), [columns]);
|
||||||
|
|
||||||
|
// const getColumnByItemId = (itemId: number) => {
|
||||||
|
// const item = items.find(item => item.id === itemId);
|
||||||
|
// if (!item) return;
|
||||||
|
// return columnes.find(column => column.id === item.column.id);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const swipeSliderDuringDrag = () => {
|
||||||
|
// const activeColumn = getColumnByItemId(activeId);
|
||||||
|
// const swiperActiveColumn =
|
||||||
|
// columnes[swiperRef.current?.swiper.activeIndex ?? 0];
|
||||||
|
// if (swiperActiveColumn.id !== activeColumn?.id) return;
|
||||||
|
//
|
||||||
|
// const activeColumnLexorank = activeColumn?.lexorank;
|
||||||
|
// let overColumnLexorank: string | undefined;
|
||||||
|
//
|
||||||
|
// if (typeof over.id === "string" && isColumnId(over.id)) {
|
||||||
|
// const overColumnId = getColumnId(over.id);
|
||||||
|
// overColumnLexorank = columnes.find(
|
||||||
|
// s => s.id === overColumnId
|
||||||
|
// )?.lexorank;
|
||||||
|
// } else {
|
||||||
|
// overColumnLexorank = getColumnByItemId(Number(over.id))?.lexorank;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (
|
||||||
|
// !activeColumnLexorank ||
|
||||||
|
// !overColumnLexorank ||
|
||||||
|
// !swiperRef.current?.swiper
|
||||||
|
// )
|
||||||
|
// return;
|
||||||
|
//
|
||||||
|
// const activeIndex = sortedColumns.findIndex(
|
||||||
|
// s => s.lexorank === activeColumnLexorank
|
||||||
|
// );
|
||||||
|
// const overIndex = sortedColumns.findIndex(
|
||||||
|
// s => s.lexorank === overColumnLexorank
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// if (activeIndex > overIndex) {
|
||||||
|
// swiperRef.current.swiper.slidePrev();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (activeIndex < overIndex) {
|
||||||
|
// swiperRef.current.swiper.slideNext();
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const lastOperation = useRef<Operation | null>(null);
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (lastOperation.current === null) return;
|
||||||
|
// const { outcome, trigger } = lastOperation.current;
|
||||||
|
//
|
||||||
|
// if (outcome.type === "column-reorder") {
|
||||||
|
// const { startIndex, finishIndex } = outcome;
|
||||||
|
//
|
||||||
|
// const sourceColumn = sortedColumns[finishIndex];
|
||||||
|
//
|
||||||
|
// const entry = registry.getColumn(sourceColumn.id);
|
||||||
|
// triggerPostMoveFlash(entry.element);
|
||||||
|
//
|
||||||
|
// console.log(
|
||||||
|
// `You've moved ${sourceColumn.name} from position ${
|
||||||
|
// startIndex + 1
|
||||||
|
// } to position ${finishIndex + 1} of ${sortedColumns.length}.`
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (outcome.type === "card-reorder") {
|
||||||
|
// const { columnId, startIndex, finishIndex } = outcome;
|
||||||
|
//
|
||||||
|
// const column = sortedColumns.find(s => s.id === columnId);
|
||||||
|
// if (!column) return;
|
||||||
|
// const columnItems = items.filter(d => d.column.id === columnId);
|
||||||
|
// const item = columnItems[finishIndex];
|
||||||
|
//
|
||||||
|
// const entry = registry.getCard(item.id);
|
||||||
|
// triggerPostMoveFlash(entry.element);
|
||||||
|
//
|
||||||
|
// if (trigger !== "keyboard") return;
|
||||||
|
//
|
||||||
|
// console.log(
|
||||||
|
// `You've moved ${item.name} from position ${
|
||||||
|
// startIndex + 1
|
||||||
|
// } to position ${finishIndex + 1} of ${columnItems.length} in the ${column.name} column.`
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (outcome.type === "card-move") {
|
||||||
|
// const {
|
||||||
|
// finishColumnId,
|
||||||
|
// itemIndexInStartColumn,
|
||||||
|
// itemIndexInFinishColumn,
|
||||||
|
// } = outcome;
|
||||||
|
//
|
||||||
|
// const destColumn = sortedColumns.find(
|
||||||
|
// s => s.id === finishColumnId
|
||||||
|
// );
|
||||||
|
// if (!destColumn) return;
|
||||||
|
// const columnItems = items.filter(
|
||||||
|
// d => d.column.id === destColumn.id
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const item = columnItems[itemIndexInFinishColumn];
|
||||||
|
//
|
||||||
|
// const finishPosition =
|
||||||
|
// typeof itemIndexInFinishColumn === "number"
|
||||||
|
// ? itemIndexInFinishColumn + 1
|
||||||
|
// : columnItems.length;
|
||||||
|
//
|
||||||
|
// const entry = registry.getCard(item.id);
|
||||||
|
// triggerPostMoveFlash(entry.element);
|
||||||
|
//
|
||||||
|
// if (trigger !== "keyboard") return;
|
||||||
|
//
|
||||||
|
// console.log(
|
||||||
|
// `You've moved ${item.name} from position ${
|
||||||
|
// itemIndexInStartColumn + 1
|
||||||
|
// } to position ${finishPosition} in the ${destColumn.name} column.`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }, [lastOperation, registry]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return liveRegion.cleanup();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [instanceId] = useState(() => Symbol("instance-id"));
|
||||||
|
|
||||||
|
const { onDrop } = useResolveDrop<ColumnType, ItemType, GroupType>({
|
||||||
|
sortedColumns,
|
||||||
|
updateColumn,
|
||||||
|
updateItem,
|
||||||
|
getColumnItemsGroups,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mixedItemsAndGroups: Array<ItemType | GroupType> = sortByLexorank([
|
||||||
|
...items,
|
||||||
|
...groups,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return combine(
|
||||||
|
monitorForElements({
|
||||||
|
canMonitor: ({ source }) =>
|
||||||
|
source.data.instanceId === instanceId,
|
||||||
|
onDrop,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [items, sortedColumns, instanceId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instanceId,
|
||||||
|
mixedItemsAndGroups,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [DndFunnelContextProvider, useDndFunnelContext] = makeContext<
|
||||||
|
DndFunnelContextState<any, any>,
|
||||||
|
FunnelDndProps<any, any, any>
|
||||||
|
>(useFunnelContextState, "DndFunnel");
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import useGetNewRankForFunnel from "@/components/dnd-pragmatic/DndFunnel/hooks/useGetNewRankForFunnel";
|
||||||
|
import {
|
||||||
|
BaseColumnType,
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
|
||||||
|
type Props<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
sortedColumns: ColumnType[];
|
||||||
|
updateColumn: (id: number, lexorank: string) => void;
|
||||||
|
updateItem: (id: number, lexorank: string, columnId: number) => void;
|
||||||
|
getColumnItemsGroups: (columnId: number) => (ItemType | GroupType)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFunnelActions = <
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>({
|
||||||
|
sortedColumns,
|
||||||
|
updateColumn,
|
||||||
|
updateItem,
|
||||||
|
getColumnItemsGroups,
|
||||||
|
}: Props<ColumnType, ItemType, GroupType>) => {
|
||||||
|
const {
|
||||||
|
getNewRankForSameColumn,
|
||||||
|
getNewRankForAnotherColumn,
|
||||||
|
getNewColumnRank,
|
||||||
|
} = useGetNewRankForFunnel<ColumnType, ItemType, GroupType>();
|
||||||
|
|
||||||
|
const reorderItem = (
|
||||||
|
columnId: number,
|
||||||
|
startIndex: number,
|
||||||
|
finishIndex: number,
|
||||||
|
columnItems: ItemType[]
|
||||||
|
) => {
|
||||||
|
const startItemId = columnItems[startIndex].id;
|
||||||
|
|
||||||
|
const newLexorank = getNewRankForSameColumn(
|
||||||
|
columnItems,
|
||||||
|
finishIndex,
|
||||||
|
startItemId
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItem(startItemId, newLexorank, columnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveItem = (
|
||||||
|
startColumnId: number,
|
||||||
|
itemIndexInStartColumn: number,
|
||||||
|
finishColumnId: number,
|
||||||
|
finishItemIndex?: number
|
||||||
|
) => {
|
||||||
|
const startColumnItems = getColumnItemsGroups(startColumnId);
|
||||||
|
const startItemId = startColumnItems[itemIndexInStartColumn].id;
|
||||||
|
|
||||||
|
const finishColumnItems = getColumnItemsGroups(finishColumnId);
|
||||||
|
|
||||||
|
const newLexorank = getNewRankForAnotherColumn(
|
||||||
|
finishColumnItems,
|
||||||
|
finishItemIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItem(startItemId, newLexorank, finishColumnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderColumn = (startIndex: number, finishIndex: number) => {
|
||||||
|
const startColumnId = sortedColumns[startIndex].id;
|
||||||
|
const finishColumnId = sortedColumns[finishIndex].id;
|
||||||
|
|
||||||
|
if (startColumnId === finishColumnId) return;
|
||||||
|
const newRank = getNewColumnRank(
|
||||||
|
sortedColumns,
|
||||||
|
startColumnId,
|
||||||
|
finishColumnId
|
||||||
|
);
|
||||||
|
if (!newRank) return;
|
||||||
|
|
||||||
|
updateColumn(startColumnId, newRank);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
moveItem,
|
||||||
|
reorderColumn,
|
||||||
|
reorderItem,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFunnelActions;
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { BaseColumnType, BaseItemType } from "@/components/dnd-pragmatic/types/base";
|
||||||
|
import { getNewLexorank } from "@/utils/lexorank/generation";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
|
import { BaseGroupType } from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
|
||||||
|
interface NewRankGetters<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> {
|
||||||
|
getNewRankForSameColumn: (
|
||||||
|
columnItems: ItemType[],
|
||||||
|
overItemIndex: number,
|
||||||
|
activeItemId: number
|
||||||
|
) => string;
|
||||||
|
getNewRankForAnotherColumn: (
|
||||||
|
columnItems: ItemType[],
|
||||||
|
overItemIndex?: number
|
||||||
|
) => string;
|
||||||
|
getNewColumnRank: (
|
||||||
|
columns: ColumnType[],
|
||||||
|
activeColumnId: number,
|
||||||
|
overColumnId: number
|
||||||
|
) => string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGetNewRankForFunnel = <
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>(): NewRankGetters<ColumnType, ItemType, GroupType> => {
|
||||||
|
const getNewRankForSameColumn = (
|
||||||
|
columnItems: ItemType[],
|
||||||
|
overItemIndex: number,
|
||||||
|
activeItemId: number
|
||||||
|
): string => {
|
||||||
|
const activeItemIndex = columnItems.findIndex(
|
||||||
|
item => item.id === activeItemId
|
||||||
|
);
|
||||||
|
const [leftIndex, rightIndex] =
|
||||||
|
overItemIndex < activeItemIndex
|
||||||
|
? [overItemIndex - 1, overItemIndex]
|
||||||
|
: [overItemIndex, overItemIndex + 1];
|
||||||
|
|
||||||
|
const leftLexorank =
|
||||||
|
leftIndex >= 0
|
||||||
|
? LexoRank.parse(columnItems[leftIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank =
|
||||||
|
rightIndex < columnItems.length
|
||||||
|
? LexoRank.parse(columnItems[rightIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewRankForAnotherColumn = (
|
||||||
|
columnItems: ItemType[],
|
||||||
|
overItemIndex?: number
|
||||||
|
): string => {
|
||||||
|
if (columnItems.length === 0) return LexoRank.middle().toString();
|
||||||
|
|
||||||
|
if (!overItemIndex || overItemIndex >= columnItems.length) {
|
||||||
|
return getNewLexorank(
|
||||||
|
LexoRank.parse(columnItems[columnItems.length - 1].lexorank)
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLexorank =
|
||||||
|
overItemIndex > 0
|
||||||
|
? LexoRank.parse(columnItems[overItemIndex - 1].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank = LexoRank.parse(
|
||||||
|
columnItems[overItemIndex].lexorank
|
||||||
|
);
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewColumnRank = (
|
||||||
|
columns: ColumnType[],
|
||||||
|
activeColumnId: number,
|
||||||
|
overColumnId: number
|
||||||
|
): string | null => {
|
||||||
|
const sortedColumnsList = sortByLexorank(columns);
|
||||||
|
const overIndex = sortedColumnsList.findIndex(
|
||||||
|
s => s.id === overColumnId
|
||||||
|
);
|
||||||
|
const activeIndex = sortedColumnsList.findIndex(
|
||||||
|
s => s.id === activeColumnId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overIndex === -1 || activeIndex === -1) return null;
|
||||||
|
|
||||||
|
const [leftIndex, rightIndex] =
|
||||||
|
overIndex < activeIndex
|
||||||
|
? [overIndex - 1, overIndex]
|
||||||
|
: [overIndex, overIndex + 1];
|
||||||
|
|
||||||
|
const leftLexorank =
|
||||||
|
leftIndex >= 0 ? LexoRank.parse(columns[leftIndex].lexorank) : null;
|
||||||
|
const rightLexorank =
|
||||||
|
rightIndex < columns.length
|
||||||
|
? LexoRank.parse(columns[rightIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNewRankForSameColumn,
|
||||||
|
getNewRankForAnotherColumn,
|
||||||
|
getNewColumnRank,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetNewRankForFunnel;
|
||||||
191
src/components/dnd-pragmatic/DndFunnel/hooks/useResolveDrop.ts
Normal file
191
src/components/dnd-pragmatic/DndFunnel/hooks/useResolveDrop.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/types";
|
||||||
|
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
|
||||||
|
import {
|
||||||
|
BaseEventPayload,
|
||||||
|
ElementDragType,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/types";
|
||||||
|
import useFunnelActions from "@/components/dnd-pragmatic/DndFunnel/hooks/useFunnelActions";
|
||||||
|
import {
|
||||||
|
BaseColumnType,
|
||||||
|
BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
|
||||||
|
type Props<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
sortedColumns: ColumnType[];
|
||||||
|
getColumnItemsGroups: (columnId: number) => (ItemType | GroupType)[];
|
||||||
|
updateColumn: (id: number, lexorank: string) => void;
|
||||||
|
updateItem: (id: number, lexorank: string, columnId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useResolveDrop = <
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
>({
|
||||||
|
sortedColumns,
|
||||||
|
getColumnItemsGroups,
|
||||||
|
...props
|
||||||
|
}: Props<ColumnType, ItemType, GroupType>) => {
|
||||||
|
const { moveItem, reorderColumn, reorderItem } = useFunnelActions<
|
||||||
|
ColumnType,
|
||||||
|
ItemType,
|
||||||
|
GroupType
|
||||||
|
>({
|
||||||
|
sortedColumns,
|
||||||
|
getColumnItemsGroups,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onItemDrop = ({
|
||||||
|
location,
|
||||||
|
source,
|
||||||
|
}: BaseEventPayload<ElementDragType>) => {
|
||||||
|
const itemId: number = source.data.itemId as number;
|
||||||
|
const [, startColumnRecord] = location.initial.dropTargets;
|
||||||
|
const sourceColumnId: number = startColumnRecord.data
|
||||||
|
.columnId as number;
|
||||||
|
const sourceColumn = sortedColumns.find(s => s.id === sourceColumnId);
|
||||||
|
if (!sourceColumn) return;
|
||||||
|
const sourceColumnItems = getColumnItemsGroups(sourceColumnId);
|
||||||
|
const startItemIndex = sourceColumnItems.findIndex(
|
||||||
|
d => d.id === itemId
|
||||||
|
);
|
||||||
|
if (startItemIndex === -1) return;
|
||||||
|
|
||||||
|
if (location.current.dropTargets.length === 1) {
|
||||||
|
const [destinationColumnRecord] = location.current.dropTargets;
|
||||||
|
const destinationId: number = destinationColumnRecord.data
|
||||||
|
.columnId as number;
|
||||||
|
const destinationColumn = sortedColumns.find(
|
||||||
|
s => s.id === destinationId
|
||||||
|
);
|
||||||
|
if (!destinationColumn) return;
|
||||||
|
|
||||||
|
// reordering in same column
|
||||||
|
if (sourceColumn === destinationColumn) {
|
||||||
|
const destinationIndex = getReorderDestinationIndex({
|
||||||
|
startIndex: startItemIndex,
|
||||||
|
indexOfTarget: sourceColumnItems.length - 1,
|
||||||
|
closestEdgeOfTarget: null,
|
||||||
|
axis: "vertical",
|
||||||
|
});
|
||||||
|
reorderItem(
|
||||||
|
sourceColumn.id,
|
||||||
|
startItemIndex,
|
||||||
|
destinationIndex,
|
||||||
|
sourceColumnItems
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// moving to a new column
|
||||||
|
moveItem(sourceColumn.id, startItemIndex, destinationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropping in a column (relative to a card)
|
||||||
|
if (location.current.dropTargets.length === 2) {
|
||||||
|
const [destinationCardRecord, destinationColumnRecord] =
|
||||||
|
location.current.dropTargets;
|
||||||
|
const destinationColumnId: number = destinationColumnRecord.data
|
||||||
|
.columnId as number;
|
||||||
|
const destinationColumn = sortedColumns.find(
|
||||||
|
s => s.id === destinationColumnId
|
||||||
|
);
|
||||||
|
if (!destinationColumn) return;
|
||||||
|
const destColumnItems = getColumnItemsGroups(destinationColumnId);
|
||||||
|
|
||||||
|
const indexOfTarget = destColumnItems.findIndex(
|
||||||
|
item =>
|
||||||
|
item.id === destinationCardRecord.data.itemId &&
|
||||||
|
!("items" in item)
|
||||||
|
);
|
||||||
|
const closestEdgeOfTarget: Edge | null = extractClosestEdge(
|
||||||
|
destinationCardRecord.data
|
||||||
|
);
|
||||||
|
|
||||||
|
// case 1: ordering in the same column
|
||||||
|
if (sourceColumn === destinationColumn) {
|
||||||
|
const destinationIndex = getReorderDestinationIndex({
|
||||||
|
startIndex: startItemIndex,
|
||||||
|
indexOfTarget,
|
||||||
|
closestEdgeOfTarget,
|
||||||
|
axis: "vertical",
|
||||||
|
});
|
||||||
|
reorderItem(
|
||||||
|
sourceColumn.id,
|
||||||
|
startItemIndex,
|
||||||
|
destinationIndex,
|
||||||
|
destColumnItems
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// case 2: moving into a new column relative to a card
|
||||||
|
const destinationIndex =
|
||||||
|
closestEdgeOfTarget === "bottom"
|
||||||
|
? indexOfTarget + 1
|
||||||
|
: indexOfTarget;
|
||||||
|
|
||||||
|
moveItem(
|
||||||
|
sourceColumn.id,
|
||||||
|
startItemIndex,
|
||||||
|
destinationColumn.id,
|
||||||
|
destinationIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onColumnDrop = ({
|
||||||
|
location,
|
||||||
|
source,
|
||||||
|
}: BaseEventPayload<ElementDragType>) => {
|
||||||
|
const startIndex: number = sortedColumns.findIndex(
|
||||||
|
column => column.id === source.data.columnId
|
||||||
|
);
|
||||||
|
|
||||||
|
const target = location.current.dropTargets[0];
|
||||||
|
const indexOfTarget: number = sortedColumns.findIndex(
|
||||||
|
column => column.id === target.data.columnId
|
||||||
|
);
|
||||||
|
const closestEdgeOfTarget: Edge | null = extractClosestEdge(
|
||||||
|
target.data
|
||||||
|
);
|
||||||
|
|
||||||
|
const finishIndex = getReorderDestinationIndex({
|
||||||
|
startIndex,
|
||||||
|
indexOfTarget,
|
||||||
|
closestEdgeOfTarget,
|
||||||
|
axis: "horizontal",
|
||||||
|
});
|
||||||
|
|
||||||
|
reorderColumn(startIndex, finishIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = ({
|
||||||
|
location,
|
||||||
|
source,
|
||||||
|
}: BaseEventPayload<ElementDragType>) => {
|
||||||
|
if (!location.current.dropTargets.length) return;
|
||||||
|
|
||||||
|
if (source.data.type === "column") {
|
||||||
|
onColumnDrop({ location, source });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (source.data.type === "card") {
|
||||||
|
onItemDrop({ location, source });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDrop,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useResolveDrop;
|
||||||
12
src/components/dnd-pragmatic/DndFunnel/types/Base.ts
Normal file
12
src/components/dnd-pragmatic/DndFunnel/types/Base.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
type BaseFunnelType = {
|
||||||
|
id: number;
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseColumnType = BaseFunnelType;
|
||||||
|
|
||||||
|
export type BaseItemType = BaseFunnelType;
|
||||||
|
|
||||||
|
export type BaseGroupType<ItemType extends BaseItemType> = BaseFunnelType & {
|
||||||
|
items: ItemType[];
|
||||||
|
};
|
||||||
19
src/components/dnd-pragmatic/DndFunnel/types/DndStates.ts
Normal file
19
src/components/dnd-pragmatic/DndFunnel/types/DndStates.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
|
||||||
|
export type CardState =
|
||||||
|
| { type: "idle" }
|
||||||
|
| { type: "preview"; container: HTMLElement; rect: DOMRect }
|
||||||
|
| { type: "dragging" };
|
||||||
|
|
||||||
|
export type GroupState =
|
||||||
|
| { type: "idle" }
|
||||||
|
| { type: "is-card-over"; closestEdge: Edge | null }
|
||||||
|
| { type: "is-group-over"; closestEdge: Edge | null }
|
||||||
|
| { type: "preview"; container: HTMLElement; rect: DOMRect }
|
||||||
|
| { type: "dragging" };
|
||||||
|
|
||||||
|
export type ColumnState =
|
||||||
|
| { type: "idle" }
|
||||||
|
| { type: "is-column-over"; closestEdge: Edge | null }
|
||||||
|
| { type: "generate-safari-column-preview"; container: HTMLElement }
|
||||||
|
| { type: "generate-column-preview" };
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
BaseColumnType, BaseGroupType,
|
||||||
|
BaseItemType,
|
||||||
|
} from "@/components/dnd-pragmatic/DndFunnel/types/Base";
|
||||||
|
|
||||||
|
type FunnelDndProps<
|
||||||
|
ColumnType extends BaseColumnType,
|
||||||
|
ItemType extends BaseItemType,
|
||||||
|
GroupType extends BaseGroupType<ItemType>,
|
||||||
|
> = {
|
||||||
|
columns: ColumnType[];
|
||||||
|
updateColumn: (id: number, lexorank: string) => void;
|
||||||
|
items: ItemType[];
|
||||||
|
updateItem: (id: number, lexorank: string, columnId: number) => void;
|
||||||
|
getColumnItemsGroups: (columnId: number) => (ItemType | GroupType)[];
|
||||||
|
renderColumnHeader: (column: ColumnType) => ReactNode;
|
||||||
|
renderItem: (item: ItemType) => ReactNode;
|
||||||
|
groups: GroupType[];
|
||||||
|
renderGroup: (group: GroupType) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunnelDndProps;
|
||||||
52
src/components/dnd-pragmatic/DndFunnel/utils/registry.tsx
Normal file
52
src/components/dnd-pragmatic/DndFunnel/utils/registry.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { CleanupFn } from "@atlaskit/pragmatic-drag-and-drop/types";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
|
||||||
|
export type Entry = {
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterCardProps = {
|
||||||
|
cardId: number;
|
||||||
|
entry: Entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterColumnProps = {
|
||||||
|
columnId: number;
|
||||||
|
entry: Entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO delete
|
||||||
|
|
||||||
|
export function createRegistry() {
|
||||||
|
const cards = new Map<number, Entry>();
|
||||||
|
const columns = new Map<number, Entry>();
|
||||||
|
|
||||||
|
function registerCard({ cardId, entry }: RegisterCardProps): CleanupFn {
|
||||||
|
cards.set(cardId, entry);
|
||||||
|
return () => cards.delete(cardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerColumn({
|
||||||
|
columnId,
|
||||||
|
entry,
|
||||||
|
}: RegisterColumnProps): CleanupFn {
|
||||||
|
columns.set(columnId, entry);
|
||||||
|
return function cleanup() {
|
||||||
|
cards.delete(columnId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCard(cardId: number): Entry {
|
||||||
|
const entry = cards.get(cardId);
|
||||||
|
invariant(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumn(columnId: number): Entry {
|
||||||
|
const entry = columns.get(columnId);
|
||||||
|
invariant(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { registerCard, registerColumn, getCard, getColumn };
|
||||||
|
}
|
||||||
3
src/components/dnd/DragHandle/index.ts
Normal file
3
src/components/dnd/DragHandle/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import DragHandle from './DragHandle';
|
||||||
|
|
||||||
|
export default DragHandle;
|
||||||
42
src/components/dnd/FunnelDnd/FunnelColumn.tsx
Normal file
42
src/components/dnd/FunnelDnd/FunnelColumn.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { useDroppable } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
|
|
||||||
|
type Props<TItem> = {
|
||||||
|
id: string;
|
||||||
|
items: TItem[];
|
||||||
|
renderItem: (item: TItem) => ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FunnelColumn = <TItem extends BaseDraggable>({
|
||||||
|
id,
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
children,
|
||||||
|
}: Props<TItem>) => {
|
||||||
|
const { setNodeRef } = useDroppable({ id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<SortableContext
|
||||||
|
id={id}
|
||||||
|
items={items}
|
||||||
|
strategy={verticalListSortingStrategy}>
|
||||||
|
<Stack
|
||||||
|
gap="xs"
|
||||||
|
ref={setNodeRef}>
|
||||||
|
{items.map(renderItem)}
|
||||||
|
</Stack>
|
||||||
|
</SortableContext>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunnelColumn;
|
||||||
@ -1,43 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { ReactNode, RefObject, useMemo, useState } from "react";
|
import React, { ReactNode, RefObject } from "react";
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverEvent,
|
|
||||||
DragStartEvent,
|
|
||||||
} from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
horizontalListSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import { FreeMode, Pagination, Scrollbar } from "swiper/modules";
|
import { FreeMode, Pagination, Scrollbar } from "swiper/modules";
|
||||||
import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
|
||||||
import { Box } from "@mantine/core";
|
import { Box } from "@mantine/core";
|
||||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
||||||
import FunnelColumn from "@/components/dnd/FunnelDnd/components/FunnelColumn";
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/components/FunnelOverlay";
|
|
||||||
import {
|
|
||||||
getContainerId,
|
|
||||||
getDndContainerId,
|
|
||||||
isContainerId,
|
|
||||||
} from "@/components/dnd/FunnelDnd/utils/columnId";
|
|
||||||
import {
|
|
||||||
getGroupId,
|
|
||||||
isGroupId,
|
|
||||||
} from "@/components/dnd/FunnelDnd/utils/groupId";
|
|
||||||
import {
|
|
||||||
BaseDraggable,
|
|
||||||
BaseGroupDraggable,
|
|
||||||
} from "@/components/dnd/types/types";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||||
import SortableItem from "../SortableItem";
|
import SortableItem from "../SortableItem";
|
||||||
import classes from "./FunnelDnd.module.css";
|
import classes from "./FunnelDnd.module.css";
|
||||||
|
import { DragEndEvent, DragOverEvent, DragStartEvent } from "@dnd-kit/core";
|
||||||
|
|
||||||
type Props<TContainer, TItem, TGroup> = {
|
type Props<TContainer, TItem> = {
|
||||||
containers: TContainer[];
|
containers: TContainer[];
|
||||||
itemsAndGroups: (TItem | TGroup)[];
|
items: TItem[];
|
||||||
|
onDragStart: (event: DragStartEvent) => void;
|
||||||
onDragOver: (event: DragOverEvent) => void;
|
onDragOver: (event: DragOverEvent) => void;
|
||||||
onDragEnd: (event: DragEndEvent) => void;
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
swiperRef: RefObject<SwiperRef | null>;
|
swiperRef: RefObject<SwiperRef | null>;
|
||||||
@ -53,8 +31,11 @@ type Props<TContainer, TItem, TGroup> = {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
) => ReactNode;
|
) => ReactNode;
|
||||||
renderItem: (item: TItem) => ReactNode;
|
renderItem: (item: TItem) => ReactNode;
|
||||||
renderGroup: (group: TGroup) => ReactNode;
|
renderItemOverlay: (item: TItem) => ReactNode;
|
||||||
getItemsByContainer: (container: TContainer) => (TItem | TGroup)[];
|
getContainerId: (container: TContainer) => string;
|
||||||
|
getItemsByContainer: (container: TContainer, items: TItem[]) => TItem[];
|
||||||
|
activeContainer: TContainer | null;
|
||||||
|
activeItem: TItem | null;
|
||||||
isCreatingContainerEnabled?: boolean;
|
isCreatingContainerEnabled?: boolean;
|
||||||
disabledColumns?: boolean;
|
disabledColumns?: boolean;
|
||||||
};
|
};
|
||||||
@ -62,10 +43,10 @@ type Props<TContainer, TItem, TGroup> = {
|
|||||||
const FunnelDnd = <
|
const FunnelDnd = <
|
||||||
TContainer extends BaseDraggable,
|
TContainer extends BaseDraggable,
|
||||||
TItem extends BaseDraggable,
|
TItem extends BaseDraggable,
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
>({
|
>({
|
||||||
containers,
|
containers,
|
||||||
itemsAndGroups,
|
items,
|
||||||
|
onDragStart,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
swiperRef,
|
swiperRef,
|
||||||
@ -73,25 +54,20 @@ const FunnelDnd = <
|
|||||||
renderContainerHeader,
|
renderContainerHeader,
|
||||||
renderContainerOverlay,
|
renderContainerOverlay,
|
||||||
renderItem,
|
renderItem,
|
||||||
renderGroup,
|
renderItemOverlay,
|
||||||
|
getContainerId,
|
||||||
getItemsByContainer,
|
getItemsByContainer,
|
||||||
|
activeContainer,
|
||||||
|
activeItem,
|
||||||
isCreatingContainerEnabled = true,
|
isCreatingContainerEnabled = true,
|
||||||
disabledColumns = false,
|
disabledColumns = false,
|
||||||
}: Props<TContainer, TItem, TGroup>) => {
|
}: Props<TContainer, TItem>) => {
|
||||||
const sensors = useDndSensors();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const frequency = useMemo(() => (isMobile ? 1 : undefined), [isMobile]);
|
|
||||||
|
|
||||||
const [activeItem, setActiveItem] = useState<TItem | null>(null);
|
|
||||||
const [activeContainer, setActiveContainer] = useState<TContainer | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [activeGroup, setActiveGroup] = useState<TGroup | null>(null);
|
|
||||||
|
|
||||||
const renderContainers = () =>
|
const renderContainers = () =>
|
||||||
containers.map((container, index) => {
|
containers.map((container, index) => {
|
||||||
const containerItems = getItemsByContainer(container);
|
const containerItems = getItemsByContainer(container, items);
|
||||||
const containerId = getDndContainerId(container.id);
|
const containerId = getContainerId(container);
|
||||||
return (
|
return (
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
style={{ width: 250 }}
|
style={{ width: 250 }}
|
||||||
@ -105,9 +81,8 @@ const FunnelDnd = <
|
|||||||
container,
|
container,
|
||||||
<FunnelColumn
|
<FunnelColumn
|
||||||
id={containerId}
|
id={containerId}
|
||||||
itemsAndGroups={containerItems}
|
items={containerItems}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
renderGroup={renderGroup}
|
|
||||||
/>,
|
/>,
|
||||||
renderDraggable!,
|
renderDraggable!,
|
||||||
index
|
index
|
||||||
@ -119,127 +94,52 @@ const FunnelDnd = <
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderBody = () => {
|
if (isMobile) {
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Swiper
|
|
||||||
ref={swiperRef}
|
|
||||||
onTouchStart={swiper => {
|
|
||||||
swiper.allowTouchMove = !activeItem;
|
|
||||||
}}
|
|
||||||
onTouchMove={swiper => {
|
|
||||||
swiper.allowTouchMove = !activeItem;
|
|
||||||
}}
|
|
||||||
className={classes["swiper-container"]}
|
|
||||||
spaceBetween={15}
|
|
||||||
style={{ paddingInline: "10vw" }}
|
|
||||||
modules={[Pagination]}
|
|
||||||
freeMode={{ enabled: false }}
|
|
||||||
pagination={{ enabled: true, clickable: true }}>
|
|
||||||
{renderContainers()}
|
|
||||||
{isCreatingContainerEnabled && (
|
|
||||||
<SwiperSlide style={{ width: 250 }}>
|
|
||||||
<CreateStatusButton />
|
|
||||||
</SwiperSlide>
|
|
||||||
)}
|
|
||||||
</Swiper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Swiper
|
<Box>
|
||||||
ref={swiperRef}
|
<Swiper
|
||||||
className={classes["swiper-container"]}
|
ref={swiperRef}
|
||||||
modules={[Scrollbar, FreeMode]}
|
onTouchStart={swiper => {
|
||||||
spaceBetween={15}
|
swiper.allowTouchMove = !activeItem;
|
||||||
slidesPerView={"auto"}
|
}}
|
||||||
scrollbar={{ hide: false }}
|
onTouchMove={swiper => {
|
||||||
freeMode={{ enabled: true }}
|
swiper.allowTouchMove = !activeItem;
|
||||||
touchStartPreventDefault={false}
|
}}
|
||||||
grabCursor>
|
className={classes["swiper-container"]}
|
||||||
{renderContainers()}
|
spaceBetween={15}
|
||||||
{isCreatingContainerEnabled && (
|
style={{ paddingInline: "10vw" }}
|
||||||
<SwiperSlide style={{ width: 50 }}>
|
modules={[Pagination]}
|
||||||
<CreateStatusButton />
|
freeMode={{ enabled: false }}
|
||||||
</SwiperSlide>
|
pagination={{ enabled: true, clickable: true }}>
|
||||||
)}
|
{renderContainers()}
|
||||||
</Swiper>
|
{isCreatingContainerEnabled && (
|
||||||
|
<SwiperSlide style={{ width: 250 }}>
|
||||||
|
<CreateStatusButton />
|
||||||
|
</SwiperSlide>
|
||||||
|
)}
|
||||||
|
</Swiper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDragStart = ({ active }: DragStartEvent) => {
|
|
||||||
const activeId = active.id as string | number;
|
|
||||||
|
|
||||||
if (typeof activeId !== "string") {
|
|
||||||
const item = (itemsAndGroups.find(
|
|
||||||
item => !("items" in item) && item.id === activeId
|
|
||||||
) ?? null) as TItem | null;
|
|
||||||
setActiveItem(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isContainerId(activeId)) {
|
|
||||||
const contId = getContainerId(activeId);
|
|
||||||
setActiveContainer(
|
|
||||||
containers.find(container => container.id === contId) ?? null
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroupId(activeId)) {
|
|
||||||
const groupId = getGroupId(activeId);
|
|
||||||
const group = (itemsAndGroups.find(
|
|
||||||
group => "items" in group && group.id === groupId
|
|
||||||
) ?? null) as TGroup | null;
|
|
||||||
setActiveGroup(group);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<Swiper
|
||||||
sensors={sensors}
|
ref={swiperRef}
|
||||||
measuring={{
|
className={classes["swiper-container"]}
|
||||||
droppable: {
|
modules={[Scrollbar, FreeMode]}
|
||||||
frequency,
|
spaceBetween={15}
|
||||||
},
|
slidesPerView={"auto"}
|
||||||
}}
|
scrollbar={{ hide: false }}
|
||||||
onDragStart={onDragStart}
|
freeMode={{ enabled: true }}
|
||||||
onDragOver={onDragOver}
|
touchStartPreventDefault={false}
|
||||||
onDragEnd={state => {
|
grabCursor>
|
||||||
setActiveContainer(null);
|
{renderContainers()}
|
||||||
setActiveItem(null);
|
{isCreatingContainerEnabled && (
|
||||||
setActiveGroup(null);
|
<SwiperSlide style={{ width: 50 }}>
|
||||||
onDragEnd(state);
|
<CreateStatusButton />
|
||||||
}}>
|
</SwiperSlide>
|
||||||
<SortableContext
|
)}
|
||||||
items={containers.map(container =>
|
</Swiper>
|
||||||
getDndContainerId(container.id)
|
|
||||||
)}
|
|
||||||
strategy={horizontalListSortingStrategy}>
|
|
||||||
{renderBody()}
|
|
||||||
<FunnelOverlay
|
|
||||||
activeContainer={activeContainer}
|
|
||||||
activeItem={activeItem}
|
|
||||||
activeGroup={activeGroup}
|
|
||||||
renderContainer={container => {
|
|
||||||
const containerItems = getItemsByContainer(container);
|
|
||||||
const containerId = getDndContainerId(container.id);
|
|
||||||
return renderContainerOverlay(
|
|
||||||
container,
|
|
||||||
<FunnelColumn
|
|
||||||
id={containerId}
|
|
||||||
itemsAndGroups={containerItems}
|
|
||||||
renderItem={renderItem}
|
|
||||||
renderGroup={renderGroup}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
renderItem={renderItem}
|
|
||||||
renderGroup={renderGroup}
|
|
||||||
/>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,32 +2,28 @@ import React, { ReactNode } from "react";
|
|||||||
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||||
import styles from "@/components/dnd/FunnelDnd/FunnelDnd.module.css";
|
import styles from "@/components/dnd/FunnelDnd/FunnelDnd.module.css";
|
||||||
|
|
||||||
type Props<TContainer, TItem, TGroup> = {
|
type Props<TContainer, TItem> = {
|
||||||
activeContainer: TContainer | null;
|
activeContainer: TContainer | null;
|
||||||
activeItem: TItem | null;
|
activeItem: TItem | null;
|
||||||
activeGroup: TGroup | null;
|
|
||||||
renderContainer: (container: TContainer) => ReactNode;
|
renderContainer: (container: TContainer) => ReactNode;
|
||||||
renderItem: (item: TItem) => ReactNode;
|
renderItem: (item: TItem) => ReactNode;
|
||||||
renderGroup: (group: TGroup) => ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FunnelOverlay = <TContainer, TItem, TGroup>({
|
const FunnelOverlay = <TContainer, TItem>({
|
||||||
activeContainer,
|
activeContainer,
|
||||||
activeItem,
|
activeItem,
|
||||||
activeGroup,
|
|
||||||
renderContainer,
|
renderContainer,
|
||||||
renderItem,
|
renderItem,
|
||||||
renderGroup,
|
}: Props<TContainer, TItem>) => {
|
||||||
}: Props<TContainer, TItem, TGroup>) => {
|
|
||||||
const renderOverlay = () => {
|
|
||||||
if (activeItem) return renderItem(activeItem);
|
|
||||||
if (activeContainer) return renderContainer(activeContainer);
|
|
||||||
if (activeGroup) return renderGroup(activeGroup);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||||
<div className={styles.overlay}>{renderOverlay()}</div>
|
<div className={styles.overlay}>
|
||||||
|
{activeItem
|
||||||
|
? renderItem(activeItem)
|
||||||
|
: activeContainer
|
||||||
|
? renderContainer(activeContainer)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import React, { ReactNode } from "react";
|
|
||||||
import { useDroppable } from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import { Stack } from "@mantine/core";
|
|
||||||
import { getDndGroupId } from "@/components/dnd/FunnelDnd/utils/groupId";
|
|
||||||
import SortableItem from "@/components/dnd/SortableItem";
|
|
||||||
import {
|
|
||||||
BaseDraggable,
|
|
||||||
BaseGroupDraggable,
|
|
||||||
} from "@/components/dnd/types/types";
|
|
||||||
import isItemGroup from "@/app/deals/utils/isItemGroup";
|
|
||||||
|
|
||||||
type Props<
|
|
||||||
TItem extends BaseDraggable,
|
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
> = {
|
|
||||||
id: string;
|
|
||||||
itemsAndGroups: (TItem | TGroup)[];
|
|
||||||
renderItem: (item: TItem) => ReactNode;
|
|
||||||
renderGroup: (group: TGroup) => ReactNode;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FunnelColumn = <
|
|
||||||
TItem extends BaseDraggable,
|
|
||||||
TGroup extends BaseGroupDraggable<TItem>,
|
|
||||||
>({
|
|
||||||
id,
|
|
||||||
itemsAndGroups,
|
|
||||||
renderItem,
|
|
||||||
renderGroup,
|
|
||||||
children,
|
|
||||||
}: Props<TItem, TGroup>) => {
|
|
||||||
const { setNodeRef } = useDroppable({ id });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<SortableContext
|
|
||||||
id={id}
|
|
||||||
items={itemsAndGroups.map(itemOrGroup =>
|
|
||||||
isItemGroup(itemOrGroup)
|
|
||||||
? getDndGroupId(itemOrGroup.id)
|
|
||||||
: itemOrGroup.id
|
|
||||||
)}
|
|
||||||
strategy={verticalListSortingStrategy}>
|
|
||||||
<Stack
|
|
||||||
gap="xs"
|
|
||||||
ref={setNodeRef}>
|
|
||||||
{itemsAndGroups.map(itemOrGroup =>
|
|
||||||
"items" in itemOrGroup ? (
|
|
||||||
<SortableItem
|
|
||||||
key={`${itemOrGroup.id.toString()}g`}
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
id={getDndGroupId(itemOrGroup.id)}
|
|
||||||
renderItem={() => renderGroup(itemOrGroup)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SortableItem
|
|
||||||
key={itemOrGroup.id}
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
id={itemOrGroup.id}
|
|
||||||
renderItem={() => renderItem(itemOrGroup)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FunnelColumn;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
const CONTAINER_POSTFIX = "-con";
|
|
||||||
|
|
||||||
export const isContainerId = (rawId: string) => rawId.endsWith(CONTAINER_POSTFIX);
|
|
||||||
|
|
||||||
export const getContainerId = (rawId: string) =>
|
|
||||||
Number(rawId.replace(CONTAINER_POSTFIX, ""));
|
|
||||||
|
|
||||||
export const getDndContainerId = (id: number) =>
|
|
||||||
`${id}${CONTAINER_POSTFIX}`;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
const GROUP_POSTFIX = "-gr";
|
|
||||||
|
|
||||||
export const isGroupId = (rawId: string) => rawId.endsWith(GROUP_POSTFIX);
|
|
||||||
|
|
||||||
export const getGroupId = (rawId: string) =>
|
|
||||||
Number(rawId.replace(GROUP_POSTFIX, ""));
|
|
||||||
|
|
||||||
export const getDndGroupId = (id: number) => `${id}${GROUP_POSTFIX}`;
|
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import React, { CSSProperties, ReactNode, useCallback, useMemo } from "react";
|
||||||
|
import { useDndContext } from "@dnd-kit/core";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import DragHandle from "@/components/dnd/DragHandle";
|
||||||
|
// import collisionsShouldBeCombined from "@/components/dnd/utils/collisionsShouldBeCombined";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: number | string;
|
||||||
|
renderItem: (renderDraggable?: () => ReactNode) => ReactNode;
|
||||||
|
renderDraggable?: () => ReactNode; // if not passed - the whole item renders as draggable
|
||||||
|
disabled?: boolean;
|
||||||
|
dragHandleStyle?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableCombinableItem = ({
|
||||||
|
renderItem,
|
||||||
|
dragHandleStyle,
|
||||||
|
renderDraggable,
|
||||||
|
id,
|
||||||
|
disabled = false,
|
||||||
|
}: Props) => {
|
||||||
|
const { isDragging, setNodeRef, transform, transition, over, active } =
|
||||||
|
useSortable({
|
||||||
|
id,
|
||||||
|
animateLayoutChanges: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { collisions } = useDndContext();
|
||||||
|
|
||||||
|
// const isCurrentCombining = useMemo(
|
||||||
|
// () => collisionsShouldBeCombined({ collisions }) && over?.id === id,
|
||||||
|
// [over, collisions]
|
||||||
|
// );
|
||||||
|
|
||||||
|
const getBorder = useCallback(() => {
|
||||||
|
// if (isCurrentCombining) return "1px solid red";
|
||||||
|
return "";
|
||||||
|
}, [active, over, id]);
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
// transform: collisionsShouldBeCombined({ collisions })
|
||||||
|
// ? "none"
|
||||||
|
// : CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.4 : 1,
|
||||||
|
border: getBorder(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDragHandle = () => (
|
||||||
|
<DragHandle
|
||||||
|
id={id}
|
||||||
|
style={dragHandleStyle}
|
||||||
|
disabled={disabled}>
|
||||||
|
{renderDraggable && renderDraggable()}
|
||||||
|
</DragHandle>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}>
|
||||||
|
{renderDraggable ? (
|
||||||
|
renderItem(renderDragHandle)
|
||||||
|
) : (
|
||||||
|
<DragHandle
|
||||||
|
id={id}
|
||||||
|
style={dragHandleStyle}
|
||||||
|
disabled={disabled}>
|
||||||
|
{renderItem()}
|
||||||
|
</DragHandle>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableCombinableItem;
|
||||||
3
src/components/dnd/SortableCombinableItem/index.ts
Normal file
3
src/components/dnd/SortableCombinableItem/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SortableCombinableItem from "./SortableCombinableItem";
|
||||||
|
|
||||||
|
export default SortableCombinableItem;
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { CSSProperties, ReactNode } from "react";
|
import React, { CSSProperties, ReactNode } from "react";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import DragHandle from "@/components/dnd/DragHandle";
|
||||||
import DragHandle from "./DragHandle";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
@ -23,12 +22,6 @@ const SortableItem = ({
|
|||||||
animateLayoutChanges: () => false,
|
animateLayoutChanges: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const style: CSSProperties = {
|
|
||||||
opacity: isDragging ? 0.4 : undefined,
|
|
||||||
transform: CSS.Translate.toString(transform),
|
|
||||||
transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDragHandle = () => (
|
const renderDragHandle = () => (
|
||||||
<DragHandle
|
<DragHandle
|
||||||
id={id}
|
id={id}
|
||||||
@ -39,9 +32,7 @@ const SortableItem = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={setNodeRef}>
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}>
|
|
||||||
{renderDraggable ? (
|
{renderDraggable ? (
|
||||||
renderItem(renderDragHandle)
|
renderItem(renderDragHandle)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
export type BaseDraggable = {
|
export type BaseDraggable = {
|
||||||
id: number;
|
id: number;
|
||||||
lexorank: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BaseGroupDraggable<TItem extends BaseDraggable> = BaseDraggable & {
|
|
||||||
items: TItem[];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|||||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||||
|
|
||||||
const useProjectActions = () => {
|
const useProjectActions = () => {
|
||||||
const { selectedProject, projectsCrud, refetchProjects } =
|
const { selectedProject, projectsCrud } = useProjectsContext();
|
||||||
useProjectsContext();
|
|
||||||
const { openDrawer } = useDrawersContext();
|
const { openDrawer } = useDrawersContext();
|
||||||
|
|
||||||
const onCreateClick = () => {
|
const onCreateClick = () => {
|
||||||
@ -28,7 +27,6 @@ const useProjectActions = () => {
|
|||||||
onChange: value => projectsCrud.onUpdate(value.id, value),
|
onChange: value => projectsCrud.onUpdate(value.id, value),
|
||||||
onDelete: projectsCrud.onDelete,
|
onDelete: projectsCrud.onDelete,
|
||||||
},
|
},
|
||||||
onClose: refetchProjects,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import { lighten, Pill, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { DealTagSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
tag: Partial<DealTagSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DealTag = ({ tag }: Props) => {
|
|
||||||
const theme = useMantineColorScheme();
|
|
||||||
const isInherit = tag.tagColor!.backgroundColor === "inherit";
|
|
||||||
|
|
||||||
let color = tag.tagColor!.color;
|
|
||||||
const backgroundColor = tag.tagColor!.backgroundColor;
|
|
||||||
|
|
||||||
if (!(theme.colorScheme === "dark" || isInherit)) {
|
|
||||||
color = lighten(color, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pill
|
|
||||||
key={tag.id}
|
|
||||||
style={{
|
|
||||||
opacity: 0.7,
|
|
||||||
color,
|
|
||||||
backgroundColor,
|
|
||||||
border: "1px solid",
|
|
||||||
borderColor: color,
|
|
||||||
}}>
|
|
||||||
{tag.name}
|
|
||||||
</Pill>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealTag;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
.add-tag-button {
|
|
||||||
@mixin light {
|
|
||||||
background-color: var(--mantine-color-gray-1);
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-6);
|
|
||||||
}
|
|
||||||
color: gray;
|
|
||||||
border: 1px gray dashed;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-tag-button:hover {
|
|
||||||
@mixin light {
|
|
||||||
border-color: black;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
border-color: white;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-tag-button-icon {
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Button, Center, Checkbox, Group, Menu, Stack } from "@mantine/core";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
|
||||||
import useDealTags from "@/components/ui/DealTags/hooks/useDealTags";
|
|
||||||
import { DealTagSchema } from "@/lib/client";
|
|
||||||
import styles from "./DealTags.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
dealId?: number;
|
|
||||||
groupId?: number;
|
|
||||||
tags: DealTagSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const DealTags = ({ tags, dealId, groupId }: Props) => {
|
|
||||||
const { selectedProject } = useProjectsContext();
|
|
||||||
const { switchTag } = useDealTags();
|
|
||||||
const tagIdsSet = useMemo(() => new Set(tags.map(t => t.id)), [tags]);
|
|
||||||
|
|
||||||
if (selectedProject?.tags.length === 0) return;
|
|
||||||
|
|
||||||
const onTagClick = (tagId: number, event: MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
switchTag({ dealId, groupId, tagId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTagButton = useMemo(
|
|
||||||
() => (
|
|
||||||
<Menu withArrow>
|
|
||||||
<Menu.Target>
|
|
||||||
<Button
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
unstyled
|
|
||||||
className={classNames(styles["add-tag-button"])}>
|
|
||||||
<Center>
|
|
||||||
<IconPlus
|
|
||||||
size={"1.2em"}
|
|
||||||
className={classNames(
|
|
||||||
styles["add-tag-button-icon"]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
</Button>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Stack
|
|
||||||
p={"xs"}
|
|
||||||
gap={"sm"}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
|
||||||
{selectedProject?.tags.map(tag => (
|
|
||||||
<Group
|
|
||||||
key={tag.id}
|
|
||||||
wrap={"nowrap"}>
|
|
||||||
<Checkbox
|
|
||||||
checked={tagIdsSet.has(tag.id)}
|
|
||||||
onChange={event =>
|
|
||||||
onTagClick(
|
|
||||||
tag.id,
|
|
||||||
event as unknown as any
|
|
||||||
)
|
|
||||||
}
|
|
||||||
label={tag.name}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
),
|
|
||||||
[selectedProject?.tags, tags]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group gap={"xs"}>
|
|
||||||
{addTagButton}
|
|
||||||
{selectedProject?.tags.map(
|
|
||||||
tag =>
|
|
||||||
tagIdsSet.has(tag.id) && (
|
|
||||||
<DealTag
|
|
||||||
key={tag.id}
|
|
||||||
tag={tag}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealTags;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { SwitchDealTagRequest } from "@/lib/client";
|
|
||||||
import { switchDealTagMutation } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useDealTags = () => {
|
|
||||||
const { refetchDeals } = useDealsContext();
|
|
||||||
|
|
||||||
const switchTagMutation = useMutation({
|
|
||||||
...switchDealTagMutation(),
|
|
||||||
onSettled: refetchDeals,
|
|
||||||
});
|
|
||||||
|
|
||||||
const switchTag = (data: SwitchDealTagRequest) => {
|
|
||||||
switchTagMutation.mutate({
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
switchTag,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDealTags;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import ProjectEditorDrawer from "./ProjectEditorDrawer";
|
|
||||||
|
|
||||||
export default ProjectEditorDrawer;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { Flex } from "@mantine/core";
|
|
||||||
import TagsTabHeader from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTabHeader";
|
|
||||||
import TagsTable from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagsTable";
|
|
||||||
import { DealTagsContextProvider } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
|
||||||
import { ProjectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: ProjectSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TagsTab: FC<Props> = ({ value }) => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
h={"100%"}
|
|
||||||
direction={"column"}
|
|
||||||
gap={"xs"}>
|
|
||||||
<DealTagsContextProvider project={value}>
|
|
||||||
<TagsTabHeader />
|
|
||||||
<TagsTable />
|
|
||||||
</DealTagsContextProvider>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsTab;
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { IconCheck } from "@tabler/icons-react";
|
|
||||||
import { Group, SelectProps } from "@mantine/core";
|
|
||||||
import ObjectSelect, {
|
|
||||||
ObjectSelectProps,
|
|
||||||
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
|
||||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
|
||||||
import { DealTagColorSchema } from "@/lib/client";
|
|
||||||
import useTagColorList from "../hooks/useTagColorList";
|
|
||||||
|
|
||||||
type Props = Omit<
|
|
||||||
ObjectSelectProps<DealTagColorSchema>,
|
|
||||||
"data" | "getValueFn" | "getLabelFn"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const TagColorInput = (props: Props) => {
|
|
||||||
const { colors } = useTagColorList();
|
|
||||||
const colorsMap = new Map<string, DealTagColorSchema>(
|
|
||||||
colors.map(
|
|
||||||
color =>
|
|
||||||
[color.id.toString(), color] as [string, DealTagColorSchema]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSelectOption: SelectProps["renderOption"] = ({
|
|
||||||
option,
|
|
||||||
checked,
|
|
||||||
}) => {
|
|
||||||
const tag = {
|
|
||||||
id: Number(option.value),
|
|
||||||
name: "Тег-пример",
|
|
||||||
tagColor: colorsMap.get(option.value),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
flex="1"
|
|
||||||
gap="md">
|
|
||||||
<DealTag tag={tag} />
|
|
||||||
{option.label}
|
|
||||||
{checked && <IconCheck style={{ marginInlineStart: "auto" }} />}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ObjectSelect
|
|
||||||
label={"Цвет"}
|
|
||||||
renderOption={renderSelectOption}
|
|
||||||
data={colors}
|
|
||||||
getValueFn={color => color.id.toString()}
|
|
||||||
getLabelFn={color => color.label}
|
|
||||||
searchable
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagColorInput;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
import { Group } from "@mantine/core";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
|
||||||
|
|
||||||
const TagsTabHeader = () => {
|
|
||||||
const { onCreateClick } = useDealTagActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
pt={"xs"}
|
|
||||||
px={"xs"}>
|
|
||||||
<InlineButton onClick={onCreateClick}>
|
|
||||||
<IconPlus />
|
|
||||||
Создать
|
|
||||||
</InlineButton>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsTabHeader;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { IconMoodSad } from "@tabler/icons-react";
|
|
||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import { useDealTagsContext } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
|
||||||
import tagsTableColumns from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/tagsTableColumns";
|
|
||||||
import useDealTagActions from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/hooks/useDealTagActions";
|
|
||||||
|
|
||||||
const TagsTable = () => {
|
|
||||||
const { dealTags, dealTagsCrud } = useDealTagsContext();
|
|
||||||
const { onChangeClick } = useDealTagActions();
|
|
||||||
|
|
||||||
const columns = tagsTableColumns({
|
|
||||||
onDelete: dealTagsCrud.onDelete,
|
|
||||||
onChange: onChangeClick,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
withTableBorder
|
|
||||||
records={dealTags}
|
|
||||||
columns={columns}
|
|
||||||
groups={undefined}
|
|
||||||
style={{
|
|
||||||
marginInline: "var(--mantine-spacing-xs)",
|
|
||||||
minHeight: 200,
|
|
||||||
}}
|
|
||||||
verticalSpacing={"xs"}
|
|
||||||
emptyState={
|
|
||||||
<Group mt={dealTags.length === 0 ? "xl" : 0}>
|
|
||||||
<Text>Нет тегов</Text>
|
|
||||||
<IconMoodSad />
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsTable;
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { DealTagsCrud, useDealTagsCrud } from "@/hooks/cruds/useDealTagsCrud";
|
|
||||||
import useDealTagsList from "@/hooks/lists/useDealTagsList";
|
|
||||||
import { DealTagSchema, ProjectSchema } from "@/lib/client";
|
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
|
||||||
|
|
||||||
type DealTagsContextState = {
|
|
||||||
dealTags: DealTagSchema[];
|
|
||||||
refetchDealTags: () => void;
|
|
||||||
dealTagsCrud: DealTagsCrud;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
project: ProjectSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealTagsContextState = ({ project }: Props): DealTagsContextState => {
|
|
||||||
const dealTagsList = useDealTagsList({ projectId: project.id });
|
|
||||||
|
|
||||||
const dealTagsCrud = useDealTagsCrud({
|
|
||||||
...dealTagsList,
|
|
||||||
projectId: project.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
dealTags: dealTagsList.dealTags,
|
|
||||||
refetchDealTags: dealTagsList.refetch,
|
|
||||||
dealTagsCrud,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const [DealTagsContextProvider, useDealTagsContext] = makeContext<
|
|
||||||
DealTagsContextState,
|
|
||||||
Props
|
|
||||||
>(useDealTagsContextState, "DealTags");
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { Center } from "@mantine/core";
|
|
||||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
|
||||||
import DealTag from "@/components/ui/DealTag/DealTag";
|
|
||||||
import { DealTagSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDelete: (tag: DealTagSchema) => void;
|
|
||||||
onChange: (tag: DealTagSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useTagsTableColumns = ({ onDelete, onChange }: Props) => {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: tag => (
|
|
||||||
<UpdateDeleteTableActions
|
|
||||||
onDelete={() => onDelete(tag)}
|
|
||||||
onChange={() => onChange(tag)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Название",
|
|
||||||
accessor: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Цвет",
|
|
||||||
accessor: "tagColor.label",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Пример",
|
|
||||||
accessor: "tagColor",
|
|
||||||
render: tag => <DealTag tag={tag} />,
|
|
||||||
},
|
|
||||||
] as DataTableColumn<DealTagSchema>[],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useTagsTableColumns;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useDealTagsContext } from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/contexts/DealTagsContext";
|
|
||||||
import { DealTagSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const useDealTagActions = () => {
|
|
||||||
const { dealTagsCrud } = useDealTagsContext();
|
|
||||||
|
|
||||||
const onChangeClick = (tag: DealTagSchema) => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "dealTagModal",
|
|
||||||
innerProps: {
|
|
||||||
entity: tag,
|
|
||||||
onChange: data => dealTagsCrud.onUpdate(tag.id, data),
|
|
||||||
isEditing: true,
|
|
||||||
},
|
|
||||||
withCloseButton: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreateClick = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "dealTagModal",
|
|
||||||
innerProps: {
|
|
||||||
onCreate: dealTagsCrud.onCreate,
|
|
||||||
isEditing: false,
|
|
||||||
},
|
|
||||||
withCloseButton: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onChangeClick,
|
|
||||||
onCreateClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDealTagActions;
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { getDealTagColorsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useTagColorList = () => {
|
|
||||||
const { data, refetch } = useQuery(getDealTagColorsOptions());
|
|
||||||
|
|
||||||
return {
|
|
||||||
colors: data?.items ?? [],
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useTagColorList;
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Stack, TextInput } from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { ContextModalProps } from "@mantine/modals";
|
|
||||||
import TagColorInput from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/components/TagColorInput";
|
|
||||||
import {
|
|
||||||
CreateDealTagSchema,
|
|
||||||
DealTagSchema,
|
|
||||||
UpdateDealTagSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import BaseFormModal, {
|
|
||||||
CreateEditFormProps,
|
|
||||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
|
||||||
|
|
||||||
type Props = CreateEditFormProps<
|
|
||||||
CreateDealTagSchema,
|
|
||||||
UpdateDealTagSchema,
|
|
||||||
DealTagSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const DealTagModal = ({
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
innerProps,
|
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
const initialValues: Partial<DealTagSchema> = innerProps.isEditing
|
|
||||||
? innerProps.entity
|
|
||||||
: {
|
|
||||||
name: "",
|
|
||||||
tagColor: undefined,
|
|
||||||
tagColorId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm<Partial<DealTagSchema>>({
|
|
||||||
initialValues,
|
|
||||||
validate: {
|
|
||||||
name: name => !name && "Необходимо указать название тега",
|
|
||||||
tagColor: tagColor => !tagColor && "Необходимо указать цвет тега",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseFormModal
|
|
||||||
form={form}
|
|
||||||
closeOnSubmit
|
|
||||||
onClose={() => context.closeContextModal(id)}
|
|
||||||
{...innerProps}>
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
label={"Название"}
|
|
||||||
placeholder={"Введите название тега"}
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
<TagColorInput
|
|
||||||
placeholder={"Укажите цвет"}
|
|
||||||
{...form.getInputProps("tagColor")}
|
|
||||||
onChange={tag => {
|
|
||||||
form.setFieldValue("tagColor", tag);
|
|
||||||
form.setFieldValue("tagColorId", tag.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</BaseFormModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealTagModal;
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import ClientMarketplaceDrawer from "@/app/clients/drawers/ClientMarketplacesDrawer";
|
import ClientMarketplaceDrawer from "@/app/clients/drawers/ClientMarketplacesDrawer";
|
||||||
import BoardsMobileEditorDrawer from "@/app/deals/drawers/BoardsMobileEditorDrawer";
|
import BoardsMobileEditorDrawer from "@/app/deals/drawers/BoardsMobileEditorDrawer";
|
||||||
import DealEditorDrawer from "@/app/deals/drawers/DealEditorDrawer";
|
import DealEditorDrawer from "@/app/deals/drawers/DealEditorDrawer";
|
||||||
|
import ProjectEditorDrawer from "@/app/deals/drawers/ProjectEditorDrawer";
|
||||||
import ProjectsMobileEditorDrawer from "@/app/deals/drawers/ProjectsMobileEditorDrawer";
|
import ProjectsMobileEditorDrawer from "@/app/deals/drawers/ProjectsMobileEditorDrawer";
|
||||||
import StatusesMobileEditorDrawer from "../app/deals/drawers/StatusesMobileEditorDrawer";
|
import StatusesMobileEditorDrawer from "../app/deals/drawers/StatusesMobileEditorDrawer";
|
||||||
import ProjectEditorDrawer from "./common/ProjectEditorDrawer";
|
|
||||||
|
|
||||||
const drawerRegistry = {
|
const drawerRegistry = {
|
||||||
projectsMobileEditorDrawer: ProjectsMobileEditorDrawer,
|
projectsMobileEditorDrawer: ProjectsMobileEditorDrawer,
|
||||||
|
|||||||
@ -1,115 +1,51 @@
|
|||||||
import React from "react";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { UpdateDealGroupSchema } from "@/lib/client";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { Text } from "@mantine/core";
|
|
||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { HttpValidationError, UpdateDealGroupSchema } from "@/lib/client";
|
|
||||||
import {
|
import {
|
||||||
|
addDealMutation,
|
||||||
createDealGroupMutation,
|
createDealGroupMutation,
|
||||||
deleteDealGroupMutation,
|
removeDealMutation,
|
||||||
updateDealGroupMutation,
|
updateDealGroupMutation,
|
||||||
updateDealsInGroupMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
|
|
||||||
export type GroupsCrud = {
|
const useDealGroupCrud = () => {
|
||||||
onUpdate: (groupId: number, group: UpdateDealGroupSchema) => void;
|
const updateMutation = useMutation(updateDealGroupMutation());
|
||||||
onCreate: (mainDealId: number, otherDealIds: number[]) => void;
|
|
||||||
onUpdateDealsInGroup: (groupId: number, dealIds: number[]) => void;
|
|
||||||
onDelete: (groupId: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealGroupCrud = (): GroupsCrud => {
|
const onUpdate = (entity: UpdateDealGroupSchema) => {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const key = "getDeals";
|
|
||||||
|
|
||||||
const onError = (
|
|
||||||
error: AxiosError<HttpValidationError>,
|
|
||||||
_: any,
|
|
||||||
context: any
|
|
||||||
) => {
|
|
||||||
console.error(error);
|
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
|
||||||
if (context?.previous) {
|
|
||||||
queryClient.setQueryData([key], context.previous);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSettled = () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
predicate: (query: { queryKey: any }) =>
|
|
||||||
query.queryKey[0]?._id === key,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
|
||||||
...updateDealGroupMutation(),
|
|
||||||
onSettled,
|
|
||||||
onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUpdate = (groupId: number, entity: UpdateDealGroupSchema) => {
|
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
path: {
|
|
||||||
pk: groupId,
|
|
||||||
},
|
|
||||||
body: {
|
body: {
|
||||||
entity,
|
entity,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation(createDealGroupMutation());
|
||||||
...createDealGroupMutation(),
|
|
||||||
onSettled,
|
|
||||||
onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCreate = (mainDealId: number, otherDealIds: number[]) => {
|
const onCreate = (draggingDealId: number, hoveredDealId: number) => {
|
||||||
createMutation.mutate({
|
createMutation.mutate({
|
||||||
body: {
|
body: {
|
||||||
mainDealId,
|
draggingDealId,
|
||||||
otherDealIds,
|
hoveredDealId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDealsMutation = useMutation({
|
const addDealToGroupMutation = useMutation(addDealMutation());
|
||||||
...updateDealsInGroupMutation(),
|
|
||||||
onSettled,
|
|
||||||
onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUpdateDealsInGroup = (groupId: number, dealIds: number[]) => {
|
const onAddDeal = (dealId: number, groupId: number) => {
|
||||||
updateDealsMutation.mutate({
|
addDealToGroupMutation.mutate({
|
||||||
path: {
|
|
||||||
pk: groupId,
|
|
||||||
},
|
|
||||||
body: {
|
body: {
|
||||||
dealIds,
|
dealId,
|
||||||
|
groupId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const removeDealFromGroupMutation = useMutation(removeDealMutation());
|
||||||
...deleteDealGroupMutation(),
|
|
||||||
onSettled,
|
|
||||||
onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDelete = (groupId: number) => {
|
const onRemoveDeal = (dealId: number) => {
|
||||||
modals.openConfirmModal({
|
removeDealFromGroupMutation.mutate({
|
||||||
title: "Удаление группы",
|
body: {
|
||||||
children: <Text>Вы уверены, что хотите удалить группу?</Text>,
|
dealId,
|
||||||
confirmProps: { color: "red" },
|
|
||||||
onConfirm: () => {
|
|
||||||
deleteMutation.mutate({
|
|
||||||
path: {
|
|
||||||
pk: groupId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -117,8 +53,8 @@ const useDealGroupCrud = (): GroupsCrud => {
|
|||||||
return {
|
return {
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onCreate,
|
onCreate,
|
||||||
onUpdateDealsInGroup,
|
onAddDeal,
|
||||||
onDelete,
|
onRemoveDeal,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
|
||||||
import {
|
|
||||||
CreateDealTagSchema,
|
|
||||||
DealTagSchema,
|
|
||||||
UpdateDealTagSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import {
|
|
||||||
createDealTagMutation,
|
|
||||||
deleteDealTagMutation,
|
|
||||||
updateDealTagMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type UseDealTagsOperationsProps = {
|
|
||||||
queryKey: any[];
|
|
||||||
projectId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DealTagsCrud = {
|
|
||||||
onCreate: (data: CreateDealTagSchema) => void;
|
|
||||||
onUpdate: (dealTagId: number, dealTag: UpdateDealTagSchema) => void;
|
|
||||||
onDelete: (dealTag: DealTagSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDealTagsCrud = ({
|
|
||||||
queryKey,
|
|
||||||
projectId,
|
|
||||||
}: UseDealTagsOperationsProps): DealTagsCrud => {
|
|
||||||
return useCrudOperations<
|
|
||||||
DealTagSchema,
|
|
||||||
UpdateDealTagSchema,
|
|
||||||
CreateDealTagSchema
|
|
||||||
>({
|
|
||||||
key: "getDealTags",
|
|
||||||
queryKey,
|
|
||||||
mutations: {
|
|
||||||
create: createDealTagMutation(),
|
|
||||||
update: updateDealTagMutation(),
|
|
||||||
delete: deleteDealTagMutation(),
|
|
||||||
},
|
|
||||||
getCreateEntity: data => ({
|
|
||||||
tagColorId: data.tagColorId!,
|
|
||||||
name: data.name!,
|
|
||||||
projectId,
|
|
||||||
}),
|
|
||||||
getUpdateEntity: (old, update) => ({
|
|
||||||
...old,
|
|
||||||
name: update.name ?? old.name,
|
|
||||||
tagColor: update.tagColor ?? old.tagColor,
|
|
||||||
tagColorId: update.tagColor?.id ?? old.tagColorId,
|
|
||||||
}),
|
|
||||||
getDeleteConfirmTitle: () => "Удаление доски",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { DealTagSchema } from "@/lib/client";
|
|
||||||
import {
|
|
||||||
getDealTagsOptions,
|
|
||||||
getDealTagsQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
projectId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealTagsList = ({ projectId }: Props) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const options = {
|
|
||||||
path: { projectId },
|
|
||||||
};
|
|
||||||
const { data, refetch } = useQuery(getDealTagsOptions(options));
|
|
||||||
|
|
||||||
const queryKey = getDealTagsQueryKey(options);
|
|
||||||
|
|
||||||
const setDealTags = (dealTags: DealTagSchema[]) => {
|
|
||||||
queryClient.setQueryData(
|
|
||||||
queryKey,
|
|
||||||
(old: { items: DealTagSchema[] }) => ({
|
|
||||||
...old,
|
|
||||||
items: dealTags,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
dealTags: data?.items ?? [],
|
|
||||||
setDealTags,
|
|
||||||
refetch,
|
|
||||||
queryKey,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDealTagsList;
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddDealToGroupRequest
|
||||||
|
*/
|
||||||
|
export const zAddDealToGroupRequest = z.object({
|
||||||
|
dealId: z.int(),
|
||||||
|
groupId: z.int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddDealToGroupResponse
|
||||||
|
*/
|
||||||
|
export const zAddDealToGroupResponse = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BarcodeTemplateAttributeSchema
|
* BarcodeTemplateAttributeSchema
|
||||||
*/
|
*/
|
||||||
@ -197,8 +212,8 @@ export const zCreateClientResponse = z.object({
|
|||||||
* CreateDealGroupRequest
|
* CreateDealGroupRequest
|
||||||
*/
|
*/
|
||||||
export const zCreateDealGroupRequest = z.object({
|
export const zCreateDealGroupRequest = z.object({
|
||||||
mainDealId: z.int(),
|
draggingDealId: z.int(),
|
||||||
otherDealIds: z.array(z.int()),
|
hoveredDealId: z.int(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,27 +363,6 @@ export const zStatusSchema = z.object({
|
|||||||
color: z.string(),
|
color: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* DealTagColorSchema
|
|
||||||
*/
|
|
||||||
export const zDealTagColorSchema = z.object({
|
|
||||||
id: z.int(),
|
|
||||||
color: z.string(),
|
|
||||||
backgroundColor: z.string(),
|
|
||||||
label: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DealTagSchema
|
|
||||||
*/
|
|
||||||
export const zDealTagSchema = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
projectId: z.int(),
|
|
||||||
tagColorId: z.int(),
|
|
||||||
id: z.int(),
|
|
||||||
tagColor: zDealTagColorSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DealSchema
|
* DealSchema
|
||||||
*/
|
*/
|
||||||
@ -382,7 +376,6 @@ export const zDealSchema = z.object({
|
|||||||
offset: true,
|
offset: true,
|
||||||
}),
|
}),
|
||||||
group: z.union([zDealGroupSchema, z.null()]),
|
group: z.union([zDealGroupSchema, z.null()]),
|
||||||
tags: z.array(zDealTagSchema),
|
|
||||||
productsQuantity: z.optional(z.int()).default(0),
|
productsQuantity: z.optional(z.int()).default(0),
|
||||||
totalPrice: z.optional(z.number()).default(0),
|
totalPrice: z.optional(z.number()).default(0),
|
||||||
client: z.optional(z.union([zClientSchema, z.null()])),
|
client: z.optional(z.union([zClientSchema, z.null()])),
|
||||||
@ -433,30 +426,6 @@ export const zCreateDealServiceResponse = z.object({
|
|||||||
entity: zDealServiceSchema,
|
entity: zDealServiceSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateDealTagSchema
|
|
||||||
*/
|
|
||||||
export const zCreateDealTagSchema = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
projectId: z.int(),
|
|
||||||
tagColorId: z.int(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateDealTagRequest
|
|
||||||
*/
|
|
||||||
export const zCreateDealTagRequest = z.object({
|
|
||||||
entity: zCreateDealTagSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateDealTagResponse
|
|
||||||
*/
|
|
||||||
export const zCreateDealTagResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
entity: zDealTagSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreateMarketplaceSchema
|
* CreateMarketplaceSchema
|
||||||
*/
|
*/
|
||||||
@ -572,7 +541,6 @@ export const zProjectSchema = z.object({
|
|||||||
id: z.int(),
|
id: z.int(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
builtInModules: z.array(zBuiltInModuleSchemaOutput),
|
builtInModules: z.array(zBuiltInModuleSchemaOutput),
|
||||||
tags: z.array(zDealTagSchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -726,6 +694,20 @@ export const zDealProductAddKitResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealRemoveFromGroupRequest
|
||||||
|
*/
|
||||||
|
export const zDealRemoveFromGroupRequest = z.object({
|
||||||
|
dealId: z.int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealRemoveFromGroupResponse
|
||||||
|
*/
|
||||||
|
export const zDealRemoveFromGroupResponse = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DeleteBarcodeTemplateResponse
|
* DeleteBarcodeTemplateResponse
|
||||||
*/
|
*/
|
||||||
@ -747,13 +729,6 @@ export const zDeleteClientResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* DeleteDealGroupResponse
|
|
||||||
*/
|
|
||||||
export const zDeleteDealGroupResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DeleteDealProductResponse
|
* DeleteDealProductResponse
|
||||||
*/
|
*/
|
||||||
@ -775,13 +750,6 @@ export const zDeleteDealServiceResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* DeleteDealTagResponse
|
|
||||||
*/
|
|
||||||
export const zDeleteDealTagResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DeleteMarketplaceResponse
|
* DeleteMarketplaceResponse
|
||||||
*/
|
*/
|
||||||
@ -901,13 +869,6 @@ export const zGetDealServicesResponse = z.object({
|
|||||||
items: z.array(zDealServiceSchema),
|
items: z.array(zDealServiceSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GetDealTagsResponse
|
|
||||||
*/
|
|
||||||
export const zGetDealTagsResponse = z.object({
|
|
||||||
items: z.array(zDealTagSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PaginationInfoSchema
|
* PaginationInfoSchema
|
||||||
*/
|
*/
|
||||||
@ -957,13 +918,6 @@ export const zGetProductsResponse = z.object({
|
|||||||
paginationInfo: zPaginationInfoSchema,
|
paginationInfo: zPaginationInfoSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GetProjectResponse
|
|
||||||
*/
|
|
||||||
export const zGetProjectResponse = z.object({
|
|
||||||
entity: zProjectSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetProjectsResponse
|
* GetProjectsResponse
|
||||||
*/
|
*/
|
||||||
@ -1019,13 +973,6 @@ export const zGetStatusesResponse = z.object({
|
|||||||
items: z.array(zStatusSchema),
|
items: z.array(zStatusSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GetTagColorsResponse
|
|
||||||
*/
|
|
||||||
export const zGetTagColorsResponse = z.object({
|
|
||||||
items: z.array(zDealTagColorSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ValidationError
|
* ValidationError
|
||||||
*/
|
*/
|
||||||
@ -1069,22 +1016,6 @@ export const zProductServicesDuplicateResponse = z.object({
|
|||||||
|
|
||||||
export const zSortDir = z.enum(["asc", "desc"]);
|
export const zSortDir = z.enum(["asc", "desc"]);
|
||||||
|
|
||||||
/**
|
|
||||||
* SwitchDealTagRequest
|
|
||||||
*/
|
|
||||||
export const zSwitchDealTagRequest = z.object({
|
|
||||||
tagId: z.int(),
|
|
||||||
dealId: z.optional(z.union([z.int(), z.null()])),
|
|
||||||
groupId: z.optional(z.union([z.int(), z.null()])),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SwitchDealTagResponse
|
|
||||||
*/
|
|
||||||
export const zSwitchDealTagResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateBarcodeTemplateSchema
|
* UpdateBarcodeTemplateSchema
|
||||||
*/
|
*/
|
||||||
@ -1163,7 +1094,6 @@ export const zUpdateClientResponse = z.object({
|
|||||||
export const zUpdateDealGroupSchema = z.object({
|
export const zUpdateDealGroupSchema = z.object({
|
||||||
name: z.optional(z.union([z.string(), z.null()])),
|
name: z.optional(z.union([z.string(), z.null()])),
|
||||||
lexorank: z.optional(z.union([z.string(), z.null()])),
|
lexorank: z.optional(z.union([z.string(), z.null()])),
|
||||||
statusId: z.optional(z.union([z.int(), z.null()])),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1250,42 +1180,6 @@ export const zUpdateDealServiceResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateDealTagSchema
|
|
||||||
*/
|
|
||||||
export const zUpdateDealTagSchema = z.object({
|
|
||||||
name: z.optional(z.union([z.string(), z.null()])),
|
|
||||||
tagColor: z.optional(z.union([zDealTagColorSchema, z.null()])),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateDealTagRequest
|
|
||||||
*/
|
|
||||||
export const zUpdateDealTagRequest = z.object({
|
|
||||||
entity: zUpdateDealTagSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateDealTagResponse
|
|
||||||
*/
|
|
||||||
export const zUpdateDealTagResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateDealsInGroupRequest
|
|
||||||
*/
|
|
||||||
export const zUpdateDealsInGroupRequest = z.object({
|
|
||||||
dealIds: z.array(z.int()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateDealsInGroupResponse
|
|
||||||
*/
|
|
||||||
export const zUpdateDealsInGroupResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateMarketplaceSchema
|
* UpdateMarketplaceSchema
|
||||||
*/
|
*/
|
||||||
@ -1594,24 +1488,9 @@ export const zUpdateDealData = z.object({
|
|||||||
*/
|
*/
|
||||||
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
||||||
|
|
||||||
export const zDeleteDealGroupData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zDeleteDealGroupResponse2 = zDeleteDealGroupResponse;
|
|
||||||
|
|
||||||
export const zUpdateDealGroupData = z.object({
|
export const zUpdateDealGroupData = z.object({
|
||||||
body: zUpdateDealGroupRequest,
|
body: zUpdateDealGroupRequest,
|
||||||
path: z.object({
|
path: z.optional(z.never()),
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
query: z.optional(z.never()),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1631,34 +1510,8 @@ export const zCreateDealGroupData = z.object({
|
|||||||
*/
|
*/
|
||||||
export const zCreateDealGroupResponse2 = zCreateDealGroupResponse;
|
export const zCreateDealGroupResponse2 = zCreateDealGroupResponse;
|
||||||
|
|
||||||
export const zUpdateDealsInGroupData = z.object({
|
export const zRemoveDealData = z.object({
|
||||||
body: zUpdateDealsInGroupRequest,
|
body: zDealRemoveFromGroupRequest,
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zUpdateDealsInGroupResponse2 = zUpdateDealsInGroupResponse;
|
|
||||||
|
|
||||||
export const zGetDealTagsData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
projectId: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetDealTagsResponse2 = zGetDealTagsResponse;
|
|
||||||
|
|
||||||
export const zCreateDealTagData = z.object({
|
|
||||||
body: zCreateDealTagRequest,
|
|
||||||
path: z.optional(z.never()),
|
path: z.optional(z.never()),
|
||||||
query: z.optional(z.never()),
|
query: z.optional(z.never()),
|
||||||
});
|
});
|
||||||
@ -1666,36 +1519,10 @@ export const zCreateDealTagData = z.object({
|
|||||||
/**
|
/**
|
||||||
* Successful Response
|
* Successful Response
|
||||||
*/
|
*/
|
||||||
export const zCreateDealTagResponse2 = zCreateDealTagResponse;
|
export const zRemoveDealResponse = zDealRemoveFromGroupResponse;
|
||||||
|
|
||||||
export const zDeleteDealTagData = z.object({
|
export const zAddDealData = z.object({
|
||||||
body: z.optional(z.never()),
|
body: zAddDealToGroupRequest,
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zDeleteDealTagResponse2 = zDeleteDealTagResponse;
|
|
||||||
|
|
||||||
export const zUpdateDealTagData = z.object({
|
|
||||||
body: zUpdateDealTagRequest,
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zUpdateDealTagResponse2 = zUpdateDealTagResponse;
|
|
||||||
|
|
||||||
export const zSwitchDealTagData = z.object({
|
|
||||||
body: zSwitchDealTagRequest,
|
|
||||||
path: z.optional(z.never()),
|
path: z.optional(z.never()),
|
||||||
query: z.optional(z.never()),
|
query: z.optional(z.never()),
|
||||||
});
|
});
|
||||||
@ -1703,18 +1530,7 @@ export const zSwitchDealTagData = z.object({
|
|||||||
/**
|
/**
|
||||||
* Successful Response
|
* Successful Response
|
||||||
*/
|
*/
|
||||||
export const zSwitchDealTagResponse2 = zSwitchDealTagResponse;
|
export const zAddDealResponse = zAddDealToGroupResponse;
|
||||||
|
|
||||||
export const zGetDealTagColorsData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.optional(z.never()),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetDealTagColorsResponse = zGetTagColorsResponse;
|
|
||||||
|
|
||||||
export const zGetBuiltInModulesData = z.object({
|
export const zGetBuiltInModulesData = z.object({
|
||||||
body: z.optional(z.never()),
|
body: z.optional(z.never()),
|
||||||
@ -1727,6 +1543,117 @@ export const zGetBuiltInModulesData = z.object({
|
|||||||
*/
|
*/
|
||||||
export const zGetBuiltInModulesResponse = zGetAllBuiltInModulesResponse;
|
export const zGetBuiltInModulesResponse = zGetAllBuiltInModulesResponse;
|
||||||
|
|
||||||
|
export const zGetProjectsData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetProjectsResponse2 = zGetProjectsResponse;
|
||||||
|
|
||||||
|
export const zCreateProjectData = z.object({
|
||||||
|
body: zCreateProjectRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zCreateProjectResponse2 = zCreateProjectResponse;
|
||||||
|
|
||||||
|
export const zDeleteProjectData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
pk: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zDeleteProjectResponse2 = zDeleteProjectResponse;
|
||||||
|
|
||||||
|
export const zUpdateProjectData = z.object({
|
||||||
|
body: zUpdateProjectRequest,
|
||||||
|
path: z.object({
|
||||||
|
pk: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateProjectResponse2 = zUpdateProjectResponse;
|
||||||
|
|
||||||
|
export const zGetStatusesData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
boardId: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetStatusesResponse2 = zGetStatusesResponse;
|
||||||
|
|
||||||
|
export const zCreateStatusData = z.object({
|
||||||
|
body: zCreateStatusRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zCreateStatusResponse2 = zCreateStatusResponse;
|
||||||
|
|
||||||
|
export const zDeleteStatusData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
pk: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zDeleteStatusResponse2 = zDeleteStatusResponse;
|
||||||
|
|
||||||
|
export const zUpdateStatusData = z.object({
|
||||||
|
body: zUpdateStatusRequest,
|
||||||
|
path: z.object({
|
||||||
|
pk: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateStatusResponse2 = zUpdateStatusResponse;
|
||||||
|
|
||||||
|
export const zGetStatusHistoryData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
dealId: z.int(),
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetStatusHistoryResponse2 = zGetStatusHistoryResponse;
|
||||||
|
|
||||||
export const zGetClientsData = z.object({
|
export const zGetClientsData = z.object({
|
||||||
body: z.optional(z.never()),
|
body: z.optional(z.never()),
|
||||||
path: z.optional(z.never()),
|
path: z.optional(z.never()),
|
||||||
@ -2300,127 +2227,3 @@ export const zUpdateServicesKitData = z.object({
|
|||||||
* Successful Response
|
* Successful Response
|
||||||
*/
|
*/
|
||||||
export const zUpdateServicesKitResponse2 = zUpdateServicesKitResponse;
|
export const zUpdateServicesKitResponse2 = zUpdateServicesKitResponse;
|
||||||
|
|
||||||
export const zGetProjectsData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.optional(z.never()),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetProjectsResponse2 = zGetProjectsResponse;
|
|
||||||
|
|
||||||
export const zCreateProjectData = z.object({
|
|
||||||
body: zCreateProjectRequest,
|
|
||||||
path: z.optional(z.never()),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zCreateProjectResponse2 = zCreateProjectResponse;
|
|
||||||
|
|
||||||
export const zDeleteProjectData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zDeleteProjectResponse2 = zDeleteProjectResponse;
|
|
||||||
|
|
||||||
export const zGetProjectData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetProjectResponse2 = zGetProjectResponse;
|
|
||||||
|
|
||||||
export const zUpdateProjectData = z.object({
|
|
||||||
body: zUpdateProjectRequest,
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zUpdateProjectResponse2 = zUpdateProjectResponse;
|
|
||||||
|
|
||||||
export const zGetStatusesData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
boardId: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetStatusesResponse2 = zGetStatusesResponse;
|
|
||||||
|
|
||||||
export const zCreateStatusData = z.object({
|
|
||||||
body: zCreateStatusRequest,
|
|
||||||
path: z.optional(z.never()),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zCreateStatusResponse2 = zCreateStatusResponse;
|
|
||||||
|
|
||||||
export const zDeleteStatusData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zDeleteStatusResponse2 = zDeleteStatusResponse;
|
|
||||||
|
|
||||||
export const zUpdateStatusData = z.object({
|
|
||||||
body: zUpdateStatusRequest,
|
|
||||||
path: z.object({
|
|
||||||
pk: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zUpdateStatusResponse2 = zUpdateStatusResponse;
|
|
||||||
|
|
||||||
export const zGetStatusHistoryData = z.object({
|
|
||||||
body: z.optional(z.never()),
|
|
||||||
path: z.object({
|
|
||||||
dealId: z.int(),
|
|
||||||
}),
|
|
||||||
query: z.optional(z.never()),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
export const zGetStatusHistoryResponse2 = zGetStatusHistoryResponse;
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import {
|
|||||||
ProductServiceEditorModal,
|
ProductServiceEditorModal,
|
||||||
ServicesKitSelectModal,
|
ServicesKitSelectModal,
|
||||||
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals";
|
} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/modals";
|
||||||
import DealTagModal from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/modals/DealTagModal";
|
|
||||||
|
|
||||||
export const modals = {
|
export const modals = {
|
||||||
enterNameModal: EnterNameModal,
|
enterNameModal: EnterNameModal,
|
||||||
@ -41,5 +40,4 @@ export const modals = {
|
|||||||
printBarcodeModal: PrintBarcodeModal,
|
printBarcodeModal: PrintBarcodeModal,
|
||||||
statusColorPickerModal: ColorPickerModal,
|
statusColorPickerModal: ColorPickerModal,
|
||||||
marketplaceEditorModal: MarketplaceEditorModal,
|
marketplaceEditorModal: MarketplaceEditorModal,
|
||||||
dealTagModal: DealTagModal,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
@ -17,9 +16,9 @@ import {
|
|||||||
duplicateProductServices,
|
duplicateProductServices,
|
||||||
ServicesKitSchema,
|
ServicesKitSchema,
|
||||||
} from "@/lib/client";
|
} from "@/lib/client";
|
||||||
|
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
|
||||||
import ProductFieldsList from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductFieldsList";
|
import ProductFieldsList from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductFieldsList";
|
||||||
import ProductViewActions from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductViewActions";
|
import ProductViewActions from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductViewActions";
|
||||||
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
|
|
||||||
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
|
import { ServiceType } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/service";
|
||||||
import ProductServicesTable from "./components/ProductServicesTable";
|
import ProductServicesTable from "./components/ProductServicesTable";
|
||||||
import styles from "../../../FulfillmentBase.module.css";
|
import styles from "../../../FulfillmentBase.module.css";
|
||||||
@ -117,10 +116,10 @@ const ProductView: FC<Props> = ({ dealProduct }) => {
|
|||||||
)}
|
)}
|
||||||
<Title order={3}>{dealProduct.product.name}</Title>
|
<Title order={3}>{dealProduct.product.name}</Title>
|
||||||
<ProductFieldsList product={dealProduct.product} />
|
<ProductFieldsList product={dealProduct.product} />
|
||||||
<Text>
|
{/*<Text>*/}
|
||||||
Штрихкоды:
|
{/* Штрихкоды:*/}
|
||||||
{dealProduct.product.barcodes.join(", ")}
|
{/*{value.product.barcodes.join(", ")}*/}
|
||||||
</Text>
|
{/*</Text>*/}
|
||||||
<NumberInput
|
<NumberInput
|
||||||
suffix={" шт."}
|
suffix={" шт."}
|
||||||
value={dealProduct.quantity}
|
value={dealProduct.quantity}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { Flex } from "@mantine/core";
|
import { Flex } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||||
@ -33,28 +33,18 @@ const ProductViewActions: FC<Props> = ({ dealProduct }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPrintBarcodeClick = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "printBarcodeModal",
|
|
||||||
title: "Печать штрихкода",
|
|
||||||
withCloseButton: true,
|
|
||||||
innerProps: {
|
|
||||||
product: dealProduct.product,
|
|
||||||
defaultQuantity: dealProduct.quantity,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
mt={"auto"}
|
mt={"auto"}
|
||||||
ml={"auto"}
|
ml={"auto"}
|
||||||
gap={"sm"}>
|
gap={"sm"}>
|
||||||
<ActionIconWithTip
|
{/*<Tooltip*/}
|
||||||
onClick={onPrintBarcodeClick}
|
{/* onClick={onPrintBarcodeClick}*/}
|
||||||
tipLabel="Печать штрихкода">
|
{/* label="Печать штрихкода">*/}
|
||||||
<IconBarcode />
|
{/* <ActionIcon variant={"default"}>*/}
|
||||||
</ActionIconWithTip>
|
{/* <IconBarcode />*/}
|
||||||
|
{/* </ActionIcon>*/}
|
||||||
|
{/*</Tooltip>*/}
|
||||||
<ActionIconWithTip
|
<ActionIconWithTip
|
||||||
onClick={onProductEditClick}
|
onClick={onProductEditClick}
|
||||||
tipLabel="Редактировать товар">
|
tipLabel="Редактировать товар">
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
type BuiltInLinkData = {
|
|
||||||
icon: typeof IconPlus;
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BuiltInLinkData;
|
|
||||||
Reference in New Issue
Block a user