diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index 209b7b6581..5e6e4a2158 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -272,6 +272,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
+ current_instance = json.dumps(
+ InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
+ )
if serializer.is_valid():
serializer.save()
@@ -311,6 +314,21 @@ class InboxIssueAPIEndpoint(BaseAPIView):
issue.state = state
issue.save()
+ # create a activity for status change
+ issue_activity.delay(
+ type="inbox.activity.created",
+ requested_data=json.dumps(
+ request.data, cls=DjangoJSONEncoder
+ ),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
+ notification=False,
+ origin=request.META.get("HTTP_ORIGIN"),
+ )
+
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py
index e08434a6de..8e433a127c 100644
--- a/apiserver/plane/app/views/inbox/base.py
+++ b/apiserver/plane/app/views/inbox/base.py
@@ -391,7 +391,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
-
+ current_instance = json.dumps(
+ InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
+ )
if serializer.is_valid():
serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate
@@ -429,6 +431,21 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None:
issue.state = state
issue.save()
+ # create a activity for status change
+ issue_activity.delay(
+ type="inbox.activity.created",
+ requested_data=json.dumps(
+ request.data, cls=DjangoJSONEncoder
+ ),
+ actor_id=str(request.user.id),
+ issue_id=str(issue_id),
+ project_id=str(project_id),
+ current_instance=current_instance,
+ epoch=int(timezone.now().timestamp()),
+ notification=False,
+ origin=request.META.get("HTTP_ORIGIN"),
+ )
+
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id,
diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py
index 4076a53b19..29dc2dbf5c 100644
--- a/apiserver/plane/app/views/page/base.py
+++ b/apiserver/plane/app/views/page/base.py
@@ -1,6 +1,7 @@
# Python imports
import json
from datetime import datetime
+from django.core.serializers.json import DjangoJSONEncoder
# Django imports
from django.db import connection
@@ -142,6 +143,7 @@ class PageViewSet(BaseViewSet):
serializer = PageDetailSerializer(
page, data=request.data, partial=True
)
+ page_description = page.description_html
if serializer.is_valid():
serializer.save()
# capture the page transaction
@@ -150,11 +152,13 @@ class PageViewSet(BaseViewSet):
new_value=request.data,
old_value=json.dumps(
{
- "description_html": page.description_html,
- }
+ "description_html": page_description,
+ },
+ cls=DjangoJSONEncoder,
),
page_id=pk,
)
+
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
diff --git a/apiserver/plane/bgtasks/dummy_data_task.py b/apiserver/plane/bgtasks/dummy_data_task.py
index 7ff110e6f5..e76cdac224 100644
--- a/apiserver/plane/bgtasks/dummy_data_task.py
+++ b/apiserver/plane/bgtasks/dummy_data_task.py
@@ -1,4 +1,5 @@
# Python imports
+import uuid
import random
from datetime import datetime, timedelta
@@ -36,9 +37,11 @@ from plane.db.models import (
def create_project(workspace, user_id):
fake = Faker()
name = fake.name()
+ unique_id = str(uuid.uuid4())[:5]
+
project = Project.objects.create(
workspace=workspace,
- name=name,
+ name=f"{name}_{unique_id}",
identifier=name[
: random.randint(2, 12 if len(name) - 1 >= 12 else len(name) - 1)
].upper(),
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index 9a4e57a49f..2d55d55796 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -1553,6 +1553,46 @@ def delete_draft_issue_activity(
)
+def create_inbox_activity(
+ requested_data,
+ current_instance,
+ issue_id,
+ project_id,
+ workspace_id,
+ actor_id,
+ issue_activities,
+ epoch,
+):
+ requested_data = (
+ json.loads(requested_data) if requested_data is not None else None
+ )
+ current_instance = (
+ json.loads(current_instance) if current_instance is not None else None
+ )
+ status_dict = {
+ -2: "Pending",
+ -1: "Rejected",
+ 0: "Snoozed",
+ 1: "Accepted",
+ 2: "Duplicate",
+ }
+ if requested_data.get("status") is not None:
+ issue_activities.append(
+ IssueActivity(
+ issue_id=issue_id,
+ project_id=project_id,
+ workspace_id=workspace_id,
+ comment="updated the inbox status",
+ field="inbox",
+ verb=requested_data.get("status"),
+ actor_id=actor_id,
+ epoch=epoch,
+ old_value=status_dict.get(current_instance.get("status")),
+ new_value=status_dict.get(requested_data.get("status")),
+ )
+ )
+
+
# Receive message from room group
@shared_task
def issue_activity(
@@ -1613,6 +1653,7 @@ def issue_activity(
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
+ "inbox.activity.created": create_inbox_activity,
}
func = ACTIVITY_MAPPER.get(type)
diff --git a/apiserver/plane/bgtasks/page_transaction_task.py b/apiserver/plane/bgtasks/page_transaction_task.py
index 57f4f644e4..eceb3693e7 100644
--- a/apiserver/plane/bgtasks/page_transaction_task.py
+++ b/apiserver/plane/bgtasks/page_transaction_task.py
@@ -37,10 +37,10 @@ def page_transaction(new_value, old_value, page_id):
page = Page.objects.get(pk=page_id)
new_page_mention = PageLog.objects.filter(page_id=page_id).exists()
- old_value = json.loads(old_value)
+ old_value = json.loads(old_value) if old_value else {}
new_transactions = []
- deleted_transaction_ids = set()
+ deleted_transaction_ids = set()
# TODO - Add "issue-embed-component", "img", "todo" components
components = ["mention-component"]
@@ -49,8 +49,8 @@ def page_transaction(new_value, old_value, page_id):
new_mentions = extract_components(new_value, component)
new_mentions_ids = {mention["id"] for mention in new_mentions}
- old_mention_ids = {mention["id"] for mention in old_mentions}
- deleted_transaction_ids.update(old_mention_ids - new_mentions_ids)
+ old_mention_ids = {mention["id"] for mention in old_mentions}
+ deleted_transaction_ids.update(old_mention_ids - new_mentions_ids)
new_transactions.extend(
PageLog(
@@ -68,9 +68,9 @@ def page_transaction(new_value, old_value, page_id):
)
# Create new PageLog objects for new transactions
- PageLog.objects.bulk_create(new_transactions, batch_size=10, ignore_conflicts=True)
+ PageLog.objects.bulk_create(
+ new_transactions, batch_size=10, ignore_conflicts=True
+ )
# Delete the removed transactions
- PageLog.objects.filter(
- transaction__in=deleted_transaction_ids
- ).delete()
+ PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete()
diff --git a/packages/types/src/pragmatic.d.ts b/packages/types/src/pragmatic.d.ts
index ca47e2d37a..439e2b54fb 100644
--- a/packages/types/src/pragmatic.d.ts
+++ b/packages/types/src/pragmatic.d.ts
@@ -8,18 +8,27 @@ export type TDropTargetMiscellaneousData = {
isActiveDueToStickiness: boolean;
};
-export interface IPragmaticDropPayload {
- location: {
- initial: {
- dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
- };
- current: {
- dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
- };
- previous: {
- dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
- };
+export interface IPragmaticPayloadLocation {
+ initial: {
+ dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
};
+ current: {
+ dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
+ };
+ previous: {
+ dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[];
+ };
+}
+
+export interface IPragmaticDropPayload {
+ location: IPragmaticPayloadLocation;
source: TDropTarget;
self: TDropTarget & TDropTargetMiscellaneousData;
}
+
+export type InstructionType =
+ | "reparent"
+ | "reorder-above"
+ | "reorder-below"
+ | "make-child"
+ | "instruction-blocked";
\ No newline at end of file
diff --git a/packages/ui/src/drop-indicator.tsx b/packages/ui/src/drop-indicator.tsx
index 228c1a3301..7ffc83a4ba 100644
--- a/packages/ui/src/drop-indicator.tsx
+++ b/packages/ui/src/drop-indicator.tsx
@@ -3,9 +3,12 @@ import { cn } from "../helpers";
type Props = {
isVisible: boolean;
+ classNames?: string;
};
export const DropIndicator = (props: Props) => {
+ const { isVisible, classNames = "" } = props;
+
return (
{
before:left-0 before:relative before:block before:top-[-2px] before:h-[6px] before:w-[6px] before:rounded
after:left-[calc(100%-6px)] after:relative after:block after:top-[-8px] after:h-[6px] after:w-[6px] after:rounded`,
{
- "bg-custom-primary-100 before:bg-custom-primary-100 after:bg-custom-primary-100": props.isVisible,
- }
+ "bg-custom-primary-100 before:bg-custom-primary-100 after:bg-custom-primary-100": isVisible,
+ },
+ classNames
)}
/>
);
diff --git a/packages/ui/src/form-fields/textarea.tsx b/packages/ui/src/form-fields/textarea.tsx
index 271b76d83a..de225d68f3 100644
--- a/packages/ui/src/form-fields/textarea.tsx
+++ b/packages/ui/src/form-fields/textarea.tsx
@@ -1,6 +1,8 @@
-import * as React from "react";
+import React, { useRef } from "react";
// helpers
import { cn } from "../../helpers";
+// hooks
+import { useAutoResizeTextArea } from "../hooks/use-auto-resize-textarea";
export interface TextAreaProps extends React.TextareaHTMLAttributes
{
mode?: "primary" | "transparent";
@@ -8,21 +10,6 @@ export interface TextAreaProps extends React.TextareaHTMLAttributes when the value changes.
-const useAutoSizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: any) => {
- React.useEffect(() => {
- if (textAreaRef) {
- // We need to reset the height momentarily to get the correct scrollHeight for the textarea
- textAreaRef.style.height = "0px";
- const scrollHeight = textAreaRef.scrollHeight;
-
- // We then set the height directly, outside of the render loop
- // Trying to set this with state or a ref will product an incorrect value.
- textAreaRef.style.height = scrollHeight + "px";
- }
- }, [textAreaRef, value]);
-};
-
const TextArea = React.forwardRef((props, ref) => {
const {
id,
@@ -35,10 +22,10 @@ const TextArea = React.forwardRef((props, re
className = "",
...rest
} = props;
-
- const textAreaRef = React.useRef(ref);
-
- useAutoSizeTextArea(textAreaRef?.current, value);
+ // refs
+ const textAreaRef = useRef(ref);
+ // auto re-size
+ useAutoResizeTextArea(textAreaRef);
return (
- {workspaceProjectIds && workspaceProjectIds?.length > 0 && (
-
- {!isSearchOpen && (
-
{
- setIsSearchOpen(true);
- inputRef.current?.focus();
- }}
- >
-
-
- )}
-
+ {!isSearchOpen && (
+ {
+ setIsSearchOpen(true);
+ inputRef.current?.focus();
+ }}
>
- updateSearchQuery(e.target.value)}
- onKeyDown={handleInputKeyDown}
- />
- {isSearchOpen && (
- {
- updateSearchQuery("");
- setIsSearchOpen(false);
- }}
- >
-
-
- )}
-
+
+ )}
+
+
+ updateSearchQuery(e.target.value)}
+ onKeyDown={handleInputKeyDown}
+ />
+ {isSearchOpen && (
+ {
+ updateSearchQuery("");
+ setIsSearchOpen(false);
+ }}
+ >
+
+
+ )}
- )}
+
{
diff --git a/web/components/inbox/inbox-filter/sorting/order-by.tsx b/web/components/inbox/inbox-filter/sorting/order-by.tsx
index c01c9977ea..4eb9dde225 100644
--- a/web/components/inbox/inbox-filter/sorting/order-by.tsx
+++ b/web/components/inbox/inbox-filter/sorting/order-by.tsx
@@ -42,7 +42,7 @@ export const InboxIssueOrderByDropdown: FC = observer(() => {
{inboxSorting?.order_by?.includes(option.key) && }
))}
-
+
{INBOX_ISSUE_SORT_BY_OPTIONS.map((option) => (
= observer((props) => {
cycle_id: formData?.cycle_id || "",
module_ids: formData?.module_ids || [],
estimate_point: formData?.estimate_point || undefined,
- parent_id: formData?.parent_id || "",
+ parent_id: formData?.parent_id || null,
};
setFormSubmitting(true);
diff --git a/web/components/inbox/modals/create-edit-modal/issue-properties.tsx b/web/components/inbox/modals/create-edit-modal/issue-properties.tsx
index 8e2eda1cc2..e55834f6e0 100644
--- a/web/components/inbox/modals/create-edit-modal/issue-properties.tsx
+++ b/web/components/inbox/modals/create-edit-modal/issue-properties.tsx
@@ -177,7 +177,7 @@ export const InboxIssueProperties: FC = observer((props)
setSelectedParentIssue(issue);
}}
projectId={projectId}
- issueId={data?.id}
+ issueId={undefined}
/>
>
)}
diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/inbox.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/inbox.tsx
new file mode 100644
index 0000000000..c51c647a9c
--- /dev/null
+++ b/web/components/issues/issue-detail/issue-activity/activity/actions/inbox.tsx
@@ -0,0 +1,42 @@
+import { FC } from "react";
+import { observer } from "mobx-react";
+// hooks
+import { Inbox } from "lucide-react";
+import { useIssueDetail } from "@/hooks/store";
+// components
+import { IssueActivityBlockComponent } from "./";
+// icons
+
+type TIssueInboxActivity = { activityId: string; ends: "top" | "bottom" | undefined };
+
+export const IssueInboxActivity: FC = observer((props) => {
+ const { activityId, ends } = props;
+ // hooks
+ const {
+ activity: { getActivityById },
+ } = useIssueDetail();
+
+ const activity = getActivityById(activityId);
+
+ const getInboxActivityMessage = () => {
+ switch (activity?.verb) {
+ case "-1":
+ return "declined this issue from inbox.";
+ case "0":
+ return "snoozed this issue.";
+ case "1":
+ return "accepted this issue from inbox.";
+ case "2":
+ return "declined this issue from inbox by marking a duplicate issue.";
+ default:
+ return "updated inbox issue status.";
+ }
+ };
+
+ if (!activity) return <>>;
+ return (
+ } activityId={activityId} ends={ends}>
+ <>{getInboxActivityMessage()}>
+
+ );
+});
diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts b/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts
index 02108d70b1..7b326aaf9a 100644
--- a/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts
+++ b/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts
@@ -15,6 +15,7 @@ export * from "./label";
export * from "./link";
export * from "./attachment";
export * from "./archived-at";
+export * from "./inbox";
// helpers
export * from "./helpers/activity-block";
diff --git a/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx
index 42d23eba58..9cdf7999b4 100644
--- a/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx
+++ b/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx
@@ -21,6 +21,7 @@ import {
IssueLinkActivity,
IssueAttachmentActivity,
IssueArchivedAtActivity,
+ IssueInboxActivity,
} from "./actions";
type TIssueActivityList = {
@@ -74,6 +75,8 @@ export const IssueActivityList: FC = observer((props) => {
return ;
case "archived_at":
return ;
+ case "inbox":
+ return ;
default:
return <>>;
}
diff --git a/web/components/labels/label-block/drag-handle.tsx b/web/components/labels/label-block/drag-handle.tsx
index a71637bb94..64aaa075f6 100644
--- a/web/components/labels/label-block/drag-handle.tsx
+++ b/web/components/labels/label-block/drag-handle.tsx
@@ -1,24 +1,25 @@
-import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
+import { forwardRef } from "react";
import { MoreVertical } from "lucide-react";
interface IDragHandle {
isDragging: boolean;
- dragHandleProps: DraggableProvidedDragHandleProps;
}
-export const DragHandle = (props: IDragHandle) => {
- const { isDragging, dragHandleProps } = props;
+export const DragHandle = forwardRef((props, ref) => {
+ const { isDragging } = props;
return (
);
-};
+});
+
+DragHandle.displayName = "DragHandle";
diff --git a/web/components/labels/label-block/label-item-block.tsx b/web/components/labels/label-block/label-item-block.tsx
index d154060593..e3236c91d2 100644
--- a/web/components/labels/label-block/label-item-block.tsx
+++ b/web/components/labels/label-block/label-item-block.tsx
@@ -1,5 +1,4 @@
-import { useRef, useState } from "react";
-import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
+import { MutableRefObject, useRef, useState } from "react";
import { LucideIcon, X } from "lucide-react";
import { IIssueLabel } from "@plane/types";
//ui
@@ -24,13 +23,13 @@ interface ILabelItemBlock {
label: IIssueLabel;
isDragging: boolean;
customMenuItems: ICustomMenuItem[];
- dragHandleProps: DraggableProvidedDragHandleProps;
handleLabelDelete: (label: IIssueLabel) => void;
isLabelGroup?: boolean;
+ dragHandleRef: MutableRefObject;
}
export const LabelItemBlock = (props: ILabelItemBlock) => {
- const { label, isDragging, customMenuItems, dragHandleProps, handleLabelDelete, isLabelGroup } = props;
+ const { label, isDragging, customMenuItems, handleLabelDelete, isLabelGroup, dragHandleRef } = props;
// states
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
@@ -41,7 +40,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
return (
-
+
diff --git a/web/components/labels/label-drag-n-drop-HOC.tsx b/web/components/labels/label-drag-n-drop-HOC.tsx
new file mode 100644
index 0000000000..3898d6b7a3
--- /dev/null
+++ b/web/components/labels/label-drag-n-drop-HOC.tsx
@@ -0,0 +1,161 @@
+import { MutableRefObject, useEffect, useRef, useState } from "react";
+import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
+import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
+import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
+import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
+import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
+import { observer } from "mobx-react";
+import { createRoot } from "react-dom/client";
+// types
+import { IIssueLabel, InstructionType } from "@plane/types";
+// ui
+import { DropIndicator } from "@plane/ui";
+// components
+import { LabelName } from "./label-block/label-name";
+import { TargetData, getCanDrop, getInstructionFromPayload } from "./label-utils";
+
+type LabelDragPreviewProps = {
+ label: IIssueLabel;
+ isGroup: boolean;
+};
+
+export const LabelDragPreview = (props: LabelDragPreviewProps) => {
+ const { label, isGroup } = props;
+
+ return (
+
+
+
+ );
+};
+
+type Props = {
+ label: IIssueLabel;
+ isGroup: boolean;
+ isChild: boolean;
+ isLastChild: boolean;
+ children: (
+ isDragging: boolean,
+ isDroppingInLabel: boolean,
+ dragHandleRef: MutableRefObject
+ ) => JSX.Element;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
+};
+
+export const LabelDndHOC = observer((props: Props) => {
+ const { label, isGroup, isChild, isLastChild, children, onDrop } = props;
+
+ const [isDragging, setIsDragging] = useState(false);
+ const [instruction, setInstruction] = useState(undefined);
+ // refs
+ const labelRef = useRef(null);
+ const dragHandleRef = useRef(null);
+
+ useEffect(() => {
+ const element = labelRef.current;
+ const dragHandleElement = dragHandleRef.current;
+
+ if (!element) return;
+
+ return combine(
+ draggable({
+ element,
+ dragHandle: dragHandleElement ?? undefined,
+ getInitialData: () => ({ id: label?.id, parentId: label?.parent, isGroup, isChild }),
+ onDragStart: () => {
+ setIsDragging(true);
+ },
+ onDrop: () => {
+ setIsDragging(false);
+ },
+ onGenerateDragPreview: ({ nativeSetDragImage }) => {
+ setCustomNativeDragPreview({
+ getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
+ render: ({ container }) => {
+ const root = createRoot(container);
+ root.render( );
+ return () => root.unmount();
+ },
+ nativeSetDragImage,
+ });
+ },
+ }),
+ dropTargetForElements({
+ element,
+ canDrop: ({ source }) => getCanDrop(source, label, isChild),
+ getData: ({ input, element }) => {
+ const data = { id: label?.id, parentId: label?.parent, isGroup, isChild };
+
+ const blockedStates: InstructionType[] = [];
+
+ // if is currently a child then block make-child instruction
+ if (isChild) blockedStates.push("make-child");
+ // if is currently is not a last child then block reorder-below instruction
+ if (!isLastChild) blockedStates.push("reorder-below");
+
+ return attachInstruction(data, {
+ input,
+ element,
+ currentLevel: isChild ? 1 : 0,
+ indentPerLevel: 0,
+ mode: isLastChild ? "last-in-group" : "standard",
+ block: blockedStates,
+ });
+ },
+ onDrag: ({ self, source, location }) => {
+ const instruction = getInstructionFromPayload(self, source, location);
+ setInstruction(instruction);
+ },
+ onDragLeave: () => {
+ setInstruction(undefined);
+ },
+ onDrop: ({ source, location }) => {
+ setInstruction(undefined);
+
+ const dropTargets = location?.current?.dropTargets ?? [];
+
+ if (isChild || !dropTargets || dropTargets.length <= 0) return;
+
+ // if the label is dropped on both a child and it's parent at the same time then get only the child's drop target
+ const dropTarget =
+ dropTargets.length > 1 ? dropTargets.find((target) => target?.data?.isChild) : dropTargets[0];
+
+ let parentId: string | null = null,
+ dropAtEndOfList = false;
+
+ const dropTargetData = dropTarget?.data as TargetData;
+
+ if (!dropTarget || !dropTargetData) return;
+
+ // get possible instructions for the dropTarget
+ const instruction = getInstructionFromPayload(dropTarget, source, location);
+
+ // if instruction is make child the set parentId as current dropTarget Id or else set it as dropTarget's parentId
+ parentId = instruction === "make-child" ? dropTargetData.id : dropTargetData.parentId;
+ // if instruction is any other than make-child, i.e., reorder-above and reorder-below then set the droppedId as dropTarget's id
+ const droppedLabelId = instruction !== "make-child" ? dropTargetData.id : undefined;
+ // if instruction is to reorder-below that is enabled only for end of the last items in the list then dropAtEndOfList as true
+ if (instruction === "reorder-below") dropAtEndOfList = true;
+
+ const sourceData = source.data as TargetData;
+ if (sourceData.id) onDrop(sourceData.id as string, parentId, droppedLabelId, dropAtEndOfList);
+ },
+ })
+ );
+ }, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
+
+ const isMakeChild = instruction == "make-child";
+
+ return (
+
+
+ {children(isDragging, isMakeChild, dragHandleRef)}
+ {isLastChild && }
+
+ );
+});
diff --git a/web/components/labels/label-utils.ts b/web/components/labels/label-utils.ts
new file mode 100644
index 0000000000..1919e53f3d
--- /dev/null
+++ b/web/components/labels/label-utils.ts
@@ -0,0 +1,67 @@
+import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
+import { IIssueLabel, IPragmaticPayloadLocation, InstructionType, TDropTarget } from "@plane/types";
+
+export type TargetData = {
+ id: string;
+ parentId: string | null;
+ isGroup: boolean;
+ isChild: boolean;
+};
+
+/**
+ * extracts the Payload and translates the instruction for the current dropTarget based on drag and drop payload
+ * @param dropTarget dropTarget for which the instruction is required
+ * @param source the dragging label data that is being dragged on the dropTarget
+ * @param location location includes the data of all the dropTargets the source is being dragged on
+ * @returns Instruction for dropTarget
+ */
+export const getInstructionFromPayload = (
+ dropTarget: TDropTarget,
+ source: TDropTarget,
+ location: IPragmaticPayloadLocation
+): InstructionType | undefined => {
+ const dropTargetData = dropTarget?.data as TargetData;
+ const sourceData = source?.data as TargetData;
+
+ const allDropTargets = location?.current?.dropTargets;
+
+ // if all the dropTargets are greater than 1 meaning the source is being dragged on a group and its child at the same time
+ // and also if the dropTarget in question is also a group then, it should be a child of the current Droptarget
+ if (allDropTargets?.length > 1 && dropTargetData?.isGroup) return "make-child";
+
+ if (!dropTargetData || !sourceData) return undefined;
+
+ let instruction = extractInstruction(dropTargetData)?.type;
+
+ // If the instruction is blocked then set an instruction based on if dropTarget it is a child or not
+ if (instruction === "instruction-blocked") {
+ instruction = dropTargetData.isChild ? "reorder-above" : "make-child";
+ }
+
+ // if source that is being dragged is a group. A group cannon be a child of any other label,
+ // hence if current instruction is to be a child of dropTarget then reorder-above instead
+ if (instruction === "make-child" && sourceData.isGroup) instruction = "reorder-above";
+
+ return instruction;
+};
+
+/**
+ * This provides a boolean to indicate if the label can be dropped onto the droptarget
+ * @param source
+ * @param label
+ * @param isCurrentChild if the dropTarget is a child
+ * @returns
+ */
+export const getCanDrop = (source: TDropTarget, label: IIssueLabel | undefined, isCurrentChild: boolean) => {
+ const sourceData = source?.data;
+
+ if (!sourceData) return false;
+
+ // a label cannot be dropped on to itself and it's parent cannon be dropped on the child
+ if (sourceData.id === label?.id || sourceData.id === label?.parent) return false;
+
+ // if current dropTarget is a child and the label being dropped is a group then don't enable drop
+ if (isCurrentChild && sourceData.isGroup) return false;
+
+ return true;
+};
diff --git a/web/components/labels/project-setting-label-group.tsx b/web/components/labels/project-setting-label-group.tsx
index 0a3efc9df6..3302cb6bf1 100644
--- a/web/components/labels/project-setting-label-group.tsx
+++ b/web/components/labels/project-setting-label-group.tsx
@@ -1,51 +1,38 @@
import React, { Dispatch, SetStateAction, useState } from "react";
-import {
- Draggable,
- DraggableProvided,
- DraggableProvidedDragHandleProps,
- DraggableStateSnapshot,
- Droppable,
-} from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
-
// store
// icons
-import { IIssueLabel } from "@plane/types";
// types
-import useDraggableInPortal from "@/hooks/use-draggable-portal";
+import { IIssueLabel } from "@plane/types";
+// components
import { CreateUpdateLabelInline } from "./create-update-label-inline";
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
+import { LabelDndHOC } from "./label-drag-n-drop-HOC";
import { ProjectSettingLabelItem } from "./project-setting-label-item";
type Props = {
label: IIssueLabel;
labelChildren: IIssueLabel[];
handleLabelDelete: (label: IIssueLabel) => void;
- dragHandleProps: DraggableProvidedDragHandleProps;
- draggableSnapshot: DraggableStateSnapshot;
isUpdating: boolean;
setIsUpdating: Dispatch>;
- isDropDisabled: boolean;
+ isLastChild: boolean;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
};
export const ProjectSettingLabelGroup: React.FC = observer((props) => {
- const {
- label,
- labelChildren,
- handleLabelDelete,
- draggableSnapshot: groupDragSnapshot,
- dragHandleProps,
- isUpdating,
- setIsUpdating,
- isDropDisabled,
- } = props;
+ const { label, labelChildren, handleLabelDelete, isUpdating, setIsUpdating, isLastChild, onDrop } = props;
+ // states
const [isEditLabelForm, setEditLabelForm] = useState(false);
- const renderDraggable = useDraggableInPortal();
-
const customMenuItems: ICustomMenuItem[] = [
{
CustomIcon: Pencil,
@@ -67,101 +54,89 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => {
];
return (
-
- {({ open }) => (
- <>
-
+ {(isDragging, isDroppingInLabel, dragHandleRef) => (
+
+
- {(droppableProvided) => (
-
- <>
-
- {isEditLabelForm ? (
-
{
- setEditLabelForm(false);
- setIsUpdating(false);
- }}
- />
- ) : (
-
- )}
-
-
-
- (
+ <>
+
+ <>
+
+ {isEditLabelForm ? (
+ {
+ setEditLabelForm(false);
+ setIsUpdating(false);
+ }}
/>
-
-
-
-
-
-
- {labelChildren.map((child, index) => (
-
-
- {renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
-
-
handleLabelDelete(child)}
- draggableSnapshot={snapshot}
- dragHandleProps={provided.dragHandleProps!}
- setIsUpdating={setIsUpdating}
- isChild
- />
-
- ))}
-
-
- ))}
-
-
-
- {droppableProvided.placeholder}
- >
-
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {labelChildren.map((child, index) => (
+
+
+
handleLabelDelete(child)}
+ setIsUpdating={setIsUpdating}
+ isParentDragging={isDragging}
+ isChild
+ isLastChild={index === labelChildren.length - 1}
+ onDrop={onDrop}
+ />
+
+
+ ))}
+
+
+
+ >
+
+ >
)}
-
- >
+
+
)}
-
+
);
});
diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx
index d8bbee7191..0a596fe413 100644
--- a/web/components/labels/project-setting-label-item.tsx
+++ b/web/components/labels/project-setting-label-item.tsx
@@ -1,27 +1,32 @@
import React, { Dispatch, SetStateAction, useState } from "react";
-import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { useRouter } from "next/router";
import { X, Pencil } from "lucide-react";
+// types
import { IIssueLabel } from "@plane/types";
// hooks
import { useLabel } from "@/hooks/store";
-// types
// components
import { CreateUpdateLabelInline } from "./create-update-label-inline";
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
+import { LabelDndHOC } from "./label-drag-n-drop-HOC";
type Props = {
label: IIssueLabel;
handleLabelDelete: (label: IIssueLabel) => void;
- draggableSnapshot: DraggableStateSnapshot;
- dragHandleProps: DraggableProvidedDragHandleProps;
setIsUpdating: Dispatch>;
+ isParentDragging?: boolean;
isChild: boolean;
+ isLastChild: boolean;
+ onDrop: (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => void;
};
export const ProjectSettingLabelItem: React.FC = (props) => {
- const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props;
- const { combineTargetFor, isDragging } = draggableSnapshot;
+ const { label, setIsUpdating, handleLabelDelete, isChild, isLastChild, isParentDragging = false, onDrop } = props;
// states
const [isEditLabelForm, setEditLabelForm] = useState(false);
// router
@@ -59,31 +64,39 @@ export const ProjectSettingLabelItem: React.FC = (props) => {
];
return (
-
- {isEditLabelForm ? (
-
{
- setEditLabelForm(false);
- setIsUpdating(false);
- }}
- />
- ) : (
-
+
+ {(isDragging, isDroppingInLabel, dragHandleRef) => (
+
+
+ {isEditLabelForm ? (
+ {
+ setEditLabelForm(false);
+ setIsUpdating(false);
+ }}
+ />
+ ) : (
+
+ )}
+
+
)}
-
+
);
};
diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx
index bbc4023d10..8de57e8b20 100644
--- a/web/components/labels/project-setting-label-list.tsx
+++ b/web/components/labels/project-setting-label-list.tsx
@@ -1,12 +1,4 @@
import React, { useState, useRef } from "react";
-import {
- DragDropContext,
- Draggable,
- DraggableProvided,
- DraggableStateSnapshot,
- DropResult,
- Droppable,
-} from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { IIssueLabel } from "@plane/types";
@@ -21,20 +13,16 @@ import {
} from "@/components/labels";
import { EmptyStateType } from "@/constants/empty-state";
import { useLabel } from "@/hooks/store";
-import useDraggableInPortal from "@/hooks/use-draggable-portal";
// components
// ui
// types
// constants
-const LABELS_ROOT = "labels.root";
-
export const ProjectSettingsLabelList: React.FC = observer(() => {
// states
const [showLabelForm, setLabelForm] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [selectDeleteLabel, setSelectDeleteLabel] = useState(null);
- const [isDraggingGroup, setIsDraggingGroup] = useState(false);
// refs
const scrollToRef = useRef(null);
// router
@@ -42,44 +30,28 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const { workspaceSlug, projectId } = router.query;
// store hooks
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
- // portal
- const renderDraggable = useDraggableInPortal();
const newLabel = () => {
setIsUpdating(false);
setLabelForm(true);
};
- const onDragEnd = (result: DropResult) => {
- const { combine, draggableId, destination, source } = result;
-
- // return if dropped outside the DragDropContext
- if (!combine && !destination) return;
-
- const childLabel = draggableId.split(".")[2];
- let parentLabel: string | undefined | null = destination?.droppableId?.split(".")[3];
- const index = destination?.index || 0;
-
- const prevParentLabel: string | undefined | null = source?.droppableId?.split(".")[3];
- const prevIndex = source?.index;
-
- if (combine && combine.draggableId) parentLabel = combine?.draggableId?.split(".")[2];
-
- if (destination?.droppableId === LABELS_ROOT) parentLabel = null;
-
- if (result.reason == "DROP" && childLabel != parentLabel) {
- if (workspaceSlug && projectId) {
- updateLabelPosition(
- workspaceSlug?.toString(),
- projectId?.toString(),
- childLabel,
- parentLabel,
- index,
- prevParentLabel == parentLabel,
- prevIndex
- );
- return;
- }
+ const onDrop = (
+ draggingLabelId: string,
+ droppedParentId: string | null,
+ droppedLabelId: string | undefined,
+ dropAtEndOfList: boolean
+ ) => {
+ if (workspaceSlug && projectId) {
+ updateLabelPosition(
+ workspaceSlug?.toString(),
+ projectId?.toString(),
+ draggingLabelId,
+ droppedParentId,
+ droppedLabelId,
+ dropAtEndOfList
+ );
+ return;
}
};
@@ -118,82 +90,35 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
) : (
projectLabelsTree && (
-
-
- {(droppableProvided, droppableSnapshot) => (
-
- {projectLabelsTree.map((label, index) => {
- if (label.children && label.children.length) {
- return (
-
- {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
- const isGroup = droppableSnapshot.draggingFromThisWith?.split(".")[3] === "group";
- setIsDraggingGroup(isGroup);
-
- return (
-
-
setSelectDeleteLabel(label)}
- draggableSnapshot={snapshot}
- isUpdating={isUpdating}
- setIsUpdating={setIsUpdating}
- />
-
- );
- }}
-
- );
- }
- return (
-
- {renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
-
-
setSelectDeleteLabel(label)}
- isChild={false}
- />
-
- ))}
-
- );
- })}
- {droppableProvided.placeholder}
-
- )}
-
-
+
+ {projectLabelsTree.map((label, index) => {
+ if (label.children && label.children.length) {
+ return (
+
setSelectDeleteLabel(label)}
+ isUpdating={isUpdating}
+ setIsUpdating={setIsUpdating}
+ isLastChild={index === projectLabelsTree.length - 1}
+ onDrop={onDrop}
+ />
+ );
+ }
+ return (
+ setSelectDeleteLabel(label)}
+ isChild={false}
+ isLastChild={index === projectLabelsTree.length - 1}
+ onDrop={onDrop}
+ />
+ );
+ })}
+
)
)
) : (
diff --git a/web/components/modules/dropdowns/order-by.tsx b/web/components/modules/dropdowns/order-by.tsx
index dc309a97ac..d6fd3548b2 100644
--- a/web/components/modules/dropdowns/order-by.tsx
+++ b/web/components/modules/dropdowns/order-by.tsx
@@ -46,7 +46,7 @@ export const ModuleOrderByDropdown: React.FC = (props) => {
{value?.includes(option.key) && }
))}
-
+
{
diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx
index b0ff676092..82d63fdcc3 100644
--- a/web/components/modules/modules-list-view.tsx
+++ b/web/components/modules/modules-list-view.tsx
@@ -1,16 +1,13 @@
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useRouter } from "next/router";
-// hooks
// components
import { EmptyState } from "@/components/empty-state";
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
-// ui
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
-// assets
// constants
import { EmptyStateType } from "@/constants/empty-state";
-import { calculateTotalFilters } from "@/helpers/filter.helper";
+// hooks
import { useApplication, useEventTracker, useModule, useModuleFilter } from "@/hooks/store";
import AllFiltersImage from "public/empty-state/module/all-filters.svg";
import NameFilterImage from "public/empty-state/module/name-filter.svg";
@@ -22,14 +19,13 @@ export const ModulesListView: React.FC = observer(() => {
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker();
- const { getFilteredModuleIds, loader } = useModule();
- const { currentProjectDisplayFilters: displayFilters, searchQuery, currentProjectFilters } = useModuleFilter();
+ const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule();
+ const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter();
// derived values
+ const projectModuleIds = projectId ? getProjectModuleIds(projectId.toString()) : undefined;
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
- const totalFilters = calculateTotalFilters(currentProjectFilters ?? {});
-
- if (loader || !filteredModuleIds)
+ if (loader || !projectModuleIds || !filteredModuleIds)
return (
<>
{displayFilters?.layout === "list" && }
@@ -38,7 +34,18 @@ export const ModulesListView: React.FC = observer(() => {
>
);
- if (totalFilters > 0 && filteredModuleIds.length === 0)
+ if (projectModuleIds.length === 0)
+ return (
+ {
+ setTrackElement("Module empty state");
+ commandPaletteStore.toggleCreateModuleModal(true);
+ }}
+ />
+ );
+
+ if (filteredModuleIds.length === 0)
return (
@@ -59,55 +66,43 @@ export const ModulesListView: React.FC = observer(() => {
return (
<>
- {filteredModuleIds.length > 0 ? (
- <>
- {displayFilters?.layout === "list" && (
-
-
-
- {filteredModuleIds.map((moduleId) => (
-
- ))}
-
-
-
+ {displayFilters?.layout === "list" && (
+
+
+
+ {filteredModuleIds.map((moduleId) => (
+
+ ))}
- )}
- {displayFilters?.layout === "board" && (
-
-
-
- {filteredModuleIds.map((moduleId) => (
-
- ))}
-
-
-
-
- )}
- {displayFilters?.layout === "gantt" &&
}
- >
- ) : (
-
{
- setTrackElement("Module empty state");
- commandPaletteStore.toggleCreateModuleModal(true);
- }}
- />
+
+
+
)}
+ {displayFilters?.layout === "board" && (
+
+
+
+ {filteredModuleIds.map((moduleId) => (
+
+ ))}
+
+
+
+
+ )}
+ {displayFilters?.layout === "gantt" &&
}
>
);
});
diff --git a/web/components/modules/quick-actions.tsx b/web/components/modules/quick-actions.tsx
index a6938c10a9..a1805937e8 100644
--- a/web/components/modules/quick-actions.tsx
+++ b/web/components/modules/quick-actions.tsx
@@ -163,15 +163,14 @@ export const ModuleQuickActions: React.FC
= observer((props) => {
)}
+
{isEditingAllowed && (
-
-
-
-
- Delete module
-
-
-
+
+
+
+ Delete module
+
+
)}
>
diff --git a/web/components/pages/editor/editor-body.tsx b/web/components/pages/editor/editor-body.tsx
index 4c1f68cc6f..701c7c3491 100644
--- a/web/components/pages/editor/editor-body.tsx
+++ b/web/components/pages/editor/editor-body.tsx
@@ -121,7 +121,7 @@ export const PageEditorBody: React.FC = observer((props) => {
})}
>
-
+
;
@@ -13,27 +16,51 @@ type Props = {
export const PageEditorTitle: React.FC = observer((props) => {
const { editorRef, readOnly, title, updateTitle } = props;
+ // states
+ const [isLengthVisible, setIsLengthVisible] = useState(false);
return (
<>
{readOnly ? (
{title}
) : (
-