From 9870be61bba275da5c9a13bf27b809e5aee65345 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Tue, 30 Apr 2024 20:16:55 +0530 Subject: [PATCH] Kanban DnD improvement --- .../issue-layouts/kanban/base-kanban-root.tsx | 6 +++- .../issues/issue-layouts/kanban/default.tsx | 7 +++++ .../issue-layouts/kanban/kanban-group.tsx | 28 +++++++++++++++++-- .../issues/issue-layouts/kanban/swimlanes.tsx | 11 ++++++-- .../issues/issue-layouts/kanban/utils.ts | 28 ++++++++++++++++--- web/package.json | 1 + web/styles/globals.css | 5 ++++ yarn.lock | 21 +++++++++++++- 8 files changed, 96 insertions(+), 11 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 8c6cf900b..82e3f98f1 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -76,6 +76,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const sub_group_by = displayFilters?.sub_group_by; const group_by = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const userDisplayFilters = displayFilters || null; const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan; @@ -158,7 +160,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas issues.getIssueIds, updateIssue, group_by, - sub_group_by + sub_group_by, + orderBy !== "sort_order" ).catch((err) => { setToast({ title: "Error", @@ -259,6 +262,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas displayProperties={displayProperties} sub_group_by={sub_group_by} group_by={group_by} + orderBy={orderBy} updateIssue={updateIssue} quickActions={renderQuickActions} handleKanbanFilters={handleKanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 99bfee411..47cef9d98 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -11,6 +11,7 @@ import { TUnGroupedIssues, TIssueKanbanFilters, TIssueGroupByOptions, + TIssueOrderByOptions, } from "@plane/types"; // constants // hooks @@ -39,6 +40,7 @@ export interface IGroupByKanBan { displayProperties: IIssueDisplayProperties | undefined; sub_group_by: TIssueGroupByOptions | undefined; group_by: TIssueGroupByOptions | undefined; + orderBy: TIssueOrderByOptions | undefined; sub_group_id: string; isDragDisabled: boolean; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; @@ -87,6 +89,7 @@ const GroupByKanBan: React.FC = observer((props) => { handleOnDrop, showEmptyGroup = true, subGroupIssueHeaderCount, + orderBy, } = props; const member = useMember(); @@ -180,6 +183,7 @@ const GroupByKanBan: React.FC = observer((props) => { displayProperties={displayProperties} sub_group_by={sub_group_by} group_by={group_by} + orderBy={orderBy} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} updateIssue={updateIssue} @@ -206,6 +210,7 @@ export interface IKanBan { displayProperties: IIssueDisplayProperties | undefined; sub_group_by: TIssueGroupByOptions | undefined; group_by: TIssueGroupByOptions | undefined; + orderBy: TIssueOrderByOptions | undefined; sub_group_id?: string; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; @@ -252,6 +257,7 @@ export const KanBan: React.FC = observer((props) => { handleOnDrop, showEmptyGroup, subGroupIssueHeaderCount, + orderBy, } = props; const issueKanBanView = useKanbanView(); @@ -263,6 +269,7 @@ export const KanBan: React.FC = observer((props) => { displayProperties={displayProperties} group_by={group_by} sub_group_by={sub_group_by} + orderBy={orderBy} sub_group_id={sub_group_id} isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)} updateIssue={updateIssue} diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index d1161be2e..b8d5f5dd1 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -11,13 +11,19 @@ import { TSubGroupedIssues, TUnGroupedIssues, TIssueGroupByOptions, + TIssueOrderByOptions, } from "@plane/types"; // helpers import { cn } from "@/helpers/common.helper"; // hooks import { useProjectState } from "@/hooks/store"; //components -import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils"; +import { + KanbanDropLocation, + getSourceFromDropPayload, + getDestinationFromDropPayload, + highlightIssueOnDrop, +} from "./utils"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { @@ -45,6 +51,7 @@ interface IKanbanGroup { groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; + orderBy: TIssueOrderByOptions | undefined; } export const KanbanGroup = (props: IKanbanGroup) => { @@ -52,6 +59,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { groupId, sub_group_id, group_by, + orderBy, sub_group_by, issuesMap, displayProperties, @@ -102,13 +110,15 @@ export const KanbanGroup = (props: IKanbanGroup) => { if (!source || !destination) return; handleOnDrop(source, destination); + + highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order"); }, }), autoScrollForElements({ element, }) ); - }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]); + }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]); const prePopulateQuickAddData = ( groupByKey: string | undefined, @@ -162,16 +172,28 @@ export const KanbanGroup = (props: IKanbanGroup) => { return preloadedData; }; + const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order"; return (
+
+ Drop here to move issue +
boolean; addIssuesToView?: (issueIds: string[]) => Promise; quickAddCallback?: ( @@ -145,6 +147,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { viewId, scrollableContainerRef, handleOnDrop, + orderBy, } = props; const calculateIssueCount = (column_id: string) => { @@ -180,7 +183,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { if (subGroupByVisibilityToggle.showGroup === false) return <>; return (
-
+
= observer((props) => { viewId={viewId} scrollableContainerRef={scrollableContainerRef} handleOnDrop={handleOnDrop} + orderBy={orderBy} subGroupIssueHeaderCount={(groupByListId: string) => getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) } @@ -253,6 +257,7 @@ export interface IKanBanSwimLanes { viewId?: string; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; + orderBy: TIssueOrderByOptions | undefined; } export const KanBanSwimLanes: React.FC = observer((props) => { @@ -262,6 +267,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties, sub_group_by, group_by, + orderBy, updateIssue, storeType, quickActions, @@ -312,7 +318,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { return (
-
+
= observer((props) => { displayProperties={displayProperties} group_by={group_by} sub_group_by={sub_group_by} + orderBy={orderBy} updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index 3981455db..0c9c24f6e 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,4 +1,5 @@ import pull from "lodash/pull"; +import scrollIntoView from "smooth-scroll-into-view-if-needed"; import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types"; import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store"; @@ -87,14 +88,17 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K const handleSortOrder = ( destinationIssues: string[], destinationIssueId: string | undefined, - getIssueById: (issueId: string) => TIssue | undefined + getIssueById: (issueId: string) => TIssue | undefined, + shouldAddIssueAtTop = false ) => { const sortOrderDefaultValue = 65535; let currentIssueState = {}; const destinationIndex = destinationIssueId ? destinationIssues.indexOf(destinationIssueId) - : destinationIssues.length; + : shouldAddIssueAtTop + ? 0 + : destinationIssues.length; if (destinationIssues && destinationIssues.length > 0) { if (destinationIndex === 0) { @@ -145,7 +149,8 @@ export const handleDragDrop = async ( getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, groupBy: TIssueGroupByOptions | undefined, - subGroupBy: TIssueGroupByOptions | undefined + subGroupBy: TIssueGroupByOptions | undefined, + shouldAddIssueAtTop = false ) => { if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return; @@ -165,7 +170,7 @@ export const handleDragDrop = async ( // for both horizontal and vertical dnd updatedIssue = { ...updatedIssue, - ...handleSortOrder(destinationIssues, destination.id, getIssueById), + ...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop), }; if (source.groupId && destination.groupId && source.groupId !== destination.groupId) { @@ -207,3 +212,18 @@ export const handleDragDrop = async ( ); } }; + +export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => { + setTimeout(async () => { + const sourceElementId = elementId ?? ""; + const sourceElement = document.getElementById(sourceElementId); + if (shouldScrollIntoView && sourceElement) + await scrollIntoView(sourceElement, { behavior: "smooth", block: "center" }); + sourceElement?.classList?.add("highlight"); + setTimeout(() => { + const sourceElementId = elementId ?? ""; + const sourceElement = document.getElementById(sourceElementId); + sourceElement?.classList?.remove("highlight"); + }, 1000); + }, 200); +}; diff --git a/web/package.json b/web/package.json index a4c94d051..881c2ceac 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "react-markdown": "^8.0.7", "react-popper": "^2.3.0", "sharp": "^0.32.1", + "smooth-scroll-into-view-if-needed": "^2.0.2", "swr": "^2.1.3", "tailwind-merge": "^2.0.0", "use-debounce": "^9.0.4", diff --git a/web/styles/globals.css b/web/styles/globals.css index cc9c0b273..113ef4654 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -630,3 +630,8 @@ div.web-view-spinner div.bar12 { .scrollbar-lg::-webkit-scrollbar-thumb { border: 4px solid rgba(0, 0, 0, 0); } + +/* highlight class */ +.highlight { + border: 1px solid rgb(var(--color-primary-100)); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a33c24f88..17b430963 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2755,7 +2755,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": +"@types/react@*", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== @@ -3699,6 +3699,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +compute-scroll-into-view@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87" + integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -7633,6 +7638,13 @@ schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +scroll-into-view-if-needed@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f" + integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ== + dependencies: + compute-scroll-into-view "^3.0.2" + selecto@~1.26.3: version "1.26.3" resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212" @@ -7774,6 +7786,13 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smooth-scroll-into-view-if-needed@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-2.0.2.tgz#5bd4ebef668474d6618ce8704650082e93068371" + integrity sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q== + dependencies: + scroll-into-view-if-needed "^3.1.0" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"