diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts new file mode 100644 index 000000000..b838ee501 --- /dev/null +++ b/web/store/cycle.store.ts @@ -0,0 +1,370 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +// types +import { ICycle, TCycleView, CycleDateCheckData } from "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 { + loader: boolean; + error: any | null; + + cycleView: TCycleView; + + cycleId: string | null; + cycleMap: { + [projectId: string]: { + [cycleId: string]: ICycle; + }; + }; + cycles: { + [projectId: string]: { + [filterType: string]: string[]; + }; + }; + + // computed + getCycleById: (cycleId: string) => ICycle | null; + projectCycles: string[] | null; + projectCompletedCycles: string[] | null; + projectUpcomingCycles: string[] | null; + projectDraftCycles: string[] | null; + + // actions + validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; + + fetchCycles: ( + workspaceSlug: string, + projectId: string, + params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" + ) => Promise; + fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + + 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; + + addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; +} + +export class CycleStore implements ICycleStore { + loader: boolean = false; + error: any | null = null; + + cycleView: TCycleView = "all"; + + cycleId: string | null = null; + cycleMap: { + [projectId: string]: { + [cycleId: string]: ICycle; + }; + } = {}; + cycles: { + [projectId: string]: { + [filterType: string]: string[]; + }; + } = {}; + + // root store + rootStore; + // services + projectService; + issueService; + cycleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable, + error: observable.ref, + + cycleId: observable.ref, + cycleMap: observable.ref, + cycles: observable.ref, + + // computed + projectCycles: computed, + projectCompletedCycles: computed, + projectUpcomingCycles: computed, + projectDraftCycles: computed, + + // actions + getCycleById: action, + + fetchCycles: 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 + get projectCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + return this.cycles[projectId]?.all || null; + } + + get projectCompletedCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.completed || null; + } + + get projectUpcomingCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.upcoming || null; + } + + get projectDraftCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.draft || null; + } + + getCycleById = (cycleId: string) => this.cycleMap[this.rootStore.project][cycleId] || null; + + // actions + setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); + + validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { + try { + const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); + return response; + } catch (error) { + console.log("Failed to validate cycle dates", error); + throw error; + } + }; + + fetchCycles = async ( + workspaceSlug: string, + projectId: string, + params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" + ) => { + try { + this.loader = true; + this.error = null; + + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + ...cyclesResponse, + }, + }; + this.cycles = { + ...this.cycles, + [projectId]: { ...this.cycles[projectId], [params]: Object.keys(cyclesResponse) }, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch project cycles in project store", error); + this.loader = false; + this.error = error; + } + }; + + fetchCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [response?.id]: response, + }, + }; + }); + + return response; + } catch (error) { + console.log("Failed to fetch cycle detail from cycle store"); + throw error; + } + }; + + createCycle = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.cycleService.createCycle(workspaceSlug, projectId, data); + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [response?.id]: response, + }, + }; + }); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return response; + } catch (error) { + console.log("Failed to create cycle from cycle store"); + throw error; + } + }; + + updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => { + try { + const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); + + const currentCycle = this.cycleMap[projectId][cycleId]; + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [cycleId]: { ...currentCycle, ...data }, + }, + }; + }); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return _response; + } catch (error) { + console.log("Failed to patch cycle from cycle store"); + throw error; + } + }; + + deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const currentProjectCycles = this.cycleMap[projectId]; + delete currentProjectCycles[cycleId]; + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: currentProjectCycles, + }; + }); + + const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); + + return _response; + } catch (error) { + console.log("Failed to delete cycle from cycle store"); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + throw error; + } + }; + + addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const currentCycle = this.cycleMap[projectId][cycleId]; + + if (currentCycle.is_favorite) return; + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [cycleId]: { ...currentCycle, is_favorite: true }, + }, + }; + }); + + // updating through api. + const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); + + return response; + } catch (error) { + console.log("Failed to add cycle to favorites in the cycles store", error); + + // reset on error + const currentCycle = this.cycleMap[projectId][cycleId]; + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [cycleId]: { ...currentCycle, is_favorite: false }, + }, + }; + }); + + throw error; + } + }; + + removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const currentCycle = this.cycleMap[projectId][cycleId]; + + if (!currentCycle.is_favorite) return; + + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [cycleId]: { ...currentCycle, is_favorite: false }, + }, + }; + }); + + const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); + + return response; + } catch (error) { + console.log("Failed to remove cycle from favorites - Cycle Store", error); + + // reset on error + const currentCycle = this.cycleMap[projectId][cycleId]; + runInAction(() => { + this.cycleMap = { + ...this.cycleMap, + [projectId]: { + ...this.cycleMap[projectId], + [cycleId]: { ...currentCycle, is_favorite: true }, + }, + }; + }); + throw error; + } + }; +} diff --git a/web/store/module.store.ts b/web/store/module.store.ts new file mode 100644 index 000000000..c1300882b --- /dev/null +++ b/web/store/module.store.ts @@ -0,0 +1,446 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { ModuleService } from "services/module.service"; +// types +import { IModule, ILinkDetails } from "types"; +import { RootStore } from "store/root.store"; + +export interface IModuleStore { + // states + loader: boolean; + error: any | null; + + // observables + moduleId: string | null; + moduleMap: { + [project_id: string]: { + [module_id: string]: IModule; + }; + }; + + // actions + getModuleById: (moduleId: string) => IModule | null; + + fetchModules: (workspaceSlug: string, projectId: string) => void; + fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + + createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateModuleDetails: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => Promise; + deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + + createModuleLink: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => Promise; + updateModuleLink: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + linkId: string, + data: Partial + ) => Promise; + deleteModuleLink: (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => Promise; + + addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + + // computed + projectModules: string[] | null; +} + +export class ModulesStore implements IModuleStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + moduleId: string | null = null; + moduleMap: { + [project_id: string]: { + [module_id: string]: IModule; + }; + } = {}; + + // root store + rootStore; + + // services + projectService; + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable, + error: observable.ref, + + // observables + moduleId: observable.ref, + moduleMap: observable.ref, + + // actions + getModuleById: action, + + fetchModules: action, + fetchModuleDetails: action, + + createModule: action, + updateModuleDetails: action, + deleteModule: action, + + createModuleLink: action, + updateModuleLink: action, + deleteModuleLink: action, + + addModuleToFavorites: action, + removeModuleFromFavorites: action, + + // computed + projectModules: computed, + }); + + this.rootStore = _rootStore; + + // services + this.projectService = new ProjectService(); + this.moduleService = new ModuleService(); + } + + // computed + get projectModules() { + if (!this.rootStore.project.projectId) return null; + + return Object.keys(this.moduleMap[this.rootStore.project.projectId]) || null; + } + + getModuleById = (moduleId: string) => this.moduleMap[this.rootStore.project.projectId][moduleId] || null; + + // actions + + fetchModules = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectId); + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: modulesResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch modules list in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + fetchModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: response, + }, + }; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + console.error("Failed to fetch module details in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createModule = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.moduleService.createModule(workspaceSlug, projectId, data); + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [response.id]: response, + }, + }; + this.loader = false; + this.error = null; + }); + this.fetchModules(workspaceSlug, projectId); + return response; + } catch (error) { + console.error("Failed to create module in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => { + try { + const currentModule = this.moduleMap[projectId][moduleId]; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, ...data }, + }, + }; + }); + + const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data); + + return response; + } catch (error) { + console.error("Failed to update module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteModule = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const currentProjectModules = this.moduleMap[projectId]; + delete currentProjectModules[moduleId]; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: currentProjectModules, + }; + }); + + await this.moduleService.deleteModule(workspaceSlug, projectId, moduleId); + } catch (error) { + console.error("Failed to delete module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + } + }; + + createModuleLink = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => { + try { + const response = await this.moduleService.createModuleLink(workspaceSlug, projectId, moduleId, data); + + const currentModule = this.moduleMap[projectId][moduleId]; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, link_module: [response, ...currentModule.link_module] }, + }, + }; + }); + + return response; + } catch (error) { + console.error("Failed to create module link in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateModuleLink = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + linkId: string, + data: Partial + ) => { + try { + const response = await this.moduleService.updateModuleLink(workspaceSlug, projectId, moduleId, linkId, data); + + const currentModule = this.moduleMap[projectId][moduleId]; + const linkModules = currentModule.link_module.map((link) => (link.id === linkId ? response : link)); + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, link_module: linkModules }, + }, + }; + }); + + return response; + } catch (error) { + console.error("Failed to update module link in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteModuleLink = async (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => { + try { + const currentModule = this.moduleMap[projectId][moduleId]; + const linkModules = currentModule.link_module.filter((link) => link.id !== linkId); + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, link_module: linkModules }, + }, + }; + }); + + await this.moduleService.deleteModuleLink(workspaceSlug, projectId, moduleId, linkId); + } catch (error) { + console.error("Failed to delete module link in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const currentModule = this.moduleMap[projectId][moduleId]; + + if (currentModule.is_favorite) return; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, is_favorite: true }, + }, + }; + }); + + await this.moduleService.addModuleToFavorites(workspaceSlug, projectId, { + module: moduleId, + }); + } catch (error) { + console.error("Failed to add module to favorites in module store", error); + + const currentModule = this.moduleMap[projectId][moduleId]; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, is_favorite: false }, + }, + }; + }); + } + }; + + removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const currentModule = this.moduleMap[projectId][moduleId]; + + if (!currentModule.is_favorite) return; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, is_favorite: false }, + }, + }; + }); + + await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId); + } catch (error) { + console.error("Failed to remove module from favorites in module store", error); + + const currentModule = this.moduleMap[projectId][moduleId]; + + runInAction(() => { + this.moduleMap = { + ...this.moduleMap, + [projectId]: { + ...this.moduleMap[projectId], + [moduleId]: { ...currentModule, is_favorite: true }, + }, + }; + }); + } + }; +} diff --git a/web/store/project-view.store.ts b/web/store/project-view.store.ts new file mode 100644 index 000000000..900e67af7 --- /dev/null +++ b/web/store/project-view.store.ts @@ -0,0 +1,290 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { ViewService } from "services/view.service"; +import { RootStore } from "store/root.store"; +// types +import { IProjectView } from "types"; + +export interface IProjectViewsStore { + // states + loader: boolean; + error: any | null; + + // observables + viewId: string | null; + viewMap: { + [projectId: string]: { + [viewId: string]: IProjectView; + }; + }; + + // actions + fetchViews: (workspaceSlug: string, projectId: string) => Promise; + fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + createView: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateView: ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ) => Promise; + deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; +} + +export class ProjectViewsStore implements IProjectViewsStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + viewId: string | null = null; + viewMap: { + [projectId: string]: { + [viewId: string]: IProjectView; + }; + } = {}; + + // root store + rootStore; + + // services + viewService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + viewId: observable.ref, + viewMap: observable.ref, + + // actions + fetchViews: action, + fetchViewDetails: action, + createView: action, + updateView: action, + deleteView: action, + addViewToFavorites: action, + removeViewFromFavorites: action, + }); + + this.rootStore = _rootStore; + + this.viewService = new ViewService(); + } + + setViewId = (viewId: string | null) => { + this.viewId = viewId; + }; + + fetchViews = async (workspaceSlug: string, projectId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.viewService.getViews(workspaceSlug, projectId); + + runInAction(() => { + this.loader = false; + this.viewMap = { + ...this.viewMap, + [projectId]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchViewDetails = async (workspaceSlug: string, projectId: string, viewId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.viewService.getViewDetails(workspaceSlug, projectId, viewId); + + runInAction(() => { + this.loader = false; + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [response.id]: response, + }, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createView = async (workspaceSlug: string, projectId: string, data: Partial): Promise => { + try { + const response = await this.viewService.createView(workspaceSlug, projectId, data); + + runInAction(() => { + this.loader = false; + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [response.id]: response, + }, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateView = async ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ): Promise => { + try { + const currentView = this.viewMap[projectId][viewId]; + + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [viewId]: { ...currentView, ...data }, + }, + }; + }); + + const response = await this.viewService.patchView(workspaceSlug, projectId, viewId, data); + + return response; + } catch (error) { + this.fetchViewDetails(workspaceSlug, projectId, viewId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteView = async (workspaceSlug: string, projectId: string, viewId: string): Promise => { + try { + const currentProjectViews = this.viewMap[projectId]; + delete currentProjectViews[viewId]; + + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: currentProjectViews, + }; + }); + + await this.viewService.deleteView(workspaceSlug, projectId, viewId); + } catch (error) { + this.fetchViews(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + addViewToFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + const currentView = this.viewMap[projectId][viewId]; + + if (currentView.is_favorite) return; + + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [viewId]: { ...currentView, is_favorite: true }, + }, + }; + }); + + await this.viewService.addViewToFavorites(workspaceSlug, projectId, { + view: viewId, + }); + } catch (error) { + console.error("Failed to add view to favorites in view store", error); + + const currentView = this.viewMap[projectId][viewId]; + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [viewId]: { ...currentView, is_favorite: false }, + }, + }; + }); + } + }; + + removeViewFromFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + const currentView = this.viewMap[projectId][viewId]; + + if (!currentView.is_favorite) return; + + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [viewId]: { ...currentView, is_favorite: false }, + }, + }; + }); + + await this.viewService.removeViewFromFavorites(workspaceSlug, projectId, viewId); + } catch (error) { + console.error("Failed to remove view from favorites in view store", error); + + const currentView = this.viewMap[projectId][viewId]; + runInAction(() => { + this.viewMap = { + ...this.viewMap, + [projectId]: { + ...this.viewMap[projectId], + [viewId]: { ...currentView, is_favorite: true }, + }, + }; + }); + } + }; +} diff --git a/web/store/project/index.ts b/web/store/project/index.ts new file mode 100644 index 000000000..f99736ae8 --- /dev/null +++ b/web/store/project/index.ts @@ -0,0 +1,13 @@ +import { ProjectsStore } from "./projects.store"; +import { ProjectPublishStore } from "./project-publish.store"; +import { RootStore } from "store/root.store"; + +export class ProjectRootStore { + projects: ProjectsStore; + publish: ProjectPublishStore; + + constructor(_root: RootStore) { + this.projects = new ProjectsStore(_root); + this.publish = new ProjectPublishStore(this); + } +} diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts new file mode 100644 index 000000000..1e040f339 --- /dev/null +++ b/web/store/project/project-publish.store.ts @@ -0,0 +1,265 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { ProjectRootStore } from "./"; +// services +import { ProjectPublishService } from "services/project"; + +export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; + +export type TProjectPublishViewsSettings = { + [key in TProjectPublishViews]: boolean; +}; + +export interface IProjectPublishSettings { + id?: string; + project?: string; + comments: boolean; + reactions: boolean; + votes: boolean; + views: TProjectPublishViewsSettings; + inbox: string | null; +} + +export interface IProjectPublishStore { + generalLoader: boolean; + fetchSettingsLoader: boolean; + error: any | null; + + projectPublishSettings: IProjectPublishSettings | "not-initialized"; + + getProjectSettingsAsync: (workspaceSlug: string, projectId: string) => Promise; + publishProject: (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => Promise; + updateProjectSettingsAsync: ( + workspaceSlug: string, + projectId: string, + projectPublishId: string, + data: IProjectPublishSettings + ) => Promise; + unPublishProject: (workspaceSlug: string, projectId: string, projectPublishId: string) => Promise; +} + +export class ProjectPublishStore implements IProjectPublishStore { + // states + generalLoader: boolean = false; + fetchSettingsLoader: boolean = false; + error: any | null = null; + + // actions + project_id: string | null = null; + projectPublishSettings: IProjectPublishSettings | "not-initialized" = "not-initialized"; + + // root store + projectRootStore: ProjectRootStore; + + // services + projectPublishService; + + constructor(_projectRootStore: ProjectRootStore) { + makeObservable(this, { + // states + generalLoader: observable, + fetchSettingsLoader: observable, + error: observable, + + // observables + project_id: observable, + projectPublishSettings: observable.ref, + + // actions + getProjectSettingsAsync: action, + publishProject: action, + updateProjectSettingsAsync: action, + unPublishProject: action, + }); + + this.projectRootStore = _projectRootStore; + + // services + this.projectPublishService = new ProjectPublishService(); + } + + getProjectSettingsAsync = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.fetchSettingsLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.getProjectSettingsAsync(workspaceSlug, projectId); + + if (response && response.length > 0) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response[0]?.id, + comments: response[0]?.comments, + reactions: response[0]?.reactions, + votes: response[0]?.votes, + views: { + list: response[0]?.views?.list || false, + kanban: response[0]?.views?.kanban || false, + calendar: response[0]?.views?.calendar || false, + gantt: response[0]?.views?.gantt || false, + spreadsheet: response[0]?.views?.spreadsheet || false, + }, + inbox: response[0]?.inbox || null, + project: response[0]?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.fetchSettingsLoader = false; + this.error = null; + }); + } else { + runInAction(() => { + this.projectPublishSettings = "not-initialized"; + this.fetchSettingsLoader = false; + this.error = null; + }); + } + return response; + } catch (error) { + runInAction(() => { + this.fetchSettingsLoader = false; + this.error = error; + }); + + return error; + } + }; + + publishProject = async (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.createProjectSettingsAsync(workspaceSlug, projectId, data); + + if (response) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response?.id || null, + comments: response?.comments || false, + reactions: response?.reactions || false, + votes: response?.votes || false, + views: { ...response?.views }, + inbox: response?.inbox || null, + project: response?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.projectRootStore.projects.projectsMap = { + ...this.projectRootStore.projects.projectsMap, + [workspaceSlug]: { + ...this.projectRootStore.projects.projectsMap[workspaceSlug], + [projectId]: { + ...this.projectRootStore.projects.projectsMap[workspaceSlug][projectId], + is_deployed: true, + }, + }, + }; + this.generalLoader = false; + this.error = null; + }); + + return response; + } + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; + + updateProjectSettingsAsync = async ( + workspaceSlug: string, + projectId: string, + projectPublishId: string, + data: IProjectPublishSettings + ) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.updateProjectSettingsAsync( + workspaceSlug, + projectId, + projectPublishId, + data + ); + + if (response) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response?.id || null, + comments: response?.comments || false, + reactions: response?.reactions || false, + votes: response?.votes || false, + views: { ...response?.views }, + inbox: response?.inbox || null, + project: response?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.generalLoader = false; + this.error = null; + }); + + return response; + } + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; + + unPublishProject = async (workspaceSlug: string, projectId: string, projectPublishId: string) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.deleteProjectSettingsAsync( + workspaceSlug, + projectId, + projectPublishId + ); + + runInAction(() => { + this.projectPublishSettings = "not-initialized"; + this.projectRootStore.projects.projectsMap = { + ...this.projectRootStore.projects.projectsMap, + [workspaceSlug]: { + ...this.projectRootStore.projects.projectsMap[workspaceSlug], + [projectId]: { + ...this.projectRootStore.projects.projectsMap[workspaceSlug][projectId], + is_deployed: false, + }, + }, + }; + this.generalLoader = false; + this.error = null; + }); + + return response; + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; +} diff --git a/web/store/project/projects.store.ts b/web/store/project/projects.store.ts new file mode 100644 index 000000000..676011fab --- /dev/null +++ b/web/store/project/projects.store.ts @@ -0,0 +1,360 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +import { IssueLabelService, IssueService } from "services/issue"; +import { ProjectService, ProjectStateService } from "services/project"; +import { RootStore } from "store/root.store"; + +import { IProject } from "types"; + +export interface IProjectsStore { + loader: boolean; + error: any | null; + + searchQuery: string; + projectId: string | null; + projectsMap: { + [workspaceSlug: string]: { + [projectId: string]: IProject; // projectId: project Info + }; + }; + + // computed + searchedProjects: string[]; + workspaceProjects: string[] | null; + joinedProjects: string[]; + favoriteProjects: string[]; + currentProjectDetails: IProject | undefined; + + // actions + setSearchQuery: (query: string) => void; + getProjectById: (workspaceSlug: string, projectId: string) => IProject | null; + + fetchProjects: (workspaceSlug: string) => Promise; + fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; + + addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise; + removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise; + + orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number; + updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise; + + createProject: (workspaceSlug: string, data: any) => Promise; + updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + deleteProject: (workspaceSlug: string, projectId: string) => Promise; +} + +export class ProjectsStore implements IProjectsStore { + loader: boolean = false; + error: any | null = null; + + projectId: string | null = null; + searchQuery: string = ""; + projectsMap: { + [workspaceSlug: string]: { + [projectId: string]: IProject; // projectId: project Info + }; + }; + + // root store + rootStore: RootStore; + // service + projectService; + issueLabelService; + issueService; + stateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + searchQuery: observable.ref, + projectId: observable.ref, + projectsMap: observable.ref, + + // computed + searchedProjects: computed, + workspaceProjects: computed, + + currentProjectDetails: computed, + + joinedProjects: computed, + favoriteProjects: computed, + + // action + setSearchQuery: action, + fetchProjects: action, + fetchProjectDetails: action, + + addProjectToFavorites: action, + removeProjectFromFavorites: action, + + orderProjectsWithSortOrder: action, + updateProjectView: action, + createProject: action, + updateProject: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + this.issueLabelService = new IssueLabelService(); + this.stateService = new ProjectStateService(); + } + + get searchedProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + + const currentProjectsMap = this.projectsMap[this.rootStore.workspace.workspaceSlug]; + const projectIds = Object.keys(currentProjectsMap); + return this.searchQuery === "" + ? projectIds + : projectIds?.filter((projectId) => { + currentProjectsMap[projectId].name.toLowerCase().includes(this.searchQuery.toLowerCase()) || + currentProjectsMap[projectId].identifier.toLowerCase().includes(this.searchQuery.toLowerCase()); + }); + } + + get workspaceProjects() { + if (!this.rootStore.workspace.workspaceSlug) return null; + const currentProjectsMap = this.projectsMap[this.rootStore.workspace.workspaceSlug]; + + const projectIds = Object.keys(currentProjectsMap); + if (!projectIds) return null; + return projectIds; + } + + get currentProjectDetails() { + if (!this.projectId || !this.rootStore.workspace.workspaceSlug) return; + return this.projectsMap[this.rootStore.workspace.workspaceSlug][this.projectId]; + } + + get joinedProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + + const currentProjectsMap = this.projectsMap[this.rootStore.workspace.workspaceSlug]; + const projectIds = Object.keys(currentProjectsMap); + + return projectIds?.filter((projectId) => currentProjectsMap[projectId].is_member); + } + + get favoriteProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + + const currentProjectsMap = this.projectsMap[this.rootStore.workspace.workspaceSlug]; + const projectIds = Object.keys(currentProjectsMap); + + return projectIds?.filter((projectId) => currentProjectsMap[projectId].is_favorite); + } + + setSearchQuery = (query: string) => { + this.searchQuery = query; + }; + + /** + * get Workspace projects using workspace slug + * @param workspaceSlug + * @returns + * + */ + fetchProjects = async (workspaceSlug: string) => { + try { + const currentProjectsMap = await this.projectService.getProjects(workspaceSlug); + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: currentProjectsMap, + }; + }); + } catch (error) { + console.log("Failed to fetch project from workspace store"); + throw error; + } + }; + + fetchProjectDetails = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.projectService.getProject(workspaceSlug, projectId); + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { + ...this.projectsMap[workspaceSlug], + [projectId]: response, + }, + }; + }); + return response; + } catch (error) { + console.log("Error while fetching project details", error); + throw error; + } + }; + + getProjectById = (workspaceSlug: string, projectId: string) => { + const currentProjectsMap = this.projectsMap?.[workspaceSlug]; + if (!currentProjectsMap) return null; + + const projectInfo: IProject | null = currentProjectsMap[projectId] || null; + return projectInfo; + }; + + addProjectToFavorites = async (workspaceSlug: string, projectId: string) => { + try { + const currentProject = this.projectsMap?.[workspaceSlug]?.[projectId]; + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { + ...this.projectsMap[workspaceSlug], + [projectId]: { ...currentProject, is_favorite: true }, + }, + }; + }); + + const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId); + return response; + } catch (error) { + console.log("Failed to add project to favorite"); + await this.fetchProjects(workspaceSlug); + throw error; + } + }; + + removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => { + try { + const currentProject = this.projectsMap?.[workspaceSlug]?.[projectId]; + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { + ...this.projectsMap[workspaceSlug], + [projectId]: { ...currentProject, is_favorite: false }, + }, + }; + }); + + const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId); + await this.fetchProjects(workspaceSlug); + return response; + } catch (error) { + console.log("Failed to add project to favorite"); + throw error; + } + }; + + orderProjectsWithSortOrder = (sortIndex: number, destinationIndex: number, projectId: string) => { + try { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + if (!workspaceSlug) return 0; + + const projectsList = Object.values(this.projectsMap[workspaceSlug] || {}) || []; + let updatedSortOrder = projectsList[sortIndex].sort_order; + + if (destinationIndex === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000; + else if (destinationIndex === projectsList.length - 1) + updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000; + else { + const destinationSortingOrder = projectsList[destinationIndex].sort_order as number; + const relativeDestinationSortingOrder = + sortIndex < destinationIndex + ? (projectsList[destinationIndex + 1].sort_order as number) + : (projectsList[destinationIndex - 1].sort_order as number); + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + const currentProject = this.projectsMap?.[workspaceSlug]?.[projectId]; + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { + ...this.projectsMap[workspaceSlug], + [projectId]: { ...currentProject, sort_order: updatedSortOrder }, + }, + }; + }); + + return updatedSortOrder; + } catch (error) { + console.log("failed to update sort order of the projects"); + return 0; + } + }; + + updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: any) => { + try { + const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps); + await this.fetchProjects(workspaceSlug); + + return response; + } catch (error) { + console.log("Failed to update sort order of the projects"); + throw error; + } + }; + + createProject = async (workspaceSlug: string, data: any) => { + try { + const response = await this.projectService.createProject(workspaceSlug, data); + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { ...this.projectsMap[workspaceSlug], [response.id]: response }, + }; + }); + return response; + } catch (error) { + console.log("Failed to create project from project store"); + throw error; + } + }; + + updateProject = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const currentProject = this.projectsMap?.[workspaceSlug]?.[projectId]; + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { ...this.projectsMap[workspaceSlug], [projectId]: { ...currentProject, ...data } }, + }; + }); + + const response = await this.projectService.updateProject(workspaceSlug, projectId, data); + return response; + } catch (error) { + console.log("Failed to create project from project store"); + + this.fetchProjects(workspaceSlug); + this.fetchProjectDetails(workspaceSlug, projectId); + throw error; + } + }; + + deleteProject = async (workspaceSlug: string, projectId: string) => { + try { + const workspaceProjects = { ...this.projectsMap[workspaceSlug] }; + + delete workspaceProjects[projectId]; + + runInAction(() => { + this.projectsMap = { + ...this.projectsMap, + [workspaceSlug]: { ...workspaceProjects }, + }; + }); + + await this.projectService.deleteProject(workspaceSlug, projectId); + await this.fetchProjects(workspaceSlug); + } catch (error) { + console.log("Failed to delete project from project store"); + this.fetchProjects(workspaceSlug); + } + }; +} diff --git a/web/store/root.store.ts b/web/store/root.store.ts index e303cf495..5a1194de1 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -1,6 +1,10 @@ import { enableStaticRendering } from "mobx-react-lite"; // root stores import { AppRootStore } from "./application"; +import { ProjectRootStore } from "./project"; +import { CycleStore } from "./cycle.store"; +import { ProjectViewsStore } from "./project-view.store"; +import { ModulesStore } from "./module.store"; enableStaticRendering(typeof window === "undefined"); @@ -15,16 +19,16 @@ export class RootStore { constructor() { this.app = new AppRootStore(); - this.user = new UserRootStore(); - this.workspace = new WorkspaceRootStore(); - this.project = new ProjectRootStore(); - this.cycle = new CycleRootStore(); - this.module = new ModuleRootStore(); - this.projectView = new ProjectViewRootStore(); - this.page = new PageRootStore(); - this.issue = new IssueRootStore(); - // independent stores - this.label = new labelStore(); - this.state = new stateStore(); + // this.user = new UserRootStore(); + // this.workspace = new WorkspaceRootStore(); + this.project = new ProjectRootStore(this); + this.cycle = new CycleStore(this); + this.module = new ModulesStore(this); + this.projectView = new ProjectViewsStore(this); + // this.page = new PageRootStore(); + // this.issue = new IssueRootStore(); + // // independent stores + // this.label = new labelStore(); + // this.state = new stateStore(); } } diff --git a/web/store_legacy/module/modules.store.ts b/web/store_legacy/module/modules.store.ts index beec12ee0..a003333ad 100644 --- a/web/store_legacy/module/modules.store.ts +++ b/web/store_legacy/module/modules.store.ts @@ -3,7 +3,7 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx" import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; // types -import { RootStore } from "../root"; + import { IIssue, IModule, ILinkDetails } from "types"; import { IIssueGroupWithSubGroupsStructure, @@ -11,6 +11,7 @@ import { IIssueUnGroupedStructure, } from "../issue/issue.store"; import { IBlockUpdateData } from "components/gantt-chart"; +import { RootStore } from "store/root.store"; export interface IModuleStore { // states