Skip to content

Commit

Permalink
feat: support drag n drop
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Oct 10, 2024
1 parent 664552d commit 5170702
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 47 deletions.
54 changes: 51 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@
"react-hot-toast": "2.4.1",
"react-input-mask": "2.0.4",
"react-remark": "2.1.0",
"react-sortablejs": "6.1.4",
"remark-emoji": "4.0.1",
"sortablejs": "1.15.3",
"throttle-debounce": "5.0.0",
"tinykeys": "1.4.0",
"zod": "3.22.4"
Expand All @@ -112,8 +114,10 @@
"@types/nodemailer": "6.4.14",
"@types/pg": "8.11.4",
"@types/react": "18.2.48",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.2.19",
"@types/react-input-mask": "3.0.5",
"@types/sortablejs": "1.15.8",
"@types/throttle-debounce": "5.0.2",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
Expand Down
7 changes: 7 additions & 0 deletions src/components/Kanban/Kanban.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.KanbanColumn {
display: flex;
flex-direction: column;
}
.KanbanState{
padding: var(--gap-m) 0;
position: sticky;
Expand All @@ -7,6 +11,9 @@
background: linear-gradient(var(--background) 70%, rgba(0, 0, 0, 0) 100%);
z-index: 2;
}
.KanbanSortableList {
flex: 1;
}

.KanbanLink {
display: block;
Expand Down
193 changes: 149 additions & 44 deletions src/components/Kanban/Kanban.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { FC, MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { ComponentProps, FC, MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { KanbanColumn, KanbanContainer } from '@taskany/bricks/harmony';
import { QueryStatus } from '@tanstack/react-query';
import { nullable, useIntersectionLoader } from '@taskany/bricks';
import { ReactSortable } from 'react-sortablejs';

import { trpc } from '../../utils/trpcClient';
import { FilterById, State } from '../../../trpc/inferredTypes';
Expand All @@ -24,6 +25,7 @@ type onLoadingStateChange = (state: LoadingState) => void;

interface KanbanStateColumnProps {
state: State;
shownStates: State[];
projectId: string;
filterPreset?: FilterById;
partnershipProject?: string[];
Expand Down Expand Up @@ -66,6 +68,7 @@ const intersectionOptions = {

const KanbanStateColumn: FC<KanbanStateColumnProps> = ({
state,
shownStates,
projectId,
filterPreset,
partnershipProject,
Expand All @@ -75,22 +78,24 @@ const KanbanStateColumn: FC<KanbanStateColumnProps> = ({
preset: filterPreset,
});

const { data, isFetching, fetchNextPage, hasNextPage, status } =
trpc.v2.project.getProjectGoalsById.useInfiniteQuery(
{
id: projectId,
goalsQuery: {
...queryState,
partnershipProject: partnershipProject || undefined,
state: [state.id],
},
const getColumnQuery = useCallback(
(stateId: string) => ({
id: projectId,
goalsQuery: {
...queryState,
partnershipProject: partnershipProject || undefined,
state: [stateId],
},
{
keepPreviousData: true,
staleTime: refreshInterval,
getNextPageParam: ({ pagination }) => pagination.offset,
},
);
}),
[queryState, partnershipProject, projectId],
);

const { data, isFetching, fetchNextPage, hasNextPage, status } =
trpc.v2.project.getProjectGoalsById.useInfiniteQuery(getColumnQuery(state.id), {
keepPreviousData: true,
staleTime: refreshInterval,
getNextPageParam: ({ pagination }) => pagination.offset,
});

const goals = useMemo(() => {
if (data == null) {
Expand All @@ -103,6 +108,12 @@ const KanbanStateColumn: FC<KanbanStateColumnProps> = ({
}, []);
}, [data]);

const [list, setList] = useState(goals);

useEffect(() => {
setList(goals);
}, [goals]);

const loadingState = useMemo(() => {
if (isFetching) {
return 'loading';
Expand Down Expand Up @@ -132,37 +143,130 @@ const KanbanStateColumn: FC<KanbanStateColumnProps> = ({
intersectionOptions,
);

const stateChangeMutations = trpc.goal.switchState.useMutation();
const utils = trpc.useContext();

const onDragEnd = useCallback<NonNullable<ComponentProps<typeof ReactSortable>['onEnd']>>(
async (result) => {
const goalId = result.item.id;
const newStateId = result.to.id;
const oldStateId = result.from.id;

const state = newStateId ? shownStates.find(({ id }) => newStateId === id) : null;

if (!state) {
return;
}

const data = utils.v2.project.getProjectGoalsById.getInfiniteData(getColumnQuery(oldStateId));

const goals =
data?.pages?.reduce<(typeof data)['pages'][number]['goals']>((acc, cur) => {
acc.push(...cur.goals);
return acc;
}, []) || [];

const goal = goals.find((goal) => goal.id === goalId);

await utils.v2.project.getProjectGoalsById.setInfiniteData(getColumnQuery(oldStateId), (data) => {
if (!data) {
return {
pages: [],
pageParams: [],
};
}

return {
...data,
pages: data.pages.map((page) => ({
...page,
goals: page.goals.filter((goal) => goal.id !== goalId),
})),
};
});

if (newStateId && goal) {
await utils.v2.project.getProjectGoalsById.setInfiniteData(getColumnQuery(newStateId), (data) => {
if (!data) {
return {
pages: [],
pageParams: [],
};
}

const [first, ...rest] = data.pages;

const updatedFirst = {
...first,
goals: [goal, ...first.goals],
};

return {
...data,
pages: [updatedFirst, ...rest],
};
});
}

await stateChangeMutations.mutate(
{
id: goalId,
state,
},
{
onSettled: () => {
utils.v2.project.getProjectGoalsById.invalidate({
id: projectId,
});
},
},
);
},
[stateChangeMutations, utils, getColumnQuery, shownStates, projectId],
);

return (
<KanbanColumn>
<KanbanColumn className={s.KanbanColumn}>
<KanbanState className={s.KanbanState} state={state} />

{goals.map((goal) => {
return (
<NextLink
key={goal.id}
href={routes.goal(goal._shortId)}
onClick={onGoalPreviewShow({
_shortId: goal._shortId,
title: goal.title,
})}
className={s.KanbanLink}
>
<GoalsKanbanCard
<ReactSortable
className={s.KanbanSortableList}
id={state.id}
animation={150}
list={list}
setList={setList}
group="states"
onEnd={onDragEnd}
sort={false}
>
{list.map((goal) => {
return (
<NextLink
id={goal.id}
title={goal.title}
commentsCount={goal._count.comments ?? 0}
updatedAt={goal.updatedAt}
owner={goal.owner}
estimate={goal.estimate}
estimateType={goal.estimateType}
tags={goal.tags}
priority={goal.priority}
progress={goal._achivedCriteriaWeight}
onTagClick={setTagsFilterOutside}
/>
</NextLink>
);
})}
key={goal.id}
href={routes.goal(goal._shortId)}
onClick={onGoalPreviewShow({
_shortId: goal._shortId,
title: goal.title,
})}
className={s.KanbanLink}
>
<GoalsKanbanCard
id={goal.id}
title={goal.title}
commentsCount={goal._count.comments ?? 0}
updatedAt={goal.updatedAt}
owner={goal.owner}
estimate={goal.estimate}
estimateType={goal.estimateType}
tags={goal.tags}
priority={goal.priority}
progress={goal._achivedCriteriaWeight}
onTagClick={setTagsFilterOutside}
/>
</NextLink>
);
})}
</ReactSortable>

<div ref={ref} />
</KanbanColumn>
Expand Down Expand Up @@ -197,6 +301,7 @@ export const Kanban = ({ id, filterPreset, partnershipProject }: KanbanProps) =>
<KanbanContainer>
{shownStates.map((state) => (
<KanbanStateColumn
shownStates={shownStates}
key={state.id}
projectId={id}
state={state}
Expand Down

0 comments on commit 5170702

Please sign in to comment.