diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index af019a7ec6..cc3a343d2f 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -241,9 +241,9 @@ class IssueArchiveViewSet(BaseViewSet): ) datetime_fields = ["created_at", "updated_at"] issues = user_timezone_converter( - issue_queryset, datetime_fields, request.user.user_timezone + issues, datetime_fields, request.user.user_timezone ) - + return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index b1fd1a9bcc..fad85b79da 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -447,7 +447,7 @@ class IssueViewSet(BaseViewSet): ) datetime_fields = ["created_at", "updated_at"] issues = user_timezone_converter( - issue_queryset, datetime_fields, request.user.user_timezone + issues, datetime_fields, request.user.user_timezone ) return Response(issues, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py index fe75c61f1f..610c3c4687 100644 --- a/apiserver/plane/app/views/issue/draft.py +++ b/apiserver/plane/app/views/issue/draft.py @@ -232,7 +232,7 @@ class IssueDraftViewSet(BaseViewSet): ) datetime_fields = ["created_at", "updated_at"] issues = user_timezone_converter( - issue_queryset, datetime_fields, request.user.user_timezone + issues, datetime_fields, request.user.user_timezone ) return Response(issues, status=status.HTTP_200_OK) diff --git a/apiserver/plane/utils/user_timezone_converter.py b/apiserver/plane/utils/user_timezone_converter.py index 579b96c263..c946cfb27c 100644 --- a/apiserver/plane/utils/user_timezone_converter.py +++ b/apiserver/plane/utils/user_timezone_converter.py @@ -8,14 +8,14 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone): if isinstance(queryset, dict): queryset_values = [queryset] else: - queryset_values = list(queryset.values()) + queryset_values = list(queryset) # Iterate over the dictionaries in the list for item in queryset_values: # Iterate over the datetime fields for field in datetime_fields: # Convert the datetime field to the user's timezone - if item[field]: + if field in item and item[field]: item[field] = item[field].astimezone(user_tz) # If queryset was a single item, return a single item diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 6f2744dca8..883610ebe9 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -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", diff --git a/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx b/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx index f7218986b9..21fc36b39f 100644 --- a/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx +++ b/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx @@ -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 = ({ node }) }; return ( - - + + + +
         
diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json
index c69d0c2c80..ca2e501e67 100644
--- a/packages/editor/document-editor/package.json
+++ b/packages/editor/document-editor/package.json
@@ -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"
diff --git a/packages/editor/extensions/package.json b/packages/editor/extensions/package.json
index dfab06cdea..8c36087291 100644
--- a/packages/editor/extensions/package.json
+++ b/packages/editor/extensions/package.json
@@ -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": {
diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json
index fe1f29e1d5..6e596e925b 100644
--- a/packages/editor/rich-text-editor/package.json
+++ b/packages/editor/rich-text-editor/package.json
@@ -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",
diff --git a/packages/types/src/module/module_filters.d.ts b/packages/types/src/module/module_filters.d.ts
index 297c8046cd..e22ac152a5 100644
--- a/packages/types/src/module/module_filters.d.ts
+++ b/packages/types/src/module/module_filters.d.ts
@@ -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";
 
diff --git a/space/package.json b/space/package.json
index 9b05e573e9..c9c69512f2 100644
--- a/space/package.json
+++ b/space/package.json
@@ -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",
diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx
index 00de628b7d..37056a96f2 100644
--- a/web/components/command-palette/command-palette.tsx
+++ b/web/components/command-palette/command-palette.tsx
@@ -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") {
diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx
index ddb45b5e52..7f71892e99 100644
--- a/web/components/cycles/cycles-view.tsx
+++ b/web/components/cycles/cycles-view.tsx
@@ -26,7 +26,7 @@ export const CyclesView: FC = 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)
diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx
index 6d84f73f06..e4d85cf4bf 100644
--- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx
+++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx
@@ -61,7 +61,7 @@ export const CyclesListGanttChartView: FC = observer((props) => {
         enableBlockLeftResize={false}
         enableBlockRightResize={false}
         enableBlockMove={false}
-        enableReorder={false}
+        enableReorder
       />
     
   );
diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx
index 99ea6c94e9..2f5abc8863 100644
--- a/web/components/gantt-chart/chart/main-content.tsx
+++ b/web/components/gantt-chart/chart/main-content.tsx
@@ -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 = observer((props) => {
   const ganttContainerRef = useRef(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) => {
     const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx
index e9543e3672..1119e2e9ca 100644
--- a/web/components/gantt-chart/sidebar/cycles/block.tsx
+++ b/web/components/gantt-chart/sidebar/cycles/block.tsx
@@ -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;
 };
 
 export const CyclesSidebarBlock: React.FC = 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 = observer((props) => {
   return (
     
updateActiveBlockId(block.id)} onMouseLeave={() => updateActiveBlockId(null)} - ref={provided.innerRef} - {...provided.draggableProps} >
= observer((props) => {
-
+
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
{quickAdd ? quickAdd : null} diff --git a/web/components/gantt-chart/sidebar/utils.ts b/web/components/gantt-chart/sidebar/utils.ts new file mode 100644 index 0000000000..765c0357fe --- /dev/null +++ b/web/components/gantt-chart/sidebar/utils.ts @@ -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, + }, + }); +}; diff --git a/web/components/inbox/inbox-filter/applied-filters/state.tsx b/web/components/inbox/inbox-filter/applied-filters/state.tsx index e4f7f5fbdb..0a4d39c17e 100644 --- a/web/components/inbox/inbox-filter/applied-filters/state.tsx +++ b/web/components/inbox/inbox-filter/applied-filters/state.tsx @@ -21,7 +21,7 @@ export const InboxIssueAppliedFiltersState: FC = observer(() => { if (filteredValues.length === 0) return <>; return (
-
Status
+
State
{filteredValues.map((value) => { const optionDetail = currentOptionDetail(value); if (!optionDetail) return <>; diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 752cb670c9..eadbc0acfd 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -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 = 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, diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index c9e04c819d..87f0c2f3f8 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -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 { diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index cf4745f63b..ece8d77a3c 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -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); -}; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 83dbe57a70..84e2ae4854 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -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; 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(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>; 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 = (
setIsMenuActive(!isMenuActive)} @@ -186,76 +184,85 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { ); if (!issueDetail) return null; + const handleToggleExpand = (e: MouseEvent) => { + 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 ( <> - - -
-
- - {getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id} - - - -
- - {issueDetail.sub_issues_count > 0 && ( -
- -
- )} -
-
+ 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} > -
- -
- {issueDetail.name} +
+
+ {issueDetail.sub_issues_count > 0 && ( + + )} +
+ + +
+

+ {getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id} +

- +
+
+ +
+
+
+ +
+ {issueDetail.name} +
+
+
+
+
+ {quickActions({ + issue: issueDetail, + parentRef: cellRef, + customActionButton, + portalElement: portalElement.current, + })} +
diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 12025a6820..b306a7752d 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -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); +}; diff --git a/web/components/modules/applied-filters/root.tsx b/web/components/modules/applied-filters/root.tsx index 3011df0847..e48b8562f4 100644 --- a/web/components/modules/applied-filters/root.tsx +++ b/web/components/modules/applied-filters/root.tsx @@ -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) => 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) => { - 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) => {
); })} + {!isArchived && isFavoriteFilterApplied && ( +
+
+ Modules +
+ Favorite + {isEditingAllowed && ( + + )} +
+
+
+ )} {isEditingAllowed && (
)} diff --git a/web/components/modules/dropdowns/order-by.tsx b/web/components/modules/dropdowns/order-by.tsx index d6fd3548b2..cecda8e888 100644 --- a/web/components/modules/dropdowns/order-by.tsx +++ b/web/components/modules/dropdowns/order-by.tsx @@ -19,6 +19,7 @@ export const ModuleOrderByDropdown: React.FC = (props) => { const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key)); const isDescending = value?.[0] === "-"; + const isManual = value?.includes("sort_order"); return ( = (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) => { {value?.includes(option.key) && } ))} -
- { - if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions); - }} - > - Ascending - {!isDescending && } - - { - if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions); - }} - > - Descending - {isDescending && } - + {!isManual && ( + <> +
+ { + if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions); + }} + > + Ascending + {!isDescending && } + + { + if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions); + }} + > + Descending + {isDescending && } + + + )}
); }; diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index f0db46033d..27768fba99 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -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 /> diff --git a/web/components/modules/module-view-header.tsx b/web/components/modules/module-view-header.tsx index 2432cc0020..426bca68c3 100644 --- a/web/components/modules/module-view-header.tsx +++ b/web/components/modules/module-view-header.tsx @@ -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 (
diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 6a82fddd43..36de22d6c9 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -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 ( <> -
-
- Module name -
- -
{displayFilters?.layout === "list" && (
diff --git a/web/components/pages/pages-list-main-content.tsx b/web/components/pages/pages-list-main-content.tsx index 60e11dd8ce..ae01085d68 100644 --- a/web/components/pages/pages-list-main-content.tsx +++ b/web/components/pages/pages-list-main-content.tsx @@ -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 = 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 = observer((props) => { if (loader === "init-loader") return ; // if no pages exist in the active page type if (pageIds?.length === 0) { - if (pageType === "public") return ; - if (pageType === "private") return ; + if (pageType === "public") + return ( + { + commandPaletteStore.toggleCreatePageModal(true); + }} + /> + ); + if (pageType === "private") + return ( + { + commandPaletteStore.toggleCreatePageModal(true); + }} + /> + ); if (pageType === "archived") return ; } // if no pages match the filter criteria diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx index 8fa1b390f6..7fc8419fe7 100644 --- a/web/components/project/create-project-form.tsx +++ b/web/components/project/create-project-form.tsx @@ -365,7 +365,7 @@ export const CreateProjectForm: FC = observer((props) => { render={({ field: { value, onChange } }) => { if (value === undefined || value === null || typeof value === "string") return ( -
+
onChange(lead === value ? null : lead)} diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index d62c8808eb..9ca6f4edb3 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -500,12 +500,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, diff --git a/web/constants/module.ts b/web/constants/module.ts index 544e810ac6..bfec73dae0 100644 --- a/web/constants/module.ts +++ b/web/constants/module.ts @@ -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", + }, ]; diff --git a/web/helpers/cycle.helper.ts b/web/helpers/cycle.helper.ts index 97cb7348c0..889487dad4 100644 --- a/web/helpers/cycle.helper.ts +++ b/web/helpers/cycle.helper.ts @@ -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; }; diff --git a/web/helpers/module.helper.ts b/web/helpers/module.helper.ts index 14e7f04889..456cdfc8bb 100644 --- a/web/helpers/module.helper.ts +++ b/web/helpers/module.helper.ts @@ -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; }; diff --git a/web/package.json b/web/package.json index 246ccf6ddf..4be72c5a5f 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 22973347f6..85be0c1400 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -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(() => { <>
- {calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && ( +
+
+ Module name +
+ +
+ {(calculateTotalFilters(currentProjectFilters ?? {}) !== 0 || currentProjectDisplayFilters?.favorites) && (
clearAllFilters(`${projectId}`)} handleRemoveFilter={handleRemoveFilter} + handleDisplayFiltersUpdate={(val) => { + if (!projectId) return; + updateDisplayFilters(projectId.toString(), val); + }} alwaysAllowEditing />
diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index f56e9f47e1..1784a013b3 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -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; }); diff --git a/web/store/module_filter.store.ts b/web/store/module_filter.store.ts index 8d9315671f..ca5886541e 100644 --- a/web/store/module_filter.store.ts +++ b/web/store/module_filter.store.ts @@ -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; }); }; } diff --git a/web/styles/globals.css b/web/styles/globals.css index c3f7a0baf2..b27d3ef453 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -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)); } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 268c041eba..487b66c6d9 100644 --- a/yarn.lock +++ b/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"