diff --git a/packages/types/src/module/module_filters.d.ts b/packages/types/src/module/module_filters.d.ts index 297c8046c..e22ac152a 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/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index ddb45b5e5..7f71892e9 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 6d84f73f0..e4d85cf4b 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 99ea6c94e..2f5abc886 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 e9543e367..1119e2e9c 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 000000000..765c0357f --- /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/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 752cb670c..eadbc0acf 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 c9e04c819..87f0c2f3f 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 cf4745f63..ece8d77a3 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/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 12025a682..b306a7752 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/dropdowns/order-by.tsx b/web/components/modules/dropdowns/order-by.tsx index d6fd3548b..cecda8e88 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 f0db46033..27768fba9 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/constants/module.ts b/web/constants/module.ts index 544e810ac..bfec73dae 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 97cb7348c..889487dad 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 14e7f0488..456cdfc8b 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 246ccf6dd..d34f97532 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", @@ -80,4 +80,4 @@ "tsconfig": "*", "typescript": "4.7.4" } -} +} \ No newline at end of file diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index f56e9f47e..1784a013b 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/styles/globals.css b/web/styles/globals.css index c3f7a0baf..b27d3ef45 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 268c041eb..28cc525bd 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"