mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix performance issue is selection of bulk ops
This commit is contained in:
parent
f1e8b1769e
commit
cd038c5400
@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
|||||||
import { TSelectionHelper, TSelectionSnapshot, useMultipleSelect } from "@/hooks/use-multiple-select";
|
import { TSelectionHelper, TSelectionSnapshot, useMultipleSelect } from "@/hooks/use-multiple-select";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: (helpers: TSelectionHelper, snapshot: TSelectionSnapshot) => React.ReactNode;
|
children: (helpers: TSelectionHelper) => React.ReactNode;
|
||||||
containerRef: React.MutableRefObject<HTMLElement | null>;
|
containerRef: React.MutableRefObject<HTMLElement | null>;
|
||||||
entities: Record<string, string[]>; // { groupID: entityIds[] }
|
entities: Record<string, string[]>; // { groupID: entityIds[] }
|
||||||
};
|
};
|
||||||
@ -11,10 +11,12 @@ type Props = {
|
|||||||
export const MultipleSelectGroup: React.FC<Props> = observer((props) => {
|
export const MultipleSelectGroup: React.FC<Props> = observer((props) => {
|
||||||
const { children, containerRef, entities } = props;
|
const { children, containerRef, entities } = props;
|
||||||
|
|
||||||
const { helpers, snapshot } = useMultipleSelect({
|
const helpers = useMultipleSelect({
|
||||||
containerRef,
|
containerRef,
|
||||||
entities,
|
entities,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <>{children(helpers, snapshot)}</>;
|
return <>{children(helpers)}</>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MultipleSelectGroup.displayName = "MultipleSelectGroup";
|
||||||
|
@ -14,15 +14,15 @@ import {
|
|||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { TSelectionHelper, TSelectionSnapshot } from "@/hooks/use-multiple-select";
|
import { TSelectionHelper, TSelectionSnapshot } from "@/hooks/use-multiple-select";
|
||||||
|
import { useMultipleSelectStore } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
selectionHelpers: TSelectionHelper;
|
selectionHelpers: TSelectionHelper;
|
||||||
snapshot: TSelectionSnapshot;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
||||||
const { className, selectionHelpers, snapshot } = props;
|
const { className, selectionHelpers } = props;
|
||||||
// states
|
// states
|
||||||
const [isBulkArchiveModalOpen, setIsBulkArchiveModalOpen] = useState(false);
|
const [isBulkArchiveModalOpen, setIsBulkArchiveModalOpen] = useState(false);
|
||||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
@ -30,10 +30,10 @@ export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// serviced values
|
// serviced values
|
||||||
const { isSelectionActive, selectedEntityIds } = snapshot;
|
const { isSelectionActive, selectedEntityIds } = useMultipleSelectStore();
|
||||||
const { handleClearSelection } = selectionHelpers;
|
const { handleClearSelection } = selectionHelpers;
|
||||||
|
|
||||||
if (!snapshot.isSelectionActive) return null;
|
if (!isSelectionActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky bottom-0 left-0 z-[2] h-14">
|
<div className="sticky bottom-0 left-0 z-[2] h-14">
|
||||||
@ -109,7 +109,10 @@ export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-7 pl-3 flex-grow">
|
<div className="h-7 pl-3 flex-grow">
|
||||||
<IssueBulkOperationsProperties selectionHelpers={selectionHelpers} snapshot={snapshot} />
|
<IssueBulkOperationsProperties
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
snapshot={{ isSelectionActive, selectedEntityIds }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,7 +139,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
>
|
>
|
||||||
{groups && (
|
{groups && (
|
||||||
<MultipleSelectGroup containerRef={containerRef} entities={entities}>
|
<MultipleSelectGroup containerRef={containerRef} entities={entities}>
|
||||||
{(helpers, snapshot) => (
|
{(helpers) => (
|
||||||
<>
|
<>
|
||||||
{groups.map(
|
{groups.map(
|
||||||
(group: IGroupByColumn) =>
|
(group: IGroupByColumn) =>
|
||||||
@ -169,7 +169,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<IssueBulkOperationsRoot selectionHelpers={helpers} snapshot={snapshot} />
|
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MultipleSelectGroup>
|
</MultipleSelectGroup>
|
||||||
@ -178,6 +178,8 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
GroupByList.displayName = "GroupByList";
|
||||||
|
|
||||||
export interface IList {
|
export interface IList {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
issuesMap: TIssueMap;
|
issuesMap: TIssueMap;
|
||||||
|
@ -33,15 +33,14 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
selectedEntityDetails,
|
|
||||||
updateSelectedEntityDetails,
|
updateSelectedEntityDetails,
|
||||||
activeEntityDetails,
|
getActiveEntityDetails,
|
||||||
updateActiveEntityDetails,
|
updateActiveEntityDetails,
|
||||||
previousActiveEntity,
|
getPreviousActiveEntity,
|
||||||
updatePreviousActiveEntity,
|
updatePreviousActiveEntity,
|
||||||
nextActiveEntity,
|
getNextActiveEntity,
|
||||||
updateNextActiveEntity,
|
updateNextActiveEntity,
|
||||||
lastSelectedEntityDetails,
|
getLastSelectedEntityDetails,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
isEntitySelected,
|
isEntitySelected,
|
||||||
isEntityActive,
|
isEntityActive,
|
||||||
@ -176,6 +175,7 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
*/
|
*/
|
||||||
const handleEntityClick = useCallback(
|
const handleEntityClick = useCallback(
|
||||||
(e: React.MouseEvent, entityID: string, groupID: string) => {
|
(e: React.MouseEvent, entityID: string, groupID: string) => {
|
||||||
|
const lastSelectedEntityDetails = getLastSelectedEntityDetails();
|
||||||
if (e.shiftKey && lastSelectedEntityDetails) {
|
if (e.shiftKey && lastSelectedEntityDetails) {
|
||||||
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
|
|
||||||
handleEntitySelection({ entityID, groupID }, false);
|
handleEntitySelection({ entityID, groupID }, false);
|
||||||
},
|
},
|
||||||
[entitiesList, handleEntitySelection, lastSelectedEntityDetails]
|
[entitiesList, handleEntitySelection, getLastSelectedEntityDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -262,6 +262,9 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!e.shiftKey) return;
|
if (!e.shiftKey) return;
|
||||||
|
|
||||||
|
const activeEntityDetails = getActiveEntityDetails();
|
||||||
|
const nextActiveEntity = getNextActiveEntity();
|
||||||
|
const previousActiveEntity = getPreviousActiveEntity();
|
||||||
if (e.key === "ArrowDown" && activeEntityDetails) {
|
if (e.key === "ArrowDown" && activeEntityDetails) {
|
||||||
if (!nextActiveEntity) return;
|
if (!nextActiveEntity) return;
|
||||||
// console.log("selected by down", elementDetails.entityID);
|
// console.log("selected by down", elementDetails.entityID);
|
||||||
@ -279,15 +282,16 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
activeEntityDetails,
|
getActiveEntityDetails,
|
||||||
handleEntitySelection,
|
handleEntitySelection,
|
||||||
lastSelectedEntityDetails?.entityID,
|
getLastSelectedEntityDetails,
|
||||||
nextActiveEntity,
|
getNextActiveEntity,
|
||||||
previousActiveEntity,
|
getPreviousActiveEntity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const activeEntityDetails = getActiveEntityDetails();
|
||||||
// set active entity id to the first entity
|
// set active entity id to the first entity
|
||||||
if (["ArrowUp", "ArrowDown"].includes(e.key) && !activeEntityDetails) {
|
if (["ArrowUp", "ArrowDown"].includes(e.key) && !activeEntityDetails) {
|
||||||
const firstElementDetails = entitiesList[0];
|
const firstElementDetails = entitiesList[0];
|
||||||
@ -317,7 +321,7 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [activeEntityDetails, entitiesList, groups, getPreviousAndNextEntities, handleActiveEntityChange]);
|
}, [getActiveEntityDetails, entitiesList, groups, getPreviousAndNextEntities, handleActiveEntityChange]);
|
||||||
|
|
||||||
// clear selection on route change
|
// clear selection on route change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -330,17 +334,6 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
};
|
};
|
||||||
}, [clearSelection, router.events]);
|
}, [clearSelection, router.events]);
|
||||||
|
|
||||||
/**
|
|
||||||
* @description snapshot of the current state of selection
|
|
||||||
*/
|
|
||||||
const snapshot: TSelectionSnapshot = useMemo(
|
|
||||||
() => ({
|
|
||||||
isSelectionActive: selectedEntityDetails.length > 0,
|
|
||||||
selectedEntityIds: selectedEntityDetails.map((en) => en.entityID),
|
|
||||||
}),
|
|
||||||
[selectedEntityDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description helper functions for selection
|
* @description helper functions for selection
|
||||||
*/
|
*/
|
||||||
@ -353,16 +346,8 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
handleGroupClick,
|
handleGroupClick,
|
||||||
isGroupSelected,
|
isGroupSelected,
|
||||||
}),
|
}),
|
||||||
[clearSelection, handleEntityClick, handleGroupClick, isEntityActive, isEntitySelected, isGroupSelected]
|
[handleEntityClick, handleGroupClick, isEntityActive, isEntitySelected, isGroupSelected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const returnValue = useMemo(
|
return helpers;
|
||||||
() => ({
|
|
||||||
helpers,
|
|
||||||
snapshot,
|
|
||||||
}),
|
|
||||||
[helpers, snapshot]
|
|
||||||
);
|
|
||||||
|
|
||||||
return returnValue;
|
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
// hooks
|
// hooks
|
||||||
import { TEntityDetails } from "@/hooks/use-multiple-select";
|
import { TEntityDetails } from "@/hooks/use-multiple-select";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "@/services/issue";
|
import { IssueService } from "@/services/issue";
|
||||||
|
import { computedFn } from "mobx-utils";
|
||||||
|
|
||||||
export type IMultipleSelectStore = {
|
export type IMultipleSelectStore = {
|
||||||
// observables
|
// observables
|
||||||
selectedEntityDetails: TEntityDetails[];
|
isSelectionActive: boolean;
|
||||||
lastSelectedEntityDetails: TEntityDetails | null;
|
selectedEntityIds: string[];
|
||||||
previousActiveEntity: TEntityDetails | null;
|
|
||||||
nextActiveEntity: TEntityDetails | null;
|
|
||||||
activeEntityDetails: TEntityDetails | null;
|
|
||||||
// helper actions
|
// helper actions
|
||||||
isEntitySelected: (entityID: string) => boolean;
|
isEntitySelected: (entityID: string) => boolean;
|
||||||
isEntityActive: (entityID: string) => boolean;
|
isEntityActive: (entityID: string) => boolean;
|
||||||
|
getLastSelectedEntityDetails: () => TEntityDetails | null;
|
||||||
|
getPreviousActiveEntity: () => TEntityDetails | null;
|
||||||
|
getNextActiveEntity: () => TEntityDetails | null;
|
||||||
|
getActiveEntityDetails: () => TEntityDetails | null;
|
||||||
// entity actions
|
// entity actions
|
||||||
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
||||||
updateLastSelectedEntityDetails: (entityDetails: TEntityDetails | null) => void;
|
updateLastSelectedEntityDetails: (entityDetails: TEntityDetails | null) => void;
|
||||||
@ -42,6 +44,8 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
|||||||
nextActiveEntity: observable,
|
nextActiveEntity: observable,
|
||||||
activeEntityDetails: observable,
|
activeEntityDetails: observable,
|
||||||
// entity actions
|
// entity actions
|
||||||
|
isSelectionActive: computed,
|
||||||
|
selectedEntityIds: computed,
|
||||||
updateSelectedEntityDetails: action,
|
updateSelectedEntityDetails: action,
|
||||||
updateLastSelectedEntityDetails: action,
|
updateLastSelectedEntityDetails: action,
|
||||||
updatePreviousActiveEntity: action,
|
updatePreviousActiveEntity: action,
|
||||||
@ -53,20 +57,35 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
|||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isSelectionActive() {
|
||||||
|
return this.selectedEntityDetails.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedEntityIds() {
|
||||||
|
return this.selectedEntityDetails.map((en) => en.entityID);
|
||||||
|
}
|
||||||
|
|
||||||
// helper actions
|
// helper actions
|
||||||
/**
|
/**
|
||||||
* @description returns if the entity is selected or not
|
* @description returns if the entity is selected or not
|
||||||
* @param {string} entityID
|
* @param {string} entityID
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isEntitySelected = (entityID: string): boolean => this.selectedEntityDetails.some((en) => en.entityID === entityID);
|
isEntitySelected = computedFn((entityID: string): boolean =>
|
||||||
|
this.selectedEntityDetails.some((en) => en.entityID === entityID)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description returns if the entity is active or not
|
* @description returns if the entity is active or not
|
||||||
* @param {string} entityID
|
* @param {string} entityID
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isEntityActive = (entityID: string): boolean => this.activeEntityDetails?.entityID === entityID;
|
isEntityActive = computedFn((entityID: string): boolean => this.activeEntityDetails?.entityID === entityID);
|
||||||
|
|
||||||
|
getLastSelectedEntityDetails = computedFn(() => this.lastSelectedEntityDetails);
|
||||||
|
getPreviousActiveEntity = computedFn(() => this.previousActiveEntity);
|
||||||
|
getNextActiveEntity = computedFn(() => this.nextActiveEntity);
|
||||||
|
getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
|
||||||
|
|
||||||
// entity actions
|
// entity actions
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user