Skip to content

Commit

Permalink
Merge pull request #157 from bitovi/refactor/add-ts-to-derive-
Browse files Browse the repository at this point in the history
TR-71: Add TS to derive and derive/work-timing
  • Loading branch information
DavidNic11 authored Oct 2, 2024
2 parents c3f9a4f + bb0a74a commit 41924cb
Show file tree
Hide file tree
Showing 15 changed files with 1,055 additions and 577 deletions.
32 changes: 0 additions & 32 deletions public/jira/derived/derive.js

This file was deleted.

86 changes: 86 additions & 0 deletions public/jira/derived/derive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, Mock, test, vi } from "vitest";
import { deriveIssue, DerivedIssue } from "./derive";
import { NormalizedIssue } from "../shared/types";
import { deriveWorkTiming, DerivedWorkTiming } from "./work-timing/work-timing";
import { getWorkStatus, DerivedWorkStatus } from "./work-status/work-status";

vi.mock("./work-timing/work-timing.js", () => ({
deriveWorkTiming: vi.fn(),
}));

vi.mock("./work-status/work-status.js", () => ({
getWorkStatus: vi.fn(),
}));

const sampleNormalizedIssue = {
confidence: 80,
storyPoints: 40,
storyPointsMedian: 35,
startDate: new Date("2023-01-01"),
dueDate: new Date("2023-01-10"),
} as NormalizedIssue;

const sampleDerivedWorkTiming = {
isConfidenceValid: true,
usedConfidence: 80,
isStoryPointsValid: true,
defaultOrStoryPoints: 40,
storyPointsDaysOfWork: 4,
isStoryPointsMedianValid: true,
defaultOrStoryPointsMedian: 35,
storyPointsMedianDaysOfWork: 3.5,
deterministicExtraPoints: 5,
deterministicExtraDaysOfWork: 0.5,
deterministicTotalPoints: 40,
deterministicTotalDaysOfWork: 4,
probablisticExtraPoints: 4,
probablisticExtraDaysOfWork: 0.4,
probablisticTotalPoints: 39,
probablisticTotalDaysOfWork: 3.9,
hasStartAndDueDate: true,
startAndDueDateDaysOfWork: 9,
hasSprintStartAndEndDate: true,
sprintDaysOfWork: 7,
sprintStartData: {
start: new Date("2023-01-02"),
startFrom: { message: "Sprint 1", reference: sampleNormalizedIssue },
},
endSprintData: {
due: new Date("2023-01-09"),
dueTo: { message: "Sprint 1", reference: sampleNormalizedIssue },
},
start: new Date("2023-01-01"),
startFrom: { message: "start from", reference: sampleNormalizedIssue },
due: new Date("2023-01-10"),
dueTo: { message: "due to", reference: sampleNormalizedIssue },
totalDaysOfWork: 9,
defaultOrTotalDaysOfWork: 9,
completedDaysOfWork: 9,
} as DerivedWorkTiming;

const sampleDerivedWorkStatus: DerivedWorkStatus = {
statusType: "qa",
workType: "qa",
};

describe("derive", () => {
beforeEach(() => {
vi.resetAllMocks();
});

test("should correctly derive issue with valid data", () => {
(deriveWorkTiming as Mock).mockReturnValue(sampleDerivedWorkTiming);

(getWorkStatus as Mock).mockReturnValue(sampleDerivedWorkStatus);

const result: DerivedIssue = deriveIssue(sampleNormalizedIssue, {
uncertaintyWeight: 80,
});

expect(result).toEqual({
...sampleNormalizedIssue,
derivedTiming: sampleDerivedWorkTiming,
derivedStatus: sampleDerivedWorkStatus,
});
});
});
81 changes: 81 additions & 0 deletions public/jira/derived/derive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
DerivedWorkTiming,
deriveWorkTiming,
WorkTimingConfig,
} from "./work-timing/work-timing";
import {
DerivedWorkStatus,
getWorkStatus,
WorkStatusConfig,
} from "./work-status/work-status";
import { normalizeIssue, NormalizeIssueConfig } from "../normalized/normalize";
import { JiraIssue, NormalizedIssue } from "../shared/types";

/**
* @typedef {import("../shared/types.js").NormalizedIssue & {
* derivedTiming: import("./work-timing/work-timing.js").DerivedWorkTiming
* } & {derivedStatus: import("./work-status/work-status.ts").DerivedWorkStatus}} DerivedIssue
*/
export type DerivedIssue = NormalizedIssue & {
derivedTiming: DerivedWorkTiming;
derivedStatus: DerivedWorkStatus;
};

/**
* Adds derived data
* @param {NormalizedIssue} normalizedIssue
* @return {DerivedIssue}
*/
export function deriveIssue(
issue: NormalizedIssue,
options: Partial<WorkStatusConfig & WorkTimingConfig> & {
uncertaintyWeight?: number;
} = {}
): DerivedIssue {
const derivedTiming = deriveWorkTiming(issue, options);
const derivedStatus = getWorkStatus(issue, options);

return {
derivedTiming,
derivedStatus,
...issue,
};
}

/**
*
* @param {Array<JiraIssue>} issues
* @returns {Array<DerivedIssue>}
*/
export function normalizeAndDeriveIssues(
issues: Array<JiraIssue>,
options: Partial<
NormalizeIssueConfig & WorkStatusConfig & WorkTimingConfig
> & {
uncertaintyWeight?: number;
}
): DerivedIssue[] {
return issues.map((issue: JiraIssue) =>
deriveIssue(normalizeIssue(issue, options), options)
);
}

/**
*
* @param {DerivedIssue} derivedIssue
*/
export function derivedToCSVFormat(derivedIssue: DerivedIssue) {
return {
...derivedIssue.issue.fields,
changelog: derivedIssue.issue.changelog,
"Project key": derivedIssue.team.name,
"Issue key": derivedIssue.key,
url: derivedIssue.url,
"Issue Type": derivedIssue.type,
"Parent Link": derivedIssue.parentKey,
Status: derivedIssue.status,
workType: derivedIssue.derivedStatus.workType,
workingBusinessDays: derivedIssue.derivedTiming.totalDaysOfWork,
weightedEstimate: derivedIssue.derivedTiming.deterministicTotalPoints,
};
}
40 changes: 27 additions & 13 deletions public/jira/derived/work-status/work-status.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
import { expect, test } from "vitest";
import { getWorkStatus, statusCategoryMap, workType } from "./work-status.ts";
import { getWorkStatus, statusCategoryMap, workType } from "./work-status";
import { NormalizedIssue } from "../../shared/types";

const unrecognizedStatusTestCase = {
issue: { summary: "Some other summary", labels: ["other"], status: "UnknownStatus" },
issue: {
summary: "Some other summary",
labels: ["other"],
status: "UnknownStatus",
} as NormalizedIssue,
expected: { workType: "dev", statusType: "dev" },
description: "returns default workType 'dev' and statusType 'dev' when status is unrecognized",
description:
"returns default workType 'dev' and statusType 'dev' when status is unrecognized",
};

const summaryWithPrefix = workType.map((workType) => {
return {
issue: { summary: `${workType}: rest`, labels: [] },
issue: {
summary: `${workType}: rest`,
labels: ["other"],
} as NormalizedIssue,
expected: { workType, statusType: "dev" },
description: `workType with ${workType} summary prefix`,
};
});

const inLabels = workType.map((workType) => {
return {
issue: { summary: `${workType}: rest`, labels: [workType] },
issue: {
summary: `${workType}: rest`,
labels: [workType],
} as NormalizedIssue,
expected: { workType, statusType: "dev" },
description: `workType with ${workType} labels`,
};
});

const statuses = Object.entries(statusCategoryMap).map(([key, value]) => {
return {
issue: { status: key },
issue: { status: key } as NormalizedIssue,
expected: { statusType: value, workType: "dev" },
description: `statusType with status ${key}`,
};
});

test.each([unrecognizedStatusTestCase, ...summaryWithPrefix, ...inLabels, ...statuses])(
"getWorkStatus $description",
({ issue, expected }) => {
const result = getWorkStatus(issue);
expect(result).toEqual(expected);
}
);
test.each([
unrecognizedStatusTestCase,
...summaryWithPrefix,
...inLabels,
...statuses,
])("getWorkStatus $description", ({ issue, expected }) => {
const result = getWorkStatus(issue);
expect(result).toEqual(expected);
});
40 changes: 26 additions & 14 deletions public/jira/derived/work-status/work-status.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DefaultsToConfig, NormalizedIssue } from "../../shared/types";

/**
* This module is repsonsible for determining the correct workType ("design", "dev", "qa", "uat")
* and statusType ("qa", "uat", "todo", "done", "blocked") for an issue.
Expand Down Expand Up @@ -29,6 +31,11 @@ type Status =

type StatusCategory = "qa" | "uat" | "todo" | "done" | "blocked";

export type DerivedWorkStatus = {
statusType: StatusCategory | "dev";
workType: WorkType;
};

export const statusCategoryMap = (function () {
const items = [
["qa", inQAStatus],
Expand All @@ -50,8 +57,11 @@ export const statusCategoryMap = (function () {
return statusCategoryMap;
})();

export function getStatusTypeDefault(issue: { status?: string }): StatusCategory | "dev" {
const statusCategory = statusCategoryMap[(issue?.status || "").toLowerCase()];
export function getStatusTypeDefault(
normalizedIssue: NormalizedIssue
): StatusCategory | "dev" {
const statusCategory =
statusCategoryMap[(normalizedIssue?.status || "").toLowerCase()];
if (statusCategory) {
return statusCategory;
} else {
Expand All @@ -61,14 +71,18 @@ export function getStatusTypeDefault(issue: { status?: string }): StatusCategory

const workPrefix = workType.map((wt) => wt + ":");

function getWorkTypeDefault(normalizedIssue: { summary?: string; labels?: string[] }): WorkType {
let wp = workPrefix.find((wp) => (normalizedIssue?.summary || "").toLowerCase().indexOf(wp) === 0);
function getWorkTypeDefault(normalizedIssue: NormalizedIssue): WorkType {
let wp = workPrefix.find(
(wp) => (normalizedIssue?.summary || "").toLowerCase().indexOf(wp) === 0
);

if (wp) {
return wp.slice(0, -1) as WorkType;
}

wp = workType.find((wt) => normalizedIssue.labels?.map((label) => label.toLowerCase()).includes(wt));
wp = workType.find((wt) =>
normalizedIssue.labels?.map((label) => label.toLowerCase()).includes(wt)
);

if (wp) {
return wp as WorkType;
Expand All @@ -82,17 +96,15 @@ const defaults = {
getStatusTypeDefault,
};

// TODO: See if other files beside normalize need this and if they do pull it out
type DefaultsToConfig<T> = {
[K in keyof T as K extends `${infer FnName}Default` ? FnName : never]: T[K];
};

type WorkStatusConfig = DefaultsToConfig<typeof defaults>;
export type WorkStatusConfig = DefaultsToConfig<typeof defaults>;

export function getWorkStatus(
normalizedIssue: { summary?: string; labels?: string[]; status?: string },
{ getStatusType = getStatusTypeDefault, getWorkType = getWorkTypeDefault }: Partial<WorkStatusConfig> = {}
) {
normalizedIssue: NormalizedIssue,
{
getStatusType = getStatusTypeDefault,
getWorkType = getWorkTypeDefault,
}: Partial<WorkStatusConfig> = {}
): DerivedWorkStatus {
return {
statusType: getStatusType(normalizedIssue),
workType: getWorkType(normalizedIssue),
Expand Down
Loading

0 comments on commit 41924cb

Please sign in to comment.