mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'preview' of github.com:makeplane/plane into preview
This commit is contained in:
commit
f09dd3d782
@ -241,9 +241,9 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issues = user_timezone_converter(
|
issues = user_timezone_converter(
|
||||||
issue_queryset, datetime_fields, request.user.user_timezone
|
issues, datetime_fields, request.user.user_timezone
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
@ -447,7 +447,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issues = user_timezone_converter(
|
issues = user_timezone_converter(
|
||||||
issue_queryset, datetime_fields, request.user.user_timezone
|
issues, datetime_fields, request.user.user_timezone
|
||||||
)
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issues = user_timezone_converter(
|
issues = user_timezone_converter(
|
||||||
issue_queryset, datetime_fields, request.user.user_timezone
|
issues, datetime_fields, request.user.user_timezone
|
||||||
)
|
)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@ -8,14 +8,14 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
|||||||
if isinstance(queryset, dict):
|
if isinstance(queryset, dict):
|
||||||
queryset_values = [queryset]
|
queryset_values = [queryset]
|
||||||
else:
|
else:
|
||||||
queryset_values = list(queryset.values())
|
queryset_values = list(queryset)
|
||||||
|
|
||||||
# Iterate over the dictionaries in the list
|
# Iterate over the dictionaries in the list
|
||||||
for item in queryset_values:
|
for item in queryset_values:
|
||||||
# Iterate over the datetime fields
|
# Iterate over the datetime fields
|
||||||
for field in datetime_fields:
|
for field in datetime_fields:
|
||||||
# Convert the datetime field to the user's timezone
|
# Convert the datetime field to the user's timezone
|
||||||
if item[field]:
|
if field in item and item[field]:
|
||||||
item[field] = item[field].astimezone(user_tz)
|
item[field] = item[field].astimezone(user_tz)
|
||||||
|
|
||||||
# If queryset was a single item, return a single item
|
# If queryset was a single item, return a single item
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
"jsx-dom-cjs": "^8.0.3",
|
"jsx-dom-cjs": "^8.0.3",
|
||||||
"linkifyjs": "^4.1.3",
|
"linkifyjs": "^4.1.3",
|
||||||
"lowlight": "^3.0.0",
|
"lowlight": "^3.0.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.378.0",
|
||||||
"prosemirror-codemark": "^0.4.2",
|
"prosemirror-codemark": "^0.4.2",
|
||||||
"react-moveable": "^0.54.2",
|
"react-moveable": "^0.54.2",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
|
@ -5,6 +5,7 @@ import ts from "highlight.js/lib/languages/typescript";
|
|||||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
import { cn } from "src/lib/utils";
|
import { cn } from "src/lib/utils";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
|
|
||||||
// we just have ts support for now
|
// we just have ts support for now
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
@ -30,23 +31,25 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="code-block relative">
|
<NodeViewWrapper className="code-block relative group/code">
|
||||||
<button
|
<Tooltip tooltipContent="Copy code">
|
||||||
type="button"
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"group absolute top-2 right-2 z-10 flex items-center justify-center w-8 h-8 rounded-md bg-custom-background-80 border border-custom-border-200 transition duration-150 ease-in-out",
|
className={cn(
|
||||||
{
|
"group/button hidden group-hover/code:flex items-center justify-center absolute top-2 right-2 z-10 size-8 rounded-md bg-custom-background-80 border border-custom-border-200 transition duration-150 ease-in-out",
|
||||||
"bg-green-500/10 hover:bg-green-500/10 active:bg-green-500/10": copied,
|
{
|
||||||
}
|
"bg-green-500/10 hover:bg-green-500/10 active:bg-green-500/10": copied,
|
||||||
)}
|
}
|
||||||
onClick={copyToClipboard}
|
)}
|
||||||
>
|
onClick={copyToClipboard}
|
||||||
{copied ? (
|
>
|
||||||
<CheckIcon className="h-3 w-3 text-green-500" strokeWidth={3} />
|
{copied ? (
|
||||||
) : (
|
<CheckIcon className="h-3 w-3 text-green-500" strokeWidth={3} />
|
||||||
<CopyIcon className="h-3 w-3 text-custom-text-300 group-hover:text-custom-text-100" />
|
) : (
|
||||||
)}
|
<CopyIcon className="h-3 w-3 text-custom-text-300 group-hover/button:text-custom-text-100" />
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4">
|
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4">
|
||||||
<NodeViewContent as="code" className="whitespace-[pre-wrap]" />
|
<NodeViewContent as="code" className="whitespace-[pre-wrap]" />
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"@tiptap/core": "^2.1.13",
|
"@tiptap/core": "^2.1.13",
|
||||||
"@tiptap/pm": "^2.1.13",
|
"@tiptap/pm": "^2.1.13",
|
||||||
"@tiptap/suggestion": "^2.1.13",
|
"@tiptap/suggestion": "^2.1.13",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.378.0",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"@tiptap/pm": "^2.1.13",
|
"@tiptap/pm": "^2.1.13",
|
||||||
"@tiptap/react": "^2.1.13",
|
"@tiptap/react": "^2.1.13",
|
||||||
"@tiptap/suggestion": "^2.1.13",
|
"@tiptap/suggestion": "^2.1.13",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.378.0",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@plane/editor-extensions": "*",
|
"@plane/editor-extensions": "*",
|
||||||
"@tiptap/core": "^2.1.13",
|
"@tiptap/core": "^2.1.13",
|
||||||
"lucide-react": "^0.294.0"
|
"lucide-react": "^0.378.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.15.3",
|
"@types/node": "18.15.3",
|
||||||
|
@ -8,7 +8,8 @@ export type TModuleOrderByOptions =
|
|||||||
| "target_date"
|
| "target_date"
|
||||||
| "-target_date"
|
| "-target_date"
|
||||||
| "created_at"
|
| "created_at"
|
||||||
| "-created_at";
|
| "-created_at"
|
||||||
|
| "sort_order";
|
||||||
|
|
||||||
export type TModuleLayoutOptions = "list" | "board" | "gantt";
|
export type TModuleLayoutOptions = "list" | "board" | "gantt";
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lowlight": "^2.9.0",
|
"lowlight": "^2.9.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.378.0",
|
||||||
"mobx": "^6.10.0",
|
"mobx": "^6.10.0",
|
||||||
"mobx-react-lite": "^4.0.3",
|
"mobx-react-lite": "^4.0.3",
|
||||||
"next": "^14.0.3",
|
"next": "^14.0.3",
|
||||||
|
@ -200,6 +200,11 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
|
|
||||||
const keyPressed = key.toLowerCase();
|
const keyPressed = key.toLowerCase();
|
||||||
const cmdClicked = ctrlKey || metaKey;
|
const cmdClicked = ctrlKey || metaKey;
|
||||||
|
|
||||||
|
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleCommandPaletteModal(true);
|
||||||
|
}
|
||||||
// if on input, textarea or editor, don't do anything
|
// if on input, textarea or editor, don't do anything
|
||||||
if (
|
if (
|
||||||
e.target instanceof HTMLTextAreaElement ||
|
e.target instanceof HTMLTextAreaElement ||
|
||||||
@ -209,10 +214,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (cmdClicked) {
|
if (cmdClicked) {
|
||||||
if (keyPressed === "k") {
|
if (keyPressed === "c" && altKey) {
|
||||||
e.preventDefault();
|
|
||||||
toggleCommandPaletteModal(true);
|
|
||||||
} else if (keyPressed === "c" && altKey) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
copyIssueUrlToClipboard();
|
copyIssueUrlToClipboard();
|
||||||
} else if (keyPressed === "b") {
|
} else if (keyPressed === "b") {
|
||||||
|
@ -26,7 +26,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||||||
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
|
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
|
||||||
const { searchQuery } = useCycleFilter();
|
const { searchQuery } = useCycleFilter();
|
||||||
// derived values
|
// derived values
|
||||||
const filteredCycleIds = getFilteredCycleIds(projectId);
|
const filteredCycleIds = getFilteredCycleIds(projectId, layout === "gantt");
|
||||||
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
||||||
|
|
||||||
if (loader || !filteredCycleIds)
|
if (loader || !filteredCycleIds)
|
||||||
|
@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
enableBlockLeftResize={false}
|
enableBlockLeftResize={false}
|
||||||
enableBlockRightResize={false}
|
enableBlockRightResize={false}
|
||||||
enableBlockMove={false}
|
enableBlockMove={false}
|
||||||
enableReorder={false}
|
enableReorder
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
@ -62,6 +64,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
||||||
// chart hook
|
// chart hook
|
||||||
const { currentView, currentViewData } = useGanttChart();
|
const { currentView, currentViewData } = useGanttChart();
|
||||||
|
|
||||||
|
// Enable Auto Scroll for Ganttlist
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ganttContainerRef.current;
|
||||||
|
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
autoScrollForElements({
|
||||||
|
element,
|
||||||
|
getAllowedAxis: () => "vertical",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [ganttContainerRef?.current]);
|
||||||
// handling scroll functionality
|
// handling scroll functionality
|
||||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||||
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
import { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -16,12 +16,12 @@ import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
|||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
provided: DraggableProvided;
|
isDragging: boolean;
|
||||||
snapshot: DraggableStateSnapshot;
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
const { block, enableReorder, provided, snapshot } = props;
|
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||||
|
|
||||||
@ -30,12 +30,10 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded bg-custom-background-80": snapshot.isDragging,
|
"rounded bg-custom-background-80": isDragging,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
@ -50,7 +48,7 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
||||||
{...provided.dragHandleProps}
|
ref={dragHandleRef}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-3.5 w-3.5" />
|
<MoreVertical className="h-3.5 w-3.5" />
|
||||||
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
import { MutableRefObject } from "react";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
||||||
|
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||||
|
import { handleOrderChange } from "../utils";
|
||||||
import { CyclesSidebarBlock } from "./block";
|
import { CyclesSidebarBlock } from "./block";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
@ -16,85 +18,43 @@ type Props = {
|
|||||||
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||||
const { blockUpdateHandler, blocks, enableReorder } = props;
|
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||||
|
|
||||||
const handleOrderChange = (result: DropResult) => {
|
const handleOnDrop = (
|
||||||
if (!blocks) return;
|
draggingBlockId: string | undefined,
|
||||||
|
droppedBlockId: string | undefined,
|
||||||
const { source, destination } = result;
|
dropAtEndOfList: boolean
|
||||||
|
) => {
|
||||||
// return if dropped outside the list
|
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||||
if (!destination) return;
|
|
||||||
|
|
||||||
// return if dropped on the same index
|
|
||||||
if (source.index === destination.index) return;
|
|
||||||
|
|
||||||
let updatedSortOrder = blocks[source.index].sort_order;
|
|
||||||
|
|
||||||
// update the sort order to the lowest if dropped at the top
|
|
||||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
|
||||||
// update the sort order to the highest if dropped at the bottom
|
|
||||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
|
||||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
|
||||||
else {
|
|
||||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
|
||||||
const relativeDestinationSortingOrder =
|
|
||||||
source.index < destination.index
|
|
||||||
? blocks[destination.index + 1].sort_order
|
|
||||||
: blocks[destination.index - 1].sort_order;
|
|
||||||
|
|
||||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
|
||||||
const removedElement = blocks.splice(source.index, 1)[0];
|
|
||||||
blocks.splice(destination.index, 0, removedElement);
|
|
||||||
|
|
||||||
// call the block update handler with the updated sort order, new and old index
|
|
||||||
blockUpdateHandler(removedElement.data, {
|
|
||||||
sort_order: {
|
|
||||||
destinationIndex: destination.index,
|
|
||||||
newSortOrder: updatedSortOrder,
|
|
||||||
sourceIndex: source.index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<div className="h-full">
|
||||||
<Droppable droppableId="gantt-sidebar">
|
{blocks ? (
|
||||||
{(droppableProvided) => (
|
blocks.map((block, index) => (
|
||||||
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
<GanttDnDHOC
|
||||||
<>
|
key={block.id}
|
||||||
{blocks ? (
|
id={block.id}
|
||||||
blocks.map((block, index) => (
|
isLastChild={index === blocks.length - 1}
|
||||||
<Draggable
|
isDragEnabled={enableReorder}
|
||||||
key={`sidebar-block-${block.id}`}
|
onDrop={handleOnDrop}
|
||||||
draggableId={`sidebar-block-${block.id}`}
|
>
|
||||||
index={index}
|
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||||
isDragDisabled={!enableReorder}
|
<CyclesSidebarBlock
|
||||||
>
|
block={block}
|
||||||
{(provided, snapshot) => (
|
enableReorder={enableReorder}
|
||||||
<CyclesSidebarBlock
|
isDragging={isDragging}
|
||||||
block={block}
|
dragHandleRef={dragHandleRef}
|
||||||
enableReorder={enableReorder}
|
/>
|
||||||
provided={provided}
|
)}
|
||||||
snapshot={snapshot}
|
</GanttDnDHOC>
|
||||||
/>
|
))
|
||||||
)}
|
) : (
|
||||||
</Draggable>
|
<Loader className="space-y-3 pr-2">
|
||||||
))
|
<Loader.Item height="34px" />
|
||||||
) : (
|
<Loader.Item height="34px" />
|
||||||
<Loader className="space-y-3 pr-2">
|
<Loader.Item height="34px" />
|
||||||
<Loader.Item height="34px" />
|
<Loader.Item height="34px" />
|
||||||
<Loader.Item height="34px" />
|
</Loader>
|
||||||
<Loader.Item height="34px" />
|
)}
|
||||||
<Loader.Item height="34px" />
|
</div>
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
{droppableProvided.placeholder}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
104
web/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
104
web/components/gantt-chart/sidebar/gantt-dnd-HOC.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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 { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { DropIndicator } from "@plane/ui";
|
||||||
|
import { HIGHLIGHT_WITH_LINE, highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||||
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
isLastChild: boolean;
|
||||||
|
isDragEnabled: boolean;
|
||||||
|
children: (isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => JSX.Element;
|
||||||
|
onDrop: (draggingBlockId: string | undefined, droppedBlockId: string | undefined, dropAtEndOfList: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttDnDHOC = observer((props: Props) => {
|
||||||
|
const { id, isLastChild, children, onDrop, isDragEnabled } = props;
|
||||||
|
// states
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined);
|
||||||
|
// refs
|
||||||
|
const blockRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = blockRef.current;
|
||||||
|
const dragHandleElement = dragHandleRef.current;
|
||||||
|
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
draggable({
|
||||||
|
element,
|
||||||
|
canDrag: () => isDragEnabled,
|
||||||
|
dragHandle: dragHandleElement ?? undefined,
|
||||||
|
getInitialData: () => ({ id }),
|
||||||
|
onDragStart: () => {
|
||||||
|
setIsDragging(true);
|
||||||
|
},
|
||||||
|
onDrop: () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element,
|
||||||
|
canDrop: ({ source }) => source?.data?.id !== id,
|
||||||
|
getData: ({ input, element }) => {
|
||||||
|
const data = { id };
|
||||||
|
|
||||||
|
// attach instruction for last in list
|
||||||
|
return attachInstruction(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
currentLevel: 0,
|
||||||
|
indentPerLevel: 0,
|
||||||
|
mode: isLastChild ? "last-in-group" : "standard",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDrag: ({ self }) => {
|
||||||
|
const extractedInstruction = extractInstruction(self?.data)?.type;
|
||||||
|
// check if the highlight is to be shown above or below
|
||||||
|
setInstruction(
|
||||||
|
extractedInstruction
|
||||||
|
? extractedInstruction === "reorder-below" && isLastChild
|
||||||
|
? "DRAG_BELOW"
|
||||||
|
: "DRAG_OVER"
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDragLeave: () => {
|
||||||
|
setInstruction(undefined);
|
||||||
|
},
|
||||||
|
onDrop: ({ self, source }) => {
|
||||||
|
setInstruction(undefined);
|
||||||
|
const extractedInstruction = extractInstruction(self?.data)?.type;
|
||||||
|
const currentInstruction = extractedInstruction
|
||||||
|
? extractedInstruction === "reorder-below" && isLastChild
|
||||||
|
? "DRAG_BELOW"
|
||||||
|
: "DRAG_OVER"
|
||||||
|
: undefined;
|
||||||
|
if (!currentInstruction) return;
|
||||||
|
|
||||||
|
const sourceId = source?.data?.id as string | undefined;
|
||||||
|
const destinationId = self?.data?.id as string | undefined;
|
||||||
|
|
||||||
|
onDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW");
|
||||||
|
highlightIssueOnDrop(source?.element?.id, false, true);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [blockRef?.current, dragHandleRef?.current, isLastChild, onDrop]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(blockRef, () => blockRef?.current?.classList?.remove(HIGHLIGHT_WITH_LINE));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={`issue-draggable-${id}`} className={"relative"} ref={blockRef}>
|
||||||
|
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
|
||||||
|
{children(isDragging, dragHandleRef)}
|
||||||
|
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./cycles";
|
export * from "./cycles";
|
||||||
export * from "./issues";
|
export * from "./issues";
|
||||||
export * from "./modules";
|
export * from "./modules";
|
||||||
export * from "./project-views";
|
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
import React, { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -17,12 +17,12 @@ import { IGanttBlock } from "../../types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
provided: DraggableProvided;
|
isDragging: boolean;
|
||||||
snapshot: DraggableStateSnapshot;
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
|
export const IssuesSidebarBlock = observer((props: Props) => {
|
||||||
const { block, enableReorder, provided, snapshot } = props;
|
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||||
const { getIsIssuePeeked } = useIssueDetail();
|
const { getIsIssuePeeked } = useIssueDetail();
|
||||||
@ -32,15 +32,13 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded bg-custom-background-80": snapshot.isDragging,
|
"rounded bg-custom-background-80": isDragging,
|
||||||
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
|
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
|
||||||
block.data.id
|
block.data.id
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
@ -54,7 +52,7 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
||||||
{...provided.dragHandleProps}
|
ref={dragHandleRef}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-3.5 w-3.5" />
|
<MoreVertical className="h-3.5 w-3.5" />
|
||||||
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { MutableRefObject } from "react";
|
||||||
// components
|
// components
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||||
|
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||||
|
import { handleOrderChange } from "../utils";
|
||||||
import { IssuesSidebarBlock } from "./block";
|
import { IssuesSidebarBlock } from "./block";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -16,92 +18,50 @@ type Props = {
|
|||||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||||
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
|
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
|
||||||
|
|
||||||
const handleOrderChange = (result: DropResult) => {
|
const handleOnDrop = (
|
||||||
if (!blocks) return;
|
draggingBlockId: string | undefined,
|
||||||
|
droppedBlockId: string | undefined,
|
||||||
const { source, destination } = result;
|
dropAtEndOfList: boolean
|
||||||
|
) => {
|
||||||
// return if dropped outside the list
|
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||||
if (!destination) return;
|
|
||||||
|
|
||||||
// return if dropped on the same index
|
|
||||||
if (source.index === destination.index) return;
|
|
||||||
|
|
||||||
let updatedSortOrder = blocks[source.index].sort_order;
|
|
||||||
|
|
||||||
// update the sort order to the lowest if dropped at the top
|
|
||||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
|
||||||
// update the sort order to the highest if dropped at the bottom
|
|
||||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
|
||||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
|
||||||
else {
|
|
||||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
|
||||||
const relativeDestinationSortingOrder =
|
|
||||||
source.index < destination.index
|
|
||||||
? blocks[destination.index + 1].sort_order
|
|
||||||
: blocks[destination.index - 1].sort_order;
|
|
||||||
|
|
||||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
|
||||||
const removedElement = blocks.splice(source.index, 1)[0];
|
|
||||||
blocks.splice(destination.index, 0, removedElement);
|
|
||||||
|
|
||||||
// call the block update handler with the updated sort order, new and old index
|
|
||||||
blockUpdateHandler(removedElement.data, {
|
|
||||||
sort_order: {
|
|
||||||
destinationIndex: destination.index,
|
|
||||||
newSortOrder: updatedSortOrder,
|
|
||||||
sourceIndex: source.index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<div>
|
||||||
<Droppable droppableId="gantt-sidebar">
|
{blocks ? (
|
||||||
{(droppableProvided) => (
|
blocks.map((block, index) => {
|
||||||
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
|
||||||
<>
|
|
||||||
{blocks ? (
|
|
||||||
blocks.map((block, index) => {
|
|
||||||
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
|
|
||||||
|
|
||||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<GanttDnDHOC
|
||||||
key={`sidebar-block-${block.id}`}
|
key={block.id}
|
||||||
draggableId={`sidebar-block-${block.id}`}
|
id={block.id}
|
||||||
index={index}
|
isLastChild={index === blocks.length - 1}
|
||||||
isDragDisabled={!enableReorder}
|
isDragEnabled={enableReorder}
|
||||||
>
|
onDrop={handleOnDrop}
|
||||||
{(provided, snapshot) => (
|
>
|
||||||
<IssuesSidebarBlock
|
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||||
block={block}
|
<IssuesSidebarBlock
|
||||||
enableReorder={enableReorder}
|
block={block}
|
||||||
provided={provided}
|
enableReorder={enableReorder}
|
||||||
snapshot={snapshot}
|
isDragging={isDragging}
|
||||||
/>
|
dragHandleRef={dragHandleRef}
|
||||||
)}
|
/>
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-3 pr-2">
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
)}
|
||||||
{droppableProvided.placeholder}
|
</GanttDnDHOC>
|
||||||
</>
|
);
|
||||||
</div>
|
})
|
||||||
)}
|
) : (
|
||||||
</Droppable>
|
<Loader className="space-y-3 pr-2">
|
||||||
</DragDropContext>
|
<Loader.Item height="34px" />
|
||||||
|
<Loader.Item height="34px" />
|
||||||
|
<Loader.Item height="34px" />
|
||||||
|
<Loader.Item height="34px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
import { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -16,12 +16,12 @@ import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
|||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
provided: DraggableProvided;
|
isDragging: boolean;
|
||||||
snapshot: DraggableStateSnapshot;
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
const { block, enableReorder, provided, snapshot } = props;
|
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||||
|
|
||||||
@ -30,12 +30,10 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn({
|
className={cn({
|
||||||
"rounded bg-custom-background-80": snapshot.isDragging,
|
"rounded bg-custom-background-80": isDragging,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
@ -50,7 +48,7 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
||||||
{...provided.dragHandleProps}
|
ref={dragHandleRef}
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-3.5 w-3.5" />
|
<MoreVertical className="h-3.5 w-3.5" />
|
||||||
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { MutableRefObject } from "react";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||||
|
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||||
|
import { handleOrderChange } from "../utils";
|
||||||
import { ModulesSidebarBlock } from "./block";
|
import { ModulesSidebarBlock } from "./block";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
@ -16,85 +18,43 @@ type Props = {
|
|||||||
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||||
const { blockUpdateHandler, blocks, enableReorder } = props;
|
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||||
|
|
||||||
const handleOrderChange = (result: DropResult) => {
|
const handleOnDrop = (
|
||||||
if (!blocks) return;
|
draggingBlockId: string | undefined,
|
||||||
|
droppedBlockId: string | undefined,
|
||||||
const { source, destination } = result;
|
dropAtEndOfList: boolean
|
||||||
|
) => {
|
||||||
// return if dropped outside the list
|
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||||
if (!destination) return;
|
|
||||||
|
|
||||||
// return if dropped on the same index
|
|
||||||
if (source.index === destination.index) return;
|
|
||||||
|
|
||||||
let updatedSortOrder = blocks[source.index].sort_order;
|
|
||||||
|
|
||||||
// update the sort order to the lowest if dropped at the top
|
|
||||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
|
||||||
// update the sort order to the highest if dropped at the bottom
|
|
||||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
|
||||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
|
||||||
else {
|
|
||||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
|
||||||
const relativeDestinationSortingOrder =
|
|
||||||
source.index < destination.index
|
|
||||||
? blocks[destination.index + 1].sort_order
|
|
||||||
: blocks[destination.index - 1].sort_order;
|
|
||||||
|
|
||||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
|
||||||
const removedElement = blocks.splice(source.index, 1)[0];
|
|
||||||
blocks.splice(destination.index, 0, removedElement);
|
|
||||||
|
|
||||||
// call the block update handler with the updated sort order, new and old index
|
|
||||||
blockUpdateHandler(removedElement.data, {
|
|
||||||
sort_order: {
|
|
||||||
destinationIndex: destination.index,
|
|
||||||
newSortOrder: updatedSortOrder,
|
|
||||||
sourceIndex: source.index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<div className="h-full">
|
||||||
<Droppable droppableId="gantt-sidebar">
|
{blocks ? (
|
||||||
{(droppableProvided) => (
|
blocks.map((block, index) => (
|
||||||
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
<GanttDnDHOC
|
||||||
<>
|
key={block.id}
|
||||||
{blocks ? (
|
id={block.id}
|
||||||
blocks.map((block, index) => (
|
isLastChild={index === blocks.length - 1}
|
||||||
<Draggable
|
isDragEnabled={enableReorder}
|
||||||
key={`sidebar-block-${block.id}`}
|
onDrop={handleOnDrop}
|
||||||
draggableId={`sidebar-block-${block.id}`}
|
>
|
||||||
index={index}
|
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||||
isDragDisabled={!enableReorder}
|
<ModulesSidebarBlock
|
||||||
>
|
block={block}
|
||||||
{(provided, snapshot) => (
|
enableReorder={enableReorder}
|
||||||
<ModulesSidebarBlock
|
isDragging={isDragging}
|
||||||
block={block}
|
dragHandleRef={dragHandleRef}
|
||||||
enableReorder={enableReorder}
|
/>
|
||||||
provided={provided}
|
)}
|
||||||
snapshot={snapshot}
|
</GanttDnDHOC>
|
||||||
/>
|
))
|
||||||
)}
|
) : (
|
||||||
</Draggable>
|
<Loader className="space-y-3 pr-2">
|
||||||
))
|
<Loader.Item height="34px" />
|
||||||
) : (
|
<Loader.Item height="34px" />
|
||||||
<Loader className="space-y-3 pr-2">
|
<Loader.Item height="34px" />
|
||||||
<Loader.Item height="34px" />
|
<Loader.Item height="34px" />
|
||||||
<Loader.Item height="34px" />
|
</Loader>
|
||||||
<Loader.Item height="34px" />
|
)}
|
||||||
<Loader.Item height="34px" />
|
</div>
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
{droppableProvided.placeholder}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
|
||||||
// ui
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
|
||||||
import { IssuesSidebarBlock } from "./issues/block";
|
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
|
||||||
blocks: IGanttBlock[] | null;
|
|
||||||
enableReorder: boolean;
|
|
||||||
enableQuickIssueCreate?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
|
||||||
const { blockUpdateHandler, blocks, enableReorder } = props;
|
|
||||||
|
|
||||||
const handleOrderChange = (result: DropResult) => {
|
|
||||||
if (!blocks) return;
|
|
||||||
|
|
||||||
const { source, destination } = result;
|
|
||||||
|
|
||||||
// return if dropped outside the list
|
|
||||||
if (!destination) return;
|
|
||||||
|
|
||||||
// return if dropped on the same index
|
|
||||||
if (source.index === destination.index) return;
|
|
||||||
|
|
||||||
let updatedSortOrder = blocks[source.index].sort_order;
|
|
||||||
|
|
||||||
// update the sort order to the lowest if dropped at the top
|
|
||||||
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
|
||||||
// update the sort order to the highest if dropped at the bottom
|
|
||||||
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
|
||||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
|
||||||
else {
|
|
||||||
const destinationSortingOrder = blocks[destination.index].sort_order;
|
|
||||||
const relativeDestinationSortingOrder =
|
|
||||||
source.index < destination.index
|
|
||||||
? blocks[destination.index + 1].sort_order
|
|
||||||
: blocks[destination.index - 1].sort_order;
|
|
||||||
|
|
||||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the element from the source index and insert it at the destination index without updating the entire array
|
|
||||||
const removedElement = blocks.splice(source.index, 1)[0];
|
|
||||||
blocks.splice(destination.index, 0, removedElement);
|
|
||||||
|
|
||||||
// call the block update handler with the updated sort order, new and old index
|
|
||||||
blockUpdateHandler(removedElement.data, {
|
|
||||||
sort_order: {
|
|
||||||
destinationIndex: destination.index,
|
|
||||||
newSortOrder: updatedSortOrder,
|
|
||||||
sourceIndex: source.index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
|
||||||
<Droppable droppableId="gantt-sidebar">
|
|
||||||
{(droppableProvided) => (
|
|
||||||
<div
|
|
||||||
className="mt-3 max-h-full overflow-y-auto pl-2.5"
|
|
||||||
ref={droppableProvided.innerRef}
|
|
||||||
{...droppableProvided.droppableProps}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{blocks ? (
|
|
||||||
blocks.map((block, index) => (
|
|
||||||
<Draggable
|
|
||||||
key={`sidebar-block-${block.id}`}
|
|
||||||
draggableId={`sidebar-block-${block.id}`}
|
|
||||||
index={index}
|
|
||||||
isDragDisabled={!enableReorder}
|
|
||||||
>
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<IssuesSidebarBlock
|
|
||||||
block={block}
|
|
||||||
enableReorder={enableReorder}
|
|
||||||
provided={provided}
|
|
||||||
snapshot={snapshot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-3 pr-2">
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
<Loader.Item height="34px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
{droppableProvided.placeholder}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
|
||||||
};
|
|
@ -34,7 +34,7 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
|
|||||||
<h6>Duration</h6>
|
<h6>Duration</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
|
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
|
||||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
||||||
</div>
|
</div>
|
||||||
{quickAdd ? quickAdd : null}
|
{quickAdd ? quickAdd : null}
|
||||||
|
42
web/components/gantt-chart/sidebar/utils.ts
Normal file
42
web/components/gantt-chart/sidebar/utils.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
|
||||||
|
export const handleOrderChange = (
|
||||||
|
draggingBlockId: string | undefined,
|
||||||
|
droppedBlockId: string | undefined,
|
||||||
|
dropAtEndOfList: boolean,
|
||||||
|
blocks: IGanttBlock[] | null,
|
||||||
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void
|
||||||
|
) => {
|
||||||
|
if (!blocks || !draggingBlockId || !droppedBlockId) return;
|
||||||
|
|
||||||
|
const sourceBlockIndex = blocks.findIndex((block) => block.id === draggingBlockId);
|
||||||
|
const destinationBlockIndex = dropAtEndOfList
|
||||||
|
? blocks.length
|
||||||
|
: blocks.findIndex((block) => block.id === droppedBlockId);
|
||||||
|
|
||||||
|
// return if dropped outside the list
|
||||||
|
if (sourceBlockIndex === -1 || destinationBlockIndex === -1 || sourceBlockIndex === destinationBlockIndex) return;
|
||||||
|
|
||||||
|
let updatedSortOrder = blocks[sourceBlockIndex].sort_order;
|
||||||
|
|
||||||
|
// update the sort order to the lowest if dropped at the top
|
||||||
|
if (destinationBlockIndex === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||||
|
// update the sort order to the highest if dropped at the bottom
|
||||||
|
else if (destinationBlockIndex === blocks.length) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
||||||
|
// update the sort order to the average of the two adjacent blocks if dropped in between
|
||||||
|
else {
|
||||||
|
const destinationSortingOrder = blocks[destinationBlockIndex].sort_order;
|
||||||
|
const relativeDestinationSortingOrder = blocks[destinationBlockIndex - 1].sort_order;
|
||||||
|
|
||||||
|
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the block update handler with the updated sort order, new and old index
|
||||||
|
blockUpdateHandler(blocks[sourceBlockIndex].data, {
|
||||||
|
sort_order: {
|
||||||
|
destinationIndex: destinationBlockIndex,
|
||||||
|
newSortOrder: updatedSortOrder,
|
||||||
|
sourceIndex: sourceBlockIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -21,7 +21,7 @@ export const InboxIssueAppliedFiltersState: FC = observer(() => {
|
|||||||
if (filteredValues.length === 0) return <></>;
|
if (filteredValues.length === 0) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1">
|
<div className="relative flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1">
|
||||||
<div className="text-xs text-custom-text-200">Status</div>
|
<div className="text-xs text-custom-text-200">State</div>
|
||||||
{filteredValues.map((value) => {
|
{filteredValues.map((value) => {
|
||||||
const optionDetail = currentOptionDetail(value);
|
const optionDetail = currentOptionDetail(value);
|
||||||
if (!optionDetail) return <></>;
|
if (!optionDetail) return <></>;
|
||||||
|
@ -6,6 +6,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
|||||||
// hooks
|
// hooks
|
||||||
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||||
|
import { HIGHLIGHT_CLASS } from "@/components/issues/issue-layouts/utils";
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -133,7 +134,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
|||||||
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
||||||
|
|
||||||
useOutsideClickDetector(cardRef, () => {
|
useOutsideClickDetector(cardRef, () => {
|
||||||
cardRef?.current?.classList?.remove("highlight");
|
cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make Issue block both as as Draggable and,
|
// Make Issue block both as as Draggable and,
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
TIssueOrderByOptions,
|
TIssueOrderByOptions,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||||
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
@ -20,12 +21,7 @@ import { cn } from "@/helpers/common.helper";
|
|||||||
import { useProjectState } from "@/hooks/store";
|
import { useProjectState } from "@/hooks/store";
|
||||||
//components
|
//components
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import {
|
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
||||||
KanbanDropLocation,
|
|
||||||
getSourceFromDropPayload,
|
|
||||||
getDestinationFromDropPayload,
|
|
||||||
highlightIssueOnDrop,
|
|
||||||
} from "./utils";
|
|
||||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||||
|
|
||||||
interface IKanbanGroup {
|
interface IKanbanGroup {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import pull from "lodash/pull";
|
import pull from "lodash/pull";
|
||||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
|
||||||
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
||||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
||||||
|
|
||||||
@ -212,18 +211,3 @@ export const handleDragDrop = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
|
||||||
* @param elementId
|
|
||||||
* @param shouldScrollIntoView
|
|
||||||
*/
|
|
||||||
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
|
|
||||||
setTimeout(async () => {
|
|
||||||
const sourceElementId = elementId ?? "";
|
|
||||||
const sourceElement = document.getElementById(sourceElementId);
|
|
||||||
sourceElement?.classList?.add("highlight");
|
|
||||||
if (shouldScrollIntoView && sourceElement)
|
|
||||||
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// icons
|
// icons
|
||||||
@ -33,6 +33,7 @@ interface Props {
|
|||||||
containerRef: MutableRefObject<HTMLTableElement | null>;
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
issueIds: string[];
|
issueIds: string[];
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
|
spacingLeft?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetIssueRow = observer((props: Props) => {
|
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||||
@ -49,6 +50,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
containerRef,
|
containerRef,
|
||||||
issueIds,
|
issueIds,
|
||||||
spreadsheetColumnsList,
|
spreadsheetColumnsList,
|
||||||
|
spacingLeft = 14,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
@ -72,6 +74,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
|
spacingLeft={spacingLeft}
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
@ -93,6 +96,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
|
spacingLeft={spacingLeft + (displayProperties.key ? 16 : 28)}
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
@ -119,6 +123,7 @@ interface IssueRowDetailsProps {
|
|||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
setExpanded: Dispatch<SetStateAction<boolean>>;
|
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
|
spacingLeft?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||||
@ -135,6 +140,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
isExpanded,
|
isExpanded,
|
||||||
setExpanded,
|
setExpanded,
|
||||||
spreadsheetColumnsList,
|
spreadsheetColumnsList,
|
||||||
|
spacingLeft = 14,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
@ -161,22 +167,14 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
const issueDetail = issue.getIssueById(issueId);
|
||||||
|
|
||||||
const paddingLeft = `${nestingLevel * 54}px`;
|
const paddingLeft = `${spacingLeft}px`;
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
const handleToggleExpand = () => {
|
|
||||||
setExpanded((prevState) => {
|
|
||||||
if (!prevState && workspaceSlug && issueDetail)
|
|
||||||
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
|
|
||||||
return !prevState;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const customActionButton = (
|
const customActionButton = (
|
||||||
<div
|
<div
|
||||||
ref={menuActionRef}
|
ref={menuActionRef}
|
||||||
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
className={`flex items-center h-full w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
@ -186,76 +184,85 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
);
|
);
|
||||||
if (!issueDetail) return null;
|
if (!issueDetail) return null;
|
||||||
|
|
||||||
|
const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (nestingLevel >= 3) {
|
||||||
|
handleIssuePeekOverview(issueDetail);
|
||||||
|
} else {
|
||||||
|
setExpanded((prevState) => {
|
||||||
|
if (!prevState && workspaceSlug && issueDetail)
|
||||||
|
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
|
||||||
|
return !prevState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td
|
<td ref={cellRef} id={`issue-${issueDetail.id}`} tabIndex={0}>
|
||||||
ref={cellRef}
|
|
||||||
id={`issue-${issueDetail.id}`}
|
|
||||||
className={cn(
|
|
||||||
"group sticky left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
|
|
||||||
{
|
|
||||||
"border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
|
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issueDetail.id),
|
|
||||||
"shadow-[8px_22px_22px_10px_rgba(0,0,0,0.05)]": isScrolled.current,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
|
||||||
<div
|
|
||||||
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
|
|
||||||
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
|
||||||
>
|
|
||||||
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
|
|
||||||
<span
|
|
||||||
className={`flex items-center justify-center font-medium group-hover:opacity-0 ${
|
|
||||||
isMenuActive ? "opacity-0" : "opacity-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
|
||||||
{quickActions({
|
|
||||||
issue: issueDetail,
|
|
||||||
parentRef: cellRef,
|
|
||||||
customActionButton,
|
|
||||||
portalElement: portalElement.current,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{issueDetail.sub_issues_count > 0 && (
|
|
||||||
<div className="flex h-6 w-6 items-center justify-center">
|
|
||||||
<button
|
|
||||||
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-custom-background-90 hover:text-custom-text-100"
|
|
||||||
onClick={() => handleToggleExpand()}
|
|
||||||
>
|
|
||||||
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</WithDisplayPropertiesHOC>
|
|
||||||
<ControlLink
|
<ControlLink
|
||||||
id={`issue-${issueId}`}
|
id={`issue-${issueId}`}
|
||||||
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issueDetail)}
|
onClick={() => handleIssuePeekOverview(issueDetail)}
|
||||||
className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
className={cn(
|
||||||
|
"group clickable cursor-pointer sticky left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
|
||||||
|
{
|
||||||
|
"border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
|
||||||
|
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issueDetail.id),
|
||||||
|
"shadow-[8px_22px_22px_10px_rgba(0,0,0,0.05)]": isScrolled.current,
|
||||||
|
}
|
||||||
|
)}
|
||||||
disabled={!!issueDetail?.tempId}
|
disabled={!!issueDetail?.tempId}
|
||||||
>
|
>
|
||||||
<div className="w-full overflow-hidden">
|
<div
|
||||||
<Tooltip tooltipContent={issueDetail.name} isMobile={isMobile}>
|
className="flex min-w-min items-center gap-1 px-4 py-2.5 pr-0"
|
||||||
<div
|
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||||
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
|
>
|
||||||
tabIndex={-1}
|
<div className="flex h-5 w-5 items-center justify-center">
|
||||||
>
|
{issueDetail.sub_issues_count > 0 && (
|
||||||
{issueDetail.name}
|
<button
|
||||||
|
className="flex items-center justify-center h-5 w-5 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||||
|
onClick={handleToggleExpand}
|
||||||
|
>
|
||||||
|
<ChevronRight className={`h-4 w-4 ${isExpanded ? "rotate-90" : ""}`} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
|
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
|
||||||
|
<p className={`flex items-center justify-center font-medium leading-7`}>
|
||||||
|
{getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</WithDisplayPropertiesHOC>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 justify-between h-full w-full pr-4 truncate">
|
||||||
|
<div className="w-full line-clamp-1 text-sm text-custom-text-100">
|
||||||
|
<div className="w-full overflow-hidden">
|
||||||
|
<Tooltip tooltipContent={issueDetail.name} isMobile={isMobile}>
|
||||||
|
<div
|
||||||
|
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{issueDetail.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
||||||
|
{quickActions({
|
||||||
|
issue: issueDetail,
|
||||||
|
parentRef: cellRef,
|
||||||
|
customActionButton,
|
||||||
|
portalElement: portalElement.current,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||||
import { ContrastIcon } from "lucide-react";
|
import { ContrastIcon } from "lucide-react";
|
||||||
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
|
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
|
||||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||||
@ -16,6 +17,9 @@ import { IStateStore } from "@/store/state.store";
|
|||||||
// constants
|
// constants
|
||||||
// types
|
// types
|
||||||
|
|
||||||
|
export const HIGHLIGHT_CLASS = "highlight";
|
||||||
|
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||||
|
|
||||||
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
||||||
[EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false;
|
[EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false;
|
||||||
|
|
||||||
@ -240,3 +244,22 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
||||||
|
* @param elementId
|
||||||
|
* @param shouldScrollIntoView
|
||||||
|
*/
|
||||||
|
export const highlightIssueOnDrop = (
|
||||||
|
elementId: string | undefined,
|
||||||
|
shouldScrollIntoView = true,
|
||||||
|
shouldHighLightWithLine = false
|
||||||
|
) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const sourceElementId = elementId ?? "";
|
||||||
|
const sourceElement = document.getElementById(sourceElementId);
|
||||||
|
sourceElement?.classList?.add(shouldHighLightWithLine ? HIGHLIGHT_WITH_LINE : HIGHLIGHT_CLASS);
|
||||||
|
if (shouldScrollIntoView && sourceElement)
|
||||||
|
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { TModuleFilters } from "@plane/types";
|
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
||||||
// helpers
|
// helpers
|
||||||
@ -8,19 +8,30 @@ import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: TModuleFilters;
|
appliedFilters: TModuleFilters;
|
||||||
|
isFavoriteFilterApplied?: boolean;
|
||||||
handleClearAllFilters: () => void;
|
handleClearAllFilters: () => void;
|
||||||
|
handleDisplayFiltersUpdate?: (updatedDisplayProperties: Partial<TModuleDisplayFilters>) => void;
|
||||||
handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void;
|
handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void;
|
||||||
alwaysAllowEditing?: boolean;
|
alwaysAllowEditing?: boolean;
|
||||||
|
isArchived?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MEMBERS_FILTERS = ["lead", "members"];
|
const MEMBERS_FILTERS = ["lead", "members"];
|
||||||
const DATE_FILTERS = ["start_date", "target_date"];
|
const DATE_FILTERS = ["start_date", "target_date"];
|
||||||
|
|
||||||
export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
|
const {
|
||||||
|
appliedFilters,
|
||||||
|
isFavoriteFilterApplied,
|
||||||
|
handleClearAllFilters,
|
||||||
|
handleRemoveFilter,
|
||||||
|
handleDisplayFiltersUpdate,
|
||||||
|
alwaysAllowEditing,
|
||||||
|
isArchived = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
if (!appliedFilters) return null;
|
if (!appliedFilters && !isFavoriteFilterApplied) return null;
|
||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0 && !isFavoriteFilterApplied) return null;
|
||||||
|
|
||||||
const isEditingAllowed = alwaysAllowEditing;
|
const isEditingAllowed = alwaysAllowEditing;
|
||||||
|
|
||||||
@ -73,6 +84,33 @@ export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{!isArchived && isFavoriteFilterApplied && (
|
||||||
|
<div
|
||||||
|
key="module_display_filters"
|
||||||
|
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-xs text-custom-text-300">Modules</span>
|
||||||
|
<div className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
||||||
|
Favorite
|
||||||
|
{isEditingAllowed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() =>
|
||||||
|
handleDisplayFiltersUpdate &&
|
||||||
|
handleDisplayFiltersUpdate({
|
||||||
|
favorites: !isFavoriteFilterApplied,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isEditingAllowed && (
|
{isEditingAllowed && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -64,6 +64,7 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
|
|||||||
handleClearAllFilters={() => clearAllFilters(projectId.toString(), "archived")}
|
handleClearAllFilters={() => clearAllFilters(projectId.toString(), "archived")}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
alwaysAllowEditing
|
alwaysAllowEditing
|
||||||
|
isArchived
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -19,6 +19,7 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
|||||||
const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key));
|
const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key));
|
||||||
|
|
||||||
const isDescending = value?.[0] === "-";
|
const isDescending = value?.[0] === "-";
|
||||||
|
const isManual = value?.includes("sort_order");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
@ -38,7 +39,7 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
|||||||
key={option.key}
|
key={option.key}
|
||||||
className="flex items-center justify-between gap-2"
|
className="flex items-center justify-between gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isDescending) onChange(`-${option.key}` as TModuleOrderByOptions);
|
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||||
else onChange(option.key);
|
else onChange(option.key);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -46,25 +47,29 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
|||||||
{value?.includes(option.key) && <Check className="h-3 w-3" />}
|
{value?.includes(option.key) && <Check className="h-3 w-3" />}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
))}
|
||||||
<hr className="my-2 border-custom-border-200" />
|
{!isManual && (
|
||||||
<CustomMenu.MenuItem
|
<>
|
||||||
className="flex items-center justify-between gap-2"
|
<hr className="my-2 border-custom-border-200" />
|
||||||
onClick={() => {
|
<CustomMenu.MenuItem
|
||||||
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
|
className="flex items-center justify-between gap-2"
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
|
||||||
Ascending
|
}}
|
||||||
{!isDescending && <Check className="h-3 w-3" />}
|
>
|
||||||
</CustomMenu.MenuItem>
|
Ascending
|
||||||
<CustomMenu.MenuItem
|
{!isDescending && <Check className="h-3 w-3" />}
|
||||||
className="flex items-center justify-between gap-2"
|
</CustomMenu.MenuItem>
|
||||||
onClick={() => {
|
<CustomMenu.MenuItem
|
||||||
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
|
className="flex items-center justify-between gap-2"
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
|
||||||
Descending
|
}}
|
||||||
{isDescending && <Check className="h-3 w-3" />}
|
>
|
||||||
</CustomMenu.MenuItem>
|
Descending
|
||||||
|
{isDescending && <Check className="h-3 w-3" />}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { IModule } from "@plane/types";
|
|||||||
import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "@/components/gantt-chart";
|
import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "@/components/gantt-chart";
|
||||||
import { ModuleGanttBlock } from "@/components/modules";
|
import { ModuleGanttBlock } from "@/components/modules";
|
||||||
import { getDate } from "@/helpers/date-time.helper";
|
import { getDate } from "@/helpers/date-time.helper";
|
||||||
import { useModule, useProject } from "@/hooks/store";
|
import { useModule, useModuleFilter, useProject } from "@/hooks/store";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
export const ModulesListGanttChartView: React.FC = observer(() => {
|
export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||||
@ -16,6 +16,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule();
|
const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule();
|
||||||
|
const { currentProjectDisplayFilters: displayFilters } = useModuleFilter();
|
||||||
// derived values
|
// derived values
|
||||||
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
|||||||
enableBlockLeftResize={isAllowed}
|
enableBlockLeftResize={isAllowed}
|
||||||
enableBlockRightResize={isAllowed}
|
enableBlockRightResize={isAllowed}
|
||||||
enableBlockMove={isAllowed}
|
enableBlockMove={isAllowed}
|
||||||
enableReorder={isAllowed}
|
enableReorder={isAllowed && displayFilters?.order_by === "sort_order"}
|
||||||
enableAddBlock={isAllowed}
|
enableAddBlock={isAllowed}
|
||||||
showAllBlocks
|
showAllBlocks
|
||||||
/>
|
/>
|
||||||
|
@ -84,7 +84,7 @@ export const ModuleViewHeader: FC = observer(() => {
|
|||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0 || displayFilters?.favorites;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
||||||
|
@ -4,13 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
// components
|
// components
|
||||||
import { ListLayout } from "@/components/core/list";
|
import { ListLayout } from "@/components/core/list";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
|
||||||
ModuleCardItem,
|
|
||||||
ModuleListItem,
|
|
||||||
ModulePeekOverview,
|
|
||||||
ModuleViewHeader,
|
|
||||||
ModulesListGanttChartView,
|
|
||||||
} from "@/components/modules";
|
|
||||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
@ -73,12 +67,6 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-[50px] flex-shrink-0 w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="block text-sm font-medium">Module name</span>
|
|
||||||
</div>
|
|
||||||
<ModuleViewHeader />
|
|
||||||
</div>
|
|
||||||
{displayFilters?.layout === "list" && (
|
{displayFilters?.layout === "list" && (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
|
@ -8,7 +8,7 @@ import { PageLoader } from "@/components/pages";
|
|||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectPages } from "@/hooks/store";
|
import { useApplication, useProjectPages } from "@/hooks/store";
|
||||||
// assets
|
// assets
|
||||||
import AllFiltersImage from "public/empty-state/pages/all-filters.svg";
|
import AllFiltersImage from "public/empty-state/pages/all-filters.svg";
|
||||||
import NameFilterImage from "public/empty-state/pages/name-filter.svg";
|
import NameFilterImage from "public/empty-state/pages/name-filter.svg";
|
||||||
@ -23,6 +23,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const { children, pageType, projectId } = props;
|
const { children, pageType, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { loader, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(projectId);
|
const { loader, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(projectId);
|
||||||
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
// derived values
|
// derived values
|
||||||
const pageIds = getCurrentProjectPageIds(pageType);
|
const pageIds = getCurrentProjectPageIds(pageType);
|
||||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
||||||
@ -30,8 +31,24 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
|||||||
if (loader === "init-loader") return <PageLoader />;
|
if (loader === "init-loader") return <PageLoader />;
|
||||||
// if no pages exist in the active page type
|
// if no pages exist in the active page type
|
||||||
if (pageIds?.length === 0) {
|
if (pageIds?.length === 0) {
|
||||||
if (pageType === "public") return <EmptyState type={EmptyStateType.PROJECT_PAGE_PUBLIC} />;
|
if (pageType === "public")
|
||||||
if (pageType === "private") return <EmptyState type={EmptyStateType.PROJECT_PAGE_PRIVATE} />;
|
return (
|
||||||
|
<EmptyState
|
||||||
|
type={EmptyStateType.PROJECT_PAGE_PUBLIC}
|
||||||
|
primaryButtonOnClick={() => {
|
||||||
|
commandPaletteStore.toggleCreatePageModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (pageType === "private")
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
type={EmptyStateType.PROJECT_PAGE_PRIVATE}
|
||||||
|
primaryButtonOnClick={() => {
|
||||||
|
commandPaletteStore.toggleCreatePageModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (pageType === "archived") return <EmptyState type={EmptyStateType.PROJECT_PAGE_ARCHIVED} />;
|
if (pageType === "archived") return <EmptyState type={EmptyStateType.PROJECT_PAGE_ARCHIVED} />;
|
||||||
}
|
}
|
||||||
// if no pages match the filter criteria
|
// if no pages match the filter criteria
|
||||||
|
@ -365,7 +365,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
|
|||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { value, onChange } }) => {
|
||||||
if (value === undefined || value === null || typeof value === "string")
|
if (value === undefined || value === null || typeof value === "string")
|
||||||
return (
|
return (
|
||||||
<div className="h-7 flex-shrink-0" tabIndex={5}>
|
<div className="flex-shrink-0" tabIndex={5}>
|
||||||
<MemberDropdown
|
<MemberDropdown
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(lead) => onChange(lead === value ? null : lead)}
|
onChange={(lead) => onChange(lead === value ? null : lead)}
|
||||||
|
@ -500,12 +500,22 @@ const emptyStateDetails = {
|
|||||||
title: "No private pages yet",
|
title: "No private pages yet",
|
||||||
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
|
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
|
||||||
path: "/empty-state/pages/private",
|
path: "/empty-state/pages/private",
|
||||||
|
primaryButton: {
|
||||||
|
text: "Create your first page",
|
||||||
|
},
|
||||||
|
accessType: "project",
|
||||||
|
access: EUserProjectRoles.MEMBER,
|
||||||
},
|
},
|
||||||
[EmptyStateType.PROJECT_PAGE_PUBLIC]: {
|
[EmptyStateType.PROJECT_PAGE_PUBLIC]: {
|
||||||
key: EmptyStateType.PROJECT_PAGE_PUBLIC,
|
key: EmptyStateType.PROJECT_PAGE_PUBLIC,
|
||||||
title: "No public pages yet",
|
title: "No public pages yet",
|
||||||
description: "See pages shared with everyone in your project right here.",
|
description: "See pages shared with everyone in your project right here.",
|
||||||
path: "/empty-state/pages/public",
|
path: "/empty-state/pages/public",
|
||||||
|
primaryButton: {
|
||||||
|
text: "Create your first page",
|
||||||
|
},
|
||||||
|
accessType: "project",
|
||||||
|
access: EUserProjectRoles.MEMBER,
|
||||||
},
|
},
|
||||||
[EmptyStateType.PROJECT_PAGE_ARCHIVED]: {
|
[EmptyStateType.PROJECT_PAGE_ARCHIVED]: {
|
||||||
key: EmptyStateType.PROJECT_PAGE_ARCHIVED,
|
key: EmptyStateType.PROJECT_PAGE_ARCHIVED,
|
||||||
|
@ -92,4 +92,8 @@ export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: strin
|
|||||||
key: "created_at",
|
key: "created_at",
|
||||||
label: "Created date",
|
label: "Created date",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "sort_order",
|
||||||
|
label: "Manual",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -9,7 +9,7 @@ import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
|||||||
* @param {ICycle[]} cycles
|
* @param {ICycle[]} cycles
|
||||||
* @returns {ICycle[]}
|
* @returns {ICycle[]}
|
||||||
*/
|
*/
|
||||||
export const orderCycles = (cycles: ICycle[]): ICycle[] => {
|
export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] => {
|
||||||
if (cycles.length === 0) return [];
|
if (cycles.length === 0) return [];
|
||||||
|
|
||||||
const acceptedStatuses = ["current", "upcoming", "draft"];
|
const acceptedStatuses = ["current", "upcoming", "draft"];
|
||||||
@ -22,10 +22,12 @@ export const orderCycles = (cycles: ICycle[]): ICycle[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let filteredCycles = cycles.filter((c) => acceptedStatuses.includes(c.status?.toLowerCase() ?? ""));
|
let filteredCycles = cycles.filter((c) => acceptedStatuses.includes(c.status?.toLowerCase() ?? ""));
|
||||||
filteredCycles = sortBy(filteredCycles, [
|
if (sortByManual) filteredCycles = sortBy(filteredCycles, [(c) => c.sort_order]);
|
||||||
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
else
|
||||||
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
filteredCycles = sortBy(filteredCycles, [
|
||||||
]);
|
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
||||||
|
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
||||||
|
]);
|
||||||
|
|
||||||
return filteredCycles;
|
return filteredCycles;
|
||||||
};
|
};
|
||||||
|
@ -35,6 +35,7 @@ export const orderModules = (modules: IModule[], orderByKey: TModuleOrderByOptio
|
|||||||
if (orderByKey === "created_at") orderedModules = sortBy(modules, [(m) => m.created_at]);
|
if (orderByKey === "created_at") orderedModules = sortBy(modules, [(m) => m.created_at]);
|
||||||
if (orderByKey === "-created_at") orderedModules = sortBy(modules, [(m) => !m.created_at]);
|
if (orderByKey === "-created_at") orderedModules = sortBy(modules, [(m) => !m.created_at]);
|
||||||
|
|
||||||
|
if (orderByKey === "sort_order") orderedModules = sortBy(modules, [(m) => m.sort_order]);
|
||||||
return orderedModules;
|
return orderedModules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@blueprintjs/popover2": "^1.13.3",
|
"@blueprintjs/popover2": "^1.13.3",
|
||||||
"@headlessui/react": "^1.7.3",
|
"@headlessui/react": "^1.7.3",
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.368.0",
|
"lucide-react": "^0.378.0",
|
||||||
"mobx": "^6.10.0",
|
"mobx": "^6.10.0",
|
||||||
"mobx-react": "^9.1.0",
|
"mobx-react": "^9.1.0",
|
||||||
"mobx-utils": "^6.0.8",
|
"mobx-utils": "^6.0.8",
|
||||||
@ -80,4 +80,4 @@
|
|||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ import { TModuleFilters } from "@plane/types";
|
|||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { ModulesListHeader } from "@/components/headers";
|
import { ModulesListHeader } from "@/components/headers";
|
||||||
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
import { ModuleViewHeader, ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||||
// types
|
// types
|
||||||
// hooks
|
// hooks
|
||||||
import ModulesListMobileHeader from "@/components/modules/moduels-list-mobile-header";
|
import ModulesListMobileHeader from "@/components/modules/moduels-list-mobile-header";
|
||||||
@ -22,7 +22,8 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store
|
// store
|
||||||
const { getProjectById, currentProjectDetails } = useProject();
|
const { getProjectById, currentProjectDetails } = useProject();
|
||||||
const { currentProjectFilters, clearAllFilters, updateFilters } = useModuleFilter();
|
const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } =
|
||||||
|
useModuleFilter();
|
||||||
// derived values
|
// derived values
|
||||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
||||||
@ -57,12 +58,23 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
{calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
|
<div className="h-[50px] flex-shrink-0 w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="block text-sm font-medium">Module name</span>
|
||||||
|
</div>
|
||||||
|
<ModuleViewHeader />
|
||||||
|
</div>
|
||||||
|
{(calculateTotalFilters(currentProjectFilters ?? {}) !== 0 || currentProjectDisplayFilters?.favorites) && (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
<div className="border-b border-custom-border-200 px-5 py-3">
|
||||||
<ModuleAppliedFiltersList
|
<ModuleAppliedFiltersList
|
||||||
appliedFilters={currentProjectFilters ?? {}}
|
appliedFilters={currentProjectFilters ?? {}}
|
||||||
|
isFavoriteFilterApplied={currentProjectDisplayFilters?.favorites ?? false}
|
||||||
handleClearAllFilters={() => clearAllFilters(`${projectId}`)}
|
handleClearAllFilters={() => clearAllFilters(`${projectId}`)}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
handleDisplayFiltersUpdate={(val) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
updateDisplayFilters(projectId.toString(), val);
|
||||||
|
}}
|
||||||
alwaysAllowEditing
|
alwaysAllowEditing
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@ export interface ICycleStore {
|
|||||||
currentProjectActiveCycleId: string | null;
|
currentProjectActiveCycleId: string | null;
|
||||||
currentProjectArchivedCycleIds: string[] | null;
|
currentProjectArchivedCycleIds: string[] | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getFilteredCycleIds: (projectId: string) => string[] | null;
|
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
||||||
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
||||||
getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
|
getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
|
||||||
getCycleById: (cycleId: string) => ICycle | null;
|
getCycleById: (cycleId: string) => ICycle | null;
|
||||||
@ -228,7 +228,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
* @param {TCycleFilters} filters
|
* @param {TCycleFilters} filters
|
||||||
* @returns {string[] | null}
|
* @returns {string[] | null}
|
||||||
*/
|
*/
|
||||||
getFilteredCycleIds = computedFn((projectId: string) => {
|
getFilteredCycleIds = computedFn((projectId: string, sortByManual: boolean) => {
|
||||||
const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId);
|
const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId);
|
||||||
const searchQuery = this.rootStore.cycleFilter.searchQuery;
|
const searchQuery = this.rootStore.cycleFilter.searchQuery;
|
||||||
if (!this.fetchedMap[projectId]) return null;
|
if (!this.fetchedMap[projectId]) return null;
|
||||||
@ -239,7 +239,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||||
shouldFilterCycle(c, filters ?? {})
|
shouldFilterCycle(c, filters ?? {})
|
||||||
);
|
);
|
||||||
cycles = orderCycles(cycles);
|
cycles = orderCycles(cycles, sortByManual);
|
||||||
const cycleIds = cycles.map((c) => c.id);
|
const cycleIds = cycles.map((c) => c.id);
|
||||||
return cycleIds;
|
return cycleIds;
|
||||||
});
|
});
|
||||||
|
@ -177,6 +177,7 @@ export class ModuleFilterStore implements IModuleFilterStore {
|
|||||||
clearAllFilters = (projectId: string, state: keyof TModuleFiltersByState = "default") => {
|
clearAllFilters = (projectId: string, state: keyof TModuleFiltersByState = "default") => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.filters[projectId][state] = {};
|
this.filters[projectId][state] = {};
|
||||||
|
this.displayFilters[projectId].favorites = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -636,4 +636,8 @@ div.web-view-spinner div.bar12 {
|
|||||||
/* highlight class */
|
/* highlight class */
|
||||||
.highlight {
|
.highlight {
|
||||||
border: 1px solid rgb(var(--color-primary-100)) !important;
|
border: 1px solid rgb(var(--color-primary-100)) !important;
|
||||||
|
}
|
||||||
|
.highlight-with-line {
|
||||||
|
border-left: 5px solid rgb(var(--color-primary-100)) !important;
|
||||||
|
background: rgb(var(--color-background-80));
|
||||||
}
|
}
|
26
yarn.lock
26
yarn.lock
@ -29,10 +29,10 @@
|
|||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.0.3":
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.3.0":
|
||||||
version "1.0.3"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.0.3.tgz#bb088a3d8eb77d9454dfdead433e594c5f793dae"
|
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.3.0.tgz#6af382a2d75924f5f0699ebf1b348e2ea8d5a2cd"
|
||||||
integrity sha512-gfXwzQZRsWSLd/88IRNKwf2e5r3smZlDGLopg5tFkSqsL03VDHgd6wch6OfPhRK2kSCrT1uXv5n9hOpVBu678g==
|
integrity sha512-8wjKAI5qSrLojt8ZJ2WhoS5P75oBu5g0yMpAnTDgfqFyQnkt5Uc1txCRWpG26SS1mv19nm8ak9XHF2DOugVfpw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atlaskit/pragmatic-drag-and-drop" "^1.1.0"
|
"@atlaskit/pragmatic-drag-and-drop" "^1.1.0"
|
||||||
"@babel/runtime" "^7.0.0"
|
"@babel/runtime" "^7.0.0"
|
||||||
@ -5896,20 +5896,10 @@ lru-cache@^6.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
|
||||||
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
|
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
|
||||||
|
|
||||||
lucide-react@^0.294.0:
|
lucide-react@^0.378.0:
|
||||||
version "0.294.0"
|
version "0.378.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.294.0.tgz#dc406e1e7e2f722cf93218fe5b31cf3c95778817"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.378.0.tgz#232acb99c6baedfa90959a2c0dd11327b058bde8"
|
||||||
integrity sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==
|
integrity sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==
|
||||||
|
|
||||||
lucide-react@^0.309.0:
|
|
||||||
version "0.309.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.309.0.tgz#7369893cb4b074a0a0b1d3acdc6fd9a8bdb5add1"
|
|
||||||
integrity sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==
|
|
||||||
|
|
||||||
lucide-react@^0.368.0:
|
|
||||||
version "0.368.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.368.0.tgz#3c0ee63f4f7d30ae63b621b2b8f04f9e409ee6e7"
|
|
||||||
integrity sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==
|
|
||||||
|
|
||||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||||
version "0.25.9"
|
version "0.25.9"
|
||||||
|
Loading…
Reference in New Issue
Block a user