import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { isFuture, isPast } from "date-fns"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; // types import { ICycle, CycleDateCheckData } from "@plane/types"; // mobx import { RootStore } from "store/root.store"; // services import { ProjectService } from "services/project"; import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; export interface ICycleStore { // observables cycleMap: Record; activeCycleMap: Record; // TODO: Merge these two into single map // computed currentProjectCycleIds: string[] | null; currentProjectCompletedCycleIds: string[] | null; currentProjectUpcomingCycleIds: string[] | null; currentProjectIncompleteCycleIds: string[] | null; currentProjectDraftCycleIds: string[] | null; currentProjectActiveCycleId: string | null; // computed actions getCycleById: (cycleId: string) => ICycle | null; getActiveCycleById: (cycleId: string) => ICycle | null; getProjectCycleIds: (projectId: string) => string[] | null; // actions validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; // fetch fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; // crud createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateCycleDetails: ( workspaceSlug: string, projectId: string, cycleId: string, data: Partial ) => Promise; deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; // favorites addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; } export class CycleStore implements ICycleStore { // observables cycleMap: Record = {}; activeCycleMap: Record = {}; // root store rootStore; // services projectService; issueService; cycleService; constructor(_rootStore: RootStore) { makeObservable(this, { // observables cycleMap: observable, activeCycleMap: observable, // computed currentProjectCycleIds: computed, currentProjectCompletedCycleIds: computed, currentProjectUpcomingCycleIds: computed, currentProjectIncompleteCycleIds: computed, currentProjectDraftCycleIds: computed, currentProjectActiveCycleId: computed, // computed actions getCycleById: action, getActiveCycleById: action, getProjectCycleIds: action, // actions fetchAllCycles: action, fetchActiveCycle: action, fetchCycleDetails: action, createCycle: action, updateCycleDetails: action, deleteCycle: action, addCycleToFavorites: action, removeCycleFromFavorites: action, }); this.rootStore = _rootStore; this.projectService = new ProjectService(); this.issueService = new IssueService(); this.cycleService = new CycleService(); } // computed /** * returns all cycle ids for a project */ get currentProjectCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId); allCycles = sortBy(allCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const allCycleIds = allCycles.map((c) => c.id); return allCycleIds || null; } /** * returns all completed cycle ids for a project */ get currentProjectCompletedCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => { const hasEndDatePassed = isPast(new Date(c.end_date ?? "")); return c.project === projectId && hasEndDatePassed; }); completedCycles = sortBy(completedCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const completedCycleIds = completedCycles.map((c) => c.id); return completedCycleIds || null; } /** * returns all upcoming cycle ids for a project */ get currentProjectUpcomingCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => { const isStartDateUpcoming = isFuture(new Date(c.start_date ?? "")); return c.project === projectId && isStartDateUpcoming; }); upcomingCycles = sortBy(upcomingCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const upcomingCycleIds = upcomingCycles.map((c) => c.id); return upcomingCycleIds || null; } /** * returns all incomplete cycle ids for a project */ get currentProjectIncompleteCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => { const hasEndDatePassed = isPast(new Date(c.end_date ?? "")); return c.project === projectId && !hasEndDatePassed; }); incompleteCycles = sortBy(incompleteCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const incompleteCycleIds = incompleteCycles.map((c) => c.id); return incompleteCycleIds || null; } /** * returns all draft cycle ids for a project */ get currentProjectDraftCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; let draftCycles = Object.values(this.cycleMap ?? {}).filter( (c) => c.project === projectId && !c.start_date && !c.end_date ); draftCycles = sortBy(draftCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const draftCycleIds = draftCycles.map((c) => c.id); return draftCycleIds || null; } /** * returns active cycle id for a project */ get currentProjectActiveCycleId() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; const activeCycle = Object.keys(this.activeCycleMap ?? {}).find( (cycleId) => this.activeCycleMap?.[cycleId]?.project === projectId ); return activeCycle || null; } /** * @description returns cycle details by cycle id * @param cycleId * @returns */ getCycleById = (cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null; /** * @description returns active cycle details by cycle id * @param cycleId * @returns */ getActiveCycleById = (cycleId: string): ICycle | null => this.activeCycleMap?.[cycleId] ?? null; /** * @description returns list of cycle ids of the project id passed as argument * @param projectId */ getProjectCycleIds = (projectId: string): string[] | null => { let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId); cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); const cycleIds = cycles.map((c) => c.id); return cycleIds || null; }; /** * @description validates cycle dates * @param workspaceSlug * @param projectId * @param payload * @returns */ validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); /** * @description fetches all cycles for a project * @param workspaceSlug * @param projectId * @returns */ fetchAllCycles = async (workspaceSlug: string, projectId: string) => await this.cycleService.getCyclesWithParams(workspaceSlug, projectId).then((response) => { runInAction(() => { response.forEach((cycle) => { set(this.cycleMap, [cycle.id], cycle); }); }); return response; }); /** * @description fetches active cycle for a project * @param workspaceSlug * @param projectId * @returns */ fetchActiveCycle = async (workspaceSlug: string, projectId: string) => await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, "current").then((response) => { runInAction(() => { response.forEach((cycle) => { set(this.activeCycleMap, [cycle.id], cycle); }); }); return response; }); /** * @description fetches cycle details * @param workspaceSlug * @param projectId * @param cycleId * @returns */ fetchCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string) => await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId).then((response) => { runInAction(() => { set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response }); set(this.activeCycleMap, [response.id], { ...this.activeCycleMap?.[response.id], ...response }); }); return response; }); /** * @description creates a new cycle * @param workspaceSlug * @param projectId * @param data * @returns */ createCycle = async (workspaceSlug: string, projectId: string, data: Partial) => await this.cycleService.createCycle(workspaceSlug, projectId, data).then((response) => { runInAction(() => { set(this.cycleMap, [response.id], response); set(this.activeCycleMap, [response.id], response); }); return response; }); /** * @description updates cycle details * @param workspaceSlug * @param projectId * @param cycleId * @param data * @returns */ updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => { try { runInAction(() => { set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data }); set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data }); }); const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); return response; } catch (error) { console.log("Failed to patch cycle from cycle store"); this.fetchAllCycles(workspaceSlug, projectId); this.fetchActiveCycle(workspaceSlug, projectId); throw error; } }; /** * @description deletes a cycle * @param workspaceSlug * @param projectId * @param cycleId */ deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId).then(() => { runInAction(() => { delete this.cycleMap[cycleId]; delete this.activeCycleMap[cycleId]; }); }); /** * @description adds a cycle to favorites * @param workspaceSlug * @param projectId * @param cycleId * @returns */ addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { const currentCycle = this.getCycleById(cycleId); const currentActiveCycle = this.getActiveCycleById(cycleId); try { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); }); // updating through api. const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); return response; } catch (error) { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); }); throw error; } }; /** * @description removes a cycle from favorites * @param workspaceSlug * @param projectId * @param cycleId * @returns */ removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { const currentCycle = this.getCycleById(cycleId); const currentActiveCycle = this.getActiveCycleById(cycleId); try { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); }); const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); return response; } catch (error) { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); }); throw error; } }; }