refactor: project settings (#2575)

* refactor: project setting estimate

* refactor: project setting label

* refactor: project setting state

* refactor: project setting integration

* refactor: project settings member

* fix: estimate not updating

* fix: estimate not in observable

* fix: build error
This commit is contained in:
Dakshesh Jain 2023-11-01 13:42:51 +05:30 committed by GitHub
parent 80e6d7e1ea
commit 2d64caef90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2308 additions and 1929 deletions

View file

@ -1,2 +1,5 @@
export * from "./project_publish.store";
export * from "./project.store";
export * from "./project_estimates.store";
export * from "./project_label_store";
export * from "./project_state.store";

View file

@ -57,7 +57,7 @@ export interface IProjectStore {
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<any>;
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -70,6 +70,15 @@ export interface IProjectStore {
createProject: (workspaceSlug: string, data: any) => Promise<any>;
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
// write operations
removeMemberFromProject: (workspaceSlug: string, projectId: string, memberId: string) => Promise<void>;
updateMember: (
workspaceSlug: string,
projectId: string,
memberId: string,
data: Partial<IProjectMember>
) => Promise<IProjectMember>;
}
export class ProjectStore implements IProjectStore {
@ -117,6 +126,7 @@ export class ProjectStore implements IProjectStore {
states: observable.ref,
labels: observable.ref,
members: observable.ref,
estimates: observable.ref,
// computed
searchedProjects: computed,
@ -140,6 +150,7 @@ export class ProjectStore implements IProjectStore {
getProjectStateById: action,
getProjectLabelById: action,
getProjectMemberById: action,
getProjectEstimateById: action,
fetchProjectStates: action,
fetchProjectLabels: action,
@ -154,6 +165,10 @@ export class ProjectStore implements IProjectStore {
createProject: action,
updateProject: action,
leaveProject: action,
// write operations
removeMemberFromProject: action,
updateMember: action,
});
this.rootStore = _rootStore;
@ -605,4 +620,58 @@ export class ProjectStore implements IProjectStore {
console.log("Failed to delete project from project store");
}
};
removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => {
const originalMembers = this.projectMembers || [];
runInAction(() => {
this.members = {
...this.members,
[projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [],
};
});
try {
await this.projectService.deleteProjectMember(workspaceSlug, projectId, memberId);
await this.fetchProjectMembers(workspaceSlug, projectId);
} catch (error) {
console.log("Failed to delete project from project store");
// revert back to original members in case of error
runInAction(() => {
this.members = {
...this.members,
[projectId]: originalMembers,
};
});
}
};
updateMember = async (workspaceSlug: string, projectId: string, memberId: string, data: Partial<IProjectMember>) => {
const originalMembers = this.projectMembers || [];
runInAction(() => {
this.members = {
...this.members,
[projectId]: (this.projectMembers || [])?.map((member) =>
member.id === memberId ? { ...member, ...data } : member
),
};
});
try {
const response = await this.projectService.updateProjectMember(workspaceSlug, projectId, memberId, data);
await this.fetchProjectMembers(workspaceSlug, projectId);
return response;
} catch (error) {
console.log("Failed to update project member from project store");
// revert back to original members in case of error
runInAction(() => {
this.members = {
...this.members,
[projectId]: originalMembers,
};
});
throw error;
}
};
}

View file

@ -0,0 +1,141 @@
import { observable, action, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "../root";
import { IEstimate, IEstimateFormData } from "types";
// services
import { ProjectService, ProjectEstimateService } from "services/project";
export interface IProjectEstimateStore {
loader: boolean;
error: any | null;
// estimates
createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>;
updateEstimate: (
workspaceSlug: string,
projectId: string,
estimateId: string,
data: IEstimateFormData
) => Promise<IEstimate>;
deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise<void>;
}
export class ProjectEstimatesStore implements IProjectEstimateStore {
loader: boolean = false;
error: any | null = null;
// root store
rootStore;
// service
projectService;
estimateService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable,
error: observable,
// estimates
createEstimate: action,
updateEstimate: action,
deleteEstimate: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.estimateService = new ProjectEstimateService();
}
createEstimate = async (workspaceSlug: string, projectId: string, data: IEstimateFormData) => {
try {
const response = await this.estimateService.createEstimate(
workspaceSlug,
projectId,
data,
this.rootStore.user.currentUser!
);
const responseEstimate = {
...response.estimate,
points: response.estimate_points,
};
runInAction(() => {
this.rootStore.project.estimates = {
...this.rootStore.project.estimates,
[projectId]: [responseEstimate, ...(this.rootStore.project.estimates?.[projectId] || [])],
};
});
return response;
} catch (error) {
console.log("Failed to create estimate from project store");
throw error;
}
};
updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) => {
const originalEstimates = this.rootStore.project.getProjectEstimateById(estimateId);
runInAction(() => {
this.rootStore.project.estimates = {
...this.rootStore.project.estimates,
[projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) =>
estimate.id === estimateId ? { ...estimate, ...data.estimate } : estimate
),
};
});
try {
const response = await this.estimateService.patchEstimate(
workspaceSlug,
projectId,
estimateId,
data,
this.rootStore.user.currentUser!
);
await this.rootStore.project.fetchProjectEstimates(workspaceSlug, projectId);
return response;
} catch (error) {
console.log("Failed to update estimate from project store");
runInAction(() => {
this.rootStore.project.estimates = {
...this.rootStore.project.estimates,
[projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) =>
estimate.id === estimateId ? { ...estimate, ...originalEstimates } : estimate
),
};
});
throw error;
}
};
deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => {
const originalEstimateList = this.rootStore.project.projectEstimates || [];
runInAction(() => {
this.rootStore.project.estimates = {
...this.rootStore.project.estimates,
[projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.filter(
(estimate) => estimate.id !== estimateId
),
};
});
try {
// deleting using api
await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId, this.rootStore.user.currentUser!);
} catch (error) {
console.log("Failed to delete estimate from project store");
// reverting back to original estimate list
runInAction(() => {
this.rootStore.project.estimates = {
...this.rootStore.project.estimates,
[projectId]: originalEstimateList,
};
});
}
};
}

View file

@ -0,0 +1,140 @@
import { observable, action, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "../root";
import { IIssueLabels } from "types";
// services
import { IssueLabelService } from "services/issue";
import { ProjectService } from "services/project";
export interface IProjectLabelStore {
loader: boolean;
error: any | null;
// labels
createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabels>) => Promise<IIssueLabels>;
updateLabel: (
workspaceSlug: string,
projectId: string,
labelId: string,
data: Partial<IIssueLabels>
) => Promise<IIssueLabels>;
deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>;
}
export class ProjectLabelStore implements IProjectLabelStore {
loader: boolean = false;
error: any | null = null;
// root store
rootStore;
// service
projectService;
issueLabelService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable,
error: observable,
// labels
createLabel: action,
updateLabel: action,
deleteLabel: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.issueLabelService = new IssueLabelService();
}
createLabel = async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabels>) => {
try {
const response = await this.issueLabelService.createIssueLabel(
workspaceSlug,
projectId,
data,
this.rootStore.user.currentUser!
);
runInAction(() => {
this.rootStore.project.labels = {
...this.rootStore.project.labels,
[projectId]: [response, ...(this.rootStore.project.labels?.[projectId] || [])],
};
});
return response;
} catch (error) {
console.log("Failed to create label from project store");
throw error;
}
};
updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial<IIssueLabels>) => {
const originalLabel = this.rootStore.project.getProjectLabelById(labelId);
runInAction(() => {
this.rootStore.project.labels = {
...this.rootStore.project.labels,
[projectId]:
this.rootStore.project.labels?.[projectId]?.map((label) =>
label.id === labelId ? { ...label, ...data } : label
) || [],
};
});
try {
const response = await this.issueLabelService.patchIssueLabel(
workspaceSlug,
projectId,
labelId,
data,
this.rootStore.user.currentUser!
);
return response;
} catch (error) {
console.log("Failed to update label from project store");
runInAction(() => {
this.rootStore.project.labels = {
...this.rootStore.project.labels,
[projectId]: (this.rootStore.project.labels?.[projectId] || [])?.map((label) =>
label.id === labelId ? { ...label, ...originalLabel } : label
),
} as any;
});
throw error;
}
};
deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => {
const originalLabelList = this.rootStore.project.projectLabels;
runInAction(() => {
this.rootStore.project.labels = {
...this.rootStore.project.labels,
[projectId]: (this.rootStore.project.labels?.[projectId] || [])?.filter((label) => label.id !== labelId),
};
});
try {
// deleting using api
await this.issueLabelService.deleteIssueLabel(
workspaceSlug,
projectId,
labelId,
this.rootStore.user.currentUser!
);
} catch (error) {
console.log("Failed to delete label from project store");
// reverting back to original label list
runInAction(() => {
this.rootStore.project.labels = {
...this.rootStore.project.labels,
[projectId]: originalLabelList || [],
};
});
}
};
}

View file

@ -0,0 +1,279 @@
import { observable, action, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "../root";
import { IState } from "types";
// services
import { ProjectService, ProjectStateService } from "services/project";
import { groupBy, orderArrayBy } from "helpers/array.helper";
import { orderStateGroups } from "helpers/state.helper";
export interface IProjectStateStore {
loader: boolean;
error: any | null;
// states
createState: (workspaceSlug: string, projectId: string, data: Partial<IState>) => Promise<IState>;
updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial<IState>) => Promise<IState>;
deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise<void>;
markStateAsDefault: (workspaceSlug: string, projectId: string, stateId: string) => Promise<void>;
moveStatePosition: (
workspaceSlug: string,
projectId: string,
stateId: string,
direction: "up" | "down",
groupIndex: number
) => Promise<void>;
}
export class ProjectStateStore implements IProjectStateStore {
loader: boolean = false;
error: any | null = null;
// root store
rootStore;
// service
projectService;
stateService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable,
error: observable,
// states
createState: action,
updateState: action,
deleteState: action,
markStateAsDefault: action,
moveStatePosition: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.stateService = new ProjectStateService();
}
createState = async (workspaceSlug: string, projectId: string, data: Partial<IState>) => {
try {
const response = await this.stateService.createState(
workspaceSlug,
projectId,
data,
this.rootStore.user.currentUser!
);
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[response.group]: [...(this.rootStore.project.states?.[projectId]?.[response.group] || []), response],
},
};
});
return response;
} catch (error) {
console.log("Failed to create state from project store");
throw error;
}
};
updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial<IState>) => {
const originalStates = this.rootStore.project.states;
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[data.group as string]: (this.rootStore.project.states?.[projectId]?.[data.group as string] || []).map(
(state) => (state.id === stateId ? { ...state, ...data } : state)
),
},
};
});
try {
const response = await this.stateService.patchState(
workspaceSlug,
projectId,
stateId,
data,
this.rootStore.user.currentUser!
);
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[response.group]: (this.rootStore.project.states?.[projectId]?.[response.group] || []).map((state) =>
state.id === stateId ? { ...state, ...response } : state
),
},
};
});
return response;
} catch (error) {
console.log("Failed to update state from project store");
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[data.group as string]: originalStates || [],
},
} as any;
});
throw error;
}
};
deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => {
const originalStates = this.rootStore.project.projectStates;
try {
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[originalStates?.[0]?.group || ""]: (
this.rootStore.project.states?.[projectId]?.[originalStates?.[0]?.group || ""] || []
).filter((state) => state.id !== stateId),
},
};
});
// deleting using api
await this.stateService.deleteState(workspaceSlug, projectId, stateId, this.rootStore.user.currentUser!);
} catch (error) {
console.log("Failed to delete state from project store");
// reverting back to original label list
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: {
...this.rootStore.project.states?.[projectId],
[originalStates?.[0]?.group || ""]: originalStates || [],
},
};
});
}
};
markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => {
const states = this.rootStore.project.projectStates;
const currentDefaultState = states?.find((state) => state.default);
let newStateList =
states?.map((state) => {
if (state.id === stateId) return { ...state, default: true };
if (state.id === currentDefaultState?.id) return { ...state, default: false };
return state;
}) ?? [];
newStateList = orderArrayBy(newStateList, "sequence", "ascending");
const newOrderedStateGroups = orderStateGroups(groupBy(newStateList, "group"));
const oldOrderedStateGroup = this.rootStore.project.states?.[projectId] || {}; // for reverting back to old state group if api fails
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: newOrderedStateGroups || {},
};
});
// updating using api
try {
this.stateService.patchState(
workspaceSlug,
projectId,
stateId,
{ default: true },
this.rootStore.user.currentUser!
);
if (currentDefaultState)
this.stateService.patchState(
workspaceSlug,
projectId,
currentDefaultState.id,
{ default: false },
this.rootStore.user.currentUser!
);
} catch (err) {
console.log("Failed to mark state as default");
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: oldOrderedStateGroup,
};
});
}
};
moveStatePosition = async (
workspaceSlug: string,
projectId: string,
stateId: string,
direction: "up" | "down",
groupIndex: number
) => {
const SEQUENCE_GAP = 15000;
let newSequence = SEQUENCE_GAP;
const states = this.rootStore.project.projectStates || [];
const groupedStates = groupBy(states || [], "group");
const selectedState = states?.find((state) => state.id === stateId);
const groupStates = states?.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;
}
const newStateList = states?.map((state) => {
if (state.id === stateId) return { ...state, sequence: newSequence };
return state;
});
const newOrderedStateGroups = orderStateGroups(
groupBy(orderArrayBy(newStateList, "sequence", "ascending"), "group")
);
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: newOrderedStateGroups || {},
};
});
// updating using api
try {
await this.stateService.patchState(
workspaceSlug,
projectId,
stateId,
{ sequence: newSequence },
this.rootStore.user.currentUser!
);
} catch (err) {
console.log("Failed to move state position");
// reverting back to old state group if api fails
runInAction(() => {
this.rootStore.project.states = {
...this.rootStore.project.states,
[projectId]: groupedStates,
};
});
}
};
}

View file

@ -19,7 +19,18 @@ import {
IssueQuickAddStore,
} from "store/issue";
import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace";
import { IProjectPublishStore, IProjectStore, ProjectPublishStore, ProjectStore } from "store/project";
import {
IProjectPublishStore,
IProjectStore,
ProjectPublishStore,
ProjectStore,
IProjectStateStore,
ProjectStateStore,
IProjectLabelStore,
ProjectLabelStore,
ProjectEstimatesStore,
IProjectEstimateStore,
} from "store/project";
import {
IModuleFilterStore,
IModuleIssueKanBanViewStore,
@ -99,6 +110,9 @@ export class RootStore {
projectPublish: IProjectPublishStore;
project: IProjectStore;
projectState: IProjectStateStore;
projectLabel: IProjectLabelStore;
projectEstimates: IProjectEstimateStore;
issue: IIssueStore;
module: IModuleStore;
@ -154,6 +168,9 @@ export class RootStore {
this.workspaceFilter = new WorkspaceFilterStore(this);
this.project = new ProjectStore(this);
this.projectState = new ProjectStateStore(this);
this.projectLabel = new ProjectLabelStore(this);
this.projectEstimates = new ProjectEstimatesStore(this);
this.projectPublish = new ProjectPublishStore(this);
this.module = new ModuleStore(this);