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"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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