From dd65d03d3383f32de617f2baafac6503efb88ede Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:12:24 +0530 Subject: [PATCH 1/6] [WEB-1184] feat: issue bulk operations (#4674) * feat: issue bulk operations * style: bulk operations action bar * chore: remove edition separation --- .../components/admin-sidebar/help-section.tsx | 13 +- web/components/gantt-chart/blocks/block.tsx | 22 +- .../gantt-chart/blocks/blocks-list.tsx | 15 +- .../gantt-chart/chart/main-content.tsx | 97 ++++--- web/components/gantt-chart/chart/root.tsx | 3 + web/components/gantt-chart/constants.ts | 2 + web/components/gantt-chart/root.tsx | 3 + .../gantt-chart/sidebar/cycles/block.tsx | 2 +- .../gantt-chart/sidebar/issues/block.tsx | 64 +++-- .../gantt-chart/sidebar/issues/sidebar.tsx | 12 +- .../gantt-chart/sidebar/modules/block.tsx | 2 +- web/components/gantt-chart/sidebar/root.tsx | 53 +++- .../issues/bulk-operations/index.ts | 2 + .../issues/bulk-operations/root.tsx | 19 ++ .../issues/bulk-operations/upgrade-banner.tsx | 32 +++ web/components/issues/index.ts | 1 + .../issue-layouts/gantt/base-gantt-root.tsx | 1 + .../issue-layouts/list/base-list-root.tsx | 21 +- .../issues/issue-layouts/list/block-root.tsx | 9 +- .../issues/issue-layouts/list/block.tsx | 86 ++++-- .../issues/issue-layouts/list/blocks-list.tsx | 46 ++-- .../issues/issue-layouts/list/default.tsx | 102 ++++--- .../list/headers/group-by-card.tsx | 249 ++++++++++-------- .../issues/issue-layouts/list/list-group.tsx | 9 +- .../spreadsheet/columns/assignee-column.tsx | 4 +- .../spreadsheet/columns/attachment-column.tsx | 4 +- .../spreadsheet/columns/created-on-column.tsx | 5 +- .../spreadsheet/columns/cycle-column.tsx | 19 +- .../spreadsheet/columns/due-date-column.tsx | 13 +- .../spreadsheet/columns/estimate-column.tsx | 8 +- .../spreadsheet/columns/label-column.tsx | 8 +- .../spreadsheet/columns/link-column.tsx | 4 +- .../spreadsheet/columns/module-column.tsx | 19 +- .../spreadsheet/columns/priority-column.tsx | 4 +- .../spreadsheet/columns/start-date-column.tsx | 4 +- .../spreadsheet/columns/state-column.tsx | 4 +- .../spreadsheet/columns/sub-issue-column.tsx | 2 +- .../spreadsheet/columns/updated-on-column.tsx | 5 +- .../spreadsheet/issue-column.tsx | 15 +- .../issue-layouts/spreadsheet/issue-row.tsx | 108 ++++++-- .../spreadsheet/spreadsheet-header.tsx | 57 +++- .../spreadsheet/spreadsheet-table.tsx | 6 + .../spreadsheet/spreadsheet-view.tsx | 68 +++-- web/components/workspace/help-section.tsx | 13 +- web/constants/common.ts | 2 + web/constants/spreadsheet.ts | 14 +- 46 files changed, 831 insertions(+), 420 deletions(-) create mode 100644 web/components/issues/bulk-operations/index.ts create mode 100644 web/components/issues/bulk-operations/root.tsx create mode 100644 web/components/issues/bulk-operations/upgrade-banner.tsx diff --git a/admin/components/admin-sidebar/help-section.tsx b/admin/components/admin-sidebar/help-section.tsx index 56ccbcd844..38edd06fc2 100644 --- a/admin/components/admin-sidebar/help-section.tsx +++ b/admin/components/admin-sidebar/help-section.tsx @@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { Transition } from "@headlessui/react"; +// ui import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; +// helpers +import { WEB_BASE_URL, cn } from "@/helpers/common.helper"; // hooks -import { WEB_BASE_URL } from "@/helpers/common.helper"; import { useTheme } from "@/hooks/store"; // assets import packageJson from "package.json"; @@ -42,9 +44,12 @@ export const HelpSection: FC = observer(() => { return (
diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx index 0897b6b39e..a33a0fd9f3 100644 --- a/web/components/gantt-chart/blocks/block.tsx +++ b/web/components/gantt-chart/blocks/block.tsx @@ -1,15 +1,16 @@ import { observer } from "mobx-react"; -// hooks -// components // helpers import { cn } from "@/helpers/common.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +// hooks import { useIssueDetail } from "@/hooks/store"; -// types +import { TSelectionHelper } from "@/hooks/use-multiple-select"; // constants import { BLOCK_HEIGHT } from "../constants"; +// components import { ChartAddBlock, ChartDraggable } from "../helpers"; import { useGanttChart } from "../hooks"; +// types import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { @@ -21,6 +22,7 @@ type Props = { enableBlockMove: boolean; enableAddBlock: boolean; ganttContainerRef: React.RefObject; + selectionHelpers: TSelectionHelper; }; export const GanttChartBlock: React.FC = observer((props) => { @@ -33,6 +35,7 @@ export const GanttChartBlock: React.FC = observer((props) => { enableBlockMove, enableAddBlock, ganttContainerRef, + selectionHelpers, } = props; // store hooks const { updateActiveBlockId, isBlockActive } = useGanttChart(); @@ -70,6 +73,10 @@ export const GanttChartBlock: React.FC = observer((props) => { }); }; + const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id); + const isBlockFocused = selectionHelpers.getIsEntityActive(block.id); + const isBlockHoveredOn = isBlockActive(block.id); + return (
= observer((props) => { >
updateActiveBlockId(block.id)} onMouseLeave={() => updateActiveBlockId(null)} diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index 8eb1d87725..6fd22b254e 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,10 +1,12 @@ import { FC } from "react"; -// components -import { HEADER_HEIGHT } from "../constants"; -import { IBlockUpdateData, IGanttBlock } from "../types"; -import { GanttChartBlock } from "./block"; -// types +// hooks +import { TSelectionHelper } from "@/hooks/use-multiple-select"; // constants +import { HEADER_HEIGHT } from "../constants"; +// types +import { IBlockUpdateData, IGanttBlock } from "../types"; +// components +import { GanttChartBlock } from "./block"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -17,6 +19,7 @@ export type GanttChartBlocksProps = { enableAddBlock: boolean; ganttContainerRef: React.RefObject; showAllBlocks: boolean; + selectionHelpers: TSelectionHelper; }; export const GanttChartBlocksList: FC = (props) => { @@ -31,6 +34,7 @@ export const GanttChartBlocksList: FC = (props) => { enableAddBlock, ganttContainerRef, showAllBlocks, + selectionHelpers, } = props; return ( @@ -56,6 +60,7 @@ export const GanttChartBlocksList: FC = (props) => { enableBlockMove={enableBlockMove} enableAddBlock={enableAddBlock} ganttContainerRef={ganttContainerRef} + selectionHelpers={selectionHelpers} /> ); })} diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 2f5abc8863..e3b9722379 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -2,8 +2,8 @@ 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 +import { MultipleSelectGroup } from "@/components/core"; import { BiWeekChartView, DayChartView, @@ -18,8 +18,12 @@ import { WeekChartView, YearChartView, } from "@/components/gantt-chart"; +import { IssueBulkOperationsRoot } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; +// constants +import { GANTT_SELECT_GROUP } from "../constants"; +// hooks import { useGanttChart } from "../hooks/use-gantt-chart"; type Props = { @@ -33,6 +37,7 @@ type Props = { enableBlockRightResize: boolean; enableReorder: boolean; enableAddBlock: boolean; + enableSelection: boolean; itemsContainerWidth: number; showAllBlocks: boolean; sidebarToRender: (props: any) => React.ReactNode; @@ -53,6 +58,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { enableBlockRightResize, enableReorder, enableAddBlock, + enableSelection, itemsContainerWidth, showAllBlocks, sidebarToRender, @@ -107,43 +113,58 @@ export const GanttChartMainContent: React.FC = observer((props) => { const ActiveChartView = CHART_VIEW_COMPONENTS[currentView]; return ( -
block.id) ?? [], + }} > - -
- - {currentViewData && ( - - )} -
-
+ {(helpers) => ( + <> +
+ +
+ + {currentViewData && ( + + )} +
+
+ + + )} + ); }); diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx index 395e0771cd..d961047e93 100644 --- a/web/components/gantt-chart/chart/root.tsx +++ b/web/components/gantt-chart/chart/root.tsx @@ -32,6 +32,7 @@ type ChartViewRootProps = { enableBlockMove: boolean; enableReorder: boolean; enableAddBlock: boolean; + enableSelection: boolean; bottomSpacing: boolean; showAllBlocks: boolean; quickAdd?: React.JSX.Element | undefined; @@ -51,6 +52,7 @@ export const ChartViewRoot: FC = observer((props) => { enableBlockMove, enableReorder, enableAddBlock, + enableSelection, bottomSpacing, showAllBlocks, quickAdd, @@ -184,6 +186,7 @@ export const ChartViewRoot: FC = observer((props) => { enableBlockRightResize={enableBlockRightResize} enableReorder={enableReorder} enableAddBlock={enableAddBlock} + enableSelection={enableSelection} itemsContainerWidth={itemsContainerWidth} showAllBlocks={showAllBlocks} sidebarToRender={sidebarToRender} diff --git a/web/components/gantt-chart/constants.ts b/web/components/gantt-chart/constants.ts index 958985cf16..52167a4984 100644 --- a/web/components/gantt-chart/constants.ts +++ b/web/components/gantt-chart/constants.ts @@ -3,3 +3,5 @@ export const BLOCK_HEIGHT = 44; export const HEADER_HEIGHT = 60; export const SIDEBAR_WIDTH = 360; + +export const GANTT_SELECT_GROUP = "gantt-issues"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 10c1c0d98d..267dfe5c58 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -18,6 +18,7 @@ type GanttChartRootProps = { enableBlockMove?: boolean; enableReorder?: boolean; enableAddBlock?: boolean; + enableSelection?: boolean; bottomSpacing?: boolean; showAllBlocks?: boolean; }; @@ -36,6 +37,7 @@ export const GanttChartRoot: FC = (props) => { enableBlockMove = false, enableReorder = false, enableAddBlock = false, + enableSelection = false, bottomSpacing = false, showAllBlocks = false, quickAdd, @@ -56,6 +58,7 @@ export const GanttChartRoot: FC = (props) => { enableBlockMove={enableBlockMove} enableReorder={enableReorder} enableAddBlock={enableAddBlock} + enableSelection={enableSelection} bottomSpacing={bottomSpacing} showAllBlocks={showAllBlocks} quickAdd={quickAdd} diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx index 1119e2e9ca..7922e86a62 100644 --- a/web/components/gantt-chart/sidebar/cycles/block.tsx +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -38,7 +38,7 @@ export const CyclesSidebarBlock: React.FC = observer((props) => {
; + selectionHelpers?: TSelectionHelper; }; export const IssuesSidebarBlock = observer((props: Props) => { - const { block, enableReorder, isDragging, dragHandleRef } = props; + const { block, enableReorder, enableSelection, isDragging, dragHandleRef, selectionHelpers } = props; // store hooks const { updateActiveBlockId, isBlockActive } = useGanttChart(); const { getIsIssuePeeked } = useIssueDetail(); const duration = findTotalDaysInRange(block.start_date, block.target_date); + const isIssueSelected = selectionHelpers?.getIsEntitySelected(block.id); + const isIssueFocused = selectionHelpers?.getIsEntityActive(block.id); + const isBlockHoveredOn = isBlockActive(block.id); + return (
updateActiveBlockId(block.id)} onMouseLeave={() => updateActiveBlockId(null)} >
- {enableReorder && ( - - )} +
+ {enableReorder && ( + + )} + {enableSelection && selectionHelpers && ( + + )} +
diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index 7da30216d4..f01a12b6dd 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -1,22 +1,26 @@ import { MutableRefObject } from "react"; -// components // ui import { Loader } from "@plane/ui"; -// types +// components import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types"; +// hooks +import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { GanttDnDHOC } from "../gantt-dnd-HOC"; import { handleOrderChange } from "../utils"; +// types import { IssuesSidebarBlock } from "./block"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blocks: IGanttBlock[] | null; enableReorder: boolean; + enableSelection: boolean; showAllBlocks?: boolean; + selectionHelpers?: TSelectionHelper; }; export const IssueGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; + const { blockUpdateHandler, blocks, enableReorder, enableSelection, showAllBlocks = false, selectionHelpers } = props; const handleOnDrop = ( draggingBlockId: string | undefined, @@ -47,8 +51,10 @@ export const IssueGanttSidebar: React.FC = (props) => { )} diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx index e79a654019..e6b28d54ae 100644 --- a/web/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -38,7 +38,7 @@ export const ModulesSidebarBlock: React.FC = observer((props) => {
void; enableReorder: boolean; + enableSelection: boolean; sidebarToRender: (props: any) => React.ReactNode; title: string; quickAdd?: React.JSX.Element | undefined; + selectionHelpers: TSelectionHelper; }; -export const GanttChartSidebar: React.FC = (props) => { - const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props; +export const GanttChartSidebar: React.FC = observer((props) => { + const { + blocks, + blockUpdateHandler, + enableReorder, + enableSelection, + sidebarToRender, + title, + quickAdd, + selectionHelpers, + } = props; + + const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty"; return (
= (props) => { }} >
-
{title}
+
+ {enableSelection && ( +
+ +
+ )} +
{title}
+
Duration
- {sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} + {sidebarToRender?.({ title, blockUpdateHandler, blocks, enableReorder, enableSelection, selectionHelpers })}
{quickAdd ? quickAdd : null}
); -}; +}); diff --git a/web/components/issues/bulk-operations/index.ts b/web/components/issues/bulk-operations/index.ts new file mode 100644 index 0000000000..f8c733c7ba --- /dev/null +++ b/web/components/issues/bulk-operations/index.ts @@ -0,0 +1,2 @@ +export * from "./root"; +export * from "./upgrade-banner"; diff --git a/web/components/issues/bulk-operations/root.tsx b/web/components/issues/bulk-operations/root.tsx new file mode 100644 index 0000000000..957f18609a --- /dev/null +++ b/web/components/issues/bulk-operations/root.tsx @@ -0,0 +1,19 @@ +import { observer } from "mobx-react"; +// components +import { BulkOperationsUpgradeBanner } from "@/components/issues"; +// hooks +import { useMultipleSelectStore } from "@/hooks/store"; + +type Props = { + className?: string; +}; + +export const IssueBulkOperationsRoot: React.FC = observer((props) => { + const { className } = props; + // store hooks + const { isSelectionActive } = useMultipleSelectStore(); + + if (!isSelectionActive) return null; + + return ; +}); diff --git a/web/components/issues/bulk-operations/upgrade-banner.tsx b/web/components/issues/bulk-operations/upgrade-banner.tsx new file mode 100644 index 0000000000..c96e6d2104 --- /dev/null +++ b/web/components/issues/bulk-operations/upgrade-banner.tsx @@ -0,0 +1,32 @@ +// ui +import { getButtonStyling } from "@plane/ui"; +// constants +import { MARKETING_PLANE_ONE_PAGE_LINK } from "@/constants/common"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + className?: string; +}; + +export const BulkOperationsUpgradeBanner: React.FC = (props) => { + const { className } = props; + + return ( +
+
+

+ Change state, priority, and more for several issues at once. Save three minutes on an average per operation. +

+ + Upgrade to One + +
+
+ ); +}; diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 83431f5beb..454d851109 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -1,4 +1,5 @@ export * from "./attachment"; +export * from "./bulk-operations"; export * from "./issue-modal"; export * from "./delete-issue-modal"; export * from "./issue-layouts"; diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 30aba9e92e..17df47163f 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -72,6 +72,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan enableBlockMove={isAllowed} enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed} enableAddBlock={isAllowed} + enableSelection={isAllowed} quickAdd={ enableIssueCreation && isAllowed ? ( diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index b58bdce2c3..73385a0945 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,16 +1,16 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; -// types +// constants import { EIssuesStoreType } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; +// hooks import { useIssues, useUser } from "@/hooks/store"; import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop"; import { useIssuesActions } from "@/hooks/use-issues-actions"; // components import { List } from "./default"; +// types import { IQuickActionProps, TRenderQuickActions } from "./list-view-types"; -// constants -// hooks type ListStoreType = | EIssuesStoreType.PROJECT @@ -37,22 +37,19 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { canEditPropertiesBasedOnProject, isCompletedCycle = false, } = props; - // router - //stores + // store hooks const { issuesFilter, issues } = useIssues(storeType); const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); - // mobx store const { membership: { currentProjectRole }, } = useUser(); - const { issueMap } = useIssues(); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - + // derived values const issueIds = issues?.groupedIssueIds || []; - + // auth + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; + const canEditProperties = useCallback( (projectId: string | undefined) => { const isEditingAllowedBasedOnProject = @@ -90,7 +87,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { ); return ( -
+
; + selectionHelpers: TSelectionHelper; groupId: string; isDragAllowed: boolean; canDropOverIssue: boolean; @@ -50,6 +52,7 @@ export const IssueBlockRoot: FC = observer((props) => { canDropOverIssue, isParentIssueBeingDragged = false, isLastChild = false, + selectionHelpers, } = props; // states const [isExpanded, setExpanded] = useState(false); @@ -132,6 +135,7 @@ export const IssueBlockRoot: FC = observer((props) => { setExpanded={setExpanded} nestingLevel={nestingLevel} spacingLeft={spacingLeft} + selectionHelpers={selectionHelpers} canDrag={!isSubIssue && isDragAllowed} isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging} setIsCurrentBlockDragging={setIsCurrentBlockDragging} @@ -139,9 +143,7 @@ export const IssueBlockRoot: FC = observer((props) => { {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( + subIssues?.map((subIssueId: string) => ( = observer((props) => { nestingLevel={nestingLevel + 1} spacingLeft={spacingLeft + (displayProperties?.key ? 12 : 0)} containerRef={containerRef} + selectionHelpers={selectionHelpers} groupId={groupId} isDragAllowed={isDragAllowed} canDropOverIssue={canDropOverIssue} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 2ff31ab1d4..56d88a7302 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -8,11 +8,13 @@ import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; // ui import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui"; // components +import { MultipleSelectEntityAction } from "@/components/core"; import { IssueProperties } from "@/components/issues/issue-layouts/properties"; // helpers import { cn } from "@/helpers/common.helper"; // hooks import { useAppRouter, useIssueDetail, useProject } from "@/hooks/store"; +import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { usePlatformOS } from "@/hooks/use-platform-os"; // types import { TRenderQuickActions } from "./list-view-types"; @@ -29,6 +31,7 @@ interface IssueBlockProps { spacingLeft?: number; isExpanded: boolean; setExpanded: Dispatch>; + selectionHelpers: TSelectionHelper; isCurrentBlockDragging: boolean; setIsCurrentBlockDragging: React.Dispatch>; canDrag: boolean; @@ -47,6 +50,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => { spacingLeft = 14, isExpanded, setExpanded, + selectionHelpers, isCurrentBlockDragging, setIsCurrentBlockDragging, canDrag, @@ -55,7 +59,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => { const issueRef = useRef(null); const dragHandleRef = useRef(null); // hooks - const { workspaceSlug } = useAppRouter(); + const { workspaceSlug, projectId } = useAppRouter(); const { getProjectIdentifierById } = useProject(); const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail(); @@ -98,8 +102,11 @@ export const IssueBlock = observer((props: IssueBlockProps) => { const canEditIssueProperties = canEditProperties(issue.project_id); const projectIdentifier = getProjectIdentifierById(issue.project_id); + const isIssueSelected = selectionHelpers.getIsEntitySelected(issue.id); + const isIssueActive = selectionHelpers.getIsEntityActive(issue.id); + const isSubIssue = nestingLevel !== 0; - const paddingLeft = `${spacingLeft}px`; + const marginLeft = `${spacingLeft}px`; const handleToggleExpand = (e: MouseEvent) => { e.stopPropagation(); @@ -119,39 +126,76 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
-
+
-
+ {/* drag handle */} +
-
- {subIssuesCount > 0 && ( - - )} -
+
+ {/* select checkbox */} + {projectId && canEditIssueProperties && ( + + Only issues within the current +
+ project can be selected. + + } + disabled={issue.project_id === projectId} + > +
+ +
+
+ )} + {/* sub-issues chevron */} +
+ {subIssuesCount > 0 && ( + + )}
{displayProperties && displayProperties?.key && ( -
+
{projectIdentifier}-{issue.sequence_id}
)} @@ -183,7 +227,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => { )}
{!issue?.tempId && ( -
+
{quickActions({ issue, parentRef: issueRef, diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 1d773001eb..a1d2d00964 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,6 +2,7 @@ import { FC, MutableRefObject } from "react"; // components import { TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { IssueBlockRoot } from "@/components/issues/issue-layouts/list"; +import { TSelectionHelper } from "@/hooks/use-multiple-select"; // types import { TRenderQuickActions } from "./list-view-types"; @@ -16,6 +17,7 @@ interface Props { containerRef: MutableRefObject; isDragAllowed: boolean; canDropOverIssue: boolean; + selectionHelpers: TSelectionHelper; } export const IssueBlocksList: FC = (props) => { @@ -28,33 +30,33 @@ export const IssueBlocksList: FC = (props) => { displayProperties, canEditProperties, containerRef, + selectionHelpers, isDragAllowed, canDropOverIssue, } = props; return ( -
- {issueIds && - issueIds.length > 0 && - issueIds.map((issueId: string, index) => ( - - ))} +
+ {issueIds?.map((issueId, index) => ( + + ))}
); }; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 66bc47c289..c527276ef4 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,7 +1,8 @@ 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"; -// components +import { observer } from "mobx-react"; +// types import { GroupByColumnTypes, TGroupedIssues, @@ -13,6 +14,9 @@ import { TIssueOrderByOptions, TIssueGroupByOptions, } from "@plane/types"; +// components +import { MultipleSelectGroup } from "@/components/core"; +import { IssueBulkOperationsRoot } from "@/components/issues"; // hooks import { EIssuesStoreType } from "@/constants/issue"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; @@ -46,7 +50,7 @@ export interface IGroupByList { isCompletedCycle?: boolean; } -const GroupByList: React.FC = (props) => { +const GroupByList: React.FC = observer((props) => { const { issueIds, issuesMap, @@ -113,43 +117,69 @@ const GroupByList: React.FC = (props) => { const is_list = group_by === null ? true : false; + // create groupIds array and entities object for bulk ops + const groupIds = groups.map((g) => g.id); + const orderedGroups: Record = {}; + groupIds.forEach((gID) => { + orderedGroups[gID] = []; + }); + let entities: Record = {}; + + if (is_list) { + entities = Object.assign(orderedGroups, { [groupIds[0]]: issueIds }); + } else { + entities = Object.assign(orderedGroups, { ...issueIds }); + } + return ( -
- {groups && - groups.length > 0 && - groups.map( - (group: IGroupByColumn) => - validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && ( - - ) - )} +
+ {groups && ( + + {(helpers) => ( + <> +
+ {groups.map( + (group: IGroupByColumn) => + validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && ( + + ) + )} +
+ + + )} +
+ )}
); -}; +}); + +GroupByList.displayName = "GroupByList"; export interface IList { issueIds: TGroupedIssues | TUnGroupedIssues | any; diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index d479bbeaa1..feb99a8a52 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,138 +1,169 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -// lucide icons import { CircleDashed, Plus } from "lucide-react"; -import { TIssue, ISearchIssueResponse } from "@plane/types"; -// components -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; -import { ExistingIssuesListModal } from "@/components/core"; -import { CreateUpdateIssueModal } from "@/components/issues"; -// ui -// mobx -// hooks -import { EIssuesStoreType } from "@/constants/issue"; -import { useEventTracker } from "@/hooks/store"; // types +import { TIssue, ISearchIssueResponse } from "@plane/types"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ExistingIssuesListModal, MultipleSelectGroupAction } from "@/components/core"; +import { CreateUpdateIssueModal } from "@/components/issues"; +// constants +import { EIssuesStoreType } from "@/constants/issue"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useEventTracker } from "@/hooks/store"; +import { TSelectionHelper } from "@/hooks/use-multiple-select"; interface IHeaderGroupByCard { + groupID: string; icon?: React.ReactNode; title: string; count: number; issuePayload: Partial; + canEditProperties: (projectId: string | undefined) => boolean; disableIssueCreation?: boolean; storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; + selectionHelpers: TSelectionHelper; } -export const HeaderGroupByCard = observer( - ({ icon, title, count, issuePayload, disableIssueCreation, storeType, addIssuesToView }: IHeaderGroupByCard) => { - const router = useRouter(); - const { workspaceSlug, projectId, moduleId, cycleId } = router.query; - // hooks - const { setTrackElement } = useEventTracker(); +export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { + const { + groupID, + icon, + title, + count, + issuePayload, + canEditProperties, + disableIssueCreation, + storeType, + addIssuesToView, + selectionHelpers, + } = props; + // states + const [isOpen, setIsOpen] = useState(false); + const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug, projectId, moduleId, cycleId } = router.query; + // hooks + const { setTrackElement } = useEventTracker(); + // derived values + const isDraftIssue = router.pathname.includes("draft-issue"); + const renderExistingIssueModal = moduleId || cycleId; + const existingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; + const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(groupID) === "empty"; + // auth + const canSelectIssues = canEditProperties(projectId?.toString()); - const [isOpen, setIsOpen] = useState(false); + const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId) return; - const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false); + const issues = data.map((i) => i.id); - const isDraftIssue = router.pathname.includes("draft-issue"); + try { + await addIssuesToView?.(issues); - const renderExistingIssueModal = moduleId || cycleId; - const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }); + } + }; - const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; - - const issues = data.map((i) => i.id); - - try { - await addIssuesToView?.(issues); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Issues added to the cycle successfully.", - }); - } catch (error) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - } - }; - - return ( - <> -
-
- {icon ? icon : } -
- -
-
{title}
-
{count || 0}
-
- - {!disableIssueCreation && - (renderExistingIssueModal ? ( - - - + return ( + <> +
+ {canSelectIssues && ( +
+ - { - setTrackElement("List layout"); - setIsOpen(true); - }} - > - Create issue - - { - setTrackElement("List layout"); - setOpenExistingIssueListModal(true); - }} - > - Add an existing issue - - - ) : ( -
+
+ )} +
+ {icon ?? } +
+ +
+
{title}
+
{count || 0}
+
+ + {!disableIssueCreation && + (renderExistingIssueModal ? ( + + + + } + > + { setTrackElement("List layout"); setIsOpen(true); }} > - -
- ))} + Create issue + + { + setTrackElement("List layout"); + setOpenExistingIssueListModal(true); + }} + > + Add an existing issue + + + ) : ( +
{ + setTrackElement("List layout"); + setIsOpen(true); + }} + > + +
+ ))} - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - isDraft={isDraftIssue} + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> + + {renderExistingIssueModal && ( + setOpenExistingIssueListModal(false)} + searchParams={existingIssuesListModalPayload} + handleOnSubmit={handleAddIssuesToView} /> - - {renderExistingIssueModal && ( - setOpenExistingIssueListModal(false)} - searchParams={ExistingIssuesListModalPayload} - handleOnSubmit={handleAddIssuesToView} - /> - )} -
- - ); - } -); + )} +
+ + ); +}); diff --git a/web/components/issues/issue-layouts/list/list-group.tsx b/web/components/issues/issue-layouts/list/list-group.tsx index 43c5f990e3..04457327e3 100644 --- a/web/components/issues/issue-layouts/list/list-group.tsx +++ b/web/components/issues/issue-layouts/list/list-group.tsx @@ -19,6 +19,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; import { DRAG_ALLOWED_GROUPS, EIssuesStoreType } from "@/constants/issue"; // hooks import { useProjectState } from "@/hooks/store"; +import { TSelectionHelper } from "@/hooks/use-multiple-select"; // components import { GroupDragOverlay } from "../group-drag-overlay"; import { @@ -58,6 +59,7 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; + selectionHelpers: TSelectionHelper; }; export const ListGroup = observer((props: Props) => { @@ -81,6 +83,7 @@ export const ListGroup = observer((props: Props) => { enableIssueQuickAdd, isCompletedCycle, storeType, + selectionHelpers, } = props; const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); @@ -190,15 +193,18 @@ export const ListGroup = observer((props: Props) => { "border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled, })} > -
+
@@ -224,6 +230,7 @@ export const ListGroup = observer((props: Props) => { containerRef={containerRef} isDragAllowed={isDragAllowed} canDropOverIssue={!canOverlayBeVisible} + selectionHelpers={selectionHelpers} /> )} diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx index 9fefd47594..d0fb5812dd 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx @@ -1,9 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; // components import { MemberDropdown } from "@/components/dropdowns"; -// types type Props = { issue: TIssue; @@ -36,7 +36,7 @@ export const SpreadsheetAssigneeColumn: React.FC = observer((props: Props buttonVariant={ issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text" } - buttonClassName="text-left" + buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" buttonContainerClassName="w-full" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx index 7fb7ef7e2d..0be3452626 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { TIssue } from "@plane/types"; // types +import { TIssue } from "@plane/types"; type Props = { issue: TIssue; @@ -11,7 +11,7 @@ export const SpreadsheetAttachmentColumn: React.FC = observer((props) => const { issue } = props; return ( -
+
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx index eea39478a5..a7845400c5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx @@ -1,9 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; -// types type Props = { issue: TIssue; @@ -11,8 +11,9 @@ type Props = { export const SpreadsheetCreatedOnColumn: React.FC = observer((props: Props) => { const { issue } = props; + return ( -
+
{renderFormattedDate(issue.created_at)}
); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx index 8cb2f43fb0..574ab6feac 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx @@ -1,14 +1,14 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { TIssue } from "@plane/types"; -// hooks -import { CycleDropdown } from "@/components/dropdowns"; -import { EIssuesStoreType } from "@/constants/issue"; -import { useEventTracker, useIssues } from "@/hooks/store"; -// components // types +import { TIssue } from "@plane/types"; +// components +import { CycleDropdown } from "@/components/dropdowns"; // constants +import { EIssuesStoreType } from "@/constants/issue"; +// hooks +import { useEventTracker, useIssues } from "@/hooks/store"; type Props = { issue: TIssue; @@ -17,11 +17,10 @@ type Props = { }; export const SpreadsheetCycleColumn: React.FC = observer((props) => { + const { issue, disabled, onClose } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; - // props - const { issue, disabled, onClose } = props; // hooks const { captureIssueEvent } = useEventTracker(); const { @@ -56,8 +55,8 @@ export const SpreadsheetCycleColumn: React.FC = observer((props) => { disabled={disabled} placeholder="Select cycle" buttonVariant="transparent-with-text" - buttonContainerClassName="w-full relative flex items-center p-2" - buttonClassName="relative leading-4 h-4.5 bg-transparent" + buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" + buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent" onClose={onClose} />
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index 58ebac58ec..f0c43a4573 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,16 +1,16 @@ import React from "react"; import { observer } from "mobx-react"; import { CalendarCheck2 } from "lucide-react"; +// types import { TIssue } from "@plane/types"; -// hooks // components import { DateDropdown } from "@/components/dropdowns"; // helpers import { cn } from "@/helpers/common.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; +// hooks import { useProjectState } from "@/hooks/store"; -// types type Props = { issue: TIssue; @@ -47,9 +47,12 @@ export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) icon={} buttonVariant="transparent-with-text" buttonContainerClassName="w-full" - buttonClassName={cn("rounded-none text-left", { - "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), - })} + buttonClassName={cn( + "rounded-none text-left group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10", + { + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), + } + )} clearIconClassName="!text-custom-text-100" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index 6acc0f6a5e..2e90cd2ba5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,8 +1,8 @@ -// components import { observer } from "mobx-react-lite"; -import { TIssue } from "@plane/types"; -import { EstimateDropdown } from "@/components/dropdowns"; // types +import { TIssue } from "@plane/types"; +// components +import { EstimateDropdown } from "@/components/dropdowns"; type Props = { issue: TIssue; @@ -25,7 +25,7 @@ export const SpreadsheetEstimateColumn: React.FC = observer((props: Props projectId={issue.project_id} disabled={disabled} buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" + buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" buttonContainerClassName="w-full" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index 439abf5f39..bb409d5637 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -1,10 +1,10 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; -// components // hooks import { useLabel } from "@/hooks/store"; -// types +// components import { IssuePropertyLabels } from "../../properties"; type Props = { @@ -27,8 +27,8 @@ export const SpreadsheetLabelColumn: React.FC = observer((props: Props) = value={issue.label_ids} defaultOptions={defaultLabelOptions} onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })} - className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80" - buttonClassName="px-2.5 h-full" + className="h-11 w-full border-b-[0.5px] border-custom-border-200" + buttonClassName="px-2.5 h-full group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" hideDropdownArrow maxRender={1} disabled={disabled} diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx index f2c11ab0f6..f8c6394297 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { TIssue } from "@plane/types"; // types +import { TIssue } from "@plane/types"; type Props = { issue: TIssue; @@ -11,7 +11,7 @@ export const SpreadsheetLinkColumn: React.FC = observer((props: Props) => const { issue } = props; return ( -
+
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx index efae44e840..2357a6791a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx @@ -2,14 +2,14 @@ import React, { useCallback } from "react"; import xor from "lodash/xor"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { TIssue } from "@plane/types"; -// hooks -import { ModuleDropdown } from "@/components/dropdowns"; -import { EIssuesStoreType } from "@/constants/issue"; -import { useEventTracker, useIssues } from "@/hooks/store"; -// components // types +import { TIssue } from "@plane/types"; +// components +import { ModuleDropdown } from "@/components/dropdowns"; // constants +import { EIssuesStoreType } from "@/constants/issue"; +// hooks +import { useEventTracker, useIssues } from "@/hooks/store"; type Props = { issue: TIssue; @@ -18,11 +18,10 @@ type Props = { }; export const SpreadsheetModuleColumn: React.FC = observer((props) => { + const { issue, disabled, onClose } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; - // props - const { issue, disabled, onClose } = props; // hooks const { captureIssueEvent } = useEventTracker(); const { @@ -65,8 +64,8 @@ export const SpreadsheetModuleColumn: React.FC = observer((props) => { disabled={disabled} placeholder="Select modules" buttonVariant="transparent-with-text" - buttonContainerClassName="w-full relative flex items-center p-2" - buttonClassName="relative leading-4 h-4.5 bg-transparent" + buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" + buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent" onClose={onClose} multiple showCount diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index 8058b70236..1e072a736a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -1,9 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; // components import { PriorityDropdown } from "@/components/dropdowns"; -// types type Props = { issue: TIssue; @@ -22,7 +22,7 @@ export const SpreadsheetPriorityColumn: React.FC = observer((props: Props onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })} disabled={disabled} buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" + buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" buttonContainerClassName="w-full" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx index aff8c7dfa7..9a17d34d4d 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx @@ -1,12 +1,12 @@ import React from "react"; import { observer } from "mobx-react"; import { CalendarClock } from "lucide-react"; +// types import { TIssue } from "@plane/types"; // components import { DateDropdown } from "@/components/dropdowns"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -// types type Props = { issue: TIssue; @@ -38,7 +38,7 @@ export const SpreadsheetStartDateColumn: React.FC = observer((props: Prop placeholder="Start date" icon={} buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" + buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" buttonContainerClassName="w-full" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx index 2015857284..f50ab4fced 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx @@ -1,9 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; // components import { StateDropdown } from "@/components/dropdowns"; -// types type Props = { issue: TIssue; @@ -23,7 +23,7 @@ export const SpreadsheetStateColumn: React.FC = observer((props) => { onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })} disabled={disabled} buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" + buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10" buttonContainerClassName="w-full" onClose={onClose} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index 8a6d26ac6c..7059545412 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -34,7 +34,7 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props
{}} className={cn( - "flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80", + "flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10", { "cursor-pointer": subIssueCount, } diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx index 60a0e6e536..08d7162d72 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx @@ -1,9 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue } from "@plane/types"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; -// types type Props = { issue: TIssue; @@ -11,8 +11,9 @@ type Props = { export const SpreadsheetUpdatedOnColumn: React.FC = observer((props: Props) => { const { issue } = props; + return ( -
+
{renderFormattedDate(issue.updated_at)}
); diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 161dd6514f..086d0fe3e0 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -1,13 +1,14 @@ import { useRef } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { IIssueDisplayProperties, TIssue } from "@plane/types"; // types -import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet"; -import { useEventTracker } from "@/hooks/store"; -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { IIssueDisplayProperties, TIssue } from "@plane/types"; // constants +import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet"; +// hooks +import { useEventTracker } from "@/hooks/store"; // components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; type Props = { displayProperties: IIssueDisplayProperties; @@ -37,7 +38,7 @@ export const IssueColumn = observer((props: Props) => { > { }) } disabled={disableUserActions} - onClose={() => { - tableCellRef?.current?.focus(); - }} + onClose={() => tableCellRef?.current?.focus()} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index eb33a13f35..771871bda5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -1,20 +1,23 @@ import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -// icons import { ChevronRight, MoreHorizontal } from "lucide-react"; +// types import { IIssueDisplayProperties, TIssue } from "@plane/types"; // ui import { ControlLink, Tooltip } from "@plane/ui"; // components +import { MultipleSelectEntityAction } from "@/components/core"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; +// constants +import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; // helper import { cn } from "@/helpers/common.helper"; // hooks import { useIssueDetail, useProject } from "@/hooks/store"; +import { TSelectionHelper } from "@/hooks/use-multiple-select"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// types // local components import { TRenderQuickActions } from "../list/list-view-types"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; @@ -34,6 +37,7 @@ interface Props { issueIds: string[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spacingLeft?: number; + selectionHelpers: TSelectionHelper; } export const SpreadsheetIssueRow = observer((props: Props) => { @@ -51,12 +55,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => { issueIds, spreadsheetColumnsList, spacingLeft = 6, + selectionHelpers, } = props; - + // states const [isExpanded, setExpanded] = useState(false); + // store hooks const { subIssues: subIssuesStore } = useIssueDetail(); - + // derived values const subIssues = subIssuesStore.subIssuesByIssueId(issueId); + const isIssueSelected = selectionHelpers.getIsEntitySelected(issueId); + const isIssueActive = selectionHelpers.getIsEntityActive(issueId); return ( <> @@ -65,7 +73,13 @@ export const SpreadsheetIssueRow = observer((props: Props) => { as="tr" defaultHeight="calc(2.75rem - 1px)" root={containerRef} - placeholderChildren={} + placeholderChildren={ + + } + classNames={cn("bg-custom-background-100 transition-[background-color]", { + "group selected-issue-row": isIssueSelected, + "border-[0.5px] border-custom-border-400": isIssueActive, + })} > { isExpanded={isExpanded} setExpanded={setExpanded} spreadsheetColumnsList={spreadsheetColumnsList} + selectionHelpers={selectionHelpers} /> {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( + subIssues?.map((subIssueId: string) => ( { containerRef={containerRef} issueIds={issueIds} spreadsheetColumnsList={spreadsheetColumnsList} + selectionHelpers={selectionHelpers} /> ))} @@ -123,6 +137,7 @@ interface IssueRowDetailsProps { setExpanded: Dispatch>; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spacingLeft?: number; + selectionHelpers: TSelectionHelper; } const IssueRowDetails = observer((props: IssueRowDetailsProps) => { @@ -140,6 +155,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { setExpanded, spreadsheetColumnsList, spacingLeft = 6, + selectionHelpers, } = props; // states const [isMenuActive, setIsMenuActive] = useState(false); @@ -148,7 +164,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const menuActionRef = useRef(null); // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; // hooks const { getProjectIdentifierById } = useProject(); const { getIsIssuePeeked, peekIssue, setPeekIssue } = useIssueDetail(); @@ -171,7 +187,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const issueDetail = issue.getIssueById(issueId); - const paddingLeft = `${spacingLeft}px`; + const marginLeft = `${spacingLeft}px`; useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); @@ -204,16 +220,22 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const disableUserActions = !canEditProperties(issueDetail.project_id); const subIssuesCount = issueDetail?.sub_issues_count ?? 0; + const isIssueSelected = selectionHelpers.getIsEntitySelected(issueDetail.id); return ( <> - + handleIssuePeekOverview(issueDetail)} className={cn( - "group clickable cursor-pointer 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", + "group clickable cursor-pointer h-11 w-[28rem] flex items-center text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200 bg-transparent group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10", { "border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id), "border border-custom-primary-70 hover:border-custom-primary-70": @@ -223,23 +245,51 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { )} disabled={!!issueDetail?.tempId} > -
-
- {/* bulk ops */} - -
- {subIssuesCount > 0 && ( - - )} -
+
+ {/* select checkbox */} + {projectId && !disableUserActions && ( + + Only issues within the current +
+ project can be selected. + + } + disabled={issueDetail.project_id === projectId} + > +
+ +
+
+ )} + {/* sub-issues chevron */} +
+ {subIssuesCount > 0 && ( + + )}
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index 63017f0e72..9de9a68e7f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -1,33 +1,68 @@ +import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // ui import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -// types -import { LayersIcon } from "@plane/ui"; // components +import { MultipleSelectGroupAction } from "@/components/core"; import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts"; +// constants +import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { TSelectionHelper } from "@/hooks/use-multiple-select"; interface Props { displayProperties: IIssueDisplayProperties; displayFilters: IIssueDisplayFilterOptions; handleDisplayFilterUpdate: (data: Partial) => void; + canEditProperties: (projectId: string | undefined) => boolean; isEstimateEnabled: boolean; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; + selectionHelpers: TSelectionHelper; } -export const SpreadsheetHeader = (props: Props) => { - const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled, spreadsheetColumnsList } = - props; +export const SpreadsheetHeader = observer((props: Props) => { + const { + displayProperties, + displayFilters, + handleDisplayFilterUpdate, + canEditProperties, + isEstimateEnabled, + spreadsheetColumnsList, + selectionHelpers, + } = props; + // router + const router = useRouter(); + const { projectId } = router.query; + // derived values + const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(SPREADSHEET_SELECT_GROUP) === "empty"; + // auth + const canSelectIssues = canEditProperties(projectId?.toString()); return ( - - - Issue - + {canSelectIssues && ( +
+ +
+ )} +
+ Issues {spreadsheetColumnsList.map((property) => ( @@ -43,4 +78,4 @@ export const SpreadsheetHeader = (props: Props) => { ); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index f548c69a5f..67bc99182f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -2,6 +2,7 @@ import { MutableRefObject, useCallback, useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; //types +import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { useTableKeyboardNavigation } from "@/hooks/use-table-keyboard-navigation"; //components import { TRenderQuickActions } from "../list/list-view-types"; @@ -20,6 +21,7 @@ type Props = { portalElement: React.MutableRefObject; containerRef: MutableRefObject; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; + selectionHelpers: TSelectionHelper; }; export const SpreadsheetTable = observer((props: Props) => { @@ -35,6 +37,7 @@ export const SpreadsheetTable = observer((props: Props) => { canEditProperties, containerRef, spreadsheetColumnsList, + selectionHelpers, } = props; // states @@ -81,8 +84,10 @@ export const SpreadsheetTable = observer((props: Props) => { displayProperties={displayProperties} displayFilters={displayFilters} handleDisplayFilterUpdate={handleDisplayFilterUpdate} + canEditProperties={canEditProperties} isEstimateEnabled={isEstimateEnabled} spreadsheetColumnsList={spreadsheetColumnsList} + selectionHelpers={selectionHelpers} /> {issueIds.map((id) => ( @@ -100,6 +105,7 @@ export const SpreadsheetTable = observer((props: Props) => { isScrolled={isScrolled} issueIds={issueIds} spreadsheetColumnsList={spreadsheetColumnsList} + selectionHelpers={selectionHelpers} /> ))} diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index a1f44d7f1e..c4b6e7d69a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -1,15 +1,18 @@ import React, { useRef } from "react"; import { observer } from "mobx-react-lite"; +// types import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; // components import { LogoSpinner } from "@/components/common"; -import { SpreadsheetQuickAddIssueForm } from "@/components/issues"; -import { SPREADSHEET_PROPERTY_LIST } from "@/constants/spreadsheet"; +import { MultipleSelectGroup } from "@/components/core"; +import { IssueBulkOperationsRoot, SpreadsheetQuickAddIssueForm } from "@/components/issues"; +// constants +import { SPREADSHEET_PROPERTY_LIST, SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; +// hooks import { useProject } from "@/hooks/store"; +// types import { TRenderQuickActions } from "../list/list-view-types"; import { SpreadsheetTable } from "./spreadsheet-table"; -// types -//hooks type Props = { displayProperties: IIssueDisplayProperties; @@ -73,28 +76,41 @@ export const SpreadsheetView: React.FC = observer((props) => { return (
-
- -
-
-
- {enableQuickCreateIssue && !disableIssueCreation && ( - - )} -
-
+ + {(helpers) => ( + <> +
+ +
+
+
+ {enableQuickCreateIssue && !disableIssueCreation && ( + + )} +
+
+ + + )} +
); }); diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index 130a62c873..513be22a51 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -1,12 +1,12 @@ import React, { useRef, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -// headless ui import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; import { Transition } from "@headlessui/react"; -// icons // ui import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useCommandPalette } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; @@ -59,9 +59,12 @@ export const WorkspaceHelpSection: React.FC = observe return ( <>
{!isCollapsed && ( diff --git a/web/constants/common.ts b/web/constants/common.ts index 71d65765c5..0c1431740d 100644 --- a/web/constants/common.ts +++ b/web/constants/common.ts @@ -3,3 +3,5 @@ export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing"; export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact"; + +export const MARKETING_PLANE_ONE_PAGE_LINK = "https://plane.so/one"; diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index d70a603a24..c45a1bf7c0 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,6 +1,16 @@ import { FC } from "react"; // icons -import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock, Users } from "lucide-react"; +import { + CalendarDays, + Link2, + Signal, + Tag, + Triangle, + Paperclip, + CalendarCheck2, + CalendarClock, + Users, +} from "lucide-react"; // types import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; // ui @@ -184,3 +194,5 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ "attachment_count", "sub_issue_count", ]; + +export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues"; From 87582604f725ed233cc4609f34bec13028ebcd2e Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 4 Jun 2024 11:22:40 +0530 Subject: [PATCH 2/6] style: fix overlapping of response container in AI popover. (#4684) --- web/components/core/modals/gpt-assistant-popover.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 5fd992a28c..099b90254e 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -210,11 +210,7 @@ export const GptAssistantPopover: React.FC = (props) => { {response !== "" && (
Response: - ${response}

`} - containerClassName={response ? "-mx-3 -my-3" : ""} - ref={responseRef} - /> + ${response}

`} ref={responseRef} />
)} {invalidResponse && ( From cad55f323487712940e155a51a83536654533735 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 4 Jun 2024 13:11:49 +0530 Subject: [PATCH 3/6] [WEB-1498] style: fix comments reaction alignment. (#4686) --- .../issues/issue-detail/reactions/reaction-selector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/issues/issue-detail/reactions/reaction-selector.tsx b/web/components/issues/issue-detail/reactions/reaction-selector.tsx index da517e975b..e3d58b5048 100644 --- a/web/components/issues/issue-detail/reactions/reaction-selector.tsx +++ b/web/components/issues/issue-detail/reactions/reaction-selector.tsx @@ -44,11 +44,11 @@ export const ReactionSelector: React.FC = (props) => { leaveTo="opacity-0 translate-y-1" > -
+
{reactionEmojis.map((emoji) => (
)} - {inboxIssuePaginationInfo?.next_page_results && ( -
+
+ {inboxIssuePaginationInfo?.next_page_results && ( + )}
- )}
)}
diff --git a/web/hooks/use-intersection-observer.tsx b/web/hooks/use-intersection-observer.tsx index 71c5f4b7f0..5ed5605113 100644 --- a/web/hooks/use-intersection-observer.tsx +++ b/web/hooks/use-intersection-observer.tsx @@ -9,12 +9,12 @@ export type UseIntersectionObserverProps = { export const useIntersectionObserver = ( containerRef: RefObject, - elementRef: RefObject, + elementRef: HTMLDivElement | null, callback: () => void, rootMargin?: string ) => { useEffect(() => { - if (elementRef.current) { + if (elementRef) { const observer = new IntersectionObserver( (entries) => { if (entries[entries.length - 1].isIntersecting) { @@ -26,16 +26,16 @@ export const useIntersectionObserver = ( rootMargin, } ); - observer.observe(elementRef.current); + observer.observe(elementRef); return () => { - if (elementRef.current) { + if (elementRef) { // eslint-disable-next-line react-hooks/exhaustive-deps - observer.unobserve(elementRef.current); + observer.unobserve(elementRef); } }; } // while removing the current from the refs, the observer is not not working as expected // fix this eslint warning with caution // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rootMargin, callback, elementRef.current, containerRef.current]); + }, [rootMargin, callback, elementRef, containerRef.current]); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx index f32adaf5d8..fc28c266c8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect } from "react"; +import { ReactElement } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; // components @@ -11,7 +11,7 @@ import { EmptyStateType } from "@/constants/empty-state"; // helpers import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks -import { useProject, useProjectInbox } from "@/hooks/store"; +import { useProject } from "@/hooks/store"; // layouts import { AppLayout } from "@/layouts/app-layout"; // types @@ -23,12 +23,6 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query; // hooks const { currentProjectDetails } = useProject(); - const { currentTab, handleCurrentTab } = useProjectInbox(); - - useEffect(() => { - if (navigationTab && currentTab != navigationTab) - handleCurrentTab(navigationTab === "open" ? EInboxIssueCurrentTab.OPEN : EInboxIssueCurrentTab.CLOSED); - }, [currentTab, navigationTab, handleCurrentTab]); // No access to inbox if (currentProjectDetails?.inbox_view === false) @@ -44,6 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox"; + const currentNavigationTab = navigationTab + ? navigationTab === "open" + ? EInboxIssueCurrentTab.OPEN + : EInboxIssueCurrentTab.CLOSED + : undefined; + if (!workspaceSlug || !projectId) return <>; return ( @@ -55,6 +55,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { projectId={projectId.toString()} inboxIssueId={inboxIssueId?.toString() || undefined} inboxAccessible={currentProjectDetails?.inbox_view || false} + navigationTab={currentNavigationTab} />
diff --git a/web/store/inbox/project-inbox.store.ts b/web/store/inbox/project-inbox.store.ts index 2aecb17cbe..2557f79797 100644 --- a/web/store/inbox/project-inbox.store.ts +++ b/web/store/inbox/project-inbox.store.ts @@ -321,8 +321,6 @@ export class ProjectInboxStore implements IProjectInboxStore { (this.inboxIssuePaginationInfo?.total_results && this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results)) ) { - this.loader = "pagination-loading"; - const queryParams = this.inboxIssueQueryParams( this.inboxFilters, this.inboxSorting, @@ -332,7 +330,6 @@ export class ProjectInboxStore implements IProjectInboxStore { const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams); runInAction(() => { - this.loader = undefined; set(this, "inboxIssuePaginationInfo", paginationInfo); if (results && results.length > 0) { const issueIds = results.map((value) => value?.issue?.id); @@ -343,7 +340,6 @@ export class ProjectInboxStore implements IProjectInboxStore { } else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false); } catch (error) { console.error("Error fetching the inbox issues", error); - this.loader = undefined; this.error = { message: "Error fetching the paginated inbox issues please try again later.", status: "pagination-error",