Optimistically update distribution (#5290)
This commit is contained in:
parent
8f8a97589d
commit
93e6c3b6e0
15 changed files with 459 additions and 9 deletions
|
|
@ -136,7 +136,7 @@ const ProgressChart: React.FC<Props> = ({
|
||||||
enableSlices="x"
|
enableSlices="x"
|
||||||
sliceTooltip={(datum) => (
|
sliceTooltip={(datum) => (
|
||||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||||
{datum.slice.points[0].data.yFormatted}
|
{datum.slice.points?.[1]?.data?.yFormatted ?? datum.slice.points[0].data.yFormatted}
|
||||||
<span className="text-custom-text-200"> {plotTitle} pending on </span>
|
<span className="text-custom-text-200"> {plotTitle} pending on </span>
|
||||||
{datum.slice.points[0].data.xFormatted}
|
{datum.slice.points[0].data.xFormatted}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,10 @@ export const STATE_GROUPS: {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key];
|
export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key];
|
||||||
|
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
|
||||||
|
export const PENDING_STATE_GROUPS = [
|
||||||
|
STATE_GROUPS.backlog.key,
|
||||||
|
STATE_GROUPS.unstarted.key,
|
||||||
|
STATE_GROUPS.started.key,
|
||||||
|
STATE_GROUPS.cancelled.key,
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { ICycle, CycleDateCheckData, TCyclePlotType } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
|
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
|
||||||
import { getDate } from "@/helpers/date-time.helper";
|
import { getDate } from "@/helpers/date-time.helper";
|
||||||
|
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "@/services/cycle.service";
|
import { CycleService } from "@/services/cycle.service";
|
||||||
import { CycleArchiveService } from "@/services/cycle_archive.service";
|
import { CycleArchiveService } from "@/services/cycle_archive.service";
|
||||||
|
|
@ -44,6 +45,7 @@ export interface ICycleStore {
|
||||||
getProjectCycleIds: (projectId: string) => string[] | null;
|
getProjectCycleIds: (projectId: string) => string[] | null;
|
||||||
getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType;
|
getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType;
|
||||||
// actions
|
// actions
|
||||||
|
updateCycleDistribution: (distributionUpdates: DistributionUpdates, cycleId: string) => void;
|
||||||
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
||||||
setPlotType: (cycleId: string, plotType: TCyclePlotType) => void;
|
setPlotType: (cycleId: string, plotType: TCyclePlotType) => void;
|
||||||
// fetch
|
// fetch
|
||||||
|
|
@ -485,6 +487,22 @@ export class CycleStore implements ICycleStore {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method updates the cycle's stats locally without fetching the updated stats from backend
|
||||||
|
* @param distributionUpdates
|
||||||
|
* @param cycleId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
updateCycleDistribution = (distributionUpdates: DistributionUpdates, cycleId: string) => {
|
||||||
|
const cycle = this.cycleMap[cycleId];
|
||||||
|
|
||||||
|
if (!cycle) return;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
updateDistribution(cycle, distributionUpdates);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description creates a new cycle
|
* @description creates a new cycle
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface IProjectEstimateStore {
|
||||||
error: TErrorCodes | undefined;
|
error: TErrorCodes | undefined;
|
||||||
// computed
|
// computed
|
||||||
currentActiveEstimateId: string | undefined;
|
currentActiveEstimateId: string | undefined;
|
||||||
|
currentActiveEstimate: IEstimate | undefined;
|
||||||
archivedEstimateIds: string[] | undefined;
|
archivedEstimateIds: string[] | undefined;
|
||||||
areEstimateEnabledByProjectId: (projectId: string) => boolean;
|
areEstimateEnabledByProjectId: (projectId: string) => boolean;
|
||||||
estimateIdsByProjectId: (projectId: string) => string[] | undefined;
|
estimateIdsByProjectId: (projectId: string) => string[] | undefined;
|
||||||
|
|
@ -61,6 +62,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||||
error: observable,
|
error: observable,
|
||||||
// computed
|
// computed
|
||||||
currentActiveEstimateId: computed,
|
currentActiveEstimateId: computed,
|
||||||
|
currentActiveEstimate: computed,
|
||||||
archivedEstimateIds: computed,
|
archivedEstimateIds: computed,
|
||||||
// actions
|
// actions
|
||||||
getWorkspaceEstimates: action,
|
getWorkspaceEstimates: action,
|
||||||
|
|
@ -85,6 +87,20 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
|
||||||
return currentActiveEstimateId?.id ?? undefined;
|
return currentActiveEstimateId?.id ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computed
|
||||||
|
/**
|
||||||
|
* @description get current active estimate for a project
|
||||||
|
* @returns { string | undefined }
|
||||||
|
*/
|
||||||
|
get currentActiveEstimate(): IEstimate | undefined {
|
||||||
|
const { projectId } = this.store.router;
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
const currentActiveEstimate = Object.values(this.estimates || {}).find(
|
||||||
|
(p) => p.project === projectId && p.last_used
|
||||||
|
);
|
||||||
|
return currentActiveEstimate ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get all archived estimate ids for a project
|
* @description get all archived estimate ids for a project
|
||||||
* @returns { string[] | undefined }
|
* @returns { string[] | undefined }
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,9 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||||
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ import {
|
||||||
ViewFlags,
|
ViewFlags,
|
||||||
TBulkOperationsPayload,
|
TBulkOperationsPayload,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
// helpers
|
||||||
|
import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper";
|
||||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||||
|
//
|
||||||
import { IIssueRootStore } from "../root.store";
|
import { IIssueRootStore } from "../root.store";
|
||||||
import { ICycleIssuesFilter } from "./filter.store";
|
import { ICycleIssuesFilter } from "./filter.store";
|
||||||
|
|
||||||
|
|
@ -134,9 +137,23 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||||
*/
|
*/
|
||||||
fetchParentStats = (workspaceSlug: string, projectId?: string | undefined, id?: string | undefined) => {
|
fetchParentStats = (workspaceSlug: string, projectId?: string | undefined, id?: string | undefined) => {
|
||||||
const cycleId = id ?? this.cycleId;
|
const cycleId = id ?? this.cycleId;
|
||||||
|
|
||||||
projectId && cycleId && this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
projectId && cycleId && this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => {
|
||||||
|
const distributionUpdates = getDistributionPathsPostUpdate(
|
||||||
|
prevIssueState,
|
||||||
|
nextIssueState,
|
||||||
|
this.rootIssueStore.rootStore.state.stateMap,
|
||||||
|
this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById
|
||||||
|
);
|
||||||
|
|
||||||
|
const cycleId = id ?? this.cycleId;
|
||||||
|
|
||||||
|
cycleId && this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||||
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
// Abstract class to be implemented to fetch parent stats such as project, module or cycle details
|
// Abstract class to be implemented to fetch parent stats such as project, module or cycle details
|
||||||
abstract fetchParentStats: (workspaceSlug: string, projectId?: string, id?: string) => void;
|
abstract fetchParentStats: (workspaceSlug: string, projectId?: string, id?: string) => void;
|
||||||
|
|
||||||
|
abstract updateParentStats: (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string) => void;
|
||||||
|
|
||||||
// current Module Id from url
|
// current Module Id from url
|
||||||
get moduleId() {
|
get moduleId() {
|
||||||
return this.rootIssueStore.moduleId;
|
return this.rootIssueStore.moduleId;
|
||||||
|
|
@ -523,7 +525,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
this.addIssue(response, shouldUpdateList);
|
this.addIssue(response, shouldUpdateList);
|
||||||
|
|
||||||
// If shouldUpdateList is true, call fetchParentStats
|
// If shouldUpdateList is true, call fetchParentStats
|
||||||
shouldUpdateList && this.fetchParentStats(workspaceSlug, projectId);
|
shouldUpdateList && (await this.fetchParentStats(workspaceSlug, projectId));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -557,6 +559,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
// Check if should Sync
|
// Check if should Sync
|
||||||
if (!shouldSync) return;
|
if (!shouldSync) return;
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
this.updateParentStats(issueBeforeUpdate, {
|
||||||
|
...issueBeforeUpdate,
|
||||||
|
...data,
|
||||||
|
} as TIssue);
|
||||||
|
|
||||||
// call API to update the issue
|
// call API to update the issue
|
||||||
await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
|
||||||
|
|
@ -633,6 +641,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
*/
|
*/
|
||||||
async removeIssue(workspaceSlug: string, projectId: string, issueId: string) {
|
async removeIssue(workspaceSlug: string, projectId: string, issueId: string) {
|
||||||
try {
|
try {
|
||||||
|
// Store Before state of the issue
|
||||||
|
const issueBeforeRemoval = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
this.updateParentStats(issueBeforeRemoval, undefined);
|
||||||
|
|
||||||
// Male API call
|
// Male API call
|
||||||
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
|
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
|
@ -659,6 +673,11 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
*/
|
*/
|
||||||
async issueArchive(workspaceSlug: string, projectId: string, issueId: string) {
|
async issueArchive(workspaceSlug: string, projectId: string, issueId: string) {
|
||||||
try {
|
try {
|
||||||
|
const issueBeforeArchive = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
this.updateParentStats(issueBeforeArchive, undefined);
|
||||||
|
|
||||||
// Male API call
|
// Male API call
|
||||||
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
|
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
|
@ -872,11 +891,16 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
*/
|
*/
|
||||||
async removeIssueFromCycle(workspaceSlug: string, projectId: string, cycleId: string, issueId: string) {
|
async removeIssueFromCycle(workspaceSlug: string, projectId: string, cycleId: string, issueId: string) {
|
||||||
try {
|
try {
|
||||||
|
const issueBeforeRemoval = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
if (this.cycleId === cycleId) this.updateParentStats(issueBeforeRemoval, undefined, cycleId);
|
||||||
|
|
||||||
// Perform an APi call to remove issue from cycle
|
// Perform an APi call to remove issue from cycle
|
||||||
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
|
|
||||||
// if cycle Id is the current Cycle Id then call fetch parent stats
|
// if cycle Id is the current Cycle Id then call fetch parent stats
|
||||||
if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId);
|
if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// If cycle Id is the current cycle Id, then, remove issue from list of issueIds
|
// If cycle Id is the current cycle Id, then, remove issue from list of issueIds
|
||||||
|
|
@ -890,11 +914,21 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds cycle to issue optimistically
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param cycleId
|
||||||
|
* @param issueId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
||||||
|
|
||||||
if (issueCycleId === cycleId) return;
|
if (issueCycleId === cycleId) return;
|
||||||
|
|
||||||
|
const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update issueIds from current store
|
// Update issueIds from current store
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
@ -906,12 +940,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
this.issueUpdate(workspaceSlug, projectId, issueId, { cycle_id: cycleId }, false);
|
this.issueUpdate(workspaceSlug, projectId, issueId, { cycle_id: cycleId }, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const issueAfterUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
if (this.cycleId === cycleId || this.cycleId === issueCycleId)
|
||||||
|
this.updateParentStats(issueBeforeUpdate, issueAfterUpdate, this.cycleId);
|
||||||
|
|
||||||
await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
||||||
issues: [issueId],
|
issues: [issueId],
|
||||||
});
|
});
|
||||||
|
|
||||||
// if cycle Id is the current Cycle Id then call fetch parent stats
|
// if cycle Id is the current Cycle Id then call fetch parent stats
|
||||||
if (this.cycleId === cycleId) this.fetchParentStats(workspaceSlug, projectId);
|
if (this.cycleId === cycleId || this.cycleId === issueCycleId)
|
||||||
|
this.fetchParentStats(workspaceSlug, projectId, this.cycleId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// remove the new issue ids from the cycle issues map
|
// remove the new issue ids from the cycle issues map
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
@ -933,6 +974,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
removeCycleFromIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
removeCycleFromIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
const issueBeforeRemoval = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
||||||
if (!issueCycleId) return;
|
if (!issueCycleId) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -945,10 +987,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
this.issueUpdate(workspaceSlug, projectId, issueId, { cycle_id: null }, false);
|
this.issueUpdate(workspaceSlug, projectId, issueId, { cycle_id: null }, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
if (this.cycleId === issueCycleId) this.updateParentStats(issueBeforeRemoval, undefined, issueCycleId);
|
||||||
|
|
||||||
// make API call
|
// make API call
|
||||||
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, issueCycleId, issueId);
|
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, issueCycleId, issueId);
|
||||||
|
|
||||||
// if cycle Id is the current Cycle Id then call fetch parent stats
|
// if cycle Id is the current Cycle Id then call fetch parent stats
|
||||||
if (this.cycleId === issueCycleId) this.fetchParentStats(workspaceSlug, projectId);
|
if (this.cycleId === issueCycleId) this.fetchParentStats(workspaceSlug, projectId, issueCycleId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// revert back changes if fails
|
// revert back changes if fails
|
||||||
// Update issueIds from current store
|
// Update issueIds from current store
|
||||||
|
|
@ -1077,7 +1123,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moduleIds.includes(this.moduleId ?? "")) {
|
if (moduleIds.includes(this.moduleId ?? "")) {
|
||||||
this.fetchParentStats(workspaceSlug, projectId);
|
this.fetchParentStats(workspaceSlug, projectId, this.moduleId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -1100,6 +1146,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
removeModuleIds: string[]
|
removeModuleIds: string[]
|
||||||
) {
|
) {
|
||||||
// keep a copy of the original module ids
|
// keep a copy of the original module ids
|
||||||
|
const issueBeforeChanges = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
const originalModuleIds = get(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"]) ?? [];
|
const originalModuleIds = get(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"]) ?? [];
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
@ -1120,6 +1167,13 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
this.issueUpdate(workspaceSlug, projectId, issueId, { module_ids: currentModuleIds }, false);
|
this.issueUpdate(workspaceSlug, projectId, issueId, { module_ids: currentModuleIds }, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const issueAfterChanges = clone(this.rootIssueStore.issues.getIssueById(issueId));
|
||||||
|
|
||||||
|
// update parent stats optimistically
|
||||||
|
if (addModuleIds.includes(this.moduleId || "") || removeModuleIds.includes(this.moduleId || "")) {
|
||||||
|
this.updateParentStats(issueBeforeChanges, issueAfterChanges, this.moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
//Perform API call
|
//Perform API call
|
||||||
await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
|
await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
|
||||||
modules: addModuleIds,
|
modules: addModuleIds,
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
TIssuesResponse,
|
TIssuesResponse,
|
||||||
TBulkOperationsPayload,
|
TBulkOperationsPayload,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
// helpers
|
||||||
|
import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper";
|
||||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||||
// services
|
//
|
||||||
// types
|
|
||||||
// store
|
|
||||||
import { IIssueRootStore } from "../root.store";
|
import { IIssueRootStore } from "../root.store";
|
||||||
import { IModuleIssuesFilter } from "./filter.store";
|
import { IModuleIssuesFilter } from "./filter.store";
|
||||||
|
|
||||||
|
|
@ -91,6 +91,26 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||||
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Parent stats before fetching from server
|
||||||
|
* @param prevIssueState
|
||||||
|
* @param nextIssueState
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => {
|
||||||
|
// get distribution updates
|
||||||
|
const distributionUpdates = getDistributionPathsPostUpdate(
|
||||||
|
prevIssueState,
|
||||||
|
nextIssueState,
|
||||||
|
this.rootIssueStore.rootStore.state.stateMap,
|
||||||
|
this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById
|
||||||
|
);
|
||||||
|
|
||||||
|
const moduleId = id ?? this.moduleId;
|
||||||
|
|
||||||
|
moduleId && this.rootIssueStore.rootStore.module.updateModuleDistribution(distributionUpdates, moduleId);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||||
|
|
||||||
fetchParentStats = () => {};
|
fetchParentStats = () => {};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||||
|
|
||||||
fetchParentStats = async () => {};
|
fetchParentStats = async () => {};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||||
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
|
||||||
|
|
||||||
fetchParentStats = () => {};
|
fetchParentStats = () => {};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
updateParentStats = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called to fetch the first issues of pagination
|
* This method is called to fetch the first issues of pagination
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import { IModule, ILinkDetails, TModulePlotType } from "@plane/types";
|
import { IModule, ILinkDetails, TModulePlotType } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
|
||||||
import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
|
import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
|
||||||
// services
|
// services
|
||||||
import { ModuleService } from "@/services/module.service";
|
import { ModuleService } from "@/services/module.service";
|
||||||
|
|
@ -35,6 +36,7 @@ export interface IModuleStore {
|
||||||
// actions
|
// actions
|
||||||
setPlotType: (moduleId: string, plotType: TModulePlotType) => void;
|
setPlotType: (moduleId: string, plotType: TModulePlotType) => void;
|
||||||
// fetch
|
// fetch
|
||||||
|
updateModuleDistribution: (distributionUpdates: DistributionUpdates, moduleId: string) => void;
|
||||||
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
|
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
|
||||||
fetchModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
fetchModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
||||||
fetchArchivedModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
fetchArchivedModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
||||||
|
|
@ -319,6 +321,22 @@ export class ModulesStore implements IModuleStore {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method updates the module's stats locally without fetching the updated stats from backend
|
||||||
|
* @param distributionUpdates
|
||||||
|
* @param moduleId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
updateModuleDistribution = (distributionUpdates: DistributionUpdates, moduleId: string) => {
|
||||||
|
const moduleInfo = this.moduleMap[moduleId];
|
||||||
|
|
||||||
|
if (!moduleInfo) return;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
updateDistribution(moduleInfo, distributionUpdates);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetch module details
|
* @description fetch module details
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
282
web/helpers/distribution-update.helper.ts
Normal file
282
web/helpers/distribution-update.helper.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import get from "lodash/get";
|
||||||
|
import set from "lodash/set";
|
||||||
|
// types
|
||||||
|
import { ICycle, IEstimatePoint, IModule, IState, TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUPS, COMPLETED_STATE_GROUPS } from "@/constants/state";
|
||||||
|
// helper
|
||||||
|
import { getDate } from "./date-time.helper";
|
||||||
|
|
||||||
|
export type DistributionObjectUpdate = {
|
||||||
|
id: string;
|
||||||
|
completed_issues?: number;
|
||||||
|
pending_issues?: number;
|
||||||
|
total_issues: number;
|
||||||
|
completed_estimates?: number;
|
||||||
|
pending_estimates?: number;
|
||||||
|
total_estimates: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChartUpdates = {
|
||||||
|
updates: {
|
||||||
|
path: string[];
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
isCompleted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DistributionUpdates = {
|
||||||
|
pathUpdates: { path: string[]; value: number }[];
|
||||||
|
assigneeUpdates: DistributionObjectUpdate[];
|
||||||
|
labelUpdates: DistributionObjectUpdate[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATE_DISTRIBUTION = {
|
||||||
|
[STATE_GROUPS.backlog.key]: {
|
||||||
|
key: STATE_GROUPS.backlog.key,
|
||||||
|
issues: "backlog_issues",
|
||||||
|
points: "backlog_estimate_points",
|
||||||
|
},
|
||||||
|
[STATE_GROUPS.unstarted.key]: {
|
||||||
|
key: STATE_GROUPS.unstarted.key,
|
||||||
|
issues: "unstarted_issues",
|
||||||
|
points: "unstarted_estimate_points",
|
||||||
|
},
|
||||||
|
[STATE_GROUPS.started.key]: {
|
||||||
|
key: STATE_GROUPS.started.key,
|
||||||
|
issues: "started_issues",
|
||||||
|
points: "started_estimate_points",
|
||||||
|
},
|
||||||
|
[STATE_GROUPS.completed.key]: {
|
||||||
|
key: STATE_GROUPS.completed.key,
|
||||||
|
issues: "completed_issues",
|
||||||
|
points: "completed_estimate_points",
|
||||||
|
},
|
||||||
|
[STATE_GROUPS.cancelled.key]: {
|
||||||
|
key: STATE_GROUPS.cancelled.key,
|
||||||
|
issues: "cancelled_issues",
|
||||||
|
points: "cancelled_estimate_points",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Distribution updates with the help of previous and next issue states
|
||||||
|
* @param prevIssueState
|
||||||
|
* @param nextIssueState
|
||||||
|
* @param stateMap
|
||||||
|
* @param estimatePointById
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getDistributionPathsPostUpdate = (
|
||||||
|
prevIssueState: TIssue | undefined,
|
||||||
|
nextIssueState: TIssue | undefined,
|
||||||
|
stateMap: Record<string, IState>,
|
||||||
|
estimatePointById?: (estimatePointId: string) => IEstimatePoint | undefined
|
||||||
|
): DistributionUpdates => {
|
||||||
|
const prevIssueDistribution = getDistributionDataOfIssue(prevIssueState, -1, stateMap, estimatePointById);
|
||||||
|
const nextIssueDistribution = getDistributionDataOfIssue(nextIssueState, 1, stateMap, estimatePointById);
|
||||||
|
|
||||||
|
const prevChartDistribution = prevIssueDistribution.chartUpdates;
|
||||||
|
const nextChartDistribution = nextIssueDistribution.chartUpdates;
|
||||||
|
|
||||||
|
let chartUpdates: {
|
||||||
|
path: string[];
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
// if the completed status of chart updates are same the get chart updates from both the issue states
|
||||||
|
if (prevChartDistribution.isCompleted === nextChartDistribution.isCompleted) {
|
||||||
|
chartUpdates = [...prevChartDistribution.updates, ...nextChartDistribution.updates];
|
||||||
|
} // if not the get chart updates from only the next update
|
||||||
|
else {
|
||||||
|
chartUpdates = [...nextChartDistribution.updates];
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge the updates from both issue states into a single object
|
||||||
|
return {
|
||||||
|
pathUpdates: [...prevIssueDistribution.pathUpdates, ...nextIssueDistribution.pathUpdates, ...chartUpdates],
|
||||||
|
assigneeUpdates: [...prevIssueDistribution.assigneeUpdates, ...nextIssueDistribution.assigneeUpdates],
|
||||||
|
labelUpdates: [...prevIssueDistribution.labelUpdates, ...nextIssueDistribution.labelUpdates],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Distribution update for a single issue state
|
||||||
|
* @param issue
|
||||||
|
* @param multiplier
|
||||||
|
* @param stateMap
|
||||||
|
* @param estimatePointById
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getDistributionDataOfIssue = (
|
||||||
|
issue: TIssue | undefined,
|
||||||
|
multiplier: -1 | 1,
|
||||||
|
stateMap: Record<string, IState>,
|
||||||
|
estimatePointById?: (estimatePointId: string) => IEstimatePoint | undefined
|
||||||
|
): DistributionUpdates & { chartUpdates: ChartUpdates } => {
|
||||||
|
const pathUpdates: { path: string[]; value: number }[] = [];
|
||||||
|
|
||||||
|
// If issue does not exist, send a default object
|
||||||
|
if (!issue) return { pathUpdates, assigneeUpdates: [], labelUpdates: [], chartUpdates: { updates: [] } };
|
||||||
|
|
||||||
|
const state = stateMap[issue.state_id ?? ""];
|
||||||
|
const stateGroup = state.group;
|
||||||
|
|
||||||
|
// get if the state is in completed state
|
||||||
|
const isCompleted = COMPLETED_STATE_GROUPS.indexOf(stateGroup) > -1;
|
||||||
|
// get estimate point in number for the issue
|
||||||
|
const estimatePoint = parseFloat(estimatePointById?.(issue.estimate_point ?? "")?.value ?? "0");
|
||||||
|
|
||||||
|
// add all the path updates that can be updated directly on the distribution object
|
||||||
|
pathUpdates.push({ path: ["total_issues"], value: multiplier });
|
||||||
|
pathUpdates.push({ path: ["total_estimate_points"], value: multiplier * estimatePoint });
|
||||||
|
|
||||||
|
// path updates for state distributions
|
||||||
|
const stateDistribution = STATE_DISTRIBUTION[stateGroup];
|
||||||
|
|
||||||
|
pathUpdates.push({ path: [stateDistribution.issues], value: multiplier });
|
||||||
|
pathUpdates.push({ path: [stateDistribution.points], value: multiplier * estimatePoint });
|
||||||
|
|
||||||
|
// get assignee and label distribution updates
|
||||||
|
const assigneeUpdates = getObjectDistributionArray(issue.assignee_ids, isCompleted, estimatePoint, multiplier);
|
||||||
|
const labelUpdates = getObjectDistributionArray(issue.label_ids, isCompleted, estimatePoint, multiplier);
|
||||||
|
|
||||||
|
// chart updates based on date of completed or not completed
|
||||||
|
const chartUpdates = getChartUpdates(isCompleted, issue.completed_at, estimatePoint, multiplier);
|
||||||
|
return {
|
||||||
|
pathUpdates,
|
||||||
|
assigneeUpdates,
|
||||||
|
labelUpdates,
|
||||||
|
chartUpdates,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is to get distribution update array for either assignees and labels object
|
||||||
|
* @param ids the assignee or label ids of issue
|
||||||
|
* @param isCompleted
|
||||||
|
* @param estimatePoint
|
||||||
|
* @param multiplier
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getObjectDistributionArray = (ids: string[], isCompleted: boolean, estimatePoint: number, multiplier: -1 | 1) => {
|
||||||
|
const objectDistributionArray: DistributionObjectUpdate[] = [];
|
||||||
|
|
||||||
|
// iterate over each id
|
||||||
|
for (const id of ids) {
|
||||||
|
const objectDistribution: DistributionObjectUpdate = {
|
||||||
|
id,
|
||||||
|
total_issues: multiplier,
|
||||||
|
total_estimates: estimatePoint * multiplier,
|
||||||
|
};
|
||||||
|
|
||||||
|
// update paths for issue counts and estimate counts
|
||||||
|
if (isCompleted) {
|
||||||
|
objectDistribution["completed_issues"] = multiplier;
|
||||||
|
objectDistribution["completed_estimates"] = estimatePoint * multiplier;
|
||||||
|
} else {
|
||||||
|
objectDistribution["pending_issues"] = multiplier;
|
||||||
|
objectDistribution["pending_estimates"] = estimatePoint * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectDistributionArray.push(objectDistribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectDistributionArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get chart distribution based of completed or not completed states
|
||||||
|
* @param isCompleted
|
||||||
|
* @param completedAt
|
||||||
|
* @param estimatePoint
|
||||||
|
* @param multiplier
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getChartUpdates = (
|
||||||
|
isCompleted: boolean,
|
||||||
|
completedAt: string | null,
|
||||||
|
estimatePoint: number,
|
||||||
|
multiplier: -1 | 1
|
||||||
|
) => {
|
||||||
|
// if completed At date does not exist use current date
|
||||||
|
let dateToUpdate = format(new Date(), "yyyy-MM-dd");
|
||||||
|
const completedAtDate = getDate(completedAt);
|
||||||
|
if (completedAt && completedAtDate) {
|
||||||
|
dateToUpdate = format(completedAtDate, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiplier based on isCompleted state, it determines if the current count is to be added or subtracted from the list
|
||||||
|
const completedAtMultiplier = isCompleted ? -1 : 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
updates: [
|
||||||
|
{ path: ["distribution", "completion_chart", dateToUpdate], value: multiplier * completedAtMultiplier },
|
||||||
|
{
|
||||||
|
path: ["estimate_distribution", "completion_chart", dateToUpdate],
|
||||||
|
value: multiplier * completedAtMultiplier * estimatePoint,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isCompleted,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to update distribution of either cycle or module object
|
||||||
|
* @param distributionObject
|
||||||
|
* @param distributionUpdates
|
||||||
|
*/
|
||||||
|
export const updateDistribution = (distributionObject: ICycle | IModule, distributionUpdates: DistributionUpdates) => {
|
||||||
|
const { pathUpdates, assigneeUpdates, labelUpdates } = distributionUpdates;
|
||||||
|
|
||||||
|
// iterate over path updates and directly apply changes on the distribution object
|
||||||
|
for (const update of pathUpdates) {
|
||||||
|
const { path, value } = update;
|
||||||
|
const currentValue: number = get(distributionObject, path);
|
||||||
|
if (currentValue !== undefined) set(distributionObject, path, (currentValue ?? 0) + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for assignee update iterate through the assignee update and apply at the respective position
|
||||||
|
for (const assigneeUpdate of assigneeUpdates) {
|
||||||
|
const { id } = assigneeUpdate;
|
||||||
|
|
||||||
|
// find and update the assignee issue counts
|
||||||
|
const issuesAssignee = distributionObject.distribution?.assignees?.find((assignee) => assignee.assignee_id === id);
|
||||||
|
if (issuesAssignee) {
|
||||||
|
issuesAssignee.completed_issues += assigneeUpdate.completed_issues ?? 0;
|
||||||
|
issuesAssignee.pending_issues += assigneeUpdate.pending_issues ?? 0;
|
||||||
|
issuesAssignee.total_issues += assigneeUpdate.total_issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find and update the assignee points
|
||||||
|
const pointsAssignee = distributionObject.estimate_distribution?.assignees?.find(
|
||||||
|
(assignee) => assignee.assignee_id === id
|
||||||
|
);
|
||||||
|
if (pointsAssignee) {
|
||||||
|
pointsAssignee.completed_estimates += assigneeUpdate.completed_estimates ?? 0;
|
||||||
|
pointsAssignee.pending_estimates += assigneeUpdate.pending_estimates ?? 0;
|
||||||
|
pointsAssignee.total_estimates += assigneeUpdate.total_estimates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelUpdate of labelUpdates) {
|
||||||
|
const { id } = labelUpdate;
|
||||||
|
|
||||||
|
// find and update the label issue counts
|
||||||
|
const issuesLabel = distributionObject.distribution?.labels?.find((label) => label.label_id === id);
|
||||||
|
if (issuesLabel) {
|
||||||
|
issuesLabel.completed_issues += labelUpdate.completed_issues ?? 0;
|
||||||
|
issuesLabel.pending_issues += labelUpdate.pending_issues ?? 0;
|
||||||
|
issuesLabel.total_issues += labelUpdate.total_issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find and update the label points
|
||||||
|
const pointsLabel = distributionObject.estimate_distribution?.labels?.find((label) => label.label_id === id);
|
||||||
|
if (pointsLabel) {
|
||||||
|
pointsLabel.completed_estimates += labelUpdate.completed_estimates ?? 0;
|
||||||
|
pointsLabel.pending_estimates += labelUpdate.pending_estimates ?? 0;
|
||||||
|
pointsLabel.total_estimates += labelUpdate.total_estimates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue