mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
Merge pull request #215 from makeplane/sync/ce-ee
sync: merge conflicts need to be resolved
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,2 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
Hey there,<br/>
|
||||
Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.<br/>
|
||||
Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.<br/>
|
||||
If you require any assistance or have any questions, please do not hesitate to contact us.<br/>
|
||||
Thank you
|
||||
</html>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html> Hey there,<br/> Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.<br/> Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.<br/> If you require any assistance or have any questions, please do not hesitate to contact us.<br/> Thank you </html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } fr
|
||||
import { CoreEditorProps } from "src/ui/props";
|
||||
import { CoreEditorExtensions } from "src/ui/extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { getTrimmedHTML } from "src/lib/utils";
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { IMentionHighlight, IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
@@ -86,7 +85,7 @@ export const useEditor = ({
|
||||
setSavedSelection(editor.state.selection);
|
||||
},
|
||||
onUpdate: async ({ editor }) => {
|
||||
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
onChange?.(editor.getJSON(), editor.getHTML());
|
||||
},
|
||||
onDestroy: async () => {
|
||||
handleEditorReady?.(false);
|
||||
|
||||
@@ -72,10 +72,16 @@ ul[data-type="taskList"] li {
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
margin: 0.1rem 0.15rem 0 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > div {
|
||||
margin-left: 1.15rem;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
border: 1px solid rgba(var(--color-border-300)) !important;
|
||||
outline: none;
|
||||
|
||||
@@ -5,6 +5,7 @@ import ts from "highlight.js/lib/languages/typescript";
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { cn } from "src/lib/utils";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
// we just have ts support for now
|
||||
const lowlight = createLowlight(common);
|
||||
@@ -30,23 +31,25 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="code-block relative">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"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",
|
||||
{
|
||||
"bg-green-500/10 hover:bg-green-500/10 active:bg-green-500/10": copied,
|
||||
}
|
||||
)}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{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" />
|
||||
)}
|
||||
</button>
|
||||
<NodeViewWrapper className="code-block relative group/code">
|
||||
<Tooltip tooltipContent="Copy code">
|
||||
<button
|
||||
type="button"
|
||||
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,
|
||||
}
|
||||
)}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-3 w-3 text-green-500" strokeWidth={3} />
|
||||
) : (
|
||||
<CopyIcon className="h-3 w-3 text-custom-text-300 group-hover/button:text-custom-text-100" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<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]" />
|
||||
|
||||
@@ -113,7 +113,7 @@ export const CoreEditorExtensions = ({
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex",
|
||||
class: "relative",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
|
||||
@@ -79,7 +79,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex pointer-events-none",
|
||||
class: "relative pointer-events-none",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"lucide-react": "^0.309.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"lucide-react": "^0.294.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@plane/editor-core": "*",
|
||||
"@plane/editor-extensions": "*",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"lucide-react": "^0.294.0"
|
||||
"lucide-react": "^0.378.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
||||
@@ -8,7 +8,8 @@ export type TModuleOrderByOptions =
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "created_at"
|
||||
| "-created_at";
|
||||
| "-created_at"
|
||||
| "sort_order";
|
||||
|
||||
export type TModuleLayoutOptions = "list" | "board" | "gantt";
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lowlight": "^2.9.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "^14.0.3",
|
||||
|
||||
@@ -200,6 +200,11 @@ export const CommandPalette: FC = observer(() => {
|
||||
|
||||
const keyPressed = key.toLowerCase();
|
||||
const cmdClicked = ctrlKey || metaKey;
|
||||
|
||||
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
|
||||
e.preventDefault();
|
||||
toggleCommandPaletteModal(true);
|
||||
}
|
||||
// if on input, textarea or editor, don't do anything
|
||||
if (
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
@@ -209,10 +214,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
return;
|
||||
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "k") {
|
||||
e.preventDefault();
|
||||
toggleCommandPaletteModal(true);
|
||||
} else if (keyPressed === "c" && altKey) {
|
||||
if (keyPressed === "c" && altKey) {
|
||||
e.preventDefault();
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
|
||||
const { searchQuery } = useCycleFilter();
|
||||
// derived values
|
||||
const filteredCycleIds = getFilteredCycleIds(projectId);
|
||||
const filteredCycleIds = getFilteredCycleIds(projectId, layout === "gantt");
|
||||
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
||||
|
||||
if (loader || !filteredCycleIds)
|
||||
|
||||
@@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
enableBlockLeftResize={false}
|
||||
enableBlockRightResize={false}
|
||||
enableBlockMove={false}
|
||||
enableReorder={false}
|
||||
enableReorder
|
||||
/>
|
||||
</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";
|
||||
// hooks
|
||||
// components
|
||||
@@ -62,6 +64,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
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
|
||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
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 { MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
@@ -16,12 +16,12 @@ import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
enableReorder: boolean;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
isDragging: boolean;
|
||||
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||
};
|
||||
|
||||
export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, enableReorder, provided, snapshot } = props;
|
||||
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
|
||||
@@ -30,12 +30,10 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"rounded bg-custom-background-80": snapshot.isDragging,
|
||||
"rounded bg-custom-background-80": isDragging,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
id={`sidebar-block-${block.id}`}
|
||||
@@ -50,7 +48,7 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
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="-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
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { CyclesSidebarBlock } from "./block";
|
||||
// types
|
||||
|
||||
@@ -16,85 +18,43 @@ type Props = {
|
||||
export const CycleGanttSidebar: 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,
|
||||
},
|
||||
});
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<Droppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div className="h-full" 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) => (
|
||||
<CyclesSidebarBlock
|
||||
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>
|
||||
<div className="h-full">
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<CyclesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 "./issues";
|
||||
export * from "./modules";
|
||||
export * from "./project-views";
|
||||
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 { MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
@@ -17,12 +17,12 @@ import { IGanttBlock } from "../../types";
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
enableReorder: boolean;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
isDragging: boolean;
|
||||
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||
};
|
||||
|
||||
export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, enableReorder, provided, snapshot } = props;
|
||||
export const IssuesSidebarBlock = observer((props: Props) => {
|
||||
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
@@ -32,15 +32,13 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
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(
|
||||
block.data.id
|
||||
),
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
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="-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
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// types
|
||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { IssuesSidebarBlock } from "./block";
|
||||
|
||||
type Props = {
|
||||
@@ -16,92 +18,50 @@ type Props = {
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = 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,
|
||||
},
|
||||
});
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<Droppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
|
||||
<div>
|
||||
{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
|
||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
|
||||
return (
|
||||
<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>
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
/>
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</GanttDnDHOC>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<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 { MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
@@ -16,12 +16,12 @@ import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
enableReorder: boolean;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
isDragging: boolean;
|
||||
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||
};
|
||||
|
||||
export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, enableReorder, provided, snapshot } = props;
|
||||
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
|
||||
@@ -30,12 +30,10 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"rounded bg-custom-background-80": snapshot.isDragging,
|
||||
"rounded bg-custom-background-80": isDragging,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
id={`sidebar-block-${block.id}`}
|
||||
@@ -50,7 +48,7 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
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="-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
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { ModulesSidebarBlock } from "./block";
|
||||
// types
|
||||
|
||||
@@ -16,85 +18,43 @@ type Props = {
|
||||
export const ModuleGanttSidebar: 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,
|
||||
},
|
||||
});
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOrderChange}>
|
||||
<Droppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div className="h-full" 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) => (
|
||||
<ModulesSidebarBlock
|
||||
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>
|
||||
<div className="h-full">
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<ModulesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</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 })}
|
||||
</div>
|
||||
{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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -30,7 +30,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
: null,
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
? () => fetchInboxIssueById(workspaceSlug, projectId, inboxIssueId)
|
||||
: null
|
||||
: null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
}
|
||||
);
|
||||
|
||||
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const InboxIssueAppliedFiltersState: FC = observer(() => {
|
||||
if (filteredValues.length === 0) 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="text-xs text-custom-text-200">Status</div>
|
||||
<div className="text-xs text-custom-text-200">State</div>
|
||||
{filteredValues.map((value) => {
|
||||
const optionDetail = currentOptionDetail(value);
|
||||
if (!optionDetail) return <></>;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||
// hooks
|
||||
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
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 { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
||||
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;
|
||||
|
||||
useOutsideClickDetector(cardRef, () => {
|
||||
cardRef?.current?.classList?.remove("highlight");
|
||||
cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS);
|
||||
});
|
||||
|
||||
// Make Issue block both as as Draggable and,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -20,12 +21,7 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
//components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import {
|
||||
KanbanDropLocation,
|
||||
getSourceFromDropPayload,
|
||||
getDestinationFromDropPayload,
|
||||
highlightIssueOnDrop,
|
||||
} from "./utils";
|
||||
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
|
||||
interface IKanbanGroup {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pull from "lodash/pull";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
89
web/components/issues/issue-layouts/list/block-root.tsx
Normal file
89
web/components/issues/issue-layouts/list/block-root.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { FC, MutableRefObject, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { IIssueDisplayProperties, TIssue, TIssueMap } from "@plane/types";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { IssueBlock } from "@/components/issues/issue-layouts/list";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// types
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
type Props = {
|
||||
issueIds: string[];
|
||||
issueId: string;
|
||||
issuesMap: TIssueMap;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: TRenderQuickActions;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
nestingLevel: number;
|
||||
spacingLeft?: number;
|
||||
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
export const IssueBlockRoot: FC<Props> = observer((props) => {
|
||||
const {
|
||||
issueIds,
|
||||
issueId,
|
||||
issuesMap,
|
||||
updateIssue,
|
||||
quickActions,
|
||||
canEditProperties,
|
||||
displayProperties,
|
||||
nestingLevel,
|
||||
spacingLeft = 14,
|
||||
containerRef,
|
||||
} = props;
|
||||
// states
|
||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const { subIssues: subIssuesStore } = useIssueDetail();
|
||||
|
||||
if (!issueId) return null;
|
||||
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
return (
|
||||
<>
|
||||
<RenderIfVisible
|
||||
key={`${issueId}`}
|
||||
defaultHeight="3rem"
|
||||
root={containerRef}
|
||||
classNames="relative border-b border-b-custom-border-200 last:border-b-transparent"
|
||||
changingReference={issueIds}
|
||||
>
|
||||
<IssueBlock
|
||||
issueId={issueId}
|
||||
issuesMap={issuesMap}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
displayProperties={displayProperties}
|
||||
isExpanded={isExpanded}
|
||||
setExpanded={setExpanded}
|
||||
nestingLevel={nestingLevel}
|
||||
spacingLeft={spacingLeft}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<IssueBlockRoot
|
||||
key={`${subIssueId}`}
|
||||
issueIds={issueIds}
|
||||
issueId={subIssueId}
|
||||
issuesMap={issuesMap}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
displayProperties={displayProperties}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
spacingLeft={spacingLeft + (displayProperties?.key ? 19 : 0)}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useRef } from "react";
|
||||
import { Dispatch, MouseEvent, SetStateAction, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
|
||||
// ui
|
||||
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { IssueProperties } from "@/components/issues/issue-layouts/properties";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useApplication, useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
@@ -19,18 +22,35 @@ interface IssueBlockProps {
|
||||
quickActions: TRenderQuickActions;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
nestingLevel: number;
|
||||
spacingLeft?: number;
|
||||
isExpanded: boolean;
|
||||
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlockProps) => {
|
||||
const { issuesMap, issueId, updateIssue, quickActions, displayProperties, canEditProperties } = props;
|
||||
const {
|
||||
issuesMap,
|
||||
issueId,
|
||||
updateIssue,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
canEditProperties,
|
||||
nestingLevel,
|
||||
spacingLeft = 14,
|
||||
isExpanded,
|
||||
setExpanded,
|
||||
} = props;
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// hooks
|
||||
const {
|
||||
router: { workspaceSlug },
|
||||
} = useApplication();
|
||||
|
||||
// store hooks
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { getIsIssuePeeked, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail();
|
||||
|
||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||
workspaceSlug &&
|
||||
@@ -47,28 +67,52 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||
const projectIdentifier = getProjectIdentifierById(issue.project_id);
|
||||
|
||||
const paddingLeft = `${spacingLeft}px`;
|
||||
|
||||
const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (nestingLevel >= 3) {
|
||||
handleIssuePeekOverview(issue);
|
||||
} else {
|
||||
setExpanded((prevState) => {
|
||||
if (!prevState && workspaceSlug && issue)
|
||||
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issue.project_id, issue.id);
|
||||
return !prevState;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className={cn(
|
||||
"min-h-[52px] relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 p-3 text-sm",
|
||||
"min-h-[52px] relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 p-3 pl-8 text-sm",
|
||||
{
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
|
||||
"last:border-b-transparent": !getIsIssuePeeked(issue.id),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full truncate">
|
||||
<div className="flex w-full truncate" style={issue.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}>
|
||||
<div className="flex flex-grow items-center gap-3 truncate">
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{projectIdentifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{projectIdentifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issue?.is_draft ? (
|
||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { FC, MutableRefObject } from "react";
|
||||
// components
|
||||
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { IssueBlock } from "@/components/issues";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
import { IssueBlockRoot } from "@/components/issues/issue-layouts/list";
|
||||
// types
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
interface Props {
|
||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||
@@ -22,27 +21,21 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{issueIds && issueIds.length > 0 ? (
|
||||
issueIds.map((issueId: string) => {
|
||||
if (!issueId) return null;
|
||||
return (
|
||||
<RenderIfVisible
|
||||
key={`${issueId}`}
|
||||
defaultHeight="3rem"
|
||||
root={containerRef}
|
||||
classNames="relative border-b border-b-custom-border-200 last:border-b-transparent"
|
||||
changingReference={issueIds}
|
||||
>
|
||||
<IssueBlock
|
||||
issueId={issueId}
|
||||
issuesMap={issuesMap}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
displayProperties={displayProperties}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
);
|
||||
})
|
||||
issueIds.map((issueId: string) => (
|
||||
<IssueBlockRoot
|
||||
key={`${issueId}`}
|
||||
issueIds={issueIds}
|
||||
issueId={issueId}
|
||||
issuesMap={issuesMap}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
displayProperties={displayProperties}
|
||||
nestingLevel={0}
|
||||
spacingLeft={0}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./roots";
|
||||
export * from "./block-root";
|
||||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./labels";
|
||||
export * from "./all-properties";
|
||||
|
||||
@@ -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 { useRouter } from "next/router";
|
||||
// icons
|
||||
@@ -33,6 +33,7 @@ interface Props {
|
||||
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||
issueIds: string[];
|
||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||
spacingLeft?: number;
|
||||
}
|
||||
|
||||
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
@@ -49,6 +50,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
containerRef,
|
||||
issueIds,
|
||||
spreadsheetColumnsList,
|
||||
spacingLeft = 14,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||
@@ -72,6 +74,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
nestingLevel={nestingLevel}
|
||||
spacingLeft={spacingLeft}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
updateIssue={updateIssue}
|
||||
portalElement={portalElement}
|
||||
@@ -93,6 +96,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
spacingLeft={spacingLeft + (displayProperties.key ? 16 : 28)}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
updateIssue={updateIssue}
|
||||
portalElement={portalElement}
|
||||
@@ -119,6 +123,7 @@ interface IssueRowDetailsProps {
|
||||
isExpanded: boolean;
|
||||
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||
spacingLeft?: number;
|
||||
}
|
||||
|
||||
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
@@ -135,6 +140,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
isExpanded,
|
||||
setExpanded,
|
||||
spreadsheetColumnsList,
|
||||
spacingLeft = 14,
|
||||
} = props;
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
@@ -161,22 +167,14 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
|
||||
const paddingLeft = `${nestingLevel * 54}px`;
|
||||
const paddingLeft = `${spacingLeft}px`;
|
||||
|
||||
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 = (
|
||||
<div
|
||||
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"
|
||||
}`}
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
@@ -186,76 +184,85 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
);
|
||||
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);
|
||||
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
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>
|
||||
<td ref={cellRef} id={`issue-${issueDetail.id}`} tabIndex={0}>
|
||||
<ControlLink
|
||||
id={`issue-${issueId}`}
|
||||
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
||||
target="_blank"
|
||||
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}
|
||||
>
|
||||
<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
|
||||
className="flex min-w-min items-center gap-1 px-4 py-2.5 pr-0"
|
||||
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
{issueDetail.sub_issues_count > 0 && (
|
||||
<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>
|
||||
</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>
|
||||
</ControlLink>
|
||||
</td>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { ContrastIcon } from "lucide-react";
|
||||
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
@@ -16,6 +17,9 @@ import { IStateStore } from "@/store/state.store";
|
||||
// constants
|
||||
// types
|
||||
|
||||
export const HIGHLIGHT_CLASS = "highlight";
|
||||
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||
|
||||
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
||||
[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 { TModuleFilters } from "@plane/types";
|
||||
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
||||
// components
|
||||
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
||||
// helpers
|
||||
@@ -8,19 +8,30 @@ import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: TModuleFilters;
|
||||
isFavoriteFilterApplied?: boolean;
|
||||
handleClearAllFilters: () => void;
|
||||
handleDisplayFiltersUpdate?: (updatedDisplayProperties: Partial<TModuleDisplayFilters>) => void;
|
||||
handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void;
|
||||
alwaysAllowEditing?: boolean;
|
||||
isArchived?: boolean;
|
||||
};
|
||||
|
||||
const MEMBERS_FILTERS = ["lead", "members"];
|
||||
const DATE_FILTERS = ["start_date", "target_date"];
|
||||
|
||||
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 (Object.keys(appliedFilters).length === 0) return null;
|
||||
if (!appliedFilters && !isFavoriteFilterApplied) return null;
|
||||
if (Object.keys(appliedFilters).length === 0 && !isFavoriteFilterApplied) return null;
|
||||
|
||||
const isEditingAllowed = alwaysAllowEditing;
|
||||
|
||||
@@ -73,6 +84,33 @@ export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
||||
</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 && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -64,6 +64,7 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
|
||||
handleClearAllFilters={() => clearAllFilters(projectId.toString(), "archived")}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
alwaysAllowEditing
|
||||
isArchived
|
||||
/>
|
||||
</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 isDescending = value?.[0] === "-";
|
||||
const isManual = value?.includes("sort_order");
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
@@ -38,7 +39,7 @@ export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||
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" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
<CustomMenu.MenuItem
|
||||
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>
|
||||
<CustomMenu.MenuItem
|
||||
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>
|
||||
{!isManual && (
|
||||
<>
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
<CustomMenu.MenuItem
|
||||
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>
|
||||
<CustomMenu.MenuItem
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IModule } from "@plane/types";
|
||||
import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "@/components/gantt-chart";
|
||||
import { ModuleGanttBlock } from "@/components/modules";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { useModule, useProject } from "@/hooks/store";
|
||||
import { useModule, useModuleFilter, useProject } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
@@ -16,6 +16,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule();
|
||||
const { currentProjectDisplayFilters: displayFilters } = useModuleFilter();
|
||||
// derived values
|
||||
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||
|
||||
@@ -54,7 +55,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={isAllowed}
|
||||
enableReorder={isAllowed && displayFilters?.order_by === "sort_order"}
|
||||
enableAddBlock={isAllowed}
|
||||
showAllBlocks
|
||||
/>
|
||||
|
||||
@@ -84,7 +84,7 @@ export const ModuleViewHeader: FC = observer(() => {
|
||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
});
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0 || displayFilters?.favorites;
|
||||
|
||||
return (
|
||||
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
||||
|
||||
@@ -4,13 +4,7 @@ import { useRouter } from "next/router";
|
||||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import {
|
||||
ModuleCardItem,
|
||||
ModuleListItem,
|
||||
ModulePeekOverview,
|
||||
ModuleViewHeader,
|
||||
ModulesListGanttChartView,
|
||||
} from "@/components/modules";
|
||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
@@ -73,12 +67,6 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
|
||||
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" && (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PageLoader } from "@/components/pages";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useProjectPages } from "@/hooks/store";
|
||||
import { useApplication, useProjectPages } from "@/hooks/store";
|
||||
// assets
|
||||
import AllFiltersImage from "public/empty-state/pages/all-filters.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;
|
||||
// store hooks
|
||||
const { loader, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(projectId);
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
// derived values
|
||||
const pageIds = getCurrentProjectPageIds(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
||||
@@ -30,8 +31,24 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
if (loader === "init-loader") return <PageLoader />;
|
||||
// if no pages exist in the active page type
|
||||
if (pageIds?.length === 0) {
|
||||
if (pageType === "public") return <EmptyState type={EmptyStateType.PROJECT_PAGE_PUBLIC} />;
|
||||
if (pageType === "private") return <EmptyState type={EmptyStateType.PROJECT_PAGE_PRIVATE} />;
|
||||
if (pageType === "public")
|
||||
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 no pages match the filter criteria
|
||||
|
||||
@@ -365,7 +365,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
|
||||
render={({ field: { value, onChange } }) => {
|
||||
if (value === undefined || value === null || typeof value === "string")
|
||||
return (
|
||||
<div className="h-7 flex-shrink-0" tabIndex={5}>
|
||||
<div className="flex-shrink-0" tabIndex={5}>
|
||||
<MemberDropdown
|
||||
value={value}
|
||||
onChange={(lead) => onChange(lead === value ? null : lead)}
|
||||
|
||||
@@ -501,12 +501,22 @@ const emptyStateDetails = {
|
||||
title: "No private pages yet",
|
||||
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
|
||||
path: "/empty-state/pages/private",
|
||||
primaryButton: {
|
||||
text: "Create your first page",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
[EmptyStateType.PROJECT_PAGE_PUBLIC]: {
|
||||
key: EmptyStateType.PROJECT_PAGE_PUBLIC,
|
||||
title: "No public pages yet",
|
||||
description: "See pages shared with everyone in your project right here.",
|
||||
path: "/empty-state/pages/public",
|
||||
primaryButton: {
|
||||
text: "Create your first page",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
[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",
|
||||
label: "Created date",
|
||||
},
|
||||
{
|
||||
key: "sort_order",
|
||||
label: "Manual",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
* @param {ICycle[]} cycles
|
||||
* @returns {ICycle[]}
|
||||
*/
|
||||
export const orderCycles = (cycles: ICycle[]): ICycle[] => {
|
||||
export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] => {
|
||||
if (cycles.length === 0) return [];
|
||||
|
||||
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() ?? ""));
|
||||
filteredCycles = sortBy(filteredCycles, [
|
||||
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
||||
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
||||
]);
|
||||
if (sortByManual) filteredCycles = sortBy(filteredCycles, [(c) => c.sort_order]);
|
||||
else
|
||||
filteredCycles = sortBy(filteredCycles, [
|
||||
(c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
|
||||
(c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
|
||||
]);
|
||||
|
||||
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 === "sort_order") orderedModules = sortBy(modules, [(m) => m.sort_order]);
|
||||
return orderedModules;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
@@ -40,7 +40,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.368.0",
|
||||
"lucide-react": "^0.378.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.0",
|
||||
"mobx-utils": "^6.0.8",
|
||||
@@ -80,4 +80,4 @@
|
||||
"tsconfig": "*",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { TModuleFilters } from "@plane/types";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { ModulesListHeader } from "@/components/headers";
|
||||
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||
import { ModuleViewHeader, ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||
// types
|
||||
// hooks
|
||||
import ModulesListMobileHeader from "@/components/modules/moduels-list-mobile-header";
|
||||
@@ -22,7 +22,8 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
const { currentProjectFilters, clearAllFilters, updateFilters } = useModuleFilter();
|
||||
const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } =
|
||||
useModuleFilter();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
||||
@@ -57,12 +58,23 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<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">
|
||||
<ModuleAppliedFiltersList
|
||||
appliedFilters={currentProjectFilters ?? {}}
|
||||
isFavoriteFilterApplied={currentProjectDisplayFilters?.favorites ?? false}
|
||||
handleClearAllFilters={() => clearAllFilters(`${projectId}`)}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
handleDisplayFiltersUpdate={(val) => {
|
||||
if (!projectId) return;
|
||||
updateDisplayFilters(projectId.toString(), val);
|
||||
}}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface ICycleStore {
|
||||
currentProjectActiveCycleId: string | null;
|
||||
currentProjectArchivedCycleIds: string[] | null;
|
||||
// computed actions
|
||||
getFilteredCycleIds: (projectId: string) => string[] | null;
|
||||
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
||||
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
||||
getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
|
||||
getCycleById: (cycleId: string) => ICycle | null;
|
||||
@@ -228,7 +228,7 @@ export class CycleStore implements ICycleStore {
|
||||
* @param {TCycleFilters} filters
|
||||
* @returns {string[] | null}
|
||||
*/
|
||||
getFilteredCycleIds = computedFn((projectId: string) => {
|
||||
getFilteredCycleIds = computedFn((projectId: string, sortByManual: boolean) => {
|
||||
const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId);
|
||||
const searchQuery = this.rootStore.cycleFilter.searchQuery;
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
@@ -239,7 +239,7 @@ export class CycleStore implements ICycleStore {
|
||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||
shouldFilterCycle(c, filters ?? {})
|
||||
);
|
||||
cycles = orderCycles(cycles);
|
||||
cycles = orderCycles(cycles, sortByManual);
|
||||
const cycleIds = cycles.map((c) => c.id);
|
||||
return cycleIds;
|
||||
});
|
||||
|
||||
@@ -177,6 +177,7 @@ export class ModuleFilterStore implements IModuleFilterStore {
|
||||
clearAllFilters = (projectId: string, state: keyof TModuleFiltersByState = "default") => {
|
||||
runInAction(() => {
|
||||
this.filters[projectId][state] = {};
|
||||
this.displayFilters[projectId].favorites = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -636,4 +636,8 @@ div.web-view-spinner div.bar12 {
|
||||
/* highlight class */
|
||||
.highlight {
|
||||
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"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.0.3.tgz#bb088a3d8eb77d9454dfdead433e594c5f793dae"
|
||||
integrity sha512-gfXwzQZRsWSLd/88IRNKwf2e5r3smZlDGLopg5tFkSqsL03VDHgd6wch6OfPhRK2kSCrT1uXv5n9hOpVBu678g==
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.3.0":
|
||||
version "1.3.0"
|
||||
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-8wjKAI5qSrLojt8ZJ2WhoS5P75oBu5g0yMpAnTDgfqFyQnkt5Uc1txCRWpG26SS1mv19nm8ak9XHF2DOugVfpw==
|
||||
dependencies:
|
||||
"@atlaskit/pragmatic-drag-and-drop" "^1.1.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"
|
||||
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
|
||||
|
||||
lucide-react@^0.294.0:
|
||||
version "0.294.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.294.0.tgz#dc406e1e7e2f722cf93218fe5b31cf3c95778817"
|
||||
integrity sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==
|
||||
|
||||
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==
|
||||
lucide-react@^0.378.0:
|
||||
version "0.378.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.378.0.tgz#232acb99c6baedfa90959a2c0dd11327b058bde8"
|
||||
integrity sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
|
||||
Reference in New Issue
Block a user