Skip to content

Commit

Permalink
Merge pull request #195 from bitovi/TR-119-team-all-issues-settings
Browse files Browse the repository at this point in the history
Tr 119 team all issues settings
  • Loading branch information
binaryberserker authored Oct 22, 2024
2 parents d978d61 + 137c250 commit ef2a916
Show file tree
Hide file tree
Showing 36 changed files with 839 additions and 484 deletions.
135 changes: 66 additions & 69 deletions public/jira/derived/work-timing/work-timing.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { getBusinessDatesCount } from "../../../status-helpers";
import {
estimateExtraPoints,
sampleExtraPoints,
} from "../../../shared/confidence";
import { estimateExtraPoints, sampleExtraPoints } from "../../../shared/confidence";
import {
DueData,
getStartDateAndDueDataFromFieldsOrSprints,
getStartDateAndDueDataFromSprints,
StartData,
} from "../../../shared/issue-data/date-data";
import {
DefaultsToConfig,
NormalizedIssue,
NormalizedTeam,
} from "../../shared/types";
import { DefaultsToConfig, NormalizedIssue, NormalizedTeam } from "../../shared/types";

export type DerivedWorkTiming = {
isConfidenceValid: boolean;
Expand Down Expand Up @@ -83,91 +76,66 @@ export function deriveWorkTiming(
uncertaintyWeight = 80,
}: Partial<WorkTimingConfig> & { uncertaintyWeight?: number } = {}
): DerivedWorkTiming {

const isConfidenceValid = isConfidenceValueValid(normalizedIssue.confidence);

const usedConfidence = isConfidenceValid
? normalizedIssue.confidence!
: getDefaultConfidence(normalizedIssue.team);

const isStoryPointsValid = isStoryPointsValueValid(
normalizedIssue.storyPoints
);

const usedConfidence = isConfidenceValid ? normalizedIssue.confidence! : getDefaultConfidence(normalizedIssue.team);

const isStoryPointsValid = isStoryPointsValueValid(normalizedIssue.storyPoints);
const defaultOrStoryPoints = isStoryPointsValid
? normalizedIssue.storyPoints!
: getDefaultStoryPoints(normalizedIssue.team);

const storyPointsDaysOfWork =
defaultOrStoryPoints / normalizedIssue.team.pointsPerDayPerTrack;

const isStoryPointsMedianValid = isStoryPointsValueValid(
normalizedIssue.storyPointsMedian
);

const storyPointsDaysOfWork = defaultOrStoryPoints / normalizedIssue.team.pointsPerDayPerTrack;

const isStoryPointsMedianValid = isStoryPointsValueValid(normalizedIssue.storyPointsMedian);

const defaultOrStoryPointsMedian = isStoryPointsMedianValid
? normalizedIssue.storyPointsMedian!
: getDefaultStoryPoints(normalizedIssue.team);

const storyPointsMedianDaysOfWork =
defaultOrStoryPointsMedian / normalizedIssue.team.pointsPerDayPerTrack;
const deterministicExtraPoints = estimateExtraPoints(
defaultOrStoryPointsMedian,
usedConfidence,
uncertaintyWeight
);
const deterministicExtraDaysOfWork =
deterministicExtraPoints / normalizedIssue.team.pointsPerDayPerTrack;
const deterministicTotalPoints =
defaultOrStoryPointsMedian + deterministicExtraPoints;
const deterministicTotalDaysOfWork =
deterministicTotalPoints / normalizedIssue.team.pointsPerDayPerTrack;
const probablisticExtraPoints = sampleExtraPoints(
defaultOrStoryPointsMedian,
usedConfidence
);
const probablisticExtraDaysOfWork =
probablisticExtraPoints / normalizedIssue.team.pointsPerDayPerTrack;
const probablisticTotalPoints =
defaultOrStoryPointsMedian + probablisticExtraPoints;
const probablisticTotalDaysOfWork =
probablisticTotalPoints / normalizedIssue.team.pointsPerDayPerTrack;
const hasStartAndDueDate = Boolean(
normalizedIssue.dueDate && normalizedIssue.startDate
);
const storyPointsMedianDaysOfWork = defaultOrStoryPointsMedian / normalizedIssue.team.pointsPerDayPerTrack;
const deterministicExtraPoints = estimateExtraPoints(defaultOrStoryPointsMedian, usedConfidence, uncertaintyWeight);
const deterministicExtraDaysOfWork = deterministicExtraPoints / normalizedIssue.team.pointsPerDayPerTrack;
const deterministicTotalPoints = defaultOrStoryPointsMedian + deterministicExtraPoints;
const deterministicTotalDaysOfWork = deterministicTotalPoints / normalizedIssue.team.pointsPerDayPerTrack;

const probablisticExtraPoints = sampleExtraPoints(defaultOrStoryPointsMedian, usedConfidence);
const probablisticExtraDaysOfWork = probablisticExtraPoints / normalizedIssue.team.pointsPerDayPerTrack;
const probablisticTotalPoints = defaultOrStoryPointsMedian + probablisticExtraPoints;
const probablisticTotalDaysOfWork = probablisticTotalPoints / normalizedIssue.team.pointsPerDayPerTrack;
const hasStartAndDueDate = Boolean(normalizedIssue.dueDate && normalizedIssue.startDate);
const startAndDueDateDaysOfWork = hasStartAndDueDate
? getBusinessDatesCount(normalizedIssue.startDate, normalizedIssue.dueDate)
: null;

const { startData: sprintStartData, dueData: endSprintData } =
getStartDateAndDueDataFromSprints(normalizedIssue);
const { startData: sprintStartData, dueData: endSprintData } = getStartDateAndDueDataFromSprints(normalizedIssue);
const hasSprintStartAndEndDate = Boolean(sprintStartData && endSprintData);
let sprintDaysOfWork = hasSprintStartAndEndDate
? getBusinessDatesCount(sprintStartData?.start, endSprintData?.due)
: null;

const { startData, dueData } =
getStartDateAndDueDataFromFieldsOrSprints(normalizedIssue);
const { startData, dueData } = getStartDateAndDueDataFromFieldsOrSprints(normalizedIssue);

const datesDaysOfWork = startData && dueData ? getBusinessDatesCount(startData.start, dueData.due) : null;

let totalDaysOfWork = null;
if (datesDaysOfWork != null) {
if (!normalizedIssue.team.spreadEffortAcrossDates && datesDaysOfWork != null) {
totalDaysOfWork = datesDaysOfWork;
} else if (isStoryPointsMedianValid) {
totalDaysOfWork = deterministicTotalDaysOfWork;
} else if (isStoryPointsValid) {
totalDaysOfWork = storyPointsDaysOfWork;
}

// defaultOrTotalDaysOfWork - will be 50% confidence of 1 sprint of work
// defaultOrTotalDaysOfWork - will be 50% confidence of 1 sprint of work
// Used if there is no estimate. I don't think we need or should use this value.
const defaultOrTotalDaysOfWork =
totalDaysOfWork !== null ? totalDaysOfWork : deterministicTotalDaysOfWork;
const defaultOrTotalDaysOfWork = totalDaysOfWork !== null ? totalDaysOfWork : deterministicTotalDaysOfWork;

const completedDaysOfWork = getSelfCompletedDays(
startData,
dueData,
totalDaysOfWork
totalDaysOfWork || 0,
normalizedIssue.team.spreadEffortAcrossDates
);

return {
Expand Down Expand Up @@ -228,21 +196,50 @@ export function isStoryPointsValueValid(value: number | null): value is number {
function getSelfCompletedDays(
startData: StartData,
dueData: DueData,
daysOfWork: number | null
daysOfWork: number,
isSpreading: boolean
): number {
// These are cases where the child issue (Epic) has a valid estimation
// starting in the future
if (startData && startData.start >= new Date()) {
return 0;
}
// ending in the past
else if (dueData && dueData.due <= new Date()) {
// should this code be removed?
if (!isSpreading && startData && startData.start) {
const completedDays = getBusinessDatesCount(startData.start, dueData.due);
if (completedDays !== daysOfWork) {
console.warn("completed days should match days of work");
}
}
return daysOfWork;
}
// no dates to help
else if (!startData && !dueData) {
return 0;
}
// ending in the future with no start date
else if (!startData && dueData && dueData.due > new Date()) {
return 0;
}
// starting date in the past with no due date
else if (!dueData && startData && startData.start < new Date()) {
const completedDays = getBusinessDatesCount(startData.start, new Date());
return Math.min(completedDays, daysOfWork);
}
// now is in between start and end date
else if (startData && startData.start < new Date() && dueData && dueData.due > new Date()) {
if (isSpreading) {
const completedTimedDays = getBusinessDatesCount(startData.start, new Date());
const totalTimedDays = getBusinessDatesCount(startData.start, dueData.due);

if (startData && startData.start < new Date()) {
if (!dueData || dueData.due > new Date()) {
return getBusinessDatesCount(startData.start, new Date());
return (daysOfWork * completedTimedDays) / totalTimedDays;
} else {
return getBusinessDatesCount(startData.start, dueData.due);
return getBusinessDatesCount(startData.start, new Date());
}
}
// if there's an end date in the past ...
else if (dueData && dueData.due < new Date()) {
return daysOfWork || 0;
} else {
console.warn("we should never get here");
return 0;
}
}
10 changes: 6 additions & 4 deletions public/jira/normalized/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function getUrlDefault({ key }: Pick<JiraIssue, "key">): NormalizedIssue[
}

export function getTeamKeyDefault({ key, fields }: Pick<JiraIssue, "key" | "fields">): NormalizedIssue["team"]["name"] {
if(fields.Team?.name) {
if (fields.Team?.name) {
return fields.Team.name;
}
return key.replace(/-.*/, "");
Expand Down Expand Up @@ -122,8 +122,8 @@ export function getReleasesDefault({ fields }: Fields): NormalizedIssue["release
return [];
}
// Rollback is getting this property wrong and not always making it an array
if(!Array.isArray(fixVersions)) {
fixVersions = [fixVersions]
if (!Array.isArray(fixVersions)) {
fixVersions = [fixVersions];
}

return fixVersions.map(({ name, id }) => {
Expand All @@ -149,6 +149,8 @@ export function getDaysPerSprintDefault(teamKey: string): NormalizedIssue["team"
return 10;
}

export function getTeamSpreadsEffortAcrossDatesDefault(): NormalizedIssue["team"]["spreadEffortAcrossDates"] {
export function getTeamSpreadsEffortAcrossDatesDefault(
teamKey?: string
): NormalizedIssue["team"]["spreadEffortAcrossDates"] {
return false;
}
2 changes: 1 addition & 1 deletion public/jira/normalized/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function normalizeIssue(
parallelWorkLimit,
totalPointsPerDay,
pointsPerDayPerTrack,
spreadEffortAcrossDates: getTeamSpreadsEffortAcrossDates(),
spreadEffortAcrossDates: getTeamSpreadsEffortAcrossDates(teamName),
},
url: getUrl(issue),
status: getStatus(issue),
Expand Down
24 changes: 14 additions & 10 deletions public/react/Configure/ConfigurationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SidebarButton from "../components/SidebarButton";
import ConfigureAllTeams from "./components/Teams/ConfigureAllTeams";
import { useJiraIssueFields } from "./services/jira";
import { useAllTeamData } from "./components/Teams/services/team-configuration";
import { StorageCheck } from "./services/storage";

export interface ConfigurationPanelProps {
onBackButtonClicked: () => void;
Expand Down Expand Up @@ -44,16 +45,19 @@ const ConfigurationPanel: FC<ConfigurationPanelProps> = ({
derivedIssuesObservable={derivedIssuesObservable}
/>
</div>
{selectedTeam === "global" && (
<div className="w-96">
<ConfigureAllTeams jiraFields={jiraFields} {...configurationProps} />
</div>
)}
{!!selectedTeam && selectedTeam !== "global" && (
<div className="w-96">
<ConfigureTeams teamName={selectedTeam} jiraFields={jiraFields} {...configurationProps} />
</div>
)}
{/* checks that configuration issue exists */}
<StorageCheck>
{selectedTeam === "global" && (
<div className="w-128">
<ConfigureAllTeams jiraFields={jiraFields} {...configurationProps} />
</div>
)}
{!!selectedTeam && selectedTeam !== "global" && (
<div className="w-128">
<ConfigureTeams teamName={selectedTeam} jiraFields={jiraFields} {...configurationProps} />
</div>
)}
</StorageCheck>
</div>
);
};
Expand Down
25 changes: 5 additions & 20 deletions public/react/Configure/components/Teams/ConfigureAllTeams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSaveTeamData, useTeamData } from "./services/team-configuration";

import ConfigureTeamsForm from "./ConfigureTeamsForm";
import AllTeamsDefaultForm from "./AllTeamsDefaultsForm";
import ConfigureTeams from "./ConfigureTeams";

export interface ConfigureAllTeamsProps {
onUpdate?: (overrides: Partial<NormalizeIssueConfig>) => void;
Expand All @@ -26,16 +27,16 @@ const issueNameMapping: Record<keyof TeamConfiguration, string> = {
stories: "Stores",
};

const ConfigureAllTeams: FC<ConfigureAllTeamsProps> = ({ jiraFields, ...props }) => {
const ConfigureAllTeams: FC<ConfigureAllTeamsProps> = ({ jiraFields, onUpdate, ...props }) => {
const { userTeamData, augmentedTeamData } = useTeamData("__GLOBAL__", jiraFields);

const { save, isSaving } = useSaveTeamData({ teamName: "__GLOBAL__", issueType: "defaults" });
const { save, isSaving } = useSaveTeamData({ teamName: "__GLOBAL__", issueType: "defaults", onUpdate });

return (
<>
<Accordion startsOpen>
<AccordionTitle>
<Heading size="small">Global defaults </Heading>
<Heading size="small">Global defaults</Heading>
{isSaving && (
<div>
<Spinner size="small" label="saving" />
Expand All @@ -52,23 +53,7 @@ const ConfigureAllTeams: FC<ConfigureAllTeamsProps> = ({ jiraFields, ...props })
/>
</AccordionContent>
</Accordion>
{Object.keys(augmentedTeamData)
.filter((issueType) => issueType !== "defaults")
.map((key) => (
<Accordion key={key}>
<AccordionTitle>
<Heading size="small">{issueNameMapping[key as keyof TeamConfiguration]}</Heading>
</AccordionTitle>
<AccordionContent>
<ConfigureTeamsForm
jiraFields={jiraFields}
userData={userTeamData[key as keyof TeamConfiguration]}
augmented={augmentedTeamData[key as keyof TeamConfiguration]}
{...props}
/>
</AccordionContent>
</Accordion>
))}
<ConfigureTeams jiraFields={jiraFields} teamName="__GLOBAL__" onUpdate={onUpdate} />
</>
);
};
Expand Down
Loading

0 comments on commit ef2a916

Please sign in to comment.