Merge pull request #215 from makeplane/sync/ce-ee

sync: merge conflicts need to be resolved
This commit is contained in:
sriram veeraghanta
2024-05-08 15:46:03 +05:30
committed by GitHub
63 changed files with 1593 additions and 9019 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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",

View File

@@ -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);

View File

@@ -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;

View File

@@ -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]" />

View File

@@ -113,7 +113,7 @@ export const CoreEditorExtensions = ({
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex",
class: "relative",
},
nested: true,
}),

View File

@@ -79,7 +79,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex pointer-events-none",
class: "relative pointer-events-none",
},
nested: true,
}),

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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";

View File

@@ -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",

View File

@@ -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") {

View File

@@ -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)

View File

@@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}
enableReorder={false}
enableReorder
/>
</div>
);

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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>
);
};

View 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>
);
});

View File

@@ -1,5 +1,4 @@
export * from "./cycles";
export * from "./issues";
export * from "./modules";
export * from "./project-views";
export * from "./root";

View File

@@ -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" />

View File

@@ -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>
);
};

View File

@@ -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" />

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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}

View 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,
},
});
};

View File

@@ -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;

View File

@@ -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 <></>;

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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);
};

View 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}
/>
))}
</>
);
});

View File

@@ -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}>

View File

@@ -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>
)}

View File

@@ -1,4 +1,5 @@
export * from "./roots";
export * from "./block-root";
export * from "./block";
export * from "./roots";
export * from "./blocks-list";

View File

@@ -1 +1,2 @@
export * from "./labels";
export * from "./all-properties";

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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"

View File

@@ -64,6 +64,7 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
handleClearAllFilters={() => clearAllFilters(projectId.toString(), "archived")}
handleRemoveFilter={handleRemoveFilter}
alwaysAllowEditing
isArchived
/>
</div>
)}

View File

@@ -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>
);
};

View File

@@ -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
/>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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

View File

@@ -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)}

View File

@@ -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,

View File

@@ -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",
},
];

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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"
}
}
}

View File

@@ -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>

View File

@@ -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;
});

View File

@@ -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;
});
};
}

View File

@@ -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));
}

View File

@@ -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"