refactor: moved dnd part from Funnel into FunnelDnd
This commit is contained in:
@ -6,6 +6,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DealCard = ({ deal }: Props) => {
|
const DealCard = ({ deal }: Props) => {
|
||||||
|
console.log("deal");
|
||||||
return <Card>{deal.name}</Card>;
|
return <Card>{deal.name}</Card>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { FC, useMemo } from "react";
|
import React, { FC, useMemo } from "react";
|
||||||
import { Box } from "@mantine/core";
|
import { Box } from "@mantine/core";
|
||||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import SortableItem from "@/components/dnd/SortableItem";
|
||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
deal: DealSchema;
|
deal: DealSchema;
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
|
||||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
|
||||||
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
activeDeal: DealSchema | null;
|
|
||||||
activeStatus: StatusSchema | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
|
|
||||||
const { deals } = useStatusesContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
|
||||||
<div style={{ cursor: "grabbing" }}>
|
|
||||||
{activeDeal ? (
|
|
||||||
<DealCard deal={activeDeal} />
|
|
||||||
) : activeStatus ? (
|
|
||||||
<StatusColumn
|
|
||||||
id={`${activeStatus.id}-status`}
|
|
||||||
status={activeStatus}
|
|
||||||
deals={deals.filter(
|
|
||||||
deal => deal.statusId === activeStatus.id
|
|
||||||
)}
|
|
||||||
isDragging
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</DragOverlay>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DndOverlay;
|
|
||||||
68
src/app/deals/components/Funnel/Funnel.tsx
Normal file
68
src/app/deals/components/Funnel/Funnel.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
||||||
|
import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||||
|
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
const Funnel: FC = () => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
sortedStatuses,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragEnd,
|
||||||
|
activeStatus,
|
||||||
|
activeDeal,
|
||||||
|
} = useDealsAndStatusesDnd();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FunnelDnd
|
||||||
|
containers={sortedStatuses}
|
||||||
|
items={deals}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||||
|
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
||||||
|
sortByLexorank(
|
||||||
|
items.filter(deal => deal.statusId === status.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderContainer={(
|
||||||
|
status: StatusSchema,
|
||||||
|
funnelColumnComponent: ReactNode
|
||||||
|
) => (
|
||||||
|
<StatusColumnWrapper
|
||||||
|
status={status}
|
||||||
|
isDragging={activeStatus?.id === status.id}>
|
||||||
|
{funnelColumnComponent}
|
||||||
|
</StatusColumnWrapper>
|
||||||
|
)}
|
||||||
|
renderItem={(deal: DealSchema) => (
|
||||||
|
<DealContainer
|
||||||
|
key={deal.id}
|
||||||
|
deal={deal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
activeContainer={activeStatus}
|
||||||
|
activeItem={activeDeal}
|
||||||
|
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
||||||
|
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||||
|
<StatusColumnWrapper
|
||||||
|
status={status}
|
||||||
|
isDragging>
|
||||||
|
{children}
|
||||||
|
</StatusColumnWrapper>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Funnel;
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
import { useDroppable } from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import { Box, Stack, Text } from "@mantine/core";
|
|
||||||
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
status: StatusSchema;
|
|
||||||
deals: DealSchema[];
|
|
||||||
isDragging?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
|
|
||||||
const { setNodeRef } = useDroppable({ id });
|
|
||||||
const sortedDeals = useMemo(
|
|
||||||
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
|
||||||
[deals]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnBody = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<SortableContext
|
|
||||||
id={id}
|
|
||||||
items={sortedDeals}
|
|
||||||
strategy={verticalListSortingStrategy}>
|
|
||||||
<Stack
|
|
||||||
gap={"xs"}
|
|
||||||
ref={setNodeRef}>
|
|
||||||
{sortedDeals.map(deal => (
|
|
||||||
<DealContainer
|
|
||||||
key={deal.id}
|
|
||||||
deal={deal}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
);
|
|
||||||
}, [sortedDeals]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#eee",
|
|
||||||
padding: 2,
|
|
||||||
width: "15vw",
|
|
||||||
minWidth: 150,
|
|
||||||
}}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
cursor: "grab",
|
|
||||||
userSelect: "none",
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
}}>
|
|
||||||
{status.name}
|
|
||||||
</Text>
|
|
||||||
{columnBody}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatusColumn;
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Box, Text } from "@mantine/core";
|
||||||
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: StatusSchema;
|
||||||
|
isDragging?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumnWrapper = ({ status, children, isDragging = false }: Props) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#eee",
|
||||||
|
padding: 2,
|
||||||
|
width: "15vw",
|
||||||
|
minWidth: 150,
|
||||||
|
}}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
|
{status.name}
|
||||||
|
</Text>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumnWrapper;
|
||||||
@ -1,76 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import {
|
|
||||||
updateDealMutation,
|
|
||||||
updateStatusMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
|
|
||||||
const StatusColumns = () => {
|
|
||||||
const { refetchStatuses, refetchDeals } = useStatusesContext();
|
|
||||||
|
|
||||||
const updateStatus = useMutation({
|
|
||||||
...updateStatusMutation(),
|
|
||||||
onError: error => {
|
|
||||||
console.error(error);
|
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
|
||||||
refetchStatuses();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateDeals = useMutation({
|
|
||||||
...updateDealMutation(),
|
|
||||||
onError: error => {
|
|
||||||
console.error(error);
|
|
||||||
notifications.error({
|
|
||||||
message: error.response?.data?.detail as string | undefined,
|
|
||||||
});
|
|
||||||
refetchDeals();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDealDragEnd = (
|
|
||||||
dealId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => {
|
|
||||||
updateDeals.mutate({
|
|
||||||
path: {
|
|
||||||
dealId,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
deal: {
|
|
||||||
statusId,
|
|
||||||
lexorank,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
|
||||||
updateStatus.mutate({
|
|
||||||
path: {
|
|
||||||
statusId,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
status: {
|
|
||||||
lexorank,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusColumnsDnd
|
|
||||||
onDealDragEnd={onDealDragEnd}
|
|
||||||
onStatusDragEnd={onStatusDragEnd}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatusColumns;
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { closestCorners, DndContext } from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
horizontalListSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
|
||||||
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
|
|
||||||
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|
||||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
|
||||||
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
|
|
||||||
import useDndSensors from "../../hooks/useSensors";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDealDragEnd: (
|
|
||||||
dealId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => void;
|
|
||||||
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusColumnsDnd: FC<Props> = props => {
|
|
||||||
const { deals } = useStatusesContext();
|
|
||||||
|
|
||||||
const {
|
|
||||||
sortedStatuses,
|
|
||||||
handleDragStart,
|
|
||||||
handleDragOver,
|
|
||||||
handleDragEnd,
|
|
||||||
activeStatus,
|
|
||||||
activeDeal,
|
|
||||||
} = useDealsAndStatusesDnd(props);
|
|
||||||
|
|
||||||
const sensors = useDndSensors();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbarSize={"0.5rem"}>
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCorners}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragEnd={handleDragEnd}>
|
|
||||||
<SortableContext
|
|
||||||
items={sortedStatuses.map(status => `${status.id}-status`)}
|
|
||||||
strategy={horizontalListSortingStrategy}>
|
|
||||||
<Group
|
|
||||||
gap={"xs"}
|
|
||||||
wrap={"nowrap"}
|
|
||||||
align={"start"}>
|
|
||||||
{sortedStatuses.map(status => (
|
|
||||||
<SortableItem
|
|
||||||
key={status.id}
|
|
||||||
id={`${status.id}-status`}>
|
|
||||||
<StatusColumn
|
|
||||||
id={`${status.id}-status`}
|
|
||||||
status={status}
|
|
||||||
deals={deals}
|
|
||||||
isDragging={activeStatus?.id === status.id}
|
|
||||||
/>
|
|
||||||
</SortableItem>
|
|
||||||
))}
|
|
||||||
<DndOverlay
|
|
||||||
activeStatus={activeStatus}
|
|
||||||
activeDeal={activeDeal}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatusColumnsDnd;
|
|
||||||
@ -1,17 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, FC, useContext, useEffect } from "react";
|
import React, { createContext, FC, useContext, useEffect } from "react";
|
||||||
|
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
|
||||||
import useDealsList from "@/hooks/useDealsList";
|
import useDealsList from "@/hooks/useDealsList";
|
||||||
import useStatusesList from "@/hooks/useStatusesList";
|
import useStatusesList from "@/hooks/useStatusesList";
|
||||||
|
import {
|
||||||
|
DealSchema,
|
||||||
|
HttpValidationError,
|
||||||
|
Options,
|
||||||
|
StatusSchema,
|
||||||
|
UpdateDealData,
|
||||||
|
UpdateDealResponse,
|
||||||
|
UpdateStatusData,
|
||||||
|
UpdateStatusResponse,
|
||||||
|
} from "@/lib/client";
|
||||||
|
import {
|
||||||
|
updateDealMutation,
|
||||||
|
updateStatusMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
type StatusesContextState = {
|
type StatusesContextState = {
|
||||||
statuses: StatusSchema[];
|
statuses: StatusSchema[];
|
||||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||||
|
updateStatus: UseMutationResult<
|
||||||
|
UpdateStatusResponse,
|
||||||
|
AxiosError<HttpValidationError>,
|
||||||
|
Options<UpdateStatusData>
|
||||||
|
>;
|
||||||
|
refetchStatuses: () => void;
|
||||||
deals: DealSchema[];
|
deals: DealSchema[];
|
||||||
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
||||||
refetchStatuses: () => void;
|
updateDeal: UseMutationResult<
|
||||||
|
UpdateDealResponse,
|
||||||
|
AxiosError<HttpValidationError>,
|
||||||
|
Options<UpdateDealData>
|
||||||
|
>;
|
||||||
refetchDeals: () => void;
|
refetchDeals: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,12 +65,36 @@ const useStatusesContextState = () => {
|
|||||||
refetchStatuses();
|
refetchStatuses();
|
||||||
}, [selectedBoard]);
|
}, [selectedBoard]);
|
||||||
|
|
||||||
|
const updateStatus = useMutation({
|
||||||
|
...updateStatusMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchStatuses();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDeal = useMutation({
|
||||||
|
...updateDealMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchDeals();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
setStatuses,
|
||||||
|
updateStatus,
|
||||||
|
refetchStatuses,
|
||||||
deals,
|
deals,
|
||||||
setDeals,
|
setDeals,
|
||||||
refetchStatuses,
|
updateDeal,
|
||||||
refetchDeals,
|
refetchDeals,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,19 +7,11 @@ import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
|||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type Props = {
|
const useDealsAndStatusesDnd = () => {
|
||||||
onDealDragEnd: (
|
|
||||||
dealId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => void;
|
|
||||||
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDealsAndStatusesDnd = (props: Props) => {
|
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
const { statuses, deals, setDeals, setStatuses, updateDeal, updateStatus } =
|
||||||
|
useStatusesContext();
|
||||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -173,7 +165,20 @@ const useDealsAndStatusesDnd = (props: Props) => {
|
|||||||
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
||||||
if (!newRank) return;
|
if (!newRank) return;
|
||||||
|
|
||||||
props.onStatusDragEnd?.(activeStatusId, newRank);
|
onStatusDragEnd?.(activeStatusId, newRank);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||||
|
updateStatus.mutate({
|
||||||
|
path: {
|
||||||
|
statusId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
status: {
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
||||||
@ -189,7 +194,25 @@ const useDealsAndStatusesDnd = (props: Props) => {
|
|||||||
);
|
);
|
||||||
if (!overStatusId) return;
|
if (!overStatusId) return;
|
||||||
|
|
||||||
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDealDragEnd = (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => {
|
||||||
|
updateDeal.mutate({
|
||||||
|
path: {
|
||||||
|
dealId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
deal: {
|
||||||
|
statusId,
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Divider } from "@mantine/core";
|
import { Divider } from "@mantine/core";
|
||||||
import Boards from "@/app/deals/components/Boards/Boards";
|
import Boards from "@/app/deals/components/Boards/Boards";
|
||||||
|
import Funnel from "@/app/deals/components/Funnel/Funnel";
|
||||||
import Header from "@/app/deals/components/Header/Header";
|
import Header from "@/app/deals/components/Header/Header";
|
||||||
import StatusColumns from "@/app/deals/components/StatusColumns/StatusColumns";
|
|
||||||
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
||||||
@ -18,7 +18,7 @@ export default function DealsPage() {
|
|||||||
<Boards />
|
<Boards />
|
||||||
<Divider my={"xl"} />
|
<Divider my={"xl"} />
|
||||||
<StatusesContextProvider>
|
<StatusesContextProvider>
|
||||||
<StatusColumns />
|
<Funnel />
|
||||||
</StatusesContextProvider>
|
</StatusesContextProvider>
|
||||||
</BoardsContextProvider>
|
</BoardsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
|
|||||||
42
src/components/dnd/FunnelDnd/FunnelColumn.tsx
Normal file
42
src/components/dnd/FunnelDnd/FunnelColumn.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { useDroppable } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
|
|
||||||
|
type Props<TItem> = {
|
||||||
|
id: string;
|
||||||
|
items: TItem[];
|
||||||
|
renderItem: (item: TItem) => ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FunnelColumn = <TItem extends BaseDraggable>({
|
||||||
|
id,
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
children,
|
||||||
|
}: Props<TItem>) => {
|
||||||
|
const { setNodeRef } = useDroppable({ id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<SortableContext
|
||||||
|
id={id}
|
||||||
|
items={items}
|
||||||
|
strategy={verticalListSortingStrategy}>
|
||||||
|
<Stack
|
||||||
|
gap="xs"
|
||||||
|
ref={setNodeRef}>
|
||||||
|
{items.map(renderItem)}
|
||||||
|
</Stack>
|
||||||
|
</SortableContext>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunnelColumn;
|
||||||
126
src/components/dnd/FunnelDnd/FunnelDnd.tsx
Normal file
126
src/components/dnd/FunnelDnd/FunnelDnd.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
closestCorners,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverEvent,
|
||||||
|
DragStartEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Group, ScrollArea } from "@mantine/core";
|
||||||
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||||
|
import SortableItem from "@/components/dnd/SortableItem";
|
||||||
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
|
import FunnelColumn from "./FunnelColumn";
|
||||||
|
import FunnelOverlay from "./FunnelOverlay";
|
||||||
|
|
||||||
|
type Props<TContainer, TItem> = {
|
||||||
|
containers: TContainer[];
|
||||||
|
items: TItem[];
|
||||||
|
onDragStart: (event: DragStartEvent) => void;
|
||||||
|
onDragOver: (event: DragOverEvent) => void;
|
||||||
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
|
renderContainer: (container: TContainer, children: ReactNode) => ReactNode;
|
||||||
|
renderContainerOverlay: (
|
||||||
|
container: TContainer,
|
||||||
|
children: ReactNode
|
||||||
|
) => ReactNode;
|
||||||
|
renderItem: (item: TItem) => ReactNode;
|
||||||
|
renderItemOverlay: (item: TItem) => ReactNode;
|
||||||
|
getContainerId: (container: TContainer) => string;
|
||||||
|
getItemsByContainer: (container: TContainer, items: TItem[]) => TItem[];
|
||||||
|
activeContainer: TContainer | null;
|
||||||
|
activeItem: TItem | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FunnelDnd = <
|
||||||
|
TContainer extends BaseDraggable,
|
||||||
|
TItem extends BaseDraggable,
|
||||||
|
>({
|
||||||
|
containers,
|
||||||
|
items,
|
||||||
|
onDragStart,
|
||||||
|
onDragOver,
|
||||||
|
onDragEnd,
|
||||||
|
renderContainer,
|
||||||
|
renderContainerOverlay,
|
||||||
|
renderItem,
|
||||||
|
renderItemOverlay,
|
||||||
|
getContainerId,
|
||||||
|
getItemsByContainer,
|
||||||
|
activeContainer,
|
||||||
|
activeItem,
|
||||||
|
}: Props<TContainer, TItem>) => {
|
||||||
|
const sensors = useDndSensors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars="x"
|
||||||
|
scrollbarSize="0.5rem">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCorners}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragEnd={onDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
items={containers.map(getContainerId)}
|
||||||
|
strategy={horizontalListSortingStrategy}>
|
||||||
|
<Group
|
||||||
|
gap="xs"
|
||||||
|
wrap="nowrap"
|
||||||
|
align="start">
|
||||||
|
{containers.map(container => {
|
||||||
|
const containerItems = getItemsByContainer(
|
||||||
|
container,
|
||||||
|
items
|
||||||
|
);
|
||||||
|
const containerId = getContainerId(container);
|
||||||
|
return (
|
||||||
|
<SortableItem
|
||||||
|
key={containerId}
|
||||||
|
id={containerId}>
|
||||||
|
{renderContainer(
|
||||||
|
container,
|
||||||
|
<FunnelColumn
|
||||||
|
id={containerId}
|
||||||
|
items={containerItems}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunnelDnd;
|
||||||
30
src/components/dnd/FunnelDnd/FunnelOverlay.tsx
Normal file
30
src/components/dnd/FunnelDnd/FunnelOverlay.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||||
|
|
||||||
|
type Props<TContainer, TItem> = {
|
||||||
|
activeContainer: TContainer | null;
|
||||||
|
activeItem: TItem | null;
|
||||||
|
renderContainer: (container: TContainer) => ReactNode;
|
||||||
|
renderItem: (item: TItem) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FunnelOverlay = <TContainer, TItem>({
|
||||||
|
activeContainer,
|
||||||
|
activeItem,
|
||||||
|
renderContainer,
|
||||||
|
renderItem,
|
||||||
|
}: Props<TContainer, TItem>) => {
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeItem
|
||||||
|
? renderItem(activeItem)
|
||||||
|
: activeContainer
|
||||||
|
? renderContainer(activeContainer)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunnelOverlay;
|
||||||
3
src/components/dnd/FunnelDnd/index.ts
Normal file
3
src/components/dnd/FunnelDnd/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import FunnelDnd from "./FunnelDnd";
|
||||||
|
|
||||||
|
export default FunnelDnd;
|
||||||
@ -12,8 +12,8 @@ import { SortableContext } from "@dnd-kit/sortable";
|
|||||||
import { LexoRank } from "lexorank";
|
import { LexoRank } from "lexorank";
|
||||||
import { Box, Group } from "@mantine/core";
|
import { Box, Group } from "@mantine/core";
|
||||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||||
import { SortableItem } from "@/components/dnd/SortableDnd/SortableItem";
|
|
||||||
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
|
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
|
||||||
|
import SortableItem from "@/components/dnd/SortableItem";
|
||||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type BaseItem = {
|
type BaseItem = {
|
||||||
@ -119,9 +119,7 @@ const SortableDnd = <T extends BaseItem>({
|
|||||||
</Group>
|
</Group>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<SortableOverlay>
|
<SortableOverlay>
|
||||||
<div style={{ cursor: "grabbing" }}>
|
|
||||||
{activeItem ? renderItem(activeItem) : null}
|
{activeItem ? renderItem(activeItem) : null}
|
||||||
</div>
|
|
||||||
</SortableOverlay>
|
</SortableOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const dropAnimationConfig: DropAnimation = {
|
|||||||
export function SortableOverlay({ children }: PropsWithChildren) {
|
export function SortableOverlay({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<DragOverlay dropAnimation={dropAnimationConfig}>
|
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||||
{children}
|
<div style={{ cursor: "grabbing" }}>{children}</div>
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { CSSProperties, ReactNode, useContext } from "react";
|
import React, { CSSProperties, ReactNode, useContext } from "react";
|
||||||
import SortableItemContext from "@/components/dnd/SortableDnd/SortableItemContext";
|
import SortableItemContext from "@/components/dnd/SortableItem/SortableItemContext";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import DragHandle from "@/components/dnd/SortableDnd/DragHandle";
|
import DragHandle from "./DragHandle";
|
||||||
import SortableItemContext from "./SortableItemContext";
|
import SortableItemContext from "./SortableItemContext";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -10,7 +10,7 @@ type Props = {
|
|||||||
dragHandleStyle?: CSSProperties;
|
dragHandleStyle?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SortableItem = ({
|
const SortableItem = ({
|
||||||
children,
|
children,
|
||||||
itemStyle,
|
itemStyle,
|
||||||
id,
|
id,
|
||||||
@ -52,3 +52,5 @@ export const SortableItem = ({
|
|||||||
</SortableItemContext.Provider>
|
</SortableItemContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default SortableItem;
|
||||||
3
src/components/dnd/SortableItem/index.ts
Normal file
3
src/components/dnd/SortableItem/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SortableItem from "./SortableItem";
|
||||||
|
|
||||||
|
export default SortableItem;
|
||||||
3
src/components/dnd/types/types.ts
Normal file
3
src/components/dnd/types/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type BaseDraggable = {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
@ -2,5 +2,5 @@ import type { CreateClientConfig } from "@/lib/client/client.gen";
|
|||||||
|
|
||||||
export const createClientConfig: CreateClientConfig = config => ({
|
export const createClientConfig: CreateClientConfig = config => ({
|
||||||
...config,
|
...config,
|
||||||
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user