Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-1249] feat: Kanban multi dragndrop #4479

Merged
merged 3 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/tailwind-config-custom/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ module.exports = {
800: convertToRGB("--color-background-800"),
900: convertToRGB("--color-background-900"),
1000: "rgb(0, 0, 0)",
primary: convertToRGB(" --color-background-primary"),
error: convertToRGB(" --color-background-error"),
DEFAULT: convertToRGB("--color-background-100"),
},
text: {
Expand All @@ -110,6 +112,8 @@ module.exports = {
800: convertToRGB("--color-text-800"),
900: convertToRGB("--color-text-900"),
1000: "rgb(0, 0, 0)",
primary: convertToRGB("--color-text-primary"),
error: convertToRGB("--color-text-error"),
DEFAULT: convertToRGB("--color-text-100"),
},
border: {
Expand All @@ -119,8 +123,18 @@ module.exports = {
300: convertToRGB("--color-border-300"),
400: convertToRGB("--color-border-400"),
1000: "rgb(0, 0, 0)",
primary: convertToRGB("--color-border-primary"),
error: convertToRGB("--color-border-error"),
DEFAULT: convertToRGB("--color-border-200"),
},
error: {
10: convertToRGB("--color-error-10"),
20: convertToRGB("--color-error-20"),
30: convertToRGB("--color-error-30"),
100: convertToRGB("--color-error-100"),
200: convertToRGB("--color-error-200"),
500: convertToRGB("--color-error-500"),
},
sidebar: {
background: {
0: "rgb(255, 255, 255)",
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/issues.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ export interface IGroupByColumn {
name: string;
icon: ReactElement | undefined;
payload: Partial<TIssue>;
isDropDisabled?: boolean;
dropErrorMessage?: string;
}

export interface IIssueMap {
Expand Down
4 changes: 1 addition & 3 deletions web/components/core/render-if-visible-HOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type Props = {
alwaysRender?: boolean;
placeholderChildren?: ReactNode;
pauseHeightUpdateWhileRendering?: boolean;
changingReference?: any;
};

const RenderIfVisible: React.FC<Props> = (props) => {
Expand All @@ -27,7 +26,6 @@ const RenderIfVisible: React.FC<Props> = (props) => {
alwaysRender = false, //render the children even if it is not visible in root
placeholderChildren = null, //placeholder children
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
changingReference, //This is to force render when this reference is changed
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
const placeholderHeight = useRef<string>(defaultHeight);
Expand Down Expand Up @@ -63,7 +61,7 @@ const RenderIfVisible: React.FC<Props> = (props) => {
}
};
}
}, [intersectionRef, children, changingReference, root, verticalOffset, horizontalOffset]);
}, [intersectionRef, children, root, verticalOffset, horizontalOffset]);

//Set height after render
useEffect(() => {
Expand Down
70 changes: 65 additions & 5 deletions web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { TIssue } from "@plane/types";
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
import { DeleteIssueModal } from "@/components/issues";
import { ISSUE_DELETED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUser } from "@/hooks/store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
// store
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
// ui
// types
import { IQuickActionProps, TRenderQuickActions } from "../list/list-view-types";
//components
import { GroupDropLocation, handleGroupDragDrop, getSourceFromDropPayload } from "../utils";
import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes";
import { KanbanDropLocation, handleDragDrop, getSourceFromDropPayload } from "./utils";


export type KanbanStoreType =
| EIssuesStoreType.PROJECT
Expand Down Expand Up @@ -61,6 +65,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
} = useIssueDetail();
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
useIssuesActions(storeType);
const {
issues: { addCycleToIssue, removeCycleFromIssue },
} = useIssues(EIssuesStoreType.CYCLE);
const {
issues: { changeModulesInIssue },
} = useIssues(EIssuesStoreType.MODULE);

const deleteAreaRef = useRef<HTMLDivElement | null>(null);
const [isDragOverDelete, setIsDragOverDelete] = useState(false);
Expand Down Expand Up @@ -143,7 +153,57 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
);
}, [deleteAreaRef?.current, setIsDragOverDelete, setDraggedIssueId, setDeleteIssueModal]);

const handleOnDrop = async (source: KanbanDropLocation, destination: KanbanDropLocation) => {
/**
* update Issue on Drop, checks if modules or cycles are changed and then calls appropriate functions
* @param projectId
* @param issueId
* @param data
* @param issueUpdates
*/
const updateIssueOnDrop = async (
projectId: string,
issueId: string,
data: Partial<TIssue>,
issueUpdates: {
[groupKey: string]: {
ADD: string[];
REMOVE: string[];
};
}
) => {

const errorToastProps = {
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error while updating issue"
}
const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"];
const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"];

const isModuleChanged = Object.keys(data).includes(moduleKey);
const isCycleChanged = Object.keys(data).includes(cycleKey);

if (isCycleChanged && workspaceSlug) {
if(data[cycleKey])addCycleToIssue(workspaceSlug.toString(), projectId, data[cycleKey], issueId).catch(() => setToast(errorToastProps));
else removeCycleFromIssue(workspaceSlug.toString(), projectId, issueId).catch(() => setToast(errorToastProps))
delete data[cycleKey];
}

if (isModuleChanged && workspaceSlug && issueUpdates[moduleKey]) {
changeModulesInIssue(
workspaceSlug.toString(),
projectId,
issueId,
issueUpdates[moduleKey].ADD,
issueUpdates[moduleKey].REMOVE
).catch(() => setToast(errorToastProps));
delete data[moduleKey];
}

updateIssue && updateIssue(projectId, issueId, data).catch(() => setToast(errorToastProps));
};

const handleOnDrop = async (source: GroupDropLocation, destination: GroupDropLocation) => {
if (
source.columnId &&
destination.columnId &&
Expand All @@ -152,12 +212,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
)
return;

await handleDragDrop(
await handleGroupDragDrop(
source,
destination,
getIssueById,
issues.getIssueIds,
updateIssue,
updateIssueOnDrop,
group_by,
sub_group_by,
orderBy !== "sort_order"
Expand Down
10 changes: 6 additions & 4 deletions web/components/issues/issue-layouts/kanban/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { TRenderQuickActions } from "../list/list-view-types";
import { IssueProperties } from "../properties/all-properties";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { getIssueBlockId } from "../utils";
// ui
// types
// helper

interface IssueBlockProps {
issueId: string;
groupId: string;
subGroupId: string;
issuesMap: IIssueMap;
displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean;
Expand All @@ -30,7 +33,6 @@ interface IssueBlockProps {
quickActions: TRenderQuickActions;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
}

interface IssueDetailsBlockProps {
Expand Down Expand Up @@ -99,14 +101,15 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
const {
issueId,
groupId,
subGroupId,
issuesMap,
displayProperties,
isDragDisabled,
updateIssue,
quickActions,
canEditProperties,
scrollableContainerRef,
issueIds,
} = props;

const cardRef = useRef<HTMLAnchorElement | null>(null);
Expand Down Expand Up @@ -194,7 +197,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
}}
>
<ControlLink
id={`issue-${issue.id}`}
id={getIssueBlockId(issueId, groupId, subGroupId)}
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${
issue.id
}`}
Expand All @@ -214,7 +217,6 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
root={scrollableContainerRef}
defaultHeight="100px"
horizontalOffset={50}
changingReference={issueIds}
>
<KanbanIssueDetailsBlock
cardRef={cardRef}
Expand Down
9 changes: 5 additions & 4 deletions web/components/issues/issue-layouts/kanban/blocks-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TRenderQuickActions } from "../list/list-view-types";

interface IssueBlocksListProps {
sub_group_id: string;
columnId: string;
groupId: string;
issuesMap: IIssueMap;
issueIds: string[];
displayProperties: IIssueDisplayProperties | undefined;
Expand All @@ -21,7 +21,7 @@ interface IssueBlocksListProps {
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
const {
sub_group_id,
columnId,
groupId,
issuesMap,
issueIds,
displayProperties,
Expand All @@ -40,13 +40,15 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
if (!issueId) return null;

let draggableId = issueId;
if (columnId) draggableId = `${draggableId}__${columnId}`;
if (groupId) draggableId = `${draggableId}__${groupId}`;
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;

return (
<KanbanIssueBlock
key={draggableId}
issueId={issueId}
groupId={groupId}
subGroupId={sub_group_id}
issuesMap={issuesMap}
displayProperties={displayProperties}
updateIssue={updateIssue}
Expand All @@ -55,7 +57,6 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
/>
);
})}
Expand Down
9 changes: 5 additions & 4 deletions web/components/issues/issue-layouts/kanban/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ import { useCycle, useKanbanView, useLabel, useMember, useModule, useProject, us
// types
// parent components
import { TRenderQuickActions } from "../list/list-view-types";
import { getGroupByColumns, isWorkspaceLevel } from "../utils";
import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation } from "../utils";
// components
import { KanbanStoreType } from "./base-kanban-root";
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
import { KanbanDropLocation } from "./utils";

export interface IGroupByKanBan {
issuesMap: IIssueMap;
Expand Down Expand Up @@ -52,7 +51,7 @@ export interface IGroupByKanBan {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
Expand Down Expand Up @@ -176,6 +175,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
isDropDisabled={!!subList.isDropDisabled}
dropErrorMessage={subList.dropErrorMessage}
updateIssue={updateIssue}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
Expand Down Expand Up @@ -220,7 +221,7 @@ export interface IKanBan {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
subGroupIssueHeaderCount?: (listId: string) => number;
}

Expand Down
Loading
Loading