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"
|
||||
},
|
||||
"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/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@ -27,6 +33,7 @@
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"axios": "1.12.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
@mixin light {
|
||||
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";
|
||||
|
||||
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 DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||
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 useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import DndFunnel from "@/components/dnd-pragmatic/DndFunnel/DndFunnel";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
const Funnel: FC = () => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { deals } = useDealsContext();
|
||||
const isMobile = useIsMobile();
|
||||
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
|
||||
const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
|
||||
useDealsContext();
|
||||
|
||||
const {
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
swiperRef,
|
||||
} = useDealsAndStatusesDnd();
|
||||
const updateStatus = (statusId: number, lexorank: string) => {
|
||||
setStatuses(
|
||||
statuses.map(status =>
|
||||
status.id === statusId ? { ...status, lexorank } : status
|
||||
)
|
||||
);
|
||||
|
||||
statusesCrud.onUpdate(statusId, { lexorank });
|
||||
};
|
||||
|
||||
const updateDeal = (dealId: number, lexorank: string, statusId: number) => {
|
||||
const status = statuses.find(s => s.id === statusId);
|
||||
if (!status) return;
|
||||
setDeals(
|
||||
deals.map(deal =>
|
||||
deal.id === dealId ? { ...deal, lexorank, status } : deal
|
||||
)
|
||||
);
|
||||
dealsCrud.onUpdate(dealId, { lexorank, statusId });
|
||||
};
|
||||
|
||||
return (
|
||||
<FunnelDnd
|
||||
containers={sortedStatuses}
|
||||
items={deals}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
swiperRef={swiperRef}
|
||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
||||
sortByLexorank(
|
||||
items.filter(deal => deal.status.id === status.id)
|
||||
)
|
||||
<DndFunnel
|
||||
columns={statuses}
|
||||
updateColumn={updateStatus}
|
||||
items={dealsWithoutGroup}
|
||||
groups={groupsWithDeals}
|
||||
updateItem={updateDeal}
|
||||
getColumnItemsGroups={statusId =>
|
||||
sortByLexorank([
|
||||
...dealsWithoutGroup.filter(d => d.status.id === statusId),
|
||||
...groupsWithDeals.filter(
|
||||
g =>
|
||||
g.items.length > 0 &&
|
||||
g.items[0].status.id === statusId
|
||||
),
|
||||
])
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
funnelColumnComponent: ReactNode,
|
||||
renderDraggable,
|
||||
index
|
||||
) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
renderHeader={renderDraggable}
|
||||
createFormEnabled={index === 0}>
|
||||
{funnelColumnComponent}
|
||||
</StatusColumnWrapper>
|
||||
renderColumnHeader={status => (
|
||||
<StatusColumnHeader status={status} />
|
||||
)}
|
||||
renderContainerHeader={status => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
/>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealContainer
|
||||
renderItem={deal => (
|
||||
<DealCard
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
)}
|
||||
activeContainer={activeStatus}
|
||||
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}
|
||||
renderGroup={group => <Box flex={1}>{group.name}</Box>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Box } from "@mantine/core";
|
||||
import { Flex } from "@mantine/core";
|
||||
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
||||
import {
|
||||
BoardView,
|
||||
@ -46,7 +46,11 @@ const PageBody = () => {
|
||||
<PageBlock
|
||||
fullScreenMobile
|
||||
style={{ flex: 1 }}>
|
||||
<Box h={"100%"}>{getViewContent()}</Box>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
h={"100%"}>
|
||||
{getViewContent()}
|
||||
</Flex>
|
||||
</PageBlock>
|
||||
</DealsContextProvider>
|
||||
);
|
||||
|
||||
@ -8,10 +8,9 @@ import { StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
const StatusColumnHeader: FC<Props> = ({ status }) => {
|
||||
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
|
||||
@ -28,6 +27,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
p={"sm"}
|
||||
wrap={"nowrap"}
|
||||
mb={"xs"}
|
||||
w={"100%"}
|
||||
style={{
|
||||
borderBottom: `solid ${status.color} 3px`,
|
||||
}}>
|
||||
@ -42,14 +42,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
}}
|
||||
getChildren={startEditing => (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}>
|
||||
{status.name}
|
||||
</Text>
|
||||
<Text>{status.name}</Text>
|
||||
<StatusMenu
|
||||
board={selectedBoard}
|
||||
status={status}
|
||||
|
||||
@ -3,16 +3,20 @@
|
||||
import React from "react";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
|
||||
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
||||
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
|
||||
import useDealsList from "@/hooks/lists/useDealsList";
|
||||
import { SortingForm } from "@/hooks/utils/useSorting";
|
||||
import { DealSchema, PaginationInfoSchema } from "@/lib/client";
|
||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
|
||||
type DealsContextState = {
|
||||
deals: DealSchema[];
|
||||
setDeals: (deals: DealSchema[]) => void;
|
||||
dealsWithoutGroup: DealSchema[];
|
||||
groupsWithDeals: GroupWithDealsSchema[];
|
||||
refetchDeals: () => void;
|
||||
dealsCrud: DealsCrud;
|
||||
paginationInfo?: PaginationInfoSchema;
|
||||
@ -48,8 +52,13 @@ const useDealsContextState = ({
|
||||
statuses,
|
||||
});
|
||||
|
||||
const { dealsWithoutGroup, groupsWithDeals } =
|
||||
useDealsAndGroups(dealsListObjects);
|
||||
|
||||
return {
|
||||
...dealsListObjects,
|
||||
dealsWithoutGroup,
|
||||
groupsWithDeals,
|
||||
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";
|
||||
|
||||
import React, { ReactNode, RefObject, useMemo } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from "@dnd-kit/sortable";
|
||||
import React, { ReactNode, RefObject } from "react";
|
||||
import { FreeMode, Pagination, Scrollbar } from "swiper/modules";
|
||||
import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
|
||||
import { Box } from "@mantine/core";
|
||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay";
|
||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import SortableItem from "../SortableItem";
|
||||
import classes from "./FunnelDnd.module.css";
|
||||
import { DragEndEvent, DragOverEvent, DragStartEvent } from "@dnd-kit/core";
|
||||
|
||||
type Props<TContainer, TItem> = {
|
||||
containers: TContainer[];
|
||||
@ -73,9 +62,7 @@ const FunnelDnd = <
|
||||
isCreatingContainerEnabled = true,
|
||||
disabledColumns = false,
|
||||
}: Props<TContainer, TItem>) => {
|
||||
const sensors = useDndSensors();
|
||||
const isMobile = useIsMobile();
|
||||
const frequency = useMemo(() => (isMobile ? 1 : undefined), [isMobile]);
|
||||
|
||||
const renderContainers = () =>
|
||||
containers.map((container, index) => {
|
||||
@ -107,7 +94,6 @@ const FunnelDnd = <
|
||||
);
|
||||
});
|
||||
|
||||
const renderBody = () => {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Box>
|
||||
@ -135,6 +121,7 @@ const FunnelDnd = <
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Swiper
|
||||
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;
|
||||
|
||||
@ -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 { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import DragHandle from "./DragHandle";
|
||||
import DragHandle from "@/components/dnd/DragHandle";
|
||||
|
||||
type Props = {
|
||||
id: number | string;
|
||||
@ -23,12 +22,6 @@ const SortableItem = ({
|
||||
animateLayoutChanges: () => false,
|
||||
});
|
||||
|
||||
const style: CSSProperties = {
|
||||
opacity: isDragging ? 0.4 : undefined,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const renderDragHandle = () => (
|
||||
<DragHandle
|
||||
id={id}
|
||||
@ -39,9 +32,7 @@ const SortableItem = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
<div ref={setNodeRef}>
|
||||
{renderDraggable ? (
|
||||
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 { client as _heyApiClient } from "../client.gen";
|
||||
import {
|
||||
addDeal,
|
||||
addKitToDeal,
|
||||
addKitToDealProduct,
|
||||
createBarcodeTemplate,
|
||||
createBoard,
|
||||
createClient,
|
||||
createDeal,
|
||||
createDealGroup,
|
||||
createDealProduct,
|
||||
createDealProductService,
|
||||
createDealService,
|
||||
@ -59,10 +61,12 @@ import {
|
||||
getServicesKits,
|
||||
getStatuses,
|
||||
getStatusHistory,
|
||||
removeDeal,
|
||||
updateBarcodeTemplate,
|
||||
updateBoard,
|
||||
updateClient,
|
||||
updateDeal,
|
||||
updateDealGroup,
|
||||
updateDealProduct,
|
||||
updateDealProductService,
|
||||
updateDealService,
|
||||
@ -76,6 +80,9 @@ import {
|
||||
type Options,
|
||||
} from "../sdk.gen";
|
||||
import type {
|
||||
AddDealData,
|
||||
AddDealError,
|
||||
AddDealResponse,
|
||||
AddKitToDealData,
|
||||
AddKitToDealError,
|
||||
AddKitToDealProductData,
|
||||
@ -93,6 +100,9 @@ import type {
|
||||
CreateClientResponse2,
|
||||
CreateDealData,
|
||||
CreateDealError,
|
||||
CreateDealGroupData,
|
||||
CreateDealGroupError,
|
||||
CreateDealGroupResponse2,
|
||||
CreateDealProductData,
|
||||
CreateDealProductError,
|
||||
CreateDealProductResponse2,
|
||||
@ -194,6 +204,9 @@ import type {
|
||||
GetServicesKitsData,
|
||||
GetStatusesData,
|
||||
GetStatusHistoryData,
|
||||
RemoveDealData,
|
||||
RemoveDealError,
|
||||
RemoveDealResponse,
|
||||
UpdateBarcodeTemplateData,
|
||||
UpdateBarcodeTemplateError,
|
||||
UpdateBarcodeTemplateResponse2,
|
||||
@ -205,6 +218,9 @@ import type {
|
||||
UpdateClientResponse2,
|
||||
UpdateDealData,
|
||||
UpdateDealError,
|
||||
UpdateDealGroupData,
|
||||
UpdateDealGroupError,
|
||||
UpdateDealGroupResponse2,
|
||||
UpdateDealProductData,
|
||||
UpdateDealProductError,
|
||||
UpdateDealProductResponse2,
|
||||
@ -605,6 +621,159 @@ export const updateDealMutation = (
|
||||
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 = (
|
||||
options?: Options<GetBuiltInModulesData>
|
||||
) => createQueryKey("getBuiltInModules", options);
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
import type { Client, Options as ClientOptions, TDataShape } from "./client";
|
||||
import { client as _heyApiClient } from "./client.gen";
|
||||
import type {
|
||||
AddDealData,
|
||||
AddDealErrors,
|
||||
AddDealResponses,
|
||||
AddKitToDealData,
|
||||
AddKitToDealErrors,
|
||||
AddKitToDealProductData,
|
||||
@ -20,6 +23,9 @@ import type {
|
||||
CreateClientResponses,
|
||||
CreateDealData,
|
||||
CreateDealErrors,
|
||||
CreateDealGroupData,
|
||||
CreateDealGroupErrors,
|
||||
CreateDealGroupResponses,
|
||||
CreateDealProductData,
|
||||
CreateDealProductErrors,
|
||||
CreateDealProductResponses,
|
||||
@ -144,6 +150,9 @@ import type {
|
||||
GetStatusHistoryData,
|
||||
GetStatusHistoryErrors,
|
||||
GetStatusHistoryResponses,
|
||||
RemoveDealData,
|
||||
RemoveDealErrors,
|
||||
RemoveDealResponses,
|
||||
UpdateBarcodeTemplateData,
|
||||
UpdateBarcodeTemplateErrors,
|
||||
UpdateBarcodeTemplateResponses,
|
||||
@ -155,6 +164,9 @@ import type {
|
||||
UpdateClientResponses,
|
||||
UpdateDealData,
|
||||
UpdateDealErrors,
|
||||
UpdateDealGroupData,
|
||||
UpdateDealGroupErrors,
|
||||
UpdateDealGroupResponses,
|
||||
UpdateDealProductData,
|
||||
UpdateDealProductErrors,
|
||||
UpdateDealProductResponses,
|
||||
@ -188,6 +200,8 @@ import type {
|
||||
UpdateStatusResponses,
|
||||
} from "./types.gen";
|
||||
import {
|
||||
zAddDealData,
|
||||
zAddDealResponse,
|
||||
zAddKitToDealData,
|
||||
zAddKitToDealProductData,
|
||||
zAddKitToDealProductResponse,
|
||||
@ -199,6 +213,8 @@ import {
|
||||
zCreateClientData,
|
||||
zCreateClientResponse2,
|
||||
zCreateDealData,
|
||||
zCreateDealGroupData,
|
||||
zCreateDealGroupResponse2,
|
||||
zCreateDealProductData,
|
||||
zCreateDealProductResponse2,
|
||||
zCreateDealProductServiceData,
|
||||
@ -288,6 +304,8 @@ import {
|
||||
zGetStatusesResponse2,
|
||||
zGetStatusHistoryData,
|
||||
zGetStatusHistoryResponse2,
|
||||
zRemoveDealData,
|
||||
zRemoveDealResponse,
|
||||
zUpdateBarcodeTemplateData,
|
||||
zUpdateBarcodeTemplateResponse2,
|
||||
zUpdateBoardData,
|
||||
@ -295,6 +313,8 @@ import {
|
||||
zUpdateClientData,
|
||||
zUpdateClientResponse2,
|
||||
zUpdateDealData,
|
||||
zUpdateDealGroupData,
|
||||
zUpdateDealGroupResponse2,
|
||||
zUpdateDealProductData,
|
||||
zUpdateDealProductResponse2,
|
||||
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
|
||||
*/
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
// 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
|
||||
*/
|
||||
@ -347,6 +371,27 @@ export type CreateClientSchema = {
|
||||
details: ClientDetailsSchema;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateDealGroupRequest
|
||||
*/
|
||||
export type CreateDealGroupRequest = {
|
||||
/**
|
||||
* Draggingdealid
|
||||
*/
|
||||
draggingDealId: number;
|
||||
/**
|
||||
* Hovereddealid
|
||||
*/
|
||||
hoveredDealId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateDealGroupResponse
|
||||
*/
|
||||
export type CreateDealGroupResponse = {
|
||||
entity: DealGroupSchema;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateDealProductRequest
|
||||
*/
|
||||
@ -832,6 +877,24 @@ export type DealAddKitResponse = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DealGroupSchema
|
||||
*/
|
||||
export type DealGroupSchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Lexorank
|
||||
*/
|
||||
lexorank: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DealProductAddKitRequest
|
||||
*/
|
||||
@ -887,6 +950,26 @@ export type DealProductSchema = {
|
||||
productServices: Array<ProductServiceSchema>;
|
||||
};
|
||||
|
||||
/**
|
||||
* DealRemoveFromGroupRequest
|
||||
*/
|
||||
export type DealRemoveFromGroupRequest = {
|
||||
/**
|
||||
* Dealid
|
||||
*/
|
||||
dealId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* DealRemoveFromGroupResponse
|
||||
*/
|
||||
export type DealRemoveFromGroupResponse = {
|
||||
/**
|
||||
* Message
|
||||
*/
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DealSchema
|
||||
*/
|
||||
@ -909,6 +992,7 @@ export type DealSchema = {
|
||||
* Createdat
|
||||
*/
|
||||
createdAt: string;
|
||||
group: DealGroupSchema | null;
|
||||
/**
|
||||
* Productsquantity
|
||||
*/
|
||||
@ -1753,6 +1837,37 @@ export type UpdateClientSchema = {
|
||||
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
|
||||
*/
|
||||
@ -2466,6 +2581,110 @@ export type UpdateDealResponses = {
|
||||
export type UpdateDealResponse2 =
|
||||
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 = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@ -2,6 +2,21 @@
|
||||
|
||||
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
|
||||
*/
|
||||
@ -193,6 +208,30 @@ export const zCreateClientResponse = z.object({
|
||||
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
|
||||
*/
|
||||
@ -336,6 +375,7 @@ export const zDealSchema = z.object({
|
||||
createdAt: z.iso.datetime({
|
||||
offset: true,
|
||||
}),
|
||||
group: z.union([zDealGroupSchema, z.null()]),
|
||||
productsQuantity: z.optional(z.int()).default(0),
|
||||
totalPrice: z.optional(z.number()).default(0),
|
||||
client: z.optional(z.union([zClientSchema, z.null()])),
|
||||
@ -654,6 +694,20 @@ export const zDealProductAddKitResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DealRemoveFromGroupRequest
|
||||
*/
|
||||
export const zDealRemoveFromGroupRequest = z.object({
|
||||
dealId: z.int(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DealRemoveFromGroupResponse
|
||||
*/
|
||||
export const zDealRemoveFromGroupResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DeleteBarcodeTemplateResponse
|
||||
*/
|
||||
@ -1034,6 +1088,28 @@ export const zUpdateClientResponse = z.object({
|
||||
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
|
||||
*/
|
||||
@ -1412,6 +1488,50 @@ export const zUpdateDealData = z.object({
|
||||
*/
|
||||
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({
|
||||
body: 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