Merge branch 'preview' of https://github.com/makeplane/plane into chore/project-settings-events

This commit is contained in:
Lakhan 2024-06-05 13:57:22 +05:30
commit be60b25dad
71 changed files with 2571 additions and 2013 deletions

View File

@ -7,12 +7,12 @@ import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
import { Mails, KeyRound } from "lucide-react"; import { Mails, KeyRound } from "lucide-react";
import { TInstanceConfigurationKeys } from "@plane/types"; import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, setPromiseToast } from "@plane/ui"; import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
// hooks // hooks
// helpers // helpers
import { resolveGeneralTheme } from "@/helpers/common.helper"; import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// images // images
import githubLightModeImage from "@/public/logos/github-black.png"; import githubLightModeImage from "@/public/logos/github-black.png";
@ -45,6 +45,8 @@ const InstanceAuthenticationPage = observer(() => {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => { const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true); setIsSubmitting(true);
@ -129,7 +131,34 @@ const InstanceAuthenticationPage = observer(() => {
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> <div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? ( {formattedConfig ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="text-lg font-medium">Authentication modes</div> <div className="text-lg font-medium pb-1">Sign-up configuration</div>
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
Allow anyone to sign up without invite
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will disable self sign ups.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
Boolean(parseInt(enableSignUpConfig)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
{authenticationMethodsCard.map((method) => ( {authenticationMethodsCard.map((method) => (
<AuthenticationMethodCard <AuthenticationMethodCard
key={method.key} key={method.key}

View File

@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
// ui
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { WEB_BASE_URL, cn } from "@/helpers/common.helper";
// hooks // hooks
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store"; import { useTheme } from "@/hooks/store";
// assets // assets
import packageJson from "package.json"; import packageJson from "package.json";
@ -42,9 +44,12 @@ export const HelpSection: FC = observer(() => {
return ( return (
<div <div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${ className={cn(
isSidebarCollapsed ? "flex-col" : "" "flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
}`} {
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
> >
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}> <div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}> <Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>

View File

@ -12,6 +12,7 @@ from .base import BaseModel
def get_upload_path(instance, filename): def get_upload_path(instance, filename):
filename = filename[:50]
if instance.workspace_id is not None: if instance.workspace_id is not None:
return f"{instance.workspace.id}/{uuid4().hex}-{filename}" return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
return f"user-{uuid4().hex}-{filename}" return f"user-{uuid4().hex}-{filename}"

View File

@ -112,7 +112,7 @@ export const useEditor = ({
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) { if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
try { try {
editor.commands.setContent(value); editor.commands.setContent(value, false, { preserveWhitespace: "full" });
const currentSavedSelection = savedSelectionRef.current; const currentSavedSelection = savedSelectionRef.current;
if (currentSavedSelection) { if (currentSavedSelection) {
const docLength = editor.state.doc.content.size; const docLength = editor.state.doc.content.size;

View File

@ -50,9 +50,25 @@ export async function startImageUpload(
}; };
try { try {
const fileNameTrimmed = trimFileName(file.name);
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
const resolvedPos = view.state.doc.resolve(pos ?? 0);
const nodeBefore = resolvedPos.nodeBefore;
// if the image is at the start of the line i.e. when nodeBefore is null
if (nodeBefore === null) {
if (pos) {
// so that the image is not inserted at the next line, else incase the
// image is inserted at any line where there's some content, the
// position is kept as it is to be inserted at the next line
pos -= 1;
}
}
view.focus(); view.focus();
const src = await uploadAndValidateImage(file, uploadFile); const src = await uploadAndValidateImage(fileWithTrimmedName, uploadFile);
if (src == null) { if (src == null) {
throw new Error("Resolved image URL is undefined."); throw new Error("Resolved image URL is undefined.");
@ -112,3 +128,14 @@ async function uploadAndValidateImage(file: File, uploadFile: UploadImage): Prom
throw error; throw error;
} }
} }
function trimFileName(fileName: string, maxLength = 100) {
if (fileName.length > maxLength) {
const extension = fileName.split(".").pop();
const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1));
const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot
return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`;
}
return fileName;
}

View File

@ -20,12 +20,15 @@
"postcss": "postcss styles/globals.css -o styles/output.css --watch" "postcss": "postcss styles/globals.css -o styles/output.css --watch"
}, },
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/core": "^4.16.3", "@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3", "@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16", "emoji-picker-react": "^4.5.16",
"lodash": "^4.17.21",
"lucide-react": "^0.379.0", "lucide-react": "^0.379.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -14,3 +14,4 @@ export * from "./control-link";
export * from "./toast"; export * from "./toast";
export * from "./drag-handle"; export * from "./drag-handle";
export * from "./drop-indicator"; export * from "./drop-indicator";
export * from "./sortable";

View File

@ -0,0 +1,62 @@
import React, { useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { isEqual } from "lodash";
import { cn } from "../../helpers";
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import { DropIndicator } from "../drop-indicator";
type Props = {
children: React.ReactNode;
data: any; //@todo make this generic
className?: string;
};
const Draggable = ({ children, data, className }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState<boolean>(false); // NEW
const [isDraggedOver, setIsDraggedOver] = useState(false);
const [closestEdge, setClosestEdge] = useState<string | null>(null);
useEffect(() => {
const el = ref.current;
if (el) {
combine(
draggable({
element: el,
onDragStart: () => setDragging(true), // NEW
onDrop: () => setDragging(false), // NEW
getInitialData: () => data,
}),
dropTargetForElements({
element: el,
onDragEnter: (args) => {
setIsDraggedOver(true);
setClosestEdge(extractClosestEdge(args.self.data));
},
onDragLeave: () => setIsDraggedOver(false),
onDrop: () => {
setIsDraggedOver(false);
},
canDrop: ({ source }) => !isEqual(source.data, data) && source.data.__uuid__ === data.__uuid__,
getData: ({ input, element }) =>
attachClosestEdge(data, {
input,
element,
allowedEdges: ["top", "bottom"],
}),
})
);
}
}, [data]);
return (
<div ref={ref} className={cn(dragging && "opacity-25", className)}>
{<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />}
{children}
{<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />}
</div>
);
};
export { Draggable };

View File

@ -0,0 +1,2 @@
export * from "./sortable";
export * from "./draggable";

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Draggable } from "./draggable";
import { Sortable } from "./sortable";
const meta: Meta<typeof Sortable> = {
title: "Sortable",
component: Sortable,
};
export default meta;
type Story = StoryObj<typeof Sortable>;
const data = [
{ id: "1", name: "John Doe" },
{ id: "2", name: "Jane Doe 2" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" },
{ id: "5", name: "Charlie" },
];
export const Default: Story = {
args: {
data,
render: (item: any) => (
// <Draggable data={item} className="rounded-lg">
<div className="border ">{item.name}</div>
// </Draggable>
),
onChange: (data) => console.log(data.map(({ id }) => id)),
keyExtractor: (item: any) => item.id,
},
};

View File

@ -0,0 +1,79 @@
import React, { Fragment, useEffect, useMemo } from "react";
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { Draggable } from "./draggable";
type Props<T> = {
data: T[];
render: (item: T, index: number) => React.ReactNode;
onChange: (data: T[]) => void;
keyExtractor: (item: T, index: number) => string;
containerClassName?: string;
id: string;
};
const moveItem = <T,>(
data: T[],
source: T,
destination: T & Record<symbol, string>,
keyExtractor: (item: T, index: number) => string
) => {
const sourceIndex = data.indexOf(source);
if (sourceIndex === -1) return data;
const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0));
if (destinationIndex === -1) return data;
const symbolKey = Reflect.ownKeys(destination).find((key) => key.toString() === "Symbol(closestEdge)");
const position = symbolKey ? destination[symbolKey as symbol] : "bottom"; // Add 'as symbol' to cast symbolKey to symbol
const newData = [...data];
const [movedItem] = newData.splice(sourceIndex, 1);
let adjustedDestinationIndex = destinationIndex;
if (position === "bottom") {
adjustedDestinationIndex++;
}
// Prevent moving item out of bounds
if (adjustedDestinationIndex > newData.length) {
adjustedDestinationIndex = newData.length;
}
newData.splice(adjustedDestinationIndex, 0, movedItem);
return newData;
};
export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerClassName, id }: Props<T>) => {
useEffect(() => {
const unsubscribe = monitorForElements({
onDrop({ source, location }) {
const destination = location?.current?.dropTargets[0];
if (!destination) return;
onChange(moveItem(data, source.data as T, destination.data as T & { closestEdge: string }, keyExtractor));
},
});
// Clean up the subscription on unmount
return () => {
if (unsubscribe) unsubscribe();
};
}, [data, keyExtractor, onChange]);
const enhancedData = useMemo(() => {
const uuid = id ? id : Math.random().toString(36).substring(7);
return data.map((item) => ({ ...item, __uuid__: uuid }));
}, [data, id]);
return (
<>
{enhancedData.map((item, index) => (
<Draggable key={keyExtractor(item, index)} data={item} className={containerClassName}>
<Fragment>{render(item, index)} </Fragment>
</Draggable>
))}
</>
);
};
export default Sortable;

View File

@ -0,0 +1 @@
export * from "./sub-heading";

View File

@ -0,0 +1,15 @@
import React from "react";
import { cn } from "../../helpers";
type Props = {
children: React.ReactNode;
className?: string;
noMargin?: boolean;
};
const SubHeading = ({ children, className, noMargin }: Props) => (
<h3 className={cn("text-xl font-medium text-custom-text-200 block leading-7", !noMargin && "mb-2", className)}>
{children}
</h3>
);
export { SubHeading };

View File

@ -210,11 +210,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
{response !== "" && ( {response !== "" && (
<div className="page-block-section max-h-[8rem] text-sm"> <div className="page-block-section max-h-[8rem] text-sm">
Response: Response:
<RichTextReadOnlyEditor <RichTextReadOnlyEditor initialValue={`<p>${response}</p>`} ref={responseRef} />
initialValue={`<p>${response}</p>`}
containerClassName={response ? "-mx-3 -my-3" : ""}
ref={responseRef}
/>
</div> </div>
)} )}
{invalidResponse && ( {invalidResponse && (

View File

@ -1,15 +1,16 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// hooks
// components
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// types import { TSelectionHelper } from "@/hooks/use-multiple-select";
// constants // constants
import { BLOCK_HEIGHT } from "../constants"; import { BLOCK_HEIGHT } from "../constants";
// components
import { ChartAddBlock, ChartDraggable } from "../helpers"; import { ChartAddBlock, ChartDraggable } from "../helpers";
import { useGanttChart } from "../hooks"; import { useGanttChart } from "../hooks";
// types
import { IBlockUpdateData, IGanttBlock } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
type Props = { type Props = {
@ -21,6 +22,7 @@ type Props = {
enableBlockMove: boolean; enableBlockMove: boolean;
enableAddBlock: boolean; enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>; ganttContainerRef: React.RefObject<HTMLDivElement>;
selectionHelpers: TSelectionHelper;
}; };
export const GanttChartBlock: React.FC<Props> = observer((props) => { export const GanttChartBlock: React.FC<Props> = observer((props) => {
@ -33,6 +35,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
enableBlockMove, enableBlockMove,
enableAddBlock, enableAddBlock,
ganttContainerRef, ganttContainerRef,
selectionHelpers,
} = props; } = props;
// store hooks // store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart(); const { updateActiveBlockId, isBlockActive } = useGanttChart();
@ -70,6 +73,10 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
}); });
}; };
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
const isBlockHoveredOn = isBlockActive(block.id);
return ( return (
<div <div
key={`block-${block.id}`} key={`block-${block.id}`}
@ -80,10 +87,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
> >
<div <div
className={cn("relative h-full", { className={cn("relative h-full", {
"bg-custom-background-80": isBlockActive(block.id), "rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked( "bg-custom-background-90": isBlockHoveredOn,
block.data.id "bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isBlockSelected,
), "bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
"border border-r-0 border-custom-border-400": isBlockFocused,
})} })}
onMouseEnter={() => updateActiveBlockId(block.id)} onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)} onMouseLeave={() => updateActiveBlockId(null)}

View File

@ -1,10 +1,12 @@
import { FC } from "react"; import { FC } from "react";
// components // hooks
import { HEADER_HEIGHT } from "../constants"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
import { IBlockUpdateData, IGanttBlock } from "../types";
import { GanttChartBlock } from "./block";
// types
// constants // constants
import { HEADER_HEIGHT } from "../constants";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
// components
import { GanttChartBlock } from "./block";
export type GanttChartBlocksProps = { export type GanttChartBlocksProps = {
itemsContainerWidth: number; itemsContainerWidth: number;
@ -17,6 +19,7 @@ export type GanttChartBlocksProps = {
enableAddBlock: boolean; enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>; ganttContainerRef: React.RefObject<HTMLDivElement>;
showAllBlocks: boolean; showAllBlocks: boolean;
selectionHelpers: TSelectionHelper;
}; };
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => { export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
@ -31,6 +34,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
enableAddBlock, enableAddBlock,
ganttContainerRef, ganttContainerRef,
showAllBlocks, showAllBlocks,
selectionHelpers,
} = props; } = props;
return ( return (
@ -56,6 +60,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
enableBlockMove={enableBlockMove} enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef} ganttContainerRef={ganttContainerRef}
selectionHelpers={selectionHelpers}
/> />
); );
})} })}

View File

@ -2,8 +2,8 @@ import { useEffect, useRef } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// hooks
// components // components
import { MultipleSelectGroup } from "@/components/core";
import { import {
BiWeekChartView, BiWeekChartView,
DayChartView, DayChartView,
@ -18,8 +18,12 @@ import {
WeekChartView, WeekChartView,
YearChartView, YearChartView,
} from "@/components/gantt-chart"; } from "@/components/gantt-chart";
import { IssueBulkOperationsRoot } from "@/components/issues";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// constants
import { GANTT_SELECT_GROUP } from "../constants";
// hooks
import { useGanttChart } from "../hooks/use-gantt-chart"; import { useGanttChart } from "../hooks/use-gantt-chart";
type Props = { type Props = {
@ -33,6 +37,7 @@ type Props = {
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
enableReorder: boolean; enableReorder: boolean;
enableAddBlock: boolean; enableAddBlock: boolean;
enableSelection: boolean;
itemsContainerWidth: number; itemsContainerWidth: number;
showAllBlocks: boolean; showAllBlocks: boolean;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
@ -53,6 +58,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
enableBlockRightResize, enableBlockRightResize,
enableReorder, enableReorder,
enableAddBlock, enableAddBlock,
enableSelection,
itemsContainerWidth, itemsContainerWidth,
showAllBlocks, showAllBlocks,
sidebarToRender, sidebarToRender,
@ -107,6 +113,14 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView]; const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
return ( return (
<MultipleSelectGroup
containerRef={ganttContainerRef}
entities={{
[GANTT_SELECT_GROUP]: chartBlocks?.map((block) => block.id) ?? [],
}}
>
{(helpers) => (
<>
<div <div
// DO NOT REMOVE THE ID // DO NOT REMOVE THE ID
id="gantt-container" id="gantt-container"
@ -123,9 +137,11 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
blocks={blocks} blocks={blocks}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder} enableReorder={enableReorder}
enableSelection={enableSelection}
sidebarToRender={sidebarToRender} sidebarToRender={sidebarToRender}
title={title} title={title}
quickAdd={quickAdd} quickAdd={quickAdd}
selectionHelpers={helpers}
/> />
<div className="relative min-h-full h-max flex-shrink-0 flex-grow"> <div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView /> <ActiveChartView />
@ -141,9 +157,14 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef} ganttContainerRef={ganttContainerRef}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
selectionHelpers={helpers}
/> />
)} )}
</div> </div>
</div> </div>
<IssueBulkOperationsRoot selectionHelpers={helpers} />
</>
)}
</MultipleSelectGroup>
); );
}); });

View File

@ -32,6 +32,7 @@ type ChartViewRootProps = {
enableBlockMove: boolean; enableBlockMove: boolean;
enableReorder: boolean; enableReorder: boolean;
enableAddBlock: boolean; enableAddBlock: boolean;
enableSelection: boolean;
bottomSpacing: boolean; bottomSpacing: boolean;
showAllBlocks: boolean; showAllBlocks: boolean;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
@ -51,6 +52,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
enableBlockMove, enableBlockMove,
enableReorder, enableReorder,
enableAddBlock, enableAddBlock,
enableSelection,
bottomSpacing, bottomSpacing,
showAllBlocks, showAllBlocks,
quickAdd, quickAdd,
@ -184,6 +186,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
enableBlockRightResize={enableBlockRightResize} enableBlockRightResize={enableBlockRightResize}
enableReorder={enableReorder} enableReorder={enableReorder}
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
enableSelection={enableSelection}
itemsContainerWidth={itemsContainerWidth} itemsContainerWidth={itemsContainerWidth}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
sidebarToRender={sidebarToRender} sidebarToRender={sidebarToRender}

View File

@ -3,3 +3,5 @@ export const BLOCK_HEIGHT = 44;
export const HEADER_HEIGHT = 60; export const HEADER_HEIGHT = 60;
export const SIDEBAR_WIDTH = 360; export const SIDEBAR_WIDTH = 360;
export const GANTT_SELECT_GROUP = "gantt-issues";

View File

@ -18,6 +18,7 @@ type GanttChartRootProps = {
enableBlockMove?: boolean; enableBlockMove?: boolean;
enableReorder?: boolean; enableReorder?: boolean;
enableAddBlock?: boolean; enableAddBlock?: boolean;
enableSelection?: boolean;
bottomSpacing?: boolean; bottomSpacing?: boolean;
showAllBlocks?: boolean; showAllBlocks?: boolean;
}; };
@ -36,6 +37,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
enableBlockMove = false, enableBlockMove = false,
enableReorder = false, enableReorder = false,
enableAddBlock = false, enableAddBlock = false,
enableSelection = false,
bottomSpacing = false, bottomSpacing = false,
showAllBlocks = false, showAllBlocks = false,
quickAdd, quickAdd,
@ -56,6 +58,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
enableBlockMove={enableBlockMove} enableBlockMove={enableBlockMove}
enableReorder={enableReorder} enableReorder={enableReorder}
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
enableSelection={enableSelection}
bottomSpacing={bottomSpacing} bottomSpacing={bottomSpacing}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
quickAdd={quickAdd} quickAdd={quickAdd}

View File

@ -38,7 +38,7 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
<div <div
id={`sidebar-block-${block.id}`} id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", { className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id), "bg-custom-background-90": isBlockActive(block.id),
})} })}
style={{ style={{
height: `${BLOCK_HEIGHT}px`, height: `${BLOCK_HEIGHT}px`,

View File

@ -1,53 +1,63 @@
import React, { MutableRefObject } from "react"; import React, { MutableRefObject } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
// hooks
import { useGanttChart } from "@/components/gantt-chart/hooks";
// components // components
import { MultipleSelectEntityAction } from "@/components/core";
import { useGanttChart } from "@/components/gantt-chart/hooks";
import { IssueGanttSidebarBlock } from "@/components/issues"; import { IssueGanttSidebarBlock } from "@/components/issues";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { findTotalDaysInRange } from "@/helpers/date-time.helper"; import { findTotalDaysInRange } from "@/helpers/date-time.helper";
// hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// types import { TSelectionHelper } from "@/hooks/use-multiple-select";
// constants // constants
import { BLOCK_HEIGHT } from "../../constants"; import { BLOCK_HEIGHT, GANTT_SELECT_GROUP } from "../../constants";
// types
import { IGanttBlock } from "../../types"; import { IGanttBlock } from "../../types";
type Props = { type Props = {
block: IGanttBlock; block: IGanttBlock;
enableReorder: boolean; enableReorder: boolean;
enableSelection: boolean;
isDragging: boolean; isDragging: boolean;
dragHandleRef: MutableRefObject<HTMLButtonElement | null>; dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
selectionHelpers?: TSelectionHelper;
}; };
export const IssuesSidebarBlock = observer((props: Props) => { export const IssuesSidebarBlock = observer((props: Props) => {
const { block, enableReorder, isDragging, dragHandleRef } = props; const { block, enableReorder, enableSelection, isDragging, dragHandleRef, selectionHelpers } = props;
// store hooks // store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart(); const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { getIsIssuePeeked } = useIssueDetail(); const { getIsIssuePeeked } = useIssueDetail();
const duration = findTotalDaysInRange(block.start_date, block.target_date); 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 ( return (
<div <div
className={cn({ className={cn("group/list-block", {
"rounded bg-custom-background-80": isDragging, "rounded bg-custom-background-80": isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked( "rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
block.data.id "border border-r-0 border-custom-border-400": isIssueFocused,
),
})} })}
onMouseEnter={() => updateActiveBlockId(block.id)} onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)} onMouseLeave={() => updateActiveBlockId(null)}
> >
<div <div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", { className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id), "bg-custom-background-90": isBlockHoveredOn,
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isIssueSelected,
"bg-custom-primary-100/10": isIssueSelected && isBlockHoveredOn,
})} })}
style={{ style={{
height: `${BLOCK_HEIGHT}px`, height: `${BLOCK_HEIGHT}px`,
}} }}
> >
<div className="flex items-center gap-2">
{enableReorder && ( {enableReorder && (
<button <button
type="button" type="button"
@ -58,6 +68,20 @@ export const IssuesSidebarBlock = observer((props: Props) => {
<MoreVertical className="-ml-5 h-3.5 w-3.5" /> <MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button> </button>
)} )}
{enableSelection && selectionHelpers && (
<MultipleSelectEntityAction
className={cn(
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
{
"opacity-100 pointer-events-auto": isIssueSelected,
}
)}
groupId={GANTT_SELECT_GROUP}
id={block.id}
selectionHelpers={selectionHelpers}
/>
)}
</div>
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate"> <div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} /> <IssueGanttSidebarBlock issueId={block.data.id} />

View File

@ -1,22 +1,26 @@
import { MutableRefObject } from "react"; import { MutableRefObject } from "react";
// components
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // components
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types"; import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
// hooks
import { TSelectionHelper } from "@/hooks/use-multiple-select";
import { GanttDnDHOC } from "../gantt-dnd-HOC"; import { GanttDnDHOC } from "../gantt-dnd-HOC";
import { handleOrderChange } from "../utils"; import { handleOrderChange } from "../utils";
// types
import { IssuesSidebarBlock } from "./block"; import { IssuesSidebarBlock } from "./block";
type Props = { type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
enableReorder: boolean; enableReorder: boolean;
enableSelection: boolean;
showAllBlocks?: boolean; showAllBlocks?: boolean;
selectionHelpers?: TSelectionHelper;
}; };
export const IssueGanttSidebar: React.FC<Props> = (props) => { export const IssueGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; const { blockUpdateHandler, blocks, enableReorder, enableSelection, showAllBlocks = false, selectionHelpers } = props;
const handleOnDrop = ( const handleOnDrop = (
draggingBlockId: string | undefined, draggingBlockId: string | undefined,
@ -47,8 +51,10 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
<IssuesSidebarBlock <IssuesSidebarBlock
block={block} block={block}
enableReorder={enableReorder} enableReorder={enableReorder}
enableSelection={enableSelection}
isDragging={isDragging} isDragging={isDragging}
dragHandleRef={dragHandleRef} dragHandleRef={dragHandleRef}
selectionHelpers={selectionHelpers}
/> />
)} )}
</GanttDnDHOC> </GanttDnDHOC>

View File

@ -38,7 +38,7 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
<div <div
id={`sidebar-block-${block.id}`} id={`sidebar-block-${block.id}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", { className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": isBlockActive(block.id), "bg-custom-background-90": isBlockActive(block.id),
})} })}
style={{ style={{
height: `${BLOCK_HEIGHT}px`, height: `${BLOCK_HEIGHT}px`,

View File

@ -1,19 +1,38 @@
import { observer } from "mobx-react";
// components // components
import { MultipleSelectGroupAction } from "@/components/core";
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart"; import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// constants // constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants"; import { GANTT_SELECT_GROUP, HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
type Props = { type Props = {
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableReorder: boolean; enableReorder: boolean;
enableSelection: boolean;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
title: string; title: string;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
selectionHelpers: TSelectionHelper;
}; };
export const GanttChartSidebar: React.FC<Props> = (props) => { export const GanttChartSidebar: React.FC<Props> = observer((props) => {
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props; const {
blocks,
blockUpdateHandler,
enableReorder,
enableSelection,
sidebarToRender,
title,
quickAdd,
selectionHelpers,
} = props;
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
return ( return (
<div <div
@ -25,19 +44,39 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
}} }}
> >
<div <div
className="box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-8 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100" className="group/list-header box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-2 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
> >
<div
className={cn("flex items-center gap-2", {
"pl-2": !enableSelection,
})}
>
{enableSelection && (
<div className="flex-shrink-0 flex items-center w-3.5">
<MultipleSelectGroupAction
className={cn(
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
{
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
}
)}
groupID={GANTT_SELECT_GROUP}
selectionHelpers={selectionHelpers}
/>
</div>
)}
<h6>{title}</h6> <h6>{title}</h6>
</div>
<h6>Duration</h6> <h6>Duration</h6>
</div> </div>
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden"> <div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} {sidebarToRender?.({ title, blockUpdateHandler, blocks, enableReorder, enableSelection, selectionHelpers })}
</div> </div>
{quickAdd ? quickAdd : null} {quickAdd ? quickAdd : null}
</div> </div>
); );
}; });

View File

@ -1,6 +1,5 @@
import { FC, useState } from "react"; import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr";
import { Inbox, PanelLeft } from "lucide-react"; import { Inbox, PanelLeft } from "lucide-react";
// components // components
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
@ -10,6 +9,7 @@ import { InboxLayoutLoader } from "@/components/ui";
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store"; import { useProjectInbox } from "@/hooks/store";
@ -18,25 +18,25 @@ type TInboxIssueRoot = {
projectId: string; projectId: string;
inboxIssueId: string | undefined; inboxIssueId: string | undefined;
inboxAccessible: boolean; inboxAccessible: boolean;
navigationTab?: EInboxIssueCurrentTab | undefined;
}; };
export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => { export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible } = props; const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props;
// states // states
const [isMobileSidebar, setIsMobileSidebar] = useState(true); const [isMobileSidebar, setIsMobileSidebar] = useState(true);
// hooks // hooks
const { loader, error, fetchInboxIssues } = useProjectInbox(); const { loader, error, currentTab, handleCurrentTab, fetchInboxIssues } = useProjectInbox();
useSWR( useEffect(() => {
inboxAccessible && workspaceSlug && projectId ? `PROJECT_INBOX_ISSUES_${workspaceSlug}_${projectId}` : null, if (!inboxAccessible || !workspaceSlug || !projectId) return;
async () => { if (navigationTab && navigationTab !== currentTab) {
inboxAccessible && handleCurrentTab(navigationTab);
workspaceSlug && } else {
projectId && fetchInboxIssues(workspaceSlug.toString(), projectId.toString());
(await fetchInboxIssues(workspaceSlug.toString(), projectId.toString())); }
}, // eslint-disable-next-line react-hooks/exhaustive-deps
{ revalidateOnFocus: false, revalidateIfStale: false } }, [inboxAccessible, workspaceSlug, projectId]);
);
// loader // loader
if (loader === "init-loading") if (loader === "init-loading")

View File

@ -1,4 +1,4 @@
import { FC, useCallback, useRef } from "react"; import { FC, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { TInboxIssueCurrentTab } from "@plane/types"; import { TInboxIssueCurrentTab } from "@plane/types";
@ -37,7 +37,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
const { workspaceSlug, projectId, setIsMobileSidebar } = props; const { workspaceSlug, projectId, setIsMobileSidebar } = props;
// ref // ref
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const elementRef = useRef<HTMLDivElement>(null); const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
// store // store
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const {
@ -72,8 +72,10 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
currentTab === option?.key ? `text-custom-primary-100` : `hover:text-custom-text-200` currentTab === option?.key ? `text-custom-primary-100` : `hover:text-custom-text-200`
)} )}
onClick={() => { onClick={() => {
if (currentTab != option?.key) handleCurrentTab(option?.key); if (currentTab != option?.key) {
handleCurrentTab(option?.key);
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
}
}} }}
> >
<div>{option?.label}</div> <div>{option?.label}</div>
@ -126,15 +128,15 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
/> />
</div> </div>
)} )}
<div ref={setElementRef}>
{inboxIssuePaginationInfo?.next_page_results && ( {inboxIssuePaginationInfo?.next_page_results && (
<div ref={elementRef}>
<Loader className="mx-auto w-full space-y-4 py-4 px-2"> <Loader className="mx-auto w-full space-y-4 py-4 px-2">
<Loader.Item height="64px" width="w-100" /> <Loader.Item height="64px" width="w-100" />
<Loader.Item height="64px" width="w-100" /> <Loader.Item height="64px" width="w-100" />
</Loader> </Loader>
</div>
)} )}
</div> </div>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -0,0 +1,2 @@
export * from "./root";
export * from "./upgrade-banner";

View File

@ -0,0 +1,21 @@
import { observer } from "mobx-react";
// components
import { BulkOperationsUpgradeBanner } from "@/components/issues";
// hooks
import { useMultipleSelectStore } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
type Props = {
className?: string;
selectionHelpers: TSelectionHelper;
};
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
const { className } = props;
// store hooks
const { isSelectionActive } = useMultipleSelectStore();
if (!isSelectionActive) return null;
return <BulkOperationsUpgradeBanner className={className} />;
});

View File

@ -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> = (props) => {
const { className } = props;
return (
<div className={cn("sticky bottom-0 left-0 h-20 z-[2] px-3.5 grid place-items-center", className)}>
<div className="h-14 w-full bg-custom-primary-100/10 border-[0.5px] border-custom-primary-100/50 py-4 px-3.5 flex items-center justify-between gap-2 rounded-md">
<p className="text-custom-primary-100 font-medium">
Change state, priority, and more for several issues at once. Save three minutes on an average per operation.
</p>
<a
href={MARKETING_PLANE_ONE_PAGE_LINK}
target="_blank"
rel="noopener noreferrer"
className={cn(getButtonStyling("primary", "sm"), "flex-shrink-0")}
>
Upgrade to One
</a>
</div>
</div>
);
};

View File

@ -1,4 +1,5 @@
export * from "./attachment"; export * from "./attachment";
export * from "./bulk-operations";
export * from "./issue-modal"; export * from "./issue-modal";
export * from "./delete-issue-modal"; export * from "./delete-issue-modal";
export * from "./issue-layouts"; export * from "./issue-layouts";

View File

@ -44,11 +44,11 @@ export const ReactionSelector: React.FC<Props> = (props) => {
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel <Popover.Panel
className={`absolute -left-2 z-10 bg-custom-sidebar-background-100 ${ className={`absolute z-10 bg-custom-sidebar-background-100 ${
position === "top" ? "-top-12" : "-bottom-12" position === "top" ? "-top-12" : "-bottom-12"
}`} }`}
> >
<div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1 shadow-custom-shadow-sm"> <div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
{reactionEmojis.map((emoji) => ( {reactionEmojis.map((emoji) => (
<button <button

View File

@ -72,6 +72,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
enableBlockMove={isAllowed} enableBlockMove={isAllowed}
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed} enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
enableAddBlock={isAllowed} enableAddBlock={isAllowed}
enableSelection={isAllowed}
quickAdd={ quickAdd={
enableIssueCreation && isAllowed ? ( enableIssueCreation && isAllowed ? (
<GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} /> <GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} />

View File

@ -1,16 +1,16 @@
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types // constants
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useIssues, useUser } from "@/hooks/store"; import { useIssues, useUser } from "@/hooks/store";
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop"; import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useIssuesActions } from "@/hooks/use-issues-actions";
// components // components
import { List } from "./default"; import { List } from "./default";
// types
import { IQuickActionProps, TRenderQuickActions } from "./list-view-types"; import { IQuickActionProps, TRenderQuickActions } from "./list-view-types";
// constants
// hooks
type ListStoreType = type ListStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -37,22 +37,19 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
canEditPropertiesBasedOnProject, canEditPropertiesBasedOnProject,
isCompletedCycle = false, isCompletedCycle = false,
} = props; } = props;
// router // store hooks
//stores
const { issuesFilter, issues } = useIssues(storeType); const { issuesFilter, issues } = useIssues(storeType);
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType);
// mobx store
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { issueMap } = useIssues(); const { issueMap } = useIssues();
// derived values
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const issueIds = issues?.groupedIssueIds || []; const issueIds = issues?.groupedIssueIds || [];
// auth
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
const canEditProperties = useCallback( const canEditProperties = useCallback(
(projectId: string | undefined) => { (projectId: string | undefined) => {
const isEditingAllowedBasedOnProject = const isEditingAllowedBasedOnProject =
@ -90,7 +87,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
); );
return ( return (
<div className={`relative h-full w-full bg-custom-background-90`}> <div className="relative size-full bg-custom-background-90">
<List <List
issuesMap={issueMap} issuesMap={issueMap}
displayProperties={displayProperties} displayProperties={displayProperties}

View File

@ -10,6 +10,7 @@ import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { IssueBlock } from "@/components/issues/issue-layouts/list"; import { IssueBlock } from "@/components/issues/issue-layouts/list";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// types // types
import { HIGHLIGHT_CLASS, getIssueBlockId } from "../utils"; import { HIGHLIGHT_CLASS, getIssueBlockId } from "../utils";
@ -26,6 +27,7 @@ type Props = {
nestingLevel: number; nestingLevel: number;
spacingLeft?: number; spacingLeft?: number;
containerRef: MutableRefObject<HTMLDivElement | null>; containerRef: MutableRefObject<HTMLDivElement | null>;
selectionHelpers: TSelectionHelper;
groupId: string; groupId: string;
isDragAllowed: boolean; isDragAllowed: boolean;
canDropOverIssue: boolean; canDropOverIssue: boolean;
@ -50,6 +52,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
canDropOverIssue, canDropOverIssue,
isParentIssueBeingDragged = false, isParentIssueBeingDragged = false,
isLastChild = false, isLastChild = false,
selectionHelpers,
} = props; } = props;
// states // states
const [isExpanded, setExpanded] = useState<boolean>(false); const [isExpanded, setExpanded] = useState<boolean>(false);
@ -132,6 +135,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
setExpanded={setExpanded} setExpanded={setExpanded}
nestingLevel={nestingLevel} nestingLevel={nestingLevel}
spacingLeft={spacingLeft} spacingLeft={spacingLeft}
selectionHelpers={selectionHelpers}
canDrag={!isSubIssue && isDragAllowed} canDrag={!isSubIssue && isDragAllowed}
isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging} isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging}
setIsCurrentBlockDragging={setIsCurrentBlockDragging} setIsCurrentBlockDragging={setIsCurrentBlockDragging}
@ -139,9 +143,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
</RenderIfVisible> </RenderIfVisible>
{isExpanded && {isExpanded &&
subIssues && subIssues?.map((subIssueId: string) => (
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<IssueBlockRoot <IssueBlockRoot
key={`${subIssueId}`} key={`${subIssueId}`}
issueIds={issueIds} issueIds={issueIds}
@ -154,6 +156,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
nestingLevel={nestingLevel + 1} nestingLevel={nestingLevel + 1}
spacingLeft={spacingLeft + (displayProperties?.key ? 12 : 0)} spacingLeft={spacingLeft + (displayProperties?.key ? 12 : 0)}
containerRef={containerRef} containerRef={containerRef}
selectionHelpers={selectionHelpers}
groupId={groupId} groupId={groupId}
isDragAllowed={isDragAllowed} isDragAllowed={isDragAllowed}
canDropOverIssue={canDropOverIssue} canDropOverIssue={canDropOverIssue}

View File

@ -8,11 +8,13 @@ import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
// ui // ui
import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui"; import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui";
// components // components
import { MultipleSelectEntityAction } from "@/components/core";
import { IssueProperties } from "@/components/issues/issue-layouts/properties"; import { IssueProperties } from "@/components/issues/issue-layouts/properties";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useAppRouter, useIssueDetail, useProject } from "@/hooks/store"; import { useAppRouter, useIssueDetail, useProject } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// types // types
import { TRenderQuickActions } from "./list-view-types"; import { TRenderQuickActions } from "./list-view-types";
@ -29,6 +31,7 @@ interface IssueBlockProps {
spacingLeft?: number; spacingLeft?: number;
isExpanded: boolean; isExpanded: boolean;
setExpanded: Dispatch<SetStateAction<boolean>>; setExpanded: Dispatch<SetStateAction<boolean>>;
selectionHelpers: TSelectionHelper;
isCurrentBlockDragging: boolean; isCurrentBlockDragging: boolean;
setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>; setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>;
canDrag: boolean; canDrag: boolean;
@ -47,6 +50,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
spacingLeft = 14, spacingLeft = 14,
isExpanded, isExpanded,
setExpanded, setExpanded,
selectionHelpers,
isCurrentBlockDragging, isCurrentBlockDragging,
setIsCurrentBlockDragging, setIsCurrentBlockDragging,
canDrag, canDrag,
@ -55,7 +59,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
const issueRef = useRef<HTMLDivElement | null>(null); const issueRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef(null); const dragHandleRef = useRef(null);
// hooks // hooks
const { workspaceSlug } = useAppRouter(); const { workspaceSlug, projectId } = useAppRouter();
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById } = useProject();
const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail(); const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail();
@ -98,8 +102,11 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
const canEditIssueProperties = canEditProperties(issue.project_id); const canEditIssueProperties = canEditProperties(issue.project_id);
const projectIdentifier = getProjectIdentifierById(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<HTMLButtonElement>) => { const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
@ -119,39 +126,76 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
<div <div
ref={issueRef} ref={issueRef}
className={cn( className={cn(
"group min-h-11 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 pl-1.5 pr-1 text-sm", "group/list-block min-h-11 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 hover:bg-custom-background-90 p-3 pl-1.5 text-sm transition-colors border border-transparent",
{ {
"border border-custom-primary-70 hover:border-custom-primary-70": "border-custom-primary-70": getIsIssuePeeked(issue.id) && peekIssue?.nestingLevel === nestingLevel,
getIsIssuePeeked(issue.id) && peekIssue?.nestingLevel === nestingLevel, "border-custom-border-400": isIssueActive,
"last:border-b-transparent": !getIsIssuePeeked(issue.id), "last:border-b-transparent": !getIsIssuePeeked(issue.id) && !isIssueActive,
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isIssueSelected,
"bg-custom-background-80": isCurrentBlockDragging, "bg-custom-background-80": isCurrentBlockDragging,
} }
)} )}
> >
<div className="flex w-full truncate" style={nestingLevel !== 0 ? { paddingLeft } : {}}> <div className="flex w-full truncate">
<div className="flex flex-grow items-center gap-3 truncate"> <div className="flex flex-grow items-center gap-3 truncate">
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<div className="flex items-center"> {/* drag handle */}
<div className="size-4 flex items-center group/drag-handle">
<DragHandle <DragHandle
ref={dragHandleRef} ref={dragHandleRef}
disabled={!canDrag} disabled={!canDrag}
className={cn("opacity-0 group-hover:opacity-100", { className={cn("opacity-0 group-hover/drag-handle:opacity-100", {
"opacity-100": isCurrentBlockDragging, "opacity-100": isCurrentBlockDragging,
})} })}
/> />
<div className="flex h-5 w-5 items-center justify-center"> </div>
{/* select checkbox */}
{projectId && canEditIssueProperties && (
<Tooltip
tooltipContent={
<>
Only issues within the current
<br />
project can be selected.
</>
}
disabled={issue.project_id === projectId}
>
<div className="flex-shrink-0 grid place-items-center w-3.5">
<MultipleSelectEntityAction
className={cn(
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
{
"opacity-100 pointer-events-auto": isIssueSelected,
}
)}
groupId={groupId}
id={issue.id}
selectionHelpers={selectionHelpers}
disabled={issue.project_id !== projectId}
/>
</div>
</Tooltip>
)}
{/* sub-issues chevron */}
<div className="size-4 grid place-items-center flex-shrink-0" style={isSubIssue ? { marginLeft } : {}}>
{subIssuesCount > 0 && ( {subIssuesCount > 0 && (
<button <button
className="flex items-center justify-center h-5 w-5 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300" type="button"
className="size-4 grid place-items-center rounded-sm text-custom-text-400 hover:text-custom-text-300"
onClick={handleToggleExpand} onClick={handleToggleExpand}
> >
<ChevronRight className={`h-4 w-4 ${isExpanded ? "rotate-90" : ""}`} /> <ChevronRight
className={cn("size-4", {
"rotate-90": isExpanded,
})}
strokeWidth={2.5}
/>
</button> </button>
)} )}
</div> </div>
</div>
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="pl-1 flex-shrink-0 text-xs font-medium text-custom-text-300"> <div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{projectIdentifier}-{issue.sequence_id} {projectIdentifier}-{issue.sequence_id}
</div> </div>
)} )}
@ -183,7 +227,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
)} )}
</div> </div>
{!issue?.tempId && ( {!issue?.tempId && (
<div className="block md:hidden border border-custom-border-300 rounded "> <div className="block md:hidden border border-custom-border-300 rounded">
{quickActions({ {quickActions({
issue, issue,
parentRef: issueRef, parentRef: issueRef,

View File

@ -2,6 +2,7 @@ import { FC, MutableRefObject } from "react";
// components // components
import { TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
import { IssueBlockRoot } from "@/components/issues/issue-layouts/list"; import { IssueBlockRoot } from "@/components/issues/issue-layouts/list";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// types // types
import { TRenderQuickActions } from "./list-view-types"; import { TRenderQuickActions } from "./list-view-types";
@ -16,6 +17,7 @@ interface Props {
containerRef: MutableRefObject<HTMLDivElement | null>; containerRef: MutableRefObject<HTMLDivElement | null>;
isDragAllowed: boolean; isDragAllowed: boolean;
canDropOverIssue: boolean; canDropOverIssue: boolean;
selectionHelpers: TSelectionHelper;
} }
export const IssueBlocksList: FC<Props> = (props) => { export const IssueBlocksList: FC<Props> = (props) => {
@ -28,15 +30,14 @@ export const IssueBlocksList: FC<Props> = (props) => {
displayProperties, displayProperties,
canEditProperties, canEditProperties,
containerRef, containerRef,
selectionHelpers,
isDragAllowed, isDragAllowed,
canDropOverIssue, canDropOverIssue,
} = props; } = props;
return ( return (
<div className="relative h-full w-full"> <div className="relative size-full">
{issueIds && {issueIds?.map((issueId, index) => (
issueIds.length > 0 &&
issueIds.map((issueId: string, index) => (
<IssueBlockRoot <IssueBlockRoot
key={`${issueId}`} key={`${issueId}`}
issueIds={issueIds} issueIds={issueIds}
@ -49,6 +50,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
nestingLevel={0} nestingLevel={0}
spacingLeft={0} spacingLeft={0}
containerRef={containerRef} containerRef={containerRef}
selectionHelpers={selectionHelpers}
groupId={groupId} groupId={groupId}
isLastChild={index === issueIds.length - 1} isLastChild={index === issueIds.length - 1}
isDragAllowed={isDragAllowed} isDragAllowed={isDragAllowed}

View File

@ -1,7 +1,8 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
// components import { observer } from "mobx-react";
// types
import { import {
GroupByColumnTypes, GroupByColumnTypes,
TGroupedIssues, TGroupedIssues,
@ -13,6 +14,9 @@ import {
TIssueOrderByOptions, TIssueOrderByOptions,
TIssueGroupByOptions, TIssueGroupByOptions,
} from "@plane/types"; } from "@plane/types";
// components
import { MultipleSelectGroup } from "@/components/core";
import { IssueBulkOperationsRoot } from "@/components/issues";
// hooks // hooks
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@ -46,7 +50,7 @@ export interface IGroupByList {
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
} }
const GroupByList: React.FC<IGroupByList> = (props) => { const GroupByList: React.FC<IGroupByList> = observer((props) => {
const { const {
issueIds, issueIds,
issuesMap, issuesMap,
@ -113,14 +117,31 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const is_list = group_by === null ? true : false; 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<string, string[]> = {};
groupIds.forEach((gID) => {
orderedGroups[gID] = [];
});
let entities: Record<string, string[]> = {};
if (is_list) {
entities = Object.assign(orderedGroups, { [groupIds[0]]: issueIds });
} else {
entities = Object.assign(orderedGroups, { ...issueIds });
}
return ( return (
<div className="relative size-full flex flex-col">
{groups && (
<MultipleSelectGroup containerRef={containerRef} entities={entities}>
{(helpers) => (
<>
<div <div
ref={containerRef} ref={containerRef}
className="vertical-scrollbar scrollbar-lg relative h-full w-full overflow-auto vertical-scrollbar-margin-top-md" className="size-full overflow-auto vertical-scrollbar scrollbar-lg vertical-scrollbar-margin-top-md"
> >
{groups && {groups.map(
groups.length > 0 &&
groups.map(
(group: IGroupByColumn) => (group: IGroupByColumn) =>
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && ( validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && (
<ListGroup <ListGroup
@ -144,12 +165,21 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
viewId={viewId} viewId={viewId}
isCompletedCycle={isCompletedCycle} isCompletedCycle={isCompletedCycle}
selectionHelpers={helpers}
/> />
) )
)} )}
</div> </div>
<IssueBulkOperationsRoot selectionHelpers={helpers} />
</>
)}
</MultipleSelectGroup>
)}
</div>
); );
}; });
GroupByList.displayName = "GroupByList";
export interface IList { export interface IList {
issueIds: TGroupedIssues | TUnGroupedIssues | any; issueIds: TGroupedIssues | TUnGroupedIssues | any;

View File

@ -1,45 +1,63 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// lucide icons
import { CircleDashed, Plus } from "lucide-react"; 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 // 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 { interface IHeaderGroupByCard {
groupID: string;
icon?: React.ReactNode; icon?: React.ReactNode;
title: string; title: string;
count: number; count: number;
issuePayload: Partial<TIssue>; issuePayload: Partial<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: EIssuesStoreType; storeType: EIssuesStoreType;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
selectionHelpers: TSelectionHelper;
} }
export const HeaderGroupByCard = observer( export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
({ icon, title, count, issuePayload, disableIssueCreation, storeType, addIssuesToView }: 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 router = useRouter();
const { workspaceSlug, projectId, moduleId, cycleId } = router.query; const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
// hooks // hooks
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
// derived values
const [isOpen, setIsOpen] = useState(false);
const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false);
const isDraftIssue = router.pathname.includes("draft-issue"); const isDraftIssue = router.pathname.includes("draft-issue");
const renderExistingIssueModal = moduleId || cycleId; const renderExistingIssueModal = moduleId || cycleId;
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; const existingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(groupID) === "empty";
// auth
const canSelectIssues = canEditProperties(projectId?.toString());
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -65,9 +83,23 @@ export const HeaderGroupByCard = observer(
return ( return (
<> <>
<div className="relative flex w-full flex-shrink-0 flex-row items-center gap-1.5 py-1.5"> <div className="group/list-header relative w-full flex-shrink-0 flex items-center gap-2.5 py-1.5 pl-3.5">
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm"> {canSelectIssues && (
{icon ? icon : <CircleDashed className="h-3.5 w-3.5" strokeWidth={2} />} <div className="flex-shrink-0 flex items-center w-3.5">
<MultipleSelectGroupAction
className={cn(
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
{
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
}
)}
groupID={groupID}
selectionHelpers={selectionHelpers}
/>
</div>
)}
<div className="flex-shrink-0 grid place-items-center overflow-hidden pl-3">
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
</div> </div>
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden"> <div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
@ -127,12 +159,11 @@ export const HeaderGroupByCard = observer(
projectId={projectId?.toString()} projectId={projectId?.toString()}
isOpen={openExistingIssueListModal} isOpen={openExistingIssueListModal}
handleClose={() => setOpenExistingIssueListModal(false)} handleClose={() => setOpenExistingIssueListModal(false)}
searchParams={ExistingIssuesListModalPayload} searchParams={existingIssuesListModalPayload}
handleOnSubmit={handleAddIssuesToView} handleOnSubmit={handleAddIssuesToView}
/> />
)} )}
</div> </div>
</> </>
); );
} });
);

View File

@ -19,6 +19,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { DRAG_ALLOWED_GROUPS, EIssuesStoreType } from "@/constants/issue"; import { DRAG_ALLOWED_GROUPS, EIssuesStoreType } from "@/constants/issue";
// hooks // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// components // components
import { GroupDragOverlay } from "../group-drag-overlay"; import { GroupDragOverlay } from "../group-drag-overlay";
import { import {
@ -58,6 +59,7 @@ type Props = {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
viewId?: string; viewId?: string;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
selectionHelpers: TSelectionHelper;
}; };
export const ListGroup = observer((props: Props) => { export const ListGroup = observer((props: Props) => {
@ -81,6 +83,7 @@ export const ListGroup = observer((props: Props) => {
enableIssueQuickAdd, enableIssueQuickAdd,
isCompletedCycle, isCompletedCycle,
storeType, storeType,
selectionHelpers,
} = props; } = props;
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
@ -190,15 +193,18 @@ export const ListGroup = observer((props: Props) => {
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled, "border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled,
})} })}
> >
<div className="sticky top-0 z-[3] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 pl-5 py-1"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
<HeaderGroupByCard <HeaderGroupByCard
groupID={group.id}
icon={group.icon} icon={group.icon}
title={group.name || ""} title={group.name || ""}
count={issueCount} count={issueCount}
issuePayload={group.payload} issuePayload={group.payload}
canEditProperties={canEditProperties}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
storeType={storeType} storeType={storeType}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
selectionHelpers={selectionHelpers}
/> />
</div> </div>
@ -224,6 +230,7 @@ export const ListGroup = observer((props: Props) => {
containerRef={containerRef} containerRef={containerRef}
isDragAllowed={isDragAllowed} isDragAllowed={isDragAllowed}
canDropOverIssue={!canOverlayBeVisible} canDropOverIssue={!canOverlayBeVisible}
selectionHelpers={selectionHelpers}
/> />
)} )}

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
import { MemberDropdown } from "@/components/dropdowns"; import { MemberDropdown } from "@/components/dropdowns";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -36,7 +36,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props
buttonVariant={ buttonVariant={
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text" 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" buttonContainerClassName="w-full"
onClose={onClose} onClose={onClose}
/> />

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { TIssue } from "@plane/types";
// types // types
import { TIssue } from "@plane/types";
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -11,7 +11,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) =>
const { issue } = props; const { issue } = props;
return ( return (
<div className="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"> <div className="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">
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"} {issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
</div> </div>
); );

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// helpers // helpers
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -11,8 +11,9 @@ type Props = {
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => { export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props; const { issue } = props;
return ( return (
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80"> <div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 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">
{renderFormattedDate(issue.created_at)} {renderFormattedDate(issue.created_at)}
</div> </div>
); );

View File

@ -1,14 +1,14 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; 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 // types
import { TIssue } from "@plane/types";
// components
import { CycleDropdown } from "@/components/dropdowns";
// constants // constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useEventTracker, useIssues } from "@/hooks/store";
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -17,11 +17,10 @@ type Props = {
}; };
export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => { export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
const { issue, disabled, onClose } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// props
const { issue, disabled, onClose } = props;
// hooks // hooks
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { const {
@ -56,8 +55,8 @@ export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
disabled={disabled} disabled={disabled}
placeholder="Select cycle" placeholder="Select cycle"
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
buttonContainerClassName="w-full relative flex items-center p-2" 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" buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent"
onClose={onClose} onClose={onClose}
/> />
</div> </div>

View File

@ -1,16 +1,16 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CalendarCheck2 } from "lucide-react"; import { CalendarCheck2 } from "lucide-react";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// hooks
// components // components
import { DateDropdown } from "@/components/dropdowns"; import { DateDropdown } from "@/components/dropdowns";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
// hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -47,9 +47,12 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />} icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
buttonContainerClassName="w-full" buttonContainerClassName="w-full"
buttonClassName={cn("rounded-none text-left", { 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), "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
})} }
)}
clearIconClassName="!text-custom-text-100" clearIconClassName="!text-custom-text-100"
onClose={onClose} onClose={onClose}
/> />

View File

@ -1,8 +1,8 @@
// components
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { TIssue } from "@plane/types";
import { EstimateDropdown } from "@/components/dropdowns";
// types // types
import { TIssue } from "@plane/types";
// components
import { EstimateDropdown } from "@/components/dropdowns";
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -25,7 +25,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props
projectId={issue.project_id} projectId={issue.project_id}
disabled={disabled} disabled={disabled}
buttonVariant="transparent-with-text" 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" buttonContainerClassName="w-full"
onClose={onClose} onClose={onClose}
/> />

View File

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components
// hooks // hooks
import { useLabel } from "@/hooks/store"; import { useLabel } from "@/hooks/store";
// types // components
import { IssuePropertyLabels } from "../../properties"; import { IssuePropertyLabels } from "../../properties";
type Props = { type Props = {
@ -27,8 +27,8 @@ export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) =
value={issue.label_ids} value={issue.label_ids}
defaultOptions={defaultLabelOptions} defaultOptions={defaultLabelOptions}
onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })} 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" className="h-11 w-full border-b-[0.5px] border-custom-border-200"
buttonClassName="px-2.5 h-full" 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 hideDropdownArrow
maxRender={1} maxRender={1}
disabled={disabled} disabled={disabled}

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { TIssue } from "@plane/types";
// types // types
import { TIssue } from "@plane/types";
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -11,7 +11,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) =>
const { issue } = props; const { issue } = props;
return ( return (
<div className="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"> <div className="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">
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"} {issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
</div> </div>
); );

View File

@ -2,14 +2,14 @@ import React, { useCallback } from "react";
import xor from "lodash/xor"; import xor from "lodash/xor";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; 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 // types
import { TIssue } from "@plane/types";
// components
import { ModuleDropdown } from "@/components/dropdowns";
// constants // constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useEventTracker, useIssues } from "@/hooks/store";
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -18,11 +18,10 @@ type Props = {
}; };
export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => { export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
const { issue, disabled, onClose } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// props
const { issue, disabled, onClose } = props;
// hooks // hooks
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { const {
@ -65,8 +64,8 @@ export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
disabled={disabled} disabled={disabled}
placeholder="Select modules" placeholder="Select modules"
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
buttonContainerClassName="w-full relative flex items-center p-2" 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" buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent"
onClose={onClose} onClose={onClose}
multiple multiple
showCount showCount

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
import { PriorityDropdown } from "@/components/dropdowns"; import { PriorityDropdown } from "@/components/dropdowns";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -22,7 +22,7 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props
onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })} onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })}
disabled={disabled} disabled={disabled}
buttonVariant="transparent-with-text" 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" buttonContainerClassName="w-full"
onClose={onClose} onClose={onClose}
/> />

View File

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CalendarClock } from "lucide-react"; import { CalendarClock } from "lucide-react";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
import { DateDropdown } from "@/components/dropdowns"; import { DateDropdown } from "@/components/dropdowns";
// helpers // helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -38,7 +38,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
placeholder="Start date" placeholder="Start date"
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />} icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant="transparent-with-text" 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" buttonContainerClassName="w-full"
onClose={onClose} onClose={onClose}
/> />

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
import { StateDropdown } from "@/components/dropdowns"; import { StateDropdown } from "@/components/dropdowns";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -23,7 +23,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })} onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })}
disabled={disabled} disabled={disabled}
buttonVariant="transparent-with-text" 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" buttonContainerClassName="w-full"
onClose={onClose} onClose={onClose}
/> />

View File

@ -34,7 +34,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props
<div <div
onClick={subIssueCount ? redirectToIssueDetail : () => {}} onClick={subIssueCount ? redirectToIssueDetail : () => {}}
className={cn( 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, "cursor-pointer": subIssueCount,
} }

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// helpers // helpers
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
// types
type Props = { type Props = {
issue: TIssue; issue: TIssue;
@ -11,8 +11,9 @@ type Props = {
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => { export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props; const { issue } = props;
return ( return (
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80"> <div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 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">
{renderFormattedDate(issue.updated_at)} {renderFormattedDate(issue.updated_at)}
</div> </div>
); );

View File

@ -1,13 +1,14 @@
import { useRef } from "react"; import { useRef } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { IIssueDisplayProperties, TIssue } from "@plane/types";
// types // types
import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet"; import { IIssueDisplayProperties, TIssue } from "@plane/types";
import { useEventTracker } from "@/hooks/store";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
// constants // constants
import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet";
// hooks
import { useEventTracker } from "@/hooks/store";
// components // components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
type Props = { type Props = {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
@ -37,7 +38,7 @@ export const IssueColumn = observer((props: Props) => {
> >
<td <td
tabIndex={0} tabIndex={0}
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100" className="h-11 w-full min-w-[8rem] text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100"
ref={tableCellRef} ref={tableCellRef}
> >
<Column <Column
@ -58,9 +59,7 @@ export const IssueColumn = observer((props: Props) => {
}) })
} }
disabled={disableUserActions} disabled={disableUserActions}
onClose={() => { onClose={() => tableCellRef?.current?.focus()}
tableCellRef?.current?.focus();
}}
/> />
</td> </td>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>

View File

@ -1,20 +1,23 @@
import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react"; import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// icons
import { ChevronRight, MoreHorizontal } from "lucide-react"; import { ChevronRight, MoreHorizontal } from "lucide-react";
// types
import { IIssueDisplayProperties, TIssue } from "@plane/types"; import { IIssueDisplayProperties, TIssue } from "@plane/types";
// ui // ui
import { ControlLink, Tooltip } from "@plane/ui"; import { ControlLink, Tooltip } from "@plane/ui";
// components // components
import { MultipleSelectEntityAction } from "@/components/core";
import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import RenderIfVisible from "@/components/core/render-if-visible-HOC";
// constants
import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
// helper // helper
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useIssueDetail, useProject } from "@/hooks/store"; import { useIssueDetail, useProject } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// types
// local components // local components
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
@ -34,6 +37,7 @@ interface Props {
issueIds: string[]; issueIds: string[];
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
spacingLeft?: number; spacingLeft?: number;
selectionHelpers: TSelectionHelper;
} }
export const SpreadsheetIssueRow = observer((props: Props) => { export const SpreadsheetIssueRow = observer((props: Props) => {
@ -51,12 +55,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
issueIds, issueIds,
spreadsheetColumnsList, spreadsheetColumnsList,
spacingLeft = 6, spacingLeft = 6,
selectionHelpers,
} = props; } = props;
// states
const [isExpanded, setExpanded] = useState<boolean>(false); const [isExpanded, setExpanded] = useState<boolean>(false);
// store hooks
const { subIssues: subIssuesStore } = useIssueDetail(); const { subIssues: subIssuesStore } = useIssueDetail();
// derived values
const subIssues = subIssuesStore.subIssuesByIssueId(issueId); const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const isIssueSelected = selectionHelpers.getIsEntitySelected(issueId);
const isIssueActive = selectionHelpers.getIsEntityActive(issueId);
return ( return (
<> <>
@ -65,7 +73,13 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
as="tr" as="tr"
defaultHeight="calc(2.75rem - 1px)" defaultHeight="calc(2.75rem - 1px)"
root={containerRef} root={containerRef}
placeholderChildren={<td colSpan={100} className="border-b-[0.5px] border-custom-border-200" />} placeholderChildren={
<td colSpan={100} className="border-[0.5px] border-transparent border-b-custom-border-200" />
}
classNames={cn("bg-custom-background-100 transition-[background-color]", {
"group selected-issue-row": isIssueSelected,
"border-[0.5px] border-custom-border-400": isIssueActive,
})}
> >
<IssueRowDetails <IssueRowDetails
issueId={issueId} issueId={issueId}
@ -81,13 +95,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
isExpanded={isExpanded} isExpanded={isExpanded}
setExpanded={setExpanded} setExpanded={setExpanded}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
/> />
</RenderIfVisible> </RenderIfVisible>
{isExpanded && {isExpanded &&
subIssues && subIssues?.map((subIssueId: string) => (
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetIssueRow <SpreadsheetIssueRow
key={subIssueId} key={subIssueId}
issueId={subIssueId} issueId={subIssueId}
@ -103,6 +116,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
containerRef={containerRef} containerRef={containerRef}
issueIds={issueIds} issueIds={issueIds}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
/> />
))} ))}
</> </>
@ -123,6 +137,7 @@ interface IssueRowDetailsProps {
setExpanded: Dispatch<SetStateAction<boolean>>; setExpanded: Dispatch<SetStateAction<boolean>>;
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
spacingLeft?: number; spacingLeft?: number;
selectionHelpers: TSelectionHelper;
} }
const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
@ -140,6 +155,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
setExpanded, setExpanded,
spreadsheetColumnsList, spreadsheetColumnsList,
spacingLeft = 6, spacingLeft = 6,
selectionHelpers,
} = props; } = props;
// states // states
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
@ -148,7 +164,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const menuActionRef = useRef<HTMLDivElement | null>(null); const menuActionRef = useRef<HTMLDivElement | null>(null);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug, projectId } = router.query;
// hooks // hooks
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById } = useProject();
const { getIsIssuePeeked, peekIssue, setPeekIssue } = useIssueDetail(); const { getIsIssuePeeked, peekIssue, setPeekIssue } = useIssueDetail();
@ -171,7 +187,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const issueDetail = issue.getIssueById(issueId); const issueDetail = issue.getIssueById(issueId);
const paddingLeft = `${spacingLeft}px`; const marginLeft = `${spacingLeft}px`;
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
@ -204,16 +220,22 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const disableUserActions = !canEditProperties(issueDetail.project_id); const disableUserActions = !canEditProperties(issueDetail.project_id);
const subIssuesCount = issueDetail?.sub_issues_count ?? 0; const subIssuesCount = issueDetail?.sub_issues_count ?? 0;
const isIssueSelected = selectionHelpers.getIsEntitySelected(issueDetail.id);
return ( return (
<> <>
<td id={`issue-${issueId}`} ref={cellRef} tabIndex={0} className="sticky left-0 z-10"> <td
id={`issue-${issueId}`}
ref={cellRef}
tabIndex={0}
className="sticky left-0 z-10 group/list-block bg-custom-background-100"
>
<ControlLink <ControlLink
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`} href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
target="_blank" target="_blank"
onClick={() => handleIssuePeekOverview(issueDetail)} onClick={() => handleIssuePeekOverview(issueDetail)}
className={cn( 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-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
"border border-custom-primary-70 hover:border-custom-primary-70": "border border-custom-primary-70 hover:border-custom-primary-70":
@ -223,24 +245,52 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
)} )}
disabled={!!issueDetail?.tempId} disabled={!!issueDetail?.tempId}
> >
<div <div className="flex items-center gap-1 min-w-min py-2.5 pl-2">
className="flex min-w-min items-center gap-0.5 px-4 py-2.5 pl-1.5 pr-0" {/* select checkbox */}
style={nestingLevel !== 0 ? { paddingLeft } : {}} {projectId && !disableUserActions && (
<Tooltip
tooltipContent={
<>
Only issues within the current
<br />
project can be selected.
</>
}
disabled={issueDetail.project_id === projectId}
> >
<div className="flex items-center"> <div className="flex-shrink-0 grid place-items-center w-3.5">
{/* bulk ops */} <MultipleSelectEntityAction
<span className="size-3.5" /> className={cn(
<div className="flex size-4 items-center justify-center"> "opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
{
"opacity-100 pointer-events-auto": isIssueSelected,
}
)}
groupId={SPREADSHEET_SELECT_GROUP}
id={issueDetail.id}
selectionHelpers={selectionHelpers}
disabled={issueDetail.project_id !== projectId}
/>
</div>
</Tooltip>
)}
{/* sub-issues chevron */}
<div className="grid place-items-center size-4" style={nestingLevel !== 0 ? { marginLeft } : {}}>
{subIssuesCount > 0 && ( {subIssuesCount > 0 && (
<button <button
className="flex items-center justify-center size-4 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300" type="button"
className="grid place-items-center size-4 rounded-sm text-custom-text-400 hover:text-custom-text-300"
onClick={handleToggleExpand} onClick={handleToggleExpand}
> >
<ChevronRight className={`size-4 ${isExpanded ? "rotate-90" : ""}`} /> <ChevronRight
className={cn("size-4", {
"rotate-90": isExpanded,
})}
strokeWidth={2.5}
/>
</button> </button>
)} )}
</div> </div>
</div>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key"> <WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100"> <div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">

View File

@ -1,33 +1,68 @@
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// ui // ui
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
// types
import { LayersIcon } from "@plane/ui";
// components // components
import { MultipleSelectGroupAction } from "@/components/core";
import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts"; 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 { interface Props {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions; displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void; handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
canEditProperties: (projectId: string | undefined) => boolean;
isEstimateEnabled: boolean; isEstimateEnabled: boolean;
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
selectionHelpers: TSelectionHelper;
} }
export const SpreadsheetHeader = (props: Props) => { export const SpreadsheetHeader = observer((props: Props) => {
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled, spreadsheetColumnsList } = const {
props; 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 ( return (
<thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100"> <thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100">
<tr> <tr>
<th <th
className="sticky left-0 z-[15] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100" className="group/list-header sticky left-0 z-[15] h-11 w-[28rem] flex items-center gap-1 bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100 pl-2"
tabIndex={-1} tabIndex={-1}
> >
<span className="flex h-full w-full flex-grow items-center pl-5 px-4 py-2.5"> {canSelectIssues && (
<LayersIcon className="mr-1 h-4 w-4 text-custom-text-400" /> <div className="flex-shrink-0 flex items-center w-3.5">
Issue <MultipleSelectGroupAction
</span> className={cn(
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
{
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
}
)}
groupID={SPREADSHEET_SELECT_GROUP}
selectionHelpers={selectionHelpers}
/>
</div>
)}
<div className="size-4" />
<span className="flex h-full w-full flex-grow items-center py-2.5">Issues</span>
</th> </th>
{spreadsheetColumnsList.map((property) => ( {spreadsheetColumnsList.map((property) => (
@ -43,4 +78,4 @@ export const SpreadsheetHeader = (props: Props) => {
</tr> </tr>
</thead> </thead>
); );
}; });

View File

@ -2,6 +2,7 @@ import { MutableRefObject, useCallback, useEffect, useRef } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
//types //types
import { TSelectionHelper } from "@/hooks/use-multiple-select";
import { useTableKeyboardNavigation } from "@/hooks/use-table-keyboard-navigation"; import { useTableKeyboardNavigation } from "@/hooks/use-table-keyboard-navigation";
//components //components
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
@ -20,6 +21,7 @@ type Props = {
portalElement: React.MutableRefObject<HTMLDivElement | null>; portalElement: React.MutableRefObject<HTMLDivElement | null>;
containerRef: MutableRefObject<HTMLTableElement | null>; containerRef: MutableRefObject<HTMLTableElement | null>;
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
selectionHelpers: TSelectionHelper;
}; };
export const SpreadsheetTable = observer((props: Props) => { export const SpreadsheetTable = observer((props: Props) => {
@ -35,6 +37,7 @@ export const SpreadsheetTable = observer((props: Props) => {
canEditProperties, canEditProperties,
containerRef, containerRef,
spreadsheetColumnsList, spreadsheetColumnsList,
selectionHelpers,
} = props; } = props;
// states // states
@ -81,8 +84,10 @@ export const SpreadsheetTable = observer((props: Props) => {
displayProperties={displayProperties} displayProperties={displayProperties}
displayFilters={displayFilters} displayFilters={displayFilters}
handleDisplayFilterUpdate={handleDisplayFilterUpdate} handleDisplayFilterUpdate={handleDisplayFilterUpdate}
canEditProperties={canEditProperties}
isEstimateEnabled={isEstimateEnabled} isEstimateEnabled={isEstimateEnabled}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
/> />
<tbody> <tbody>
{issueIds.map((id) => ( {issueIds.map((id) => (
@ -100,6 +105,7 @@ export const SpreadsheetTable = observer((props: Props) => {
isScrolled={isScrolled} isScrolled={isScrolled}
issueIds={issueIds} issueIds={issueIds}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
/> />
))} ))}
</tbody> </tbody>

View File

@ -1,15 +1,18 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
// components // components
import { LogoSpinner } from "@/components/common"; import { LogoSpinner } from "@/components/common";
import { SpreadsheetQuickAddIssueForm } from "@/components/issues"; import { MultipleSelectGroup } from "@/components/core";
import { SPREADSHEET_PROPERTY_LIST } from "@/constants/spreadsheet"; import { IssueBulkOperationsRoot, SpreadsheetQuickAddIssueForm } from "@/components/issues";
// constants
import { SPREADSHEET_PROPERTY_LIST, SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
// hooks
import { useProject } from "@/hooks/store"; import { useProject } from "@/hooks/store";
// types
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { SpreadsheetTable } from "./spreadsheet-table"; import { SpreadsheetTable } from "./spreadsheet-table";
// types
//hooks
type Props = { type Props = {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
@ -73,6 +76,14 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
return ( return (
<div className="relative flex h-full w-full flex-col overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200"> <div className="relative flex h-full w-full flex-col overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
<div ref={portalRef} className="spreadsheet-menu-portal" /> <div ref={portalRef} className="spreadsheet-menu-portal" />
<MultipleSelectGroup
containerRef={containerRef}
entities={{
[SPREADSHEET_SELECT_GROUP]: issueIds,
}}
>
{(helpers) => (
<>
<div ref={containerRef} className="vertical-scrollbar horizontal-scrollbar scrollbar-lg h-full w-full"> <div ref={containerRef} className="vertical-scrollbar horizontal-scrollbar scrollbar-lg h-full w-full">
<SpreadsheetTable <SpreadsheetTable
displayProperties={displayProperties} displayProperties={displayProperties}
@ -86,6 +97,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
containerRef={containerRef} containerRef={containerRef}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={helpers}
/> />
</div> </div>
<div className="border-t border-custom-border-100"> <div className="border-t border-custom-border-100">
@ -95,6 +107,10 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
)} )}
</div> </div>
</div> </div>
<IssueBulkOperationsRoot selectionHelpers={helpers} />
</>
)}
</MultipleSelectGroup>
</div> </div>
); );
}); });

View File

@ -5,22 +5,30 @@ import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ChevronDown, ChevronRight, Plus } from "lucide-react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// types
import { IProject } from "@plane/types"; import { IProject } from "@plane/types";
// hooks // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project"; import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace"; import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { orderJoinedProjects } from "@/helpers/project.helper"; import { orderJoinedProjects } from "@/helpers/project.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
// ui
// components
// helpers
// constants
export const ProjectSidebarList: FC = observer(() => { export const ProjectSidebarList: FC = observer(() => {
// get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen
const isFavProjectsListOpenInLocalStorage = localStorage.getItem("isFavoriteProjectsListOpen");
const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen");
// states // states
const [isFavoriteProjectsListOpen, setIsFavoriteProjectsListOpen] = useState(
isFavProjectsListOpenInLocalStorage === "true"
);
const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(isAllProjectsListOpenInLocalStorage === "true");
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false); const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
@ -122,6 +130,16 @@ export const ProjectSidebarList: FC = observer(() => {
); );
}, [containerRef]); }, [containerRef]);
const toggleListDisclosure = (isOpen: boolean, type: "all" | "favorite") => {
if (type === "all") {
setIsAllProjectsListOpen(isOpen);
localStorage.setItem("isAllProjectsListOpen", isOpen.toString());
} else {
setIsFavoriteProjectsListOpen(isOpen);
localStorage.setItem("isFavoriteProjectsListOpen", isOpen.toString());
}
};
return ( return (
<> <>
{workspaceSlug && ( {workspaceSlug && (
@ -147,8 +165,7 @@ export const ProjectSidebarList: FC = observer(() => {
> >
<div> <div>
{favoriteProjects && favoriteProjects.length > 0 && ( {favoriteProjects && favoriteProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen> <Disclosure as="div" className="flex flex-col" defaultOpen={isFavoriteProjectCreate}>
{({ open }) => (
<> <>
{!isCollapsed && ( {!isCollapsed && (
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"> <div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
@ -156,9 +173,14 @@ export const ProjectSidebarList: FC = observer(() => {
as="button" as="button"
type="button" type="button"
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80" className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
onClick={() => toggleListDisclosure(!isFavoriteProjectsListOpen, "favorite")}
> >
Favorites Favorites
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />} {isFavoriteProjectsListOpen ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</Disclosure.Button> </Disclosure.Button>
{isAuthorizedUser && ( {isAuthorizedUser && (
<button <button
@ -175,6 +197,7 @@ export const ProjectSidebarList: FC = observer(() => {
</div> </div>
)} )}
<Transition <Transition
show={isFavoriteProjectsListOpen}
enter="transition duration-100 ease-out" enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0" enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100" enterTo="transform scale-100 opacity-100"
@ -182,7 +205,8 @@ export const ProjectSidebarList: FC = observer(() => {
leaveFrom="transform scale-100 opacity-100" leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0" leaveTo="transform scale-95 opacity-0"
> >
<Disclosure.Panel as="div" className="space-y-2"> {isFavoriteProjectsListOpen && (
<Disclosure.Panel as="div" className={`space-y-2`} static>
{favoriteProjects.map((projectId, index) => ( {favoriteProjects.map((projectId, index) => (
<ProjectSidebarListItem <ProjectSidebarListItem
key={projectId} key={projectId}
@ -195,16 +219,15 @@ export const ProjectSidebarList: FC = observer(() => {
/> />
))} ))}
</Disclosure.Panel> </Disclosure.Panel>
)}
</Transition> </Transition>
</> </>
)}
</Disclosure> </Disclosure>
)} )}
</div> </div>
<div> <div>
{joinedProjects && joinedProjects.length > 0 && ( {joinedProjects && joinedProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen> <Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
{({ open }) => (
<> <>
{!isCollapsed && ( {!isCollapsed && (
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"> <div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
@ -212,9 +235,14 @@ export const ProjectSidebarList: FC = observer(() => {
as="button" as="button"
type="button" type="button"
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80" className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
onClick={() => toggleListDisclosure(!isAllProjectsListOpen, "all")}
> >
Your projects Your projects
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />} {isAllProjectsListOpen ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</Disclosure.Button> </Disclosure.Button>
{isAuthorizedUser && ( {isAuthorizedUser && (
<button <button
@ -231,6 +259,7 @@ export const ProjectSidebarList: FC = observer(() => {
</div> </div>
)} )}
<Transition <Transition
show={isAllProjectsListOpen}
enter="transition duration-100 ease-out" enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0" enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100" enterTo="transform scale-100 opacity-100"
@ -238,7 +267,8 @@ export const ProjectSidebarList: FC = observer(() => {
leaveFrom="transform scale-100 opacity-100" leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0" leaveTo="transform scale-95 opacity-0"
> >
<Disclosure.Panel as="div"> {isAllProjectsListOpen && (
<Disclosure.Panel as="div" static>
{joinedProjects.map((projectId, index) => ( {joinedProjects.map((projectId, index) => (
<ProjectSidebarListItem <ProjectSidebarListItem
key={projectId} key={projectId}
@ -250,9 +280,9 @@ export const ProjectSidebarList: FC = observer(() => {
/> />
))} ))}
</Disclosure.Panel> </Disclosure.Panel>
)}
</Transition> </Transition>
</> </>
)}
</Disclosure> </Disclosure>
)} )}
</div> </div>

View File

@ -234,6 +234,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder="Name" placeholder="Name"
className="w-full" className="w-full"
autoFocus
/> />
)} )}
/> />

View File

@ -1,12 +1,12 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
// headless ui
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
// icons
// ui // ui
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useAppTheme, useCommandPalette } from "@/hooks/store"; import { useAppTheme, useCommandPalette } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
@ -59,9 +59,12 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
return ( return (
<> <>
<div <div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-[6px] ${ className={cn(
isCollapsed ? "flex-col" : "" "flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
}`} {
"flex-col h-auto py-1.5": isCollapsed,
}
)}
> >
{!isCollapsed && ( {!isCollapsed && (
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}> <Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>

View File

@ -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_PRICING_PAGE_LINK = "https://plane.so/pricing";
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact"; export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
export const MARKETING_PLANE_ONE_PAGE_LINK = "https://plane.so/one";

View File

@ -1,6 +1,16 @@
import { FC } from "react"; import { FC } from "react";
// icons // 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 // types
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
// ui // ui
@ -184,3 +194,5 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
"attachment_count", "attachment_count",
"sub_issue_count", "sub_issue_count",
]; ];
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";

View File

@ -9,12 +9,12 @@ export type UseIntersectionObserverProps = {
export const useIntersectionObserver = ( export const useIntersectionObserver = (
containerRef: RefObject<HTMLDivElement>, containerRef: RefObject<HTMLDivElement>,
elementRef: RefObject<HTMLDivElement>, elementRef: HTMLDivElement | null,
callback: () => void, callback: () => void,
rootMargin?: string rootMargin?: string
) => { ) => {
useEffect(() => { useEffect(() => {
if (elementRef.current) { if (elementRef) {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
if (entries[entries.length - 1].isIntersecting) { if (entries[entries.length - 1].isIntersecting) {
@ -26,16 +26,16 @@ export const useIntersectionObserver = (
rootMargin, rootMargin,
} }
); );
observer.observe(elementRef.current); observer.observe(elementRef);
return () => { return () => {
if (elementRef.current) { if (elementRef) {
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 // while removing the current from the refs, the observer is not not working as expected
// fix this eslint warning with caution // fix this eslint warning with caution
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [rootMargin, callback, elementRef.current, containerRef.current]); }, [rootMargin, callback, elementRef, containerRef.current]);
}; };

View File

@ -33,6 +33,7 @@ export const useMultipleSelect = (props: Props) => {
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
const { const {
selectedEntityIds,
updateSelectedEntityDetails, updateSelectedEntityDetails,
bulkUpdateSelectedEntityDetails, bulkUpdateSelectedEntityDetails,
getActiveEntityDetails, getActiveEntityDetails,
@ -45,6 +46,7 @@ export const useMultipleSelect = (props: Props) => {
clearSelection, clearSelection,
getIsEntitySelected, getIsEntitySelected,
getIsEntityActive, getIsEntityActive,
getEntityDetailsFromEntityID,
} = useMultipleSelectStore(); } = useMultipleSelectStore();
const groups = useMemo(() => Object.keys(entities), [entities]); const groups = useMemo(() => Object.keys(entities), [entities]);
@ -248,10 +250,6 @@ export const useMultipleSelect = (props: Props) => {
(groupID: string) => { (groupID: string) => {
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID); const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
const groupSelectionStatus = isGroupSelected(groupID); const groupSelectionStatus = isGroupSelected(groupID);
// groupEntities.map((entity) => {
// console.log("group click");
// handleEntitySelection(entity, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
// });
handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove"); handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
}, },
[entitiesList, handleEntitySelection, isGroupSelected] [entitiesList, handleEntitySelection, isGroupSelected]
@ -346,6 +344,19 @@ export const useMultipleSelect = (props: Props) => {
}; };
}, [clearSelection, router.events]); }, [clearSelection, router.events]);
// when entities list change, remove entityIds from the selected entities array, which are not present in the new list
useEffect(() => {
selectedEntityIds.map((entityID) => {
const isEntityPresent = entitiesList.find((en) => en.entityID === entityID);
if (!isEntityPresent) {
const entityDetails = getEntityDetailsFromEntityID(entityID);
if (entityDetails) {
handleEntitySelection(entityDetails);
}
}
});
}, [entitiesList, getEntityDetailsFromEntityID, handleEntitySelection, selectedEntityIds]);
/** /**
* @description helper functions for selection * @description helper functions for selection
*/ */

View File

@ -43,8 +43,6 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
return ( return (
<> <>
{/* TODO: Need to handle custom themes for toast */}
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<InstanceWrapper> <InstanceWrapper>
<StoreWrapper> <StoreWrapper>
<CrispWrapper user={currentUser}> <CrispWrapper user={currentUser}>
@ -56,6 +54,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
posthogAPIKey={config?.posthog_api_key || undefined} posthogAPIKey={config?.posthog_api_key || undefined}
posthogHost={config?.posthog_host || undefined} posthogHost={config?.posthog_host || undefined}
> >
{/* TODO: Need to handle custom themes for toast */}
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig> <SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider> </PostHogProvider>
</CrispWrapper> </CrispWrapper>

View File

@ -1,4 +1,4 @@
import { ReactElement, useEffect } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// components // components
@ -11,7 +11,7 @@ import { EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks // hooks
import { useProject, useProjectInbox } from "@/hooks/store"; import { useProject } from "@/hooks/store";
// layouts // layouts
import { AppLayout } from "@/layouts/app-layout"; import { AppLayout } from "@/layouts/app-layout";
// types // types
@ -23,12 +23,6 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query; const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query;
// hooks // hooks
const { currentProjectDetails } = useProject(); 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 // No access to inbox
if (currentProjectDetails?.inbox_view === false) if (currentProjectDetails?.inbox_view === false)
@ -44,6 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox"; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
const currentNavigationTab = navigationTab
? navigationTab === "open"
? EInboxIssueCurrentTab.OPEN
: EInboxIssueCurrentTab.CLOSED
: undefined;
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
return ( return (
@ -55,6 +55,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
projectId={projectId.toString()} projectId={projectId.toString()}
inboxIssueId={inboxIssueId?.toString() || undefined} inboxIssueId={inboxIssueId?.toString() || undefined}
inboxAccessible={currentProjectDetails?.inbox_view || false} inboxAccessible={currentProjectDetails?.inbox_view || false}
navigationTab={currentNavigationTab}
/> />
</div> </div>
</div> </div>

View File

@ -321,8 +321,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
(this.inboxIssuePaginationInfo?.total_results && (this.inboxIssuePaginationInfo?.total_results &&
this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results)) this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results))
) { ) {
this.loader = "pagination-loading";
const queryParams = this.inboxIssueQueryParams( const queryParams = this.inboxIssueQueryParams(
this.inboxFilters, this.inboxFilters,
this.inboxSorting, this.inboxSorting,
@ -332,7 +330,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams); const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
runInAction(() => { runInAction(() => {
this.loader = undefined;
set(this, "inboxIssuePaginationInfo", paginationInfo); set(this, "inboxIssuePaginationInfo", paginationInfo);
if (results && results.length > 0) { if (results && results.length > 0) {
const issueIds = results.map((value) => value?.issue?.id); 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); } else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
} catch (error) { } catch (error) {
console.error("Error fetching the inbox issues", error); console.error("Error fetching the inbox issues", error);
this.loader = undefined;
this.error = { this.error = {
message: "Error fetching the paginated inbox issues please try again later.", message: "Error fetching the paginated inbox issues please try again later.",
status: "pagination-error", status: "pagination-error",

View File

@ -19,6 +19,7 @@ export type IMultipleSelectStore = {
getPreviousActiveEntity: () => TEntityDetails | null; getPreviousActiveEntity: () => TEntityDetails | null;
getNextActiveEntity: () => TEntityDetails | null; getNextActiveEntity: () => TEntityDetails | null;
getActiveEntityDetails: () => TEntityDetails | null; getActiveEntityDetails: () => TEntityDetails | null;
getEntityDetailsFromEntityID: (entityID: string) => TEntityDetails | null;
// entity actions // entity actions
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void; updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void; bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
@ -119,6 +120,16 @@ export class MultipleSelectStore implements IMultipleSelectStore {
*/ */
getActiveEntityDetails = computedFn(() => this.activeEntityDetails); getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
/**
* @description get the entity details from entityID
* @param {string} entityID
* @returns {TEntityDetails | null}
*/
getEntityDetailsFromEntityID = computedFn(
(entityID: string): TEntityDetails | null =>
this.selectedEntityDetails.find((en) => en.entityID === entityID) ?? null
);
// entity actions // entity actions
/** /**
* @description add or remove entities * @description add or remove entities
@ -159,8 +170,11 @@ export class MultipleSelectStore implements IMultipleSelectStore {
if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]); if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
}); });
} else { } else {
const newEntities = differenceWith(this.selectedEntityDetails, entitiesList, (obj1, obj2) =>
isEqual(obj1.entityID, obj2.entityID)
);
runInAction(() => { runInAction(() => {
this.selectedEntityDetails = differenceWith(this.selectedEntityDetails, entitiesList, isEqual); this.selectedEntityDetails = newEntities;
}); });
} }
}; };

2748
yarn.lock

File diff suppressed because it is too large Load Diff