Optimistically update distribution (#5290)

This commit is contained in:
rahulramesha 2024-08-04 10:14:25 +05:30 committed by GitHub
parent 8f8a97589d
commit 93e6c3b6e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 459 additions and 9 deletions

View file

@ -136,7 +136,7 @@ const ProgressChart: React.FC<Props> = ({
enableSlices="x"
sliceTooltip={(datum) => (
<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>
{datum.slice.points[0].data.xFormatted}
</div>

View file

@ -40,3 +40,10 @@ export const STATE_GROUPS: {
};
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,
];

View file

@ -8,6 +8,7 @@ import { ICycle, CycleDateCheckData, TCyclePlotType } from "@plane/types";
// helpers
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
import { getDate } from "@/helpers/date-time.helper";
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
// services
import { CycleService } from "@/services/cycle.service";
import { CycleArchiveService } from "@/services/cycle_archive.service";
@ -44,6 +45,7 @@ export interface ICycleStore {
getProjectCycleIds: (projectId: string) => string[] | null;
getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType;
// actions
updateCycleDistribution: (distributionUpdates: DistributionUpdates, cycleId: string) => void;
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
setPlotType: (cycleId: string, plotType: TCyclePlotType) => void;
// fetch
@ -485,6 +487,22 @@ export class CycleStore implements ICycleStore {
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
* @param workspaceSlug

View file

@ -26,6 +26,7 @@ export interface IProjectEstimateStore {
error: TErrorCodes | undefined;
// computed
currentActiveEstimateId: string | undefined;
currentActiveEstimate: IEstimate | undefined;
archivedEstimateIds: string[] | undefined;
areEstimateEnabledByProjectId: (projectId: string) => boolean;
estimateIdsByProjectId: (projectId: string) => string[] | undefined;
@ -61,6 +62,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
error: observable,
// computed
currentActiveEstimateId: computed,
currentActiveEstimate: computed,
archivedEstimateIds: computed,
// actions
getWorkspaceEstimates: action,
@ -85,6 +87,20 @@ export class ProjectEstimateStore implements IProjectEstimateStore {
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
* @returns { string[] | undefined }

View file

@ -73,6 +73,9 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -18,7 +18,10 @@ import {
ViewFlags,
TBulkOperationsPayload,
} from "@plane/types";
// helpers
import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
//
import { IIssueRootStore } from "../root.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) => {
const cycleId = id ?? this.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
* @param workspaceSlug

View file

@ -70,6 +70,9 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -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 fetchParentStats: (workspaceSlug: string, projectId?: string, id?: string) => void;
abstract updateParentStats: (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string) => void;
// current Module Id from url
get moduleId() {
return this.rootIssueStore.moduleId;
@ -523,7 +525,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
this.addIssue(response, shouldUpdateList);
// If shouldUpdateList is true, call fetchParentStats
shouldUpdateList && this.fetchParentStats(workspaceSlug, projectId);
shouldUpdateList && (await this.fetchParentStats(workspaceSlug, projectId));
return response;
} catch (error) {
@ -557,6 +559,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
// Check if should Sync
if (!shouldSync) return;
// update parent stats optimistically
this.updateParentStats(issueBeforeUpdate, {
...issueBeforeUpdate,
...data,
} as TIssue);
// call API to update the issue
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) {
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
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) {
try {
const issueBeforeArchive = clone(this.rootIssueStore.issues.getIssueById(issueId));
// update parent stats optimistically
this.updateParentStats(issueBeforeArchive, undefined);
// Male API call
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) {
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
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
// 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(() => {
// 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) => {
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
if (issueCycleId === cycleId) return;
const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
try {
// Update issueIds from current store
runInAction(() => {
@ -906,12 +940,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
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, {
issues: [issueId],
});
// 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) {
// remove the new issue ids from the cycle issues map
runInAction(() => {
@ -933,6 +974,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
* @returns
*/
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;
if (!issueCycleId) return;
try {
@ -945,10 +987,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
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
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, issueCycleId, issueId);
// 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) {
// revert back changes if fails
// Update issueIds from current store
@ -1077,7 +1123,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
});
if (moduleIds.includes(this.moduleId ?? "")) {
this.fetchParentStats(workspaceSlug, projectId);
this.fetchParentStats(workspaceSlug, projectId, this.moduleId);
}
} catch (error) {
throw error;
@ -1100,6 +1146,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
removeModuleIds: string[]
) {
// 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"]) ?? [];
try {
runInAction(() => {
@ -1120,6 +1167,13 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
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
await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
modules: addModuleIds,

View file

@ -8,10 +8,10 @@ import {
TIssuesResponse,
TBulkOperationsPayload,
} from "@plane/types";
// helpers
import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
// services
// types
// store
//
import { IIssueRootStore } from "../root.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);
};
/**
* 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
* @param workspaceSlug

View file

@ -91,6 +91,9 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
fetchParentStats = () => {};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -70,6 +70,9 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
fetchParentStats = async () => {};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -79,6 +79,9 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -76,6 +76,9 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
fetchParentStats = () => {};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug

View file

@ -7,6 +7,7 @@ import { computedFn } from "mobx-utils";
// types
import { IModule, ILinkDetails, TModulePlotType } from "@plane/types";
// helpers
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
// services
import { ModuleService } from "@/services/module.service";
@ -35,6 +36,7 @@ export interface IModuleStore {
// actions
setPlotType: (moduleId: string, plotType: TModulePlotType) => void;
// fetch
updateModuleDistribution: (distributionUpdates: DistributionUpdates, moduleId: string) => void;
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
fetchModules: (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;
});
/**
* 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
* @param workspaceSlug

View 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;
}
}
};