forked from github/plane
[WEB-1501] dev: multiple select core components (#4667)
* dev: multiple select core components * chore: added export statement
This commit is contained in:
parent
c8c86a33f8
commit
98ebe88c86
@ -1,5 +1,6 @@
|
||||
export * from "./filters";
|
||||
export * from "./modals";
|
||||
export * from "./multiple-select";
|
||||
export * from "./sidebar";
|
||||
export * from "./activity";
|
||||
export * from "./favorite-star";
|
||||
|
36
web/components/core/multiple-select/entity-select-action.tsx
Normal file
36
web/components/core/multiple-select/entity-select-action.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
// ui
|
||||
import { Checkbox } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
groupId: string;
|
||||
id: string;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
};
|
||||
|
||||
export const MultipleSelectEntityAction: React.FC<Props> = (props) => {
|
||||
const { className, disabled = false, groupId, id, selectionHelpers } = props;
|
||||
// derived values
|
||||
const isSelected = selectionHelpers.getIsEntitySelected(id);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
className={cn("!outline-none size-3.5", className)}
|
||||
iconClassName="size-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectionHelpers.handleEntityClick(e, id, groupId);
|
||||
}}
|
||||
checked={isSelected}
|
||||
data-entity-group-id={groupId}
|
||||
data-entity-id={id}
|
||||
disabled={disabled}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
};
|
30
web/components/core/multiple-select/group-select-action.tsx
Normal file
30
web/components/core/multiple-select/group-select-action.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
// ui
|
||||
import { Checkbox } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
groupID: string;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
};
|
||||
|
||||
export const MultipleSelectGroupAction: React.FC<Props> = (props) => {
|
||||
const { className, disabled = false, groupID, selectionHelpers } = props;
|
||||
// derived values
|
||||
const groupSelectionStatus = selectionHelpers.isGroupSelected(groupID);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
className={cn("size-3.5 !outline-none", className)}
|
||||
iconClassName="size-3"
|
||||
onClick={() => selectionHelpers.handleGroupClick(groupID)}
|
||||
checked={groupSelectionStatus === "complete"}
|
||||
indeterminate={groupSelectionStatus === "partial"}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
3
web/components/core/multiple-select/index.ts
Normal file
3
web/components/core/multiple-select/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./entity-select-action";
|
||||
export * from "./group-select-action";
|
||||
export * from "./select-group";
|
22
web/components/core/multiple-select/select-group.tsx
Normal file
22
web/components/core/multiple-select/select-group.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { TSelectionHelper, useMultipleSelect } from "@/hooks/use-multiple-select";
|
||||
|
||||
type Props = {
|
||||
children: (helpers: TSelectionHelper) => React.ReactNode;
|
||||
containerRef: React.MutableRefObject<HTMLElement | null>;
|
||||
entities: Record<string, string[]>; // { groupID: entityIds[] }
|
||||
};
|
||||
|
||||
export const MultipleSelectGroup: React.FC<Props> = observer((props) => {
|
||||
const { children, containerRef, entities } = props;
|
||||
|
||||
const helpers = useMultipleSelect({
|
||||
containerRef,
|
||||
entities,
|
||||
});
|
||||
|
||||
return <>{children(helpers)}</>;
|
||||
});
|
||||
|
||||
MultipleSelectGroup.displayName = "MultipleSelectGroup";
|
@ -9,6 +9,7 @@ export * from "./use-label";
|
||||
export * from "./use-member";
|
||||
export * from "./use-mention";
|
||||
export * from "./use-module";
|
||||
export * from "./use-multiple-select-store";
|
||||
|
||||
export * from "./pages/use-project-page";
|
||||
export * from "./pages/use-page";
|
||||
|
9
web/hooks/store/use-multiple-select-store.ts
Normal file
9
web/hooks/store/use-multiple-select-store.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
|
||||
export const useMultipleSelectStore = () => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useMultipleSelectStore must be used within StoreProvider");
|
||||
return context.multipleSelect;
|
||||
};
|
365
web/hooks/use-multiple-select.ts
Normal file
365
web/hooks/use-multiple-select.ts
Normal file
@ -0,0 +1,365 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
import { useMultipleSelectStore } from "@/hooks/store";
|
||||
|
||||
export type TEntityDetails = {
|
||||
entityID: string;
|
||||
groupID: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
containerRef: React.MutableRefObject<HTMLElement | null>;
|
||||
entities: Record<string, string[]>; // { groupID: entityIds[] }
|
||||
};
|
||||
|
||||
export type TSelectionSnapshot = {
|
||||
isSelectionActive: boolean;
|
||||
selectedEntityIds: string[];
|
||||
};
|
||||
|
||||
export type TSelectionHelper = {
|
||||
handleClearSelection: () => void;
|
||||
handleEntityClick: (event: React.MouseEvent, entityID: string, groupId: string) => void;
|
||||
getIsEntitySelected: (entityID: string) => boolean;
|
||||
getIsEntityActive: (entityID: string) => boolean;
|
||||
handleGroupClick: (groupID: string) => void;
|
||||
isGroupSelected: (groupID: string) => "empty" | "partial" | "complete";
|
||||
};
|
||||
|
||||
export const useMultipleSelect = (props: Props) => {
|
||||
const { containerRef, entities } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const {
|
||||
updateSelectedEntityDetails,
|
||||
bulkUpdateSelectedEntityDetails,
|
||||
getActiveEntityDetails,
|
||||
updateActiveEntityDetails,
|
||||
getPreviousActiveEntity,
|
||||
updatePreviousActiveEntity,
|
||||
getNextActiveEntity,
|
||||
updateNextActiveEntity,
|
||||
getLastSelectedEntityDetails,
|
||||
clearSelection,
|
||||
getIsEntitySelected,
|
||||
getIsEntityActive,
|
||||
} = useMultipleSelectStore();
|
||||
|
||||
const groups = useMemo(() => Object.keys(entities), [entities]);
|
||||
|
||||
const entitiesList: TEntityDetails[] = useMemo(
|
||||
() =>
|
||||
groups
|
||||
.map((groupID) =>
|
||||
entities[groupID].map((entityID) => ({
|
||||
entityID,
|
||||
groupID,
|
||||
}))
|
||||
)
|
||||
.flat(1),
|
||||
[entities, groups]
|
||||
);
|
||||
|
||||
const getPreviousAndNextEntities = useCallback(
|
||||
(entityID: string) => {
|
||||
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
||||
|
||||
// entity position
|
||||
const isFirstEntity = currentEntityIndex === 0;
|
||||
const isLastEntity = currentEntityIndex === entitiesList.length - 1;
|
||||
|
||||
let previousEntity: TEntityDetails | null = null;
|
||||
let nextEntity: TEntityDetails | null = null;
|
||||
|
||||
if (isLastEntity) {
|
||||
nextEntity = null;
|
||||
} else {
|
||||
nextEntity = entitiesList[currentEntityIndex + 1];
|
||||
}
|
||||
|
||||
if (isFirstEntity) {
|
||||
previousEntity = null;
|
||||
} else {
|
||||
previousEntity = entitiesList[currentEntityIndex - 1];
|
||||
}
|
||||
|
||||
return {
|
||||
previousEntity,
|
||||
nextEntity,
|
||||
};
|
||||
},
|
||||
[entitiesList]
|
||||
);
|
||||
|
||||
const handleActiveEntityChange = useCallback(
|
||||
(entityDetails: TEntityDetails | null, shouldScroll: boolean = true) => {
|
||||
if (!entityDetails) {
|
||||
updateActiveEntityDetails(null);
|
||||
updatePreviousActiveEntity(null);
|
||||
updateNextActiveEntity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
updateActiveEntityDetails(entityDetails);
|
||||
|
||||
// scroll to get the active element in view
|
||||
const activeElement = document.querySelector(
|
||||
`[data-entity-id="${entityDetails.entityID}"][data-entity-group-id="${entityDetails.groupID}"]`
|
||||
);
|
||||
if (activeElement && containerRef.current && shouldScroll) {
|
||||
const SCROLL_OFFSET = 200;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const elementRect = activeElement.getBoundingClientRect();
|
||||
|
||||
const isInView =
|
||||
elementRect.top >= containerRect.top + SCROLL_OFFSET &&
|
||||
elementRect.bottom <= containerRect.bottom - SCROLL_OFFSET;
|
||||
|
||||
if (!isInView) {
|
||||
containerRef.current.scrollBy({
|
||||
top: elementRect.top < containerRect.top + SCROLL_OFFSET ? -50 : 50,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { previousEntity: previousActiveEntity, nextEntity: nextActiveEntity } = getPreviousAndNextEntities(
|
||||
entityDetails.entityID
|
||||
);
|
||||
updatePreviousActiveEntity(previousActiveEntity);
|
||||
updateNextActiveEntity(nextActiveEntity);
|
||||
},
|
||||
[
|
||||
containerRef,
|
||||
getPreviousAndNextEntities,
|
||||
updateActiveEntityDetails,
|
||||
updateNextActiveEntity,
|
||||
updatePreviousActiveEntity,
|
||||
]
|
||||
);
|
||||
|
||||
const handleEntitySelection = useCallback(
|
||||
(
|
||||
entityDetails: TEntityDetails | TEntityDetails[],
|
||||
shouldScroll: boolean = true,
|
||||
forceAction: "force-add" | "force-remove" | null = null
|
||||
) => {
|
||||
if (Array.isArray(entityDetails)) {
|
||||
bulkUpdateSelectedEntityDetails(entityDetails, forceAction === "force-add" ? "add" : "remove");
|
||||
if (forceAction === "force-add" && entityDetails.length > 0) {
|
||||
handleActiveEntityChange(entityDetails[entityDetails.length - 1], shouldScroll);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceAction) {
|
||||
if (forceAction === "force-add") {
|
||||
console.log("force adding");
|
||||
updateSelectedEntityDetails(entityDetails, "add");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
}
|
||||
if (forceAction === "force-remove") {
|
||||
updateSelectedEntityDetails(entityDetails, "remove");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = getIsEntitySelected(entityDetails.entityID);
|
||||
if (isSelected) {
|
||||
updateSelectedEntityDetails(entityDetails, "remove");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
} else {
|
||||
updateSelectedEntityDetails(entityDetails, "add");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
}
|
||||
},
|
||||
[bulkUpdateSelectedEntityDetails, getIsEntitySelected, handleActiveEntityChange, updateSelectedEntityDetails]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description toggle entity selection
|
||||
* @param {React.MouseEvent} event
|
||||
* @param {string} entityID
|
||||
* @param {string} groupID
|
||||
*/
|
||||
const handleEntityClick = useCallback(
|
||||
(e: React.MouseEvent, entityID: string, groupID: string) => {
|
||||
const lastSelectedEntityDetails = getLastSelectedEntityDetails();
|
||||
if (e.shiftKey && lastSelectedEntityDetails) {
|
||||
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
||||
|
||||
const lastEntityIndex = entitiesList.findIndex(
|
||||
(entity) => entity?.entityID === lastSelectedEntityDetails.entityID
|
||||
);
|
||||
if (lastEntityIndex < currentEntityIndex) {
|
||||
for (let i = lastEntityIndex + 1; i <= currentEntityIndex; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
} else if (lastEntityIndex > currentEntityIndex) {
|
||||
for (let i = currentEntityIndex; i <= lastEntityIndex - 1; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const startIndex = lastEntityIndex + 1;
|
||||
const endIndex = currentEntityIndex;
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handleEntitySelection({ entityID, groupID }, false);
|
||||
},
|
||||
[entitiesList, handleEntitySelection, getLastSelectedEntityDetails]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description check if any entity of the group is selected
|
||||
* @param {string} groupID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isGroupSelected = useCallback(
|
||||
(groupID: string) => {
|
||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
||||
const totalSelected = groupEntities.filter((entity) => getIsEntitySelected(entity.entityID ?? "")).length;
|
||||
if (totalSelected === 0) return "empty";
|
||||
if (totalSelected === groupEntities.length) return "complete";
|
||||
return "partial";
|
||||
},
|
||||
[entitiesList, getIsEntitySelected]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description toggle group selection
|
||||
* @param {string} groupID
|
||||
*/
|
||||
const handleGroupClick = useCallback(
|
||||
(groupID: string) => {
|
||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === 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");
|
||||
},
|
||||
[entitiesList, handleEntitySelection, isGroupSelected]
|
||||
);
|
||||
|
||||
// clear selection on escape key press
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") clearSelection();
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [clearSelection]);
|
||||
|
||||
// select entities on shift + arrow up/down key press
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.shiftKey) return;
|
||||
|
||||
const activeEntityDetails = getActiveEntityDetails();
|
||||
const nextActiveEntity = getNextActiveEntity();
|
||||
const previousActiveEntity = getPreviousActiveEntity();
|
||||
|
||||
if (e.key === "ArrowDown" && activeEntityDetails) {
|
||||
if (!nextActiveEntity) return;
|
||||
handleEntitySelection(nextActiveEntity);
|
||||
}
|
||||
if (e.key === "ArrowUp" && activeEntityDetails) {
|
||||
if (!previousActiveEntity) return;
|
||||
handleEntitySelection(previousActiveEntity);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
getActiveEntityDetails,
|
||||
handleEntitySelection,
|
||||
getLastSelectedEntityDetails,
|
||||
getNextActiveEntity,
|
||||
getPreviousActiveEntity,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey) return;
|
||||
const activeEntityDetails = getActiveEntityDetails();
|
||||
// set active entity id to the first entity
|
||||
if (["ArrowUp", "ArrowDown"].includes(e.key) && !activeEntityDetails) {
|
||||
const firstElementDetails = entitiesList[0];
|
||||
if (!firstElementDetails) return;
|
||||
handleActiveEntityChange(firstElementDetails);
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown" && activeEntityDetails) {
|
||||
if (!activeEntityDetails) return;
|
||||
const { nextEntity: nextActiveEntity } = getPreviousAndNextEntities(activeEntityDetails.entityID);
|
||||
if (nextActiveEntity) {
|
||||
handleActiveEntityChange(nextActiveEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp" && activeEntityDetails) {
|
||||
if (!activeEntityDetails) return;
|
||||
const { previousEntity: previousActiveEntity } = getPreviousAndNextEntities(activeEntityDetails.entityID);
|
||||
if (previousActiveEntity) {
|
||||
handleActiveEntityChange(previousActiveEntity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [getActiveEntityDetails, entitiesList, groups, getPreviousAndNextEntities, handleActiveEntityChange]);
|
||||
|
||||
// clear selection on route change
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => clearSelection();
|
||||
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}, [clearSelection, router.events]);
|
||||
|
||||
/**
|
||||
* @description helper functions for selection
|
||||
*/
|
||||
const helpers: TSelectionHelper = useMemo(
|
||||
() => ({
|
||||
handleClearSelection: clearSelection,
|
||||
handleEntityClick,
|
||||
getIsEntitySelected,
|
||||
getIsEntityActive,
|
||||
handleGroupClick,
|
||||
isGroupSelected,
|
||||
}),
|
||||
[clearSelection, getIsEntityActive, getIsEntitySelected, handleEntityClick, handleGroupClick, isGroupSelected]
|
||||
);
|
||||
|
||||
return helpers;
|
||||
};
|
220
web/store/multiple_select.store.ts
Normal file
220
web/store/multiple_select.store.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import differenceWith from "lodash/differenceWith";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import remove from "lodash/remove";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// hooks
|
||||
import { TEntityDetails } from "@/hooks/use-multiple-select";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
export type IMultipleSelectStore = {
|
||||
// computed functions
|
||||
isSelectionActive: boolean;
|
||||
selectedEntityIds: string[];
|
||||
// helper actions
|
||||
getIsEntitySelected: (entityID: string) => boolean;
|
||||
getIsEntityActive: (entityID: string) => boolean;
|
||||
getLastSelectedEntityDetails: () => TEntityDetails | null;
|
||||
getPreviousActiveEntity: () => TEntityDetails | null;
|
||||
getNextActiveEntity: () => TEntityDetails | null;
|
||||
getActiveEntityDetails: () => TEntityDetails | null;
|
||||
// entity actions
|
||||
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
||||
bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
|
||||
updateLastSelectedEntityDetails: (entityDetails: TEntityDetails | null) => void;
|
||||
updatePreviousActiveEntity: (entityDetails: TEntityDetails | null) => void;
|
||||
updateNextActiveEntity: (entityDetails: TEntityDetails | null) => void;
|
||||
updateActiveEntityDetails: (entityDetails: TEntityDetails | null) => void;
|
||||
clearSelection: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description the MultipleSelectStore manages multiple selection states by keeping track of the selected entities and providing a bunch of helper functions and actions to maintain the selected states
|
||||
* @description use the useMultipleSelectStore custom hook to access the observables
|
||||
* @description use the useMultipleSelect custom hook for added functionality on top of the store, including-
|
||||
* 1. Keyboard and mouse interaction
|
||||
* 2. Clear state on route change
|
||||
*/
|
||||
export class MultipleSelectStore implements IMultipleSelectStore {
|
||||
// observables
|
||||
selectedEntityDetails: TEntityDetails[] = [];
|
||||
lastSelectedEntityDetails: TEntityDetails | null = null;
|
||||
previousActiveEntity: TEntityDetails | null = null;
|
||||
nextActiveEntity: TEntityDetails | null = null;
|
||||
activeEntityDetails: TEntityDetails | null = null;
|
||||
// service
|
||||
issueService;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
selectedEntityDetails: observable,
|
||||
lastSelectedEntityDetails: observable,
|
||||
previousActiveEntity: observable,
|
||||
nextActiveEntity: observable,
|
||||
activeEntityDetails: observable,
|
||||
// computed functions
|
||||
isSelectionActive: computed,
|
||||
selectedEntityIds: computed,
|
||||
// actions
|
||||
updateSelectedEntityDetails: action,
|
||||
bulkUpdateSelectedEntityDetails: action,
|
||||
updateLastSelectedEntityDetails: action,
|
||||
updatePreviousActiveEntity: action,
|
||||
updateNextActiveEntity: action,
|
||||
updateActiveEntityDetails: action,
|
||||
clearSelection: action,
|
||||
});
|
||||
|
||||
this.issueService = new IssueService();
|
||||
}
|
||||
|
||||
get isSelectionActive() {
|
||||
return this.selectedEntityDetails.length > 0;
|
||||
}
|
||||
|
||||
get selectedEntityIds() {
|
||||
return this.selectedEntityDetails.map((en) => en.entityID);
|
||||
}
|
||||
|
||||
// helper actions
|
||||
/**
|
||||
* @description returns if the entity is selected or not
|
||||
* @param {string} entityID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsEntitySelected = computedFn((entityID: string): boolean =>
|
||||
this.selectedEntityDetails.some((en) => en.entityID === entityID)
|
||||
);
|
||||
|
||||
/**
|
||||
* @description returns if the entity is active or not
|
||||
* @param {string} entityID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsEntityActive = computedFn((entityID: string): boolean => this.activeEntityDetails?.entityID === entityID);
|
||||
|
||||
/**
|
||||
* @description get the last selected entity details
|
||||
* @returns {TEntityDetails}
|
||||
*/
|
||||
getLastSelectedEntityDetails = computedFn(() => this.lastSelectedEntityDetails);
|
||||
|
||||
/**
|
||||
* @description get the details of the entity preceding the active entity
|
||||
* @returns {TEntityDetails}
|
||||
*/
|
||||
getPreviousActiveEntity = computedFn(() => this.previousActiveEntity);
|
||||
|
||||
/**
|
||||
* @description get the details of the entity succeeding the active entity
|
||||
* @returns {TEntityDetails}
|
||||
*/
|
||||
getNextActiveEntity = computedFn(() => this.nextActiveEntity);
|
||||
|
||||
/**
|
||||
* @description get the active entity details
|
||||
* @returns {TEntityDetails}
|
||||
*/
|
||||
getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
|
||||
|
||||
// entity actions
|
||||
/**
|
||||
* @description add or remove entities
|
||||
* @param {TEntityDetails} entityDetails
|
||||
* @param {"add" | "remove"} action
|
||||
*/
|
||||
updateSelectedEntityDetails = (entityDetails: TEntityDetails, action: "add" | "remove") => {
|
||||
if (action === "add") {
|
||||
runInAction(() => {
|
||||
if (this.getIsEntitySelected(entityDetails.entityID)) {
|
||||
remove(this.selectedEntityDetails, (en) => en.entityID === entityDetails.entityID);
|
||||
}
|
||||
this.selectedEntityDetails.push(entityDetails);
|
||||
this.updateLastSelectedEntityDetails(entityDetails);
|
||||
});
|
||||
} else {
|
||||
let currentSelection = [...this.selectedEntityDetails];
|
||||
currentSelection = currentSelection.filter((en) => en.entityID !== entityDetails.entityID);
|
||||
runInAction(() => {
|
||||
remove(this.selectedEntityDetails, (en) => en.entityID === entityDetails.entityID);
|
||||
this.updateLastSelectedEntityDetails(currentSelection[currentSelection.length - 1] ?? null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description add or remove multiple entities
|
||||
* @param {TEntityDetails[]} entitiesList
|
||||
* @param {"add" | "remove"} action
|
||||
*/
|
||||
bulkUpdateSelectedEntityDetails = (entitiesList: TEntityDetails[], action: "add" | "remove") => {
|
||||
if (action === "add") {
|
||||
runInAction(() => {
|
||||
let newEntities: TEntityDetails[] = [];
|
||||
newEntities = differenceWith(this.selectedEntityDetails, entitiesList, isEqual);
|
||||
newEntities = newEntities.concat(entitiesList);
|
||||
this.selectedEntityDetails = newEntities;
|
||||
if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
|
||||
});
|
||||
} else {
|
||||
runInAction(() => {
|
||||
this.selectedEntityDetails = differenceWith(this.selectedEntityDetails, entitiesList, isEqual);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update last selected entity
|
||||
* @param {TEntityDetails} entityDetails
|
||||
*/
|
||||
updateLastSelectedEntityDetails = (entityDetails: TEntityDetails | null) => {
|
||||
runInAction(() => {
|
||||
this.lastSelectedEntityDetails = entityDetails;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update previous active entity
|
||||
* @param {TEntityDetails} entityDetails
|
||||
*/
|
||||
updatePreviousActiveEntity = (entityDetails: TEntityDetails | null) => {
|
||||
runInAction(() => {
|
||||
this.previousActiveEntity = entityDetails;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update next active entity
|
||||
* @param {TEntityDetails} entityDetails
|
||||
*/
|
||||
updateNextActiveEntity = (entityDetails: TEntityDetails | null) => {
|
||||
runInAction(() => {
|
||||
this.nextActiveEntity = entityDetails;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update active entity
|
||||
* @param {TEntityDetails} entityDetails
|
||||
*/
|
||||
updateActiveEntityDetails = (entityDetails: TEntityDetails | null) => {
|
||||
runInAction(() => {
|
||||
this.activeEntityDetails = entityDetails;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description clear selection and reset all the observables
|
||||
*/
|
||||
clearSelection = () => {
|
||||
runInAction(() => {
|
||||
this.selectedEntityDetails = [];
|
||||
this.lastSelectedEntityDetails = null;
|
||||
this.previousActiveEntity = null;
|
||||
this.nextActiveEntity = null;
|
||||
this.activeEntityDetails = null;
|
||||
});
|
||||
};
|
||||
}
|
@ -14,6 +14,7 @@ import { ILabelStore, LabelStore } from "./label.store";
|
||||
import { IMemberRootStore, MemberRootStore } from "./member";
|
||||
import { IModuleStore, ModulesStore } from "./module.store";
|
||||
import { IModuleFilterStore, ModuleFilterStore } from "./module_filter.store";
|
||||
import { IMultipleSelectStore, MultipleSelectStore } from "./multiple_select.store";
|
||||
import { IProjectPageStore, ProjectPageStore } from "./pages/project-page.store";
|
||||
import { IProjectRootStore, ProjectRootStore } from "./project";
|
||||
import { IProjectViewStore, ProjectViewStore } from "./project-view.store";
|
||||
@ -48,6 +49,7 @@ export class RootStore {
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
projectInbox: IProjectInboxStore;
|
||||
multipleSelect: IMultipleSelectStore;
|
||||
|
||||
constructor() {
|
||||
this.router = new RouterStore();
|
||||
@ -70,6 +72,7 @@ export class RootStore {
|
||||
this.theme = new ThemeStore(this);
|
||||
this.eventTracker = new EventTrackerStore(this);
|
||||
this.instance = new InstanceStore();
|
||||
this.multipleSelect = new MultipleSelectStore();
|
||||
// inbox
|
||||
this.projectInbox = new ProjectInboxStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
@ -101,5 +104,6 @@ export class RootStore {
|
||||
this.user = new UserStore(this);
|
||||
this.projectInbox = new ProjectInboxStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
this.multipleSelect = new MultipleSelectStore();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user