fix performance issue is selection of bulk ops

This commit is contained in:
rahulramesha 2024-05-24 14:13:33 +05:30
parent f1e8b1769e
commit cd038c5400
5 changed files with 61 additions and 50 deletions

View File

@ -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";

View File

@ -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>

View File

@ -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;

View File

@ -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;
}; };

View File

@ -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
/** /**