import groupBy from "lodash/groupBy"; import set from "lodash/set"; import { makeObservable, observable, computed, action, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types import { IState } from "@plane/types"; // helpers import { sortStates } from "@/helpers/state.helper"; // plane web store import { RootStore } from "@/plane-web/store/root.store"; // services import { ProjectStateService } from "@/services/project"; export interface IStateStore { //Loaders fetchedMap: Record; // observables stateMap: Record; // computed workspaceStates: IState[] | undefined; projectStates: IState[] | undefined; groupedProjectStates: Record | undefined; // computed actions getStateById: (stateId: string | null | undefined) => IState | undefined; getProjectStates: (projectId: string | null | undefined) => IState[] | undefined; // fetch actions fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; fetchWorkspaceStates: (workspaceSlug: string) => Promise; // crud actions createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateState: ( workspaceSlug: string, projectId: string, stateId: string, data: Partial ) => Promise; deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise; markStateAsDefault: (workspaceSlug: string, projectId: string, stateId: string) => Promise; moveStatePosition: ( workspaceSlug: string, projectId: string, stateId: string, direction: "up" | "down", groupIndex: number ) => Promise; } export class StateStore implements IStateStore { stateMap: Record = {}; //loaders fetchedMap: Record = {}; router; stateService; constructor(_rootStore: RootStore) { makeObservable(this, { // observables stateMap: observable, fetchedMap: observable, // computed projectStates: computed, groupedProjectStates: computed, // fetch action fetchProjectStates: action, // CRUD actions createState: action, updateState: action, deleteState: action, // state actions markStateAsDefault: action, moveStatePosition: action, }); this.stateService = new ProjectStateService(); this.router = _rootStore.router; } /** * Returns the stateMap belongs to a specific workspace */ get workspaceStates() { const workspaceSlug = this.router.workspaceSlug || ""; if (!workspaceSlug || !this.fetchedMap[workspaceSlug]) return; return sortStates(Object.values(this.stateMap)); } /** * Returns the stateMap belongs to a specific project */ get projectStates() { const projectId = this.router.projectId; const workspaceSlug = this.router.workspaceSlug || ""; if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); } /** * Returns the stateMap belongs to a specific project grouped by group */ get groupedProjectStates() { if (!this.router.projectId) return; return groupBy(this.projectStates, "group") as Record; } /** * @description returns state details using state id * @param stateId */ getStateById = computedFn((stateId: string | null | undefined) => { if (!this.stateMap || !stateId) return; return this.stateMap[stateId] ?? undefined; }); /** * Returns the stateMap belongs to a project by projectId * @param projectId * @returns IState[] */ getProjectStates = computedFn((projectId: string | null | undefined) => { const workspaceSlug = this.router.workspaceSlug || ""; if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); }); /** * fetches the stateMap of a project * @param workspaceSlug * @param projectId * @returns */ fetchProjectStates = async (workspaceSlug: string, projectId: string) => { const statesResponse = await this.stateService.getStates(workspaceSlug, projectId); runInAction(() => { statesResponse.forEach((state) => { set(this.stateMap, [state.id], state); }); set(this.fetchedMap, projectId, true); }); return statesResponse; }; /** * fetches the stateMap of all the states in workspace * @param workspaceSlug * @returns */ fetchWorkspaceStates = async (workspaceSlug: string) => { const statesResponse = await this.stateService.getWorkspaceStates(workspaceSlug); runInAction(() => { statesResponse.forEach((state) => { set(this.stateMap, [state.id], state); }); set(this.fetchedMap, workspaceSlug, true); }); return statesResponse; }; /** * creates a new state in a project and adds it to the store * @param workspaceSlug * @param projectId * @param data * @returns */ createState = async (workspaceSlug: string, projectId: string, data: Partial) => await this.stateService.createState(workspaceSlug, projectId, data).then((response) => { runInAction(() => { set(this.stateMap, [response?.id], response); }); return response; }); /** * Updates the state details in the store, in case of failure reverts back to original state * @param workspaceSlug * @param projectId * @param stateId * @param data * @returns */ updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { const originalState = this.stateMap[stateId]; try { runInAction(() => { set(this.stateMap, [stateId], { ...this.stateMap?.[stateId], ...data }); }); const response = await this.stateService.patchState(workspaceSlug, projectId, stateId, data); return response; } catch (error) { runInAction(() => { this.stateMap = { ...this.stateMap, [stateId]: originalState, }; }); throw error; } }; /** * deletes the state from the store, incase of failure reverts back to original state * @param workspaceSlug * @param projectId * @param stateId */ deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => { if (!this.stateMap?.[stateId]) return; await this.stateService.deleteState(workspaceSlug, projectId, stateId).then(() => { runInAction(() => { delete this.stateMap[stateId]; }); }); }; /** * marks a state as default in a project * @param workspaceSlug * @param projectId * @param stateId */ markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { const originalStates = this.stateMap; const currentDefaultState = Object.values(this.stateMap).find( (state) => state.project_id === projectId && state.default ); try { runInAction(() => { if (currentDefaultState) set(this.stateMap, [currentDefaultState.id, "default"], false); set(this.stateMap, [stateId, "default"], true); }); await this.stateService.markDefault(workspaceSlug, projectId, stateId); } catch (error) { // reverting back to old state group if api fails runInAction(() => { this.stateMap = originalStates; }); throw error; } }; /** * updates the sort order of a state and updates the state information using API, in case of failure reverts back to original state * @param workspaceSlug * @param projectId * @param stateId * @param direction * @param groupIndex */ moveStatePosition = async ( workspaceSlug: string, projectId: string, stateId: string, direction: "up" | "down", groupIndex: number ) => { const SEQUENCE_GAP = 15000; const originalStates = this.stateMap; try { let newSequence = SEQUENCE_GAP; const stateMap = this.projectStates || []; const selectedState = stateMap?.find((state) => state.id === stateId); const groupStates = stateMap?.filter((state) => state.group === selectedState?.group); const groupLength = groupStates.length; if (direction === "up") { if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; } else { if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; } runInAction(() => { set(this.stateMap, [stateId, "sequence"], newSequence); }); // updating using api await this.stateService.patchState(workspaceSlug, projectId, stateId, { sequence: newSequence }); } catch (err) { // reverting back to old state group if api fails runInAction(() => { this.stateMap = originalStates; }); } }; }