plane/web/store/multiple_select.store.ts

235 lines
8.0 KiB
TypeScript
Raw Permalink Normal View History

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;
getEntityDetailsFromEntityID: (entityID: string) => 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);
/**
* @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
/**
* @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 {
const newEntities = differenceWith(this.selectedEntityDetails, entitiesList, (obj1, obj2) =>
isEqual(obj1.entityID, obj2.entityID)
);
runInAction(() => {
this.selectedEntityDetails = newEntities;
});
}
};
/**
* @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;
});
};
}