Compare commits
1 Commits
main
...
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",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
flex: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: var(--color-light-white-blue);
|
background-color: var(--color-light-white-blue);
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import React, { FC, useMemo } from "react";
|
|
||||||
import { Box } from "@mantine/core";
|
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
|
||||||
import SortableItem from "@/components/dnd/SortableItem";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
deal: DealSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DealContainer: FC<Props> = ({ deal }) => {
|
|
||||||
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<SortableItem
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
id={deal.id}
|
|
||||||
renderItem={() => dealBody}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DealContainer;
|
|
||||||
@ -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,90 +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 DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
|
||||||
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 { 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 { deals } = useDealsContext();
|
const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
|
||||||
const isMobile = useIsMobile();
|
useDealsContext();
|
||||||
|
|
||||||
const {
|
const updateStatus = (statusId: number, lexorank: string) => {
|
||||||
sortedStatuses,
|
setStatuses(
|
||||||
handleDragStart,
|
statuses.map(status =>
|
||||||
handleDragOver,
|
status.id === statusId ? { ...status, lexorank } : status
|
||||||
handleDragEnd,
|
)
|
||||||
activeStatus,
|
);
|
||||||
activeDeal,
|
|
||||||
swiperRef,
|
statusesCrud.onUpdate(statusId, { lexorank });
|
||||||
} = useDealsAndStatusesDnd();
|
};
|
||||||
|
|
||||||
|
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
|
<DndFunnel
|
||||||
containers={sortedStatuses}
|
columns={statuses}
|
||||||
items={deals}
|
updateColumn={updateStatus}
|
||||||
onDragStart={handleDragStart}
|
items={dealsWithoutGroup}
|
||||||
onDragOver={handleDragOver}
|
groups={groupsWithDeals}
|
||||||
onDragEnd={handleDragEnd}
|
updateItem={updateDeal}
|
||||||
swiperRef={swiperRef}
|
getColumnItemsGroups={statusId =>
|
||||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
sortByLexorank([
|
||||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
||||||
sortByLexorank(
|
...groupsWithDeals.filter(
|
||||||
items.filter(deal => deal.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
|
<DealCard
|
||||||
status={status}
|
|
||||||
isDragging={activeStatus?.id === status.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={(deal: DealSchema) => (
|
|
||||||
<DealContainer
|
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
deal={deal}
|
deal={deal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
activeContainer={activeStatus}
|
renderGroup={group => <Box flex={1}>{group.name}</Box>}
|
||||||
activeItem={activeDeal}
|
|
||||||
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
|
||||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
renderHeader={() => (
|
|
||||||
<StatusColumnHeader
|
|
||||||
status={status}
|
|
||||||
isDragging={activeStatus?.id === status.id}
|
|
||||||
/>
|
|
||||||
)}>
|
|
||||||
{children}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
|
||||||
disabledColumns={isMobile}
|
|
||||||
isCreatingContainerEnabled={!!selectedBoard}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,10 +8,9 @@ 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();
|
||||||
|
|
||||||
@ -28,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`,
|
||||||
}}>
|
}}>
|
||||||
@ -42,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}
|
||||||
|
|||||||
@ -3,16 +3,20 @@
|
|||||||
import React 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 { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
||||||
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";
|
||||||
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
|
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||||
|
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||||
|
|
||||||
type DealsContextState = {
|
type DealsContextState = {
|
||||||
deals: DealSchema[];
|
deals: DealSchema[];
|
||||||
setDeals: (deals: DealSchema[]) => void;
|
setDeals: (deals: DealSchema[]) => void;
|
||||||
|
dealsWithoutGroup: DealSchema[];
|
||||||
|
groupsWithDeals: GroupWithDealsSchema[];
|
||||||
refetchDeals: () => void;
|
refetchDeals: () => void;
|
||||||
dealsCrud: DealsCrud;
|
dealsCrud: DealsCrud;
|
||||||
paginationInfo?: PaginationInfoSchema;
|
paginationInfo?: PaginationInfoSchema;
|
||||||
@ -48,8 +52,13 @@ const useDealsContextState = ({
|
|||||||
statuses,
|
statuses,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { dealsWithoutGroup, groupsWithDeals } =
|
||||||
|
useDealsAndGroups(dealsListObjects);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dealsListObjects,
|
...dealsListObjects,
|
||||||
|
dealsWithoutGroup,
|
||||||
|
groupsWithDeals,
|
||||||
dealsCrud,
|
dealsCrud,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
43
src/app/deals/hooks/useDealsAndGroups.ts
Normal file
43
src/app/deals/hooks/useDealsAndGroups.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { isNull } from "lodash";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
deals: DealSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDealsAndGroups = ({ deals }: Props) => {
|
||||||
|
const dealsWithoutGroup: DealSchema[] = useMemo(
|
||||||
|
() => deals.filter(d => isNull(d.group)),
|
||||||
|
[deals]
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupsWithDeals: GroupWithDealsSchema[] = useMemo(() => {
|
||||||
|
const groupsWithDealMap = new Map<number, GroupWithDealsSchema>();
|
||||||
|
|
||||||
|
for (const deal of deals) {
|
||||||
|
if (isNull(deal.group)) continue;
|
||||||
|
|
||||||
|
const groupData = groupsWithDealMap.get(deal.group.id);
|
||||||
|
if (groupData) {
|
||||||
|
groupData.items.push(deal);
|
||||||
|
groupsWithDealMap.set(deal.group.id, groupData);
|
||||||
|
} else {
|
||||||
|
groupsWithDealMap.set(deal.group.id, { ...deal.group, items: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortByLexorank(groupsWithDealMap.values().toArray());
|
||||||
|
}, [deals]);
|
||||||
|
|
||||||
|
console.log(groupsWithDeals);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dealsWithoutGroup,
|
||||||
|
groupsWithDeals,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealsAndGroups;
|
||||||
@ -1,291 +0,0 @@
|
|||||||
import { RefObject, useMemo, useRef, useState } from "react";
|
|
||||||
import { DragOverEvent, DragStartEvent, 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 { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
|
||||||
|
|
||||||
type ReturnType = {
|
|
||||||
sortedStatuses: StatusSchema[];
|
|
||||||
handleDragStart: ({ active }: DragStartEvent) => void;
|
|
||||||
handleDragOver: ({ active, over }: DragOverEvent) => void;
|
|
||||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
|
||||||
activeStatus: StatusSchema | null;
|
|
||||||
activeDeal: DealSchema | null;
|
|
||||||
swiperRef: RefObject<SwiperRef | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
|
||||||
const swiperRef = useRef<SwiperRef>(null);
|
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
|
||||||
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
|
||||||
const { deals, setDeals, dealsCrud } = 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 = deals.find(deal => deal.id === dealId);
|
|
||||||
if (!deal) return;
|
|
||||||
return statuses.find(status => status.id === deal.status.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusById = (statusId: number) => {
|
|
||||||
return statuses.find(status => status.id === statusId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const swipeSliderDuringDrag = (activeId: number, over: Over) => {
|
|
||||||
const activeStatus = getStatusByDealId(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" && isStatusId(over.id)) {
|
|
||||||
const overStatusId = getStatusId(over.id);
|
|
||||||
overStatusLexorank = statuses.find(
|
|
||||||
s => s.id === overStatusId
|
|
||||||
)?.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") {
|
|
||||||
swipeSliderDuringDrag(activeId, over);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
|
||||||
handleColumnDragOver(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleDealDragOver(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,
|
|
||||||
activeStatusId
|
|
||||||
);
|
|
||||||
if (!overStatus) return;
|
|
||||||
|
|
||||||
debouncedSetDeals(
|
|
||||||
deals.map(deal =>
|
|
||||||
deal.id === activeDealId
|
|
||||||
? {
|
|
||||||
...deal,
|
|
||||||
status: overStatus,
|
|
||||||
lexorank: newLexorank || deal.lexorank,
|
|
||||||
}
|
|
||||||
: deal
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnDragOver = (activeId: string, over: Over) => {
|
|
||||||
const activeStatusId = getStatusId(activeId);
|
|
||||||
let overStatusId: number;
|
|
||||||
|
|
||||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
|
||||||
overStatusId = getStatusId(over.id);
|
|
||||||
} else {
|
|
||||||
const deal = deals.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,
|
|
||||||
activeStatusId: number,
|
|
||||||
isOnDragEnd: boolean = false
|
|
||||||
): { overStatus?: StatusSchema; newLexorank?: string } => {
|
|
||||||
if (typeof overId === "string") {
|
|
||||||
return {
|
|
||||||
overStatus: getStatusById(getStatusId(overId)),
|
|
||||||
newLexorank: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const overDealId = Number(overId);
|
|
||||||
const overStatus = getStatusByDealId(overDealId);
|
|
||||||
|
|
||||||
if (!overStatus || (!isOnDragEnd && activeDealId === overDealId)) {
|
|
||||||
return { overStatus: undefined, newLexorank: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusDeals = sortByLexorank(
|
|
||||||
deals.filter(deal => deal.status.id === overStatus.id)
|
|
||||||
);
|
|
||||||
const overDealIndex = statusDeals.findIndex(
|
|
||||||
deal => deal.id === overDealId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeStatusId === overStatus.id) {
|
|
||||||
const newLexorank = getNewRankForSameStatus(
|
|
||||||
statusDeals,
|
|
||||||
overDealIndex,
|
|
||||||
activeDealId
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLexorank = getNewRankForAnotherStatus(
|
|
||||||
statusDeals,
|
|
||||||
overDealIndex
|
|
||||||
);
|
|
||||||
return { overStatus, newLexorank };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
|
||||||
setActiveDeal(null);
|
|
||||||
setActiveStatus(null);
|
|
||||||
if (!over) return;
|
|
||||||
|
|
||||||
const activeId: string | number = active.id;
|
|
||||||
|
|
||||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
|
||||||
handleStatusColumnDragEnd(activeId, over);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleDealDragEnd(activeId, over);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
|
|
||||||
const activeStatusId = getStatusId(activeId);
|
|
||||||
let overStatusId: number;
|
|
||||||
|
|
||||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
|
||||||
overStatusId = getStatusId(over.id);
|
|
||||||
} else {
|
|
||||||
const deal = deals.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,
|
|
||||||
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 handleDragStart = ({ active }: DragStartEvent) => {
|
|
||||||
const activeId = active.id as string | number;
|
|
||||||
|
|
||||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
|
||||||
const statusId = getStatusId(activeId);
|
|
||||||
setActiveStatus(
|
|
||||||
statuses.find(status => status.id === statusId) ?? null
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveDeal(
|
|
||||||
deals.find(deal => deal.id === (activeId as number)) ?? null
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
swiperRef,
|
|
||||||
sortedStatuses,
|
|
||||||
handleDragStart,
|
|
||||||
handleDragOver,
|
|
||||||
handleDragEnd,
|
|
||||||
activeStatus,
|
|
||||||
activeDeal,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDealsAndStatusesDnd;
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import { LexoRank } from "lexorank";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
|
||||||
import { getNewLexorank } from "@/utils/lexorank/generation";
|
|
||||||
|
|
||||||
type NewRankGetters = {
|
|
||||||
getNewRankForSameStatus: (
|
|
||||||
statusDeals: DealSchema[],
|
|
||||||
overDealIndex: number,
|
|
||||||
activeDealId: number
|
|
||||||
) => string;
|
|
||||||
getNewRankForAnotherStatus: (
|
|
||||||
statusDeals: DealSchema[],
|
|
||||||
overDealIndex: number
|
|
||||||
) => string;
|
|
||||||
getNewStatusRank: (
|
|
||||||
activeStatusId: number,
|
|
||||||
overStatusId: number
|
|
||||||
) => string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGetNewRank = (): NewRankGetters => {
|
|
||||||
const { statuses } = useStatusesContext();
|
|
||||||
|
|
||||||
const getNewRankForSameStatus = (
|
|
||||||
statusDeals: DealSchema[],
|
|
||||||
overDealIndex: number,
|
|
||||||
activeDealId: number
|
|
||||||
): string => {
|
|
||||||
const activeDealIndex = statusDeals.findIndex(
|
|
||||||
deal => deal.id === activeDealId
|
|
||||||
);
|
|
||||||
const [leftIndex, rightIndex] =
|
|
||||||
overDealIndex < activeDealIndex
|
|
||||||
? [overDealIndex - 1, overDealIndex]
|
|
||||||
: [overDealIndex, overDealIndex + 1];
|
|
||||||
|
|
||||||
const leftLexorank =
|
|
||||||
leftIndex >= 0
|
|
||||||
? LexoRank.parse(statusDeals[leftIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
const rightLexorank =
|
|
||||||
rightIndex < statusDeals.length
|
|
||||||
? LexoRank.parse(statusDeals[rightIndex].lexorank)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNewRankForAnotherStatus = (
|
|
||||||
statusDeals: DealSchema[],
|
|
||||||
overDealIndex: number
|
|
||||||
): string => {
|
|
||||||
const leftLexorank =
|
|
||||||
overDealIndex > 0
|
|
||||||
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
|
||||||
: null;
|
|
||||||
const rightLexorank = LexoRank.parse(
|
|
||||||
statusDeals[overDealIndex].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;
|
|
||||||
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;
|
||||||
@ -1,27 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { ReactNode, RefObject, useMemo } 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/FunnelColumn";
|
||||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay";
|
|
||||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
import { BaseDraggable } 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> = {
|
type Props<TContainer, TItem> = {
|
||||||
containers: TContainer[];
|
containers: TContainer[];
|
||||||
@ -73,9 +62,7 @@ const FunnelDnd = <
|
|||||||
isCreatingContainerEnabled = true,
|
isCreatingContainerEnabled = true,
|
||||||
disabledColumns = false,
|
disabledColumns = false,
|
||||||
}: Props<TContainer, TItem>) => {
|
}: Props<TContainer, TItem>) => {
|
||||||
const sensors = useDndSensors();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const frequency = useMemo(() => (isMobile ? 1 : undefined), [isMobile]);
|
|
||||||
|
|
||||||
const renderContainers = () =>
|
const renderContainers = () =>
|
||||||
containers.map((container, index) => {
|
containers.map((container, index) => {
|
||||||
@ -107,7 +94,6 @@ const FunnelDnd = <
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderBody = () => {
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@ -135,6 +121,7 @@ const FunnelDnd = <
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swiper
|
<Swiper
|
||||||
ref={swiperRef}
|
ref={swiperRef}
|
||||||
@ -156,44 +143,4 @@ const FunnelDnd = <
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
measuring={{
|
|
||||||
droppable: {
|
|
||||||
frequency,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDragOver={onDragOver}
|
|
||||||
onDragEnd={onDragEnd}>
|
|
||||||
<SortableContext
|
|
||||||
items={containers.map(getContainerId)}
|
|
||||||
strategy={horizontalListSortingStrategy}>
|
|
||||||
{renderBody()}
|
|
||||||
<FunnelOverlay
|
|
||||||
activeContainer={activeContainer}
|
|
||||||
activeItem={activeItem}
|
|
||||||
renderContainer={container => {
|
|
||||||
const containerItems = getItemsByContainer(
|
|
||||||
container,
|
|
||||||
items
|
|
||||||
);
|
|
||||||
const containerId = getContainerId(container);
|
|
||||||
return renderContainerOverlay(
|
|
||||||
container,
|
|
||||||
<FunnelColumn
|
|
||||||
id={containerId}
|
|
||||||
items={containerItems}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
renderItem={renderItemOverlay}
|
|
||||||
/>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FunnelDnd;
|
export default FunnelDnd;
|
||||||
|
|||||||
@ -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)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
61
src/hooks/cruds/useDealGroupCrud.tsx
Normal file
61
src/hooks/cruds/useDealGroupCrud.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { UpdateDealGroupSchema } from "@/lib/client";
|
||||||
|
import {
|
||||||
|
addDealMutation,
|
||||||
|
createDealGroupMutation,
|
||||||
|
removeDealMutation,
|
||||||
|
updateDealGroupMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useDealGroupCrud = () => {
|
||||||
|
const updateMutation = useMutation(updateDealGroupMutation());
|
||||||
|
|
||||||
|
const onUpdate = (entity: UpdateDealGroupSchema) => {
|
||||||
|
updateMutation.mutate({
|
||||||
|
body: {
|
||||||
|
entity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMutation = useMutation(createDealGroupMutation());
|
||||||
|
|
||||||
|
const onCreate = (draggingDealId: number, hoveredDealId: number) => {
|
||||||
|
createMutation.mutate({
|
||||||
|
body: {
|
||||||
|
draggingDealId,
|
||||||
|
hoveredDealId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDealToGroupMutation = useMutation(addDealMutation());
|
||||||
|
|
||||||
|
const onAddDeal = (dealId: number, groupId: number) => {
|
||||||
|
addDealToGroupMutation.mutate({
|
||||||
|
body: {
|
||||||
|
dealId,
|
||||||
|
groupId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDealFromGroupMutation = useMutation(removeDealMutation());
|
||||||
|
|
||||||
|
const onRemoveDeal = (dealId: number) => {
|
||||||
|
removeDealFromGroupMutation.mutate({
|
||||||
|
body: {
|
||||||
|
dealId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onUpdate,
|
||||||
|
onCreate,
|
||||||
|
onAddDeal,
|
||||||
|
onRemoveDeal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealGroupCrud;
|
||||||
@ -9,12 +9,14 @@ import {
|
|||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import { client as _heyApiClient } from "../client.gen";
|
import { client as _heyApiClient } from "../client.gen";
|
||||||
import {
|
import {
|
||||||
|
addDeal,
|
||||||
addKitToDeal,
|
addKitToDeal,
|
||||||
addKitToDealProduct,
|
addKitToDealProduct,
|
||||||
createBarcodeTemplate,
|
createBarcodeTemplate,
|
||||||
createBoard,
|
createBoard,
|
||||||
createClient,
|
createClient,
|
||||||
createDeal,
|
createDeal,
|
||||||
|
createDealGroup,
|
||||||
createDealProduct,
|
createDealProduct,
|
||||||
createDealProductService,
|
createDealProductService,
|
||||||
createDealService,
|
createDealService,
|
||||||
@ -59,10 +61,12 @@ import {
|
|||||||
getServicesKits,
|
getServicesKits,
|
||||||
getStatuses,
|
getStatuses,
|
||||||
getStatusHistory,
|
getStatusHistory,
|
||||||
|
removeDeal,
|
||||||
updateBarcodeTemplate,
|
updateBarcodeTemplate,
|
||||||
updateBoard,
|
updateBoard,
|
||||||
updateClient,
|
updateClient,
|
||||||
updateDeal,
|
updateDeal,
|
||||||
|
updateDealGroup,
|
||||||
updateDealProduct,
|
updateDealProduct,
|
||||||
updateDealProductService,
|
updateDealProductService,
|
||||||
updateDealService,
|
updateDealService,
|
||||||
@ -76,6 +80,9 @@ import {
|
|||||||
type Options,
|
type Options,
|
||||||
} from "../sdk.gen";
|
} from "../sdk.gen";
|
||||||
import type {
|
import type {
|
||||||
|
AddDealData,
|
||||||
|
AddDealError,
|
||||||
|
AddDealResponse,
|
||||||
AddKitToDealData,
|
AddKitToDealData,
|
||||||
AddKitToDealError,
|
AddKitToDealError,
|
||||||
AddKitToDealProductData,
|
AddKitToDealProductData,
|
||||||
@ -93,6 +100,9 @@ import type {
|
|||||||
CreateClientResponse2,
|
CreateClientResponse2,
|
||||||
CreateDealData,
|
CreateDealData,
|
||||||
CreateDealError,
|
CreateDealError,
|
||||||
|
CreateDealGroupData,
|
||||||
|
CreateDealGroupError,
|
||||||
|
CreateDealGroupResponse2,
|
||||||
CreateDealProductData,
|
CreateDealProductData,
|
||||||
CreateDealProductError,
|
CreateDealProductError,
|
||||||
CreateDealProductResponse2,
|
CreateDealProductResponse2,
|
||||||
@ -194,6 +204,9 @@ import type {
|
|||||||
GetServicesKitsData,
|
GetServicesKitsData,
|
||||||
GetStatusesData,
|
GetStatusesData,
|
||||||
GetStatusHistoryData,
|
GetStatusHistoryData,
|
||||||
|
RemoveDealData,
|
||||||
|
RemoveDealError,
|
||||||
|
RemoveDealResponse,
|
||||||
UpdateBarcodeTemplateData,
|
UpdateBarcodeTemplateData,
|
||||||
UpdateBarcodeTemplateError,
|
UpdateBarcodeTemplateError,
|
||||||
UpdateBarcodeTemplateResponse2,
|
UpdateBarcodeTemplateResponse2,
|
||||||
@ -205,6 +218,9 @@ import type {
|
|||||||
UpdateClientResponse2,
|
UpdateClientResponse2,
|
||||||
UpdateDealData,
|
UpdateDealData,
|
||||||
UpdateDealError,
|
UpdateDealError,
|
||||||
|
UpdateDealGroupData,
|
||||||
|
UpdateDealGroupError,
|
||||||
|
UpdateDealGroupResponse2,
|
||||||
UpdateDealProductData,
|
UpdateDealProductData,
|
||||||
UpdateDealProductError,
|
UpdateDealProductError,
|
||||||
UpdateDealProductResponse2,
|
UpdateDealProductResponse2,
|
||||||
@ -605,6 +621,159 @@ export const updateDealMutation = (
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Group
|
||||||
|
*/
|
||||||
|
export const updateDealGroupMutation = (
|
||||||
|
options?: Partial<Options<UpdateDealGroupData>>
|
||||||
|
): UseMutationOptions<
|
||||||
|
UpdateDealGroupResponse2,
|
||||||
|
AxiosError<UpdateDealGroupError>,
|
||||||
|
Options<UpdateDealGroupData>
|
||||||
|
> => {
|
||||||
|
const mutationOptions: UseMutationOptions<
|
||||||
|
UpdateDealGroupResponse2,
|
||||||
|
AxiosError<UpdateDealGroupError>,
|
||||||
|
Options<UpdateDealGroupData>
|
||||||
|
> = {
|
||||||
|
mutationFn: async localOptions => {
|
||||||
|
const { data } = await updateDealGroup({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDealGroupQueryKey = (
|
||||||
|
options: Options<CreateDealGroupData>
|
||||||
|
) => createQueryKey("createDealGroup", options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Group
|
||||||
|
*/
|
||||||
|
export const createDealGroupOptions = (
|
||||||
|
options: Options<CreateDealGroupData>
|
||||||
|
) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await createDealGroup({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: createDealGroupQueryKey(options),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Group
|
||||||
|
*/
|
||||||
|
export const createDealGroupMutation = (
|
||||||
|
options?: Partial<Options<CreateDealGroupData>>
|
||||||
|
): UseMutationOptions<
|
||||||
|
CreateDealGroupResponse2,
|
||||||
|
AxiosError<CreateDealGroupError>,
|
||||||
|
Options<CreateDealGroupData>
|
||||||
|
> => {
|
||||||
|
const mutationOptions: UseMutationOptions<
|
||||||
|
CreateDealGroupResponse2,
|
||||||
|
AxiosError<CreateDealGroupError>,
|
||||||
|
Options<CreateDealGroupData>
|
||||||
|
> = {
|
||||||
|
mutationFn: async localOptions => {
|
||||||
|
const { data } = await createDealGroup({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Deal
|
||||||
|
*/
|
||||||
|
export const removeDealMutation = (
|
||||||
|
options?: Partial<Options<RemoveDealData>>
|
||||||
|
): UseMutationOptions<
|
||||||
|
RemoveDealResponse,
|
||||||
|
AxiosError<RemoveDealError>,
|
||||||
|
Options<RemoveDealData>
|
||||||
|
> => {
|
||||||
|
const mutationOptions: UseMutationOptions<
|
||||||
|
RemoveDealResponse,
|
||||||
|
AxiosError<RemoveDealError>,
|
||||||
|
Options<RemoveDealData>
|
||||||
|
> = {
|
||||||
|
mutationFn: async localOptions => {
|
||||||
|
const { data } = await removeDeal({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addDealQueryKey = (options: Options<AddDealData>) =>
|
||||||
|
createQueryKey("addDeal", options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Deal
|
||||||
|
*/
|
||||||
|
export const addDealOptions = (options: Options<AddDealData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await addDeal({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: addDealQueryKey(options),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Deal
|
||||||
|
*/
|
||||||
|
export const addDealMutation = (
|
||||||
|
options?: Partial<Options<AddDealData>>
|
||||||
|
): UseMutationOptions<
|
||||||
|
AddDealResponse,
|
||||||
|
AxiosError<AddDealError>,
|
||||||
|
Options<AddDealData>
|
||||||
|
> => {
|
||||||
|
const mutationOptions: UseMutationOptions<
|
||||||
|
AddDealResponse,
|
||||||
|
AxiosError<AddDealError>,
|
||||||
|
Options<AddDealData>
|
||||||
|
> = {
|
||||||
|
mutationFn: async localOptions => {
|
||||||
|
const { data } = await addDeal({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export const getBuiltInModulesQueryKey = (
|
export const getBuiltInModulesQueryKey = (
|
||||||
options?: Options<GetBuiltInModulesData>
|
options?: Options<GetBuiltInModulesData>
|
||||||
) => createQueryKey("getBuiltInModules", options);
|
) => createQueryKey("getBuiltInModules", options);
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
import type { Client, Options as ClientOptions, TDataShape } from "./client";
|
import type { Client, Options as ClientOptions, TDataShape } from "./client";
|
||||||
import { client as _heyApiClient } from "./client.gen";
|
import { client as _heyApiClient } from "./client.gen";
|
||||||
import type {
|
import type {
|
||||||
|
AddDealData,
|
||||||
|
AddDealErrors,
|
||||||
|
AddDealResponses,
|
||||||
AddKitToDealData,
|
AddKitToDealData,
|
||||||
AddKitToDealErrors,
|
AddKitToDealErrors,
|
||||||
AddKitToDealProductData,
|
AddKitToDealProductData,
|
||||||
@ -20,6 +23,9 @@ import type {
|
|||||||
CreateClientResponses,
|
CreateClientResponses,
|
||||||
CreateDealData,
|
CreateDealData,
|
||||||
CreateDealErrors,
|
CreateDealErrors,
|
||||||
|
CreateDealGroupData,
|
||||||
|
CreateDealGroupErrors,
|
||||||
|
CreateDealGroupResponses,
|
||||||
CreateDealProductData,
|
CreateDealProductData,
|
||||||
CreateDealProductErrors,
|
CreateDealProductErrors,
|
||||||
CreateDealProductResponses,
|
CreateDealProductResponses,
|
||||||
@ -144,6 +150,9 @@ import type {
|
|||||||
GetStatusHistoryData,
|
GetStatusHistoryData,
|
||||||
GetStatusHistoryErrors,
|
GetStatusHistoryErrors,
|
||||||
GetStatusHistoryResponses,
|
GetStatusHistoryResponses,
|
||||||
|
RemoveDealData,
|
||||||
|
RemoveDealErrors,
|
||||||
|
RemoveDealResponses,
|
||||||
UpdateBarcodeTemplateData,
|
UpdateBarcodeTemplateData,
|
||||||
UpdateBarcodeTemplateErrors,
|
UpdateBarcodeTemplateErrors,
|
||||||
UpdateBarcodeTemplateResponses,
|
UpdateBarcodeTemplateResponses,
|
||||||
@ -155,6 +164,9 @@ import type {
|
|||||||
UpdateClientResponses,
|
UpdateClientResponses,
|
||||||
UpdateDealData,
|
UpdateDealData,
|
||||||
UpdateDealErrors,
|
UpdateDealErrors,
|
||||||
|
UpdateDealGroupData,
|
||||||
|
UpdateDealGroupErrors,
|
||||||
|
UpdateDealGroupResponses,
|
||||||
UpdateDealProductData,
|
UpdateDealProductData,
|
||||||
UpdateDealProductErrors,
|
UpdateDealProductErrors,
|
||||||
UpdateDealProductResponses,
|
UpdateDealProductResponses,
|
||||||
@ -188,6 +200,8 @@ import type {
|
|||||||
UpdateStatusResponses,
|
UpdateStatusResponses,
|
||||||
} from "./types.gen";
|
} from "./types.gen";
|
||||||
import {
|
import {
|
||||||
|
zAddDealData,
|
||||||
|
zAddDealResponse,
|
||||||
zAddKitToDealData,
|
zAddKitToDealData,
|
||||||
zAddKitToDealProductData,
|
zAddKitToDealProductData,
|
||||||
zAddKitToDealProductResponse,
|
zAddKitToDealProductResponse,
|
||||||
@ -199,6 +213,8 @@ import {
|
|||||||
zCreateClientData,
|
zCreateClientData,
|
||||||
zCreateClientResponse2,
|
zCreateClientResponse2,
|
||||||
zCreateDealData,
|
zCreateDealData,
|
||||||
|
zCreateDealGroupData,
|
||||||
|
zCreateDealGroupResponse2,
|
||||||
zCreateDealProductData,
|
zCreateDealProductData,
|
||||||
zCreateDealProductResponse2,
|
zCreateDealProductResponse2,
|
||||||
zCreateDealProductServiceData,
|
zCreateDealProductServiceData,
|
||||||
@ -288,6 +304,8 @@ import {
|
|||||||
zGetStatusesResponse2,
|
zGetStatusesResponse2,
|
||||||
zGetStatusHistoryData,
|
zGetStatusHistoryData,
|
||||||
zGetStatusHistoryResponse2,
|
zGetStatusHistoryResponse2,
|
||||||
|
zRemoveDealData,
|
||||||
|
zRemoveDealResponse,
|
||||||
zUpdateBarcodeTemplateData,
|
zUpdateBarcodeTemplateData,
|
||||||
zUpdateBarcodeTemplateResponse2,
|
zUpdateBarcodeTemplateResponse2,
|
||||||
zUpdateBoardData,
|
zUpdateBoardData,
|
||||||
@ -295,6 +313,8 @@ import {
|
|||||||
zUpdateClientData,
|
zUpdateClientData,
|
||||||
zUpdateClientResponse2,
|
zUpdateClientResponse2,
|
||||||
zUpdateDealData,
|
zUpdateDealData,
|
||||||
|
zUpdateDealGroupData,
|
||||||
|
zUpdateDealGroupResponse2,
|
||||||
zUpdateDealProductData,
|
zUpdateDealProductData,
|
||||||
zUpdateDealProductResponse2,
|
zUpdateDealProductResponse2,
|
||||||
zUpdateDealProductServiceData,
|
zUpdateDealProductServiceData,
|
||||||
@ -535,6 +555,114 @@ export const updateDeal = <ThrowOnError extends boolean = false>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Group
|
||||||
|
*/
|
||||||
|
export const updateDealGroup = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<UpdateDealGroupData, ThrowOnError>
|
||||||
|
) => {
|
||||||
|
return (options.client ?? _heyApiClient).patch<
|
||||||
|
UpdateDealGroupResponses,
|
||||||
|
UpdateDealGroupErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
requestValidator: async data => {
|
||||||
|
return await zUpdateDealGroupData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
responseValidator: async data => {
|
||||||
|
return await zUpdateDealGroupResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: "/deal-group/",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Group
|
||||||
|
*/
|
||||||
|
export const createDealGroup = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<CreateDealGroupData, ThrowOnError>
|
||||||
|
) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<
|
||||||
|
CreateDealGroupResponses,
|
||||||
|
CreateDealGroupErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
requestValidator: async data => {
|
||||||
|
return await zCreateDealGroupData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
responseValidator: async data => {
|
||||||
|
return await zCreateDealGroupResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: "/deal-group/",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Deal
|
||||||
|
*/
|
||||||
|
export const removeDeal = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<RemoveDealData, ThrowOnError>
|
||||||
|
) => {
|
||||||
|
return (options.client ?? _heyApiClient).delete<
|
||||||
|
RemoveDealResponses,
|
||||||
|
RemoveDealErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
requestValidator: async data => {
|
||||||
|
return await zRemoveDealData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
responseValidator: async data => {
|
||||||
|
return await zRemoveDealResponse.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: "/deal-group/deal",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Deal
|
||||||
|
*/
|
||||||
|
export const addDeal = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<AddDealData, ThrowOnError>
|
||||||
|
) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<
|
||||||
|
AddDealResponses,
|
||||||
|
AddDealErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
requestValidator: async data => {
|
||||||
|
return await zAddDealData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
responseValidator: async data => {
|
||||||
|
return await zAddDealResponse.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: "/deal-group/deal",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Built In Modules
|
* Get Built In Modules
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,5 +1,29 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddDealToGroupRequest
|
||||||
|
*/
|
||||||
|
export type AddDealToGroupRequest = {
|
||||||
|
/**
|
||||||
|
* Dealid
|
||||||
|
*/
|
||||||
|
dealId: number;
|
||||||
|
/**
|
||||||
|
* Groupid
|
||||||
|
*/
|
||||||
|
groupId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddDealToGroupResponse
|
||||||
|
*/
|
||||||
|
export type AddDealToGroupResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BarcodeTemplateAttributeSchema
|
* BarcodeTemplateAttributeSchema
|
||||||
*/
|
*/
|
||||||
@ -347,6 +371,27 @@ export type CreateClientSchema = {
|
|||||||
details: ClientDetailsSchema;
|
details: ClientDetailsSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateDealGroupRequest
|
||||||
|
*/
|
||||||
|
export type CreateDealGroupRequest = {
|
||||||
|
/**
|
||||||
|
* Draggingdealid
|
||||||
|
*/
|
||||||
|
draggingDealId: number;
|
||||||
|
/**
|
||||||
|
* Hovereddealid
|
||||||
|
*/
|
||||||
|
hoveredDealId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateDealGroupResponse
|
||||||
|
*/
|
||||||
|
export type CreateDealGroupResponse = {
|
||||||
|
entity: DealGroupSchema;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreateDealProductRequest
|
* CreateDealProductRequest
|
||||||
*/
|
*/
|
||||||
@ -832,6 +877,24 @@ export type DealAddKitResponse = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealGroupSchema
|
||||||
|
*/
|
||||||
|
export type DealGroupSchema = {
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string | null;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DealProductAddKitRequest
|
* DealProductAddKitRequest
|
||||||
*/
|
*/
|
||||||
@ -887,6 +950,26 @@ export type DealProductSchema = {
|
|||||||
productServices: Array<ProductServiceSchema>;
|
productServices: Array<ProductServiceSchema>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealRemoveFromGroupRequest
|
||||||
|
*/
|
||||||
|
export type DealRemoveFromGroupRequest = {
|
||||||
|
/**
|
||||||
|
* Dealid
|
||||||
|
*/
|
||||||
|
dealId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealRemoveFromGroupResponse
|
||||||
|
*/
|
||||||
|
export type DealRemoveFromGroupResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DealSchema
|
* DealSchema
|
||||||
*/
|
*/
|
||||||
@ -909,6 +992,7 @@ export type DealSchema = {
|
|||||||
* Createdat
|
* Createdat
|
||||||
*/
|
*/
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
group: DealGroupSchema | null;
|
||||||
/**
|
/**
|
||||||
* Productsquantity
|
* Productsquantity
|
||||||
*/
|
*/
|
||||||
@ -1753,6 +1837,37 @@ export type UpdateClientSchema = {
|
|||||||
details?: ClientDetailsSchema | null;
|
details?: ClientDetailsSchema | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupRequest
|
||||||
|
*/
|
||||||
|
export type UpdateDealGroupRequest = {
|
||||||
|
entity: UpdateDealGroupSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupResponse
|
||||||
|
*/
|
||||||
|
export type UpdateDealGroupResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupSchema
|
||||||
|
*/
|
||||||
|
export type UpdateDealGroupSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string | null;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateDealProductRequest
|
* UpdateDealProductRequest
|
||||||
*/
|
*/
|
||||||
@ -2466,6 +2581,110 @@ export type UpdateDealResponses = {
|
|||||||
export type UpdateDealResponse2 =
|
export type UpdateDealResponse2 =
|
||||||
UpdateDealResponses[keyof UpdateDealResponses];
|
UpdateDealResponses[keyof UpdateDealResponses];
|
||||||
|
|
||||||
|
export type UpdateDealGroupData = {
|
||||||
|
body: UpdateDealGroupRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: "/deal-group/";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealGroupErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealGroupError =
|
||||||
|
UpdateDealGroupErrors[keyof UpdateDealGroupErrors];
|
||||||
|
|
||||||
|
export type UpdateDealGroupResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: UpdateDealGroupResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealGroupResponse2 =
|
||||||
|
UpdateDealGroupResponses[keyof UpdateDealGroupResponses];
|
||||||
|
|
||||||
|
export type CreateDealGroupData = {
|
||||||
|
body: CreateDealGroupRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: "/deal-group/";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDealGroupErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDealGroupError =
|
||||||
|
CreateDealGroupErrors[keyof CreateDealGroupErrors];
|
||||||
|
|
||||||
|
export type CreateDealGroupResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: CreateDealGroupResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDealGroupResponse2 =
|
||||||
|
CreateDealGroupResponses[keyof CreateDealGroupResponses];
|
||||||
|
|
||||||
|
export type RemoveDealData = {
|
||||||
|
body: DealRemoveFromGroupRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: "/deal-group/deal";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveDealErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveDealError = RemoveDealErrors[keyof RemoveDealErrors];
|
||||||
|
|
||||||
|
export type RemoveDealResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: DealRemoveFromGroupResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveDealResponse = RemoveDealResponses[keyof RemoveDealResponses];
|
||||||
|
|
||||||
|
export type AddDealData = {
|
||||||
|
body: AddDealToGroupRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: "/deal-group/deal";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDealErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDealError = AddDealErrors[keyof AddDealErrors];
|
||||||
|
|
||||||
|
export type AddDealResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: AddDealToGroupResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDealResponse = AddDealResponses[keyof AddDealResponses];
|
||||||
|
|
||||||
export type GetBuiltInModulesData = {
|
export type GetBuiltInModulesData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
@ -193,6 +208,30 @@ export const zCreateClientResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateDealGroupRequest
|
||||||
|
*/
|
||||||
|
export const zCreateDealGroupRequest = z.object({
|
||||||
|
draggingDealId: z.int(),
|
||||||
|
hoveredDealId: z.int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealGroupSchema
|
||||||
|
*/
|
||||||
|
export const zDealGroupSchema = z.object({
|
||||||
|
id: z.int(),
|
||||||
|
name: z.optional(z.union([z.string(), z.null()])),
|
||||||
|
lexorank: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateDealGroupResponse
|
||||||
|
*/
|
||||||
|
export const zCreateDealGroupResponse = z.object({
|
||||||
|
entity: zDealGroupSchema,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreateDealProductSchema
|
* CreateDealProductSchema
|
||||||
*/
|
*/
|
||||||
@ -336,6 +375,7 @@ export const zDealSchema = z.object({
|
|||||||
createdAt: z.iso.datetime({
|
createdAt: z.iso.datetime({
|
||||||
offset: true,
|
offset: true,
|
||||||
}),
|
}),
|
||||||
|
group: z.union([zDealGroupSchema, z.null()]),
|
||||||
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()])),
|
||||||
@ -654,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
|
||||||
*/
|
*/
|
||||||
@ -1034,6 +1088,28 @@ export const zUpdateClientResponse = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupSchema
|
||||||
|
*/
|
||||||
|
export const zUpdateDealGroupSchema = z.object({
|
||||||
|
name: z.optional(z.union([z.string(), z.null()])),
|
||||||
|
lexorank: z.optional(z.union([z.string(), z.null()])),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupRequest
|
||||||
|
*/
|
||||||
|
export const zUpdateDealGroupRequest = z.object({
|
||||||
|
entity: zUpdateDealGroupSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealGroupResponse
|
||||||
|
*/
|
||||||
|
export const zUpdateDealGroupResponse = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateDealProductSchema
|
* UpdateDealProductSchema
|
||||||
*/
|
*/
|
||||||
@ -1412,6 +1488,50 @@ export const zUpdateDealData = z.object({
|
|||||||
*/
|
*/
|
||||||
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
||||||
|
|
||||||
|
export const zUpdateDealGroupData = z.object({
|
||||||
|
body: zUpdateDealGroupRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateDealGroupResponse2 = zUpdateDealGroupResponse;
|
||||||
|
|
||||||
|
export const zCreateDealGroupData = z.object({
|
||||||
|
body: zCreateDealGroupRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zCreateDealGroupResponse2 = zCreateDealGroupResponse;
|
||||||
|
|
||||||
|
export const zRemoveDealData = z.object({
|
||||||
|
body: zDealRemoveFromGroupRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zRemoveDealResponse = zDealRemoveFromGroupResponse;
|
||||||
|
|
||||||
|
export const zAddDealData = z.object({
|
||||||
|
body: zAddDealToGroupRequest,
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zAddDealResponse = zAddDealToGroupResponse;
|
||||||
|
|
||||||
export const zGetBuiltInModulesData = z.object({
|
export const zGetBuiltInModulesData = z.object({
|
||||||
body: z.optional(z.never()),
|
body: z.optional(z.never()),
|
||||||
path: z.optional(z.never()),
|
path: z.optional(z.never()),
|
||||||
|
|||||||
7
src/types/GroupWithDealsSchema.ts
Normal file
7
src/types/GroupWithDealsSchema.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { DealGroupSchema, DealSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type GroupWithDealsSchema = DealGroupSchema & {
|
||||||
|
items: DealSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupWithDealsSchema;
|
||||||
Reference in New Issue
Block a user