Skip to content

Commit

Permalink
Support custom status in every story points accumulation (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximilianruesch authored Jan 17, 2024
1 parent 5e90ba1 commit ad8aff2
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 72 deletions.
6 changes: 4 additions & 2 deletions src/components/BacklogView/Issue/IssueCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { StoryPointsBadge } from "../../common/StoryPoints/StoryPointsBadge";
import { useColorScheme } from "../../../common/color-scheme";
import { IssueLabelBadge } from "../../common/IssueLabelBadge";
import { IssueEpicBadge } from "../../common/IssueEpicBadge";
import {useCanvasStore} from "../../../lib/Store";

export function IssueCard({
issueKey,
Expand All @@ -40,6 +41,7 @@ export function IssueCard({
const queryClient = useQueryClient()
const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { issueStatusCategoryByStatusName: statusNameToCategory } = useCanvasStore();

const hoverStyles =
colorScheme === "dark"
Expand Down Expand Up @@ -93,7 +95,7 @@ export function IssueCard({
size="sm"
mr={5}
c="blue"
td={status === StatusType.DONE ? "line-through" : "none"}
td={statusNameToCategory[status] === StatusType.DONE ? "line-through" : "none"}
style={{
":hover": {
textDecoration: "underline",
Expand Down Expand Up @@ -151,7 +153,7 @@ export function IssueCard({
<Grid.Col span={3}>
<Box style={{ alignSelf: "flex-start" }}>
{storyPointsEstimate &&
<StoryPointsBadge statusType={status as StatusType} storyPointsEstimate={storyPointsEstimate} />}
<StoryPointsBadge statusType={statusNameToCategory[status]} storyPointsEstimate={storyPointsEstimate} />}
</Box>
</Grid.Col>
</Grid>
Expand Down
13 changes: 9 additions & 4 deletions src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Issue, Sprint } from "types"
import {
pluralize,
sortSprintsByActive,
storyPointsAccumulator,
} from "../helpers/backlogHelpers"
import { DraggableIssuesWrapper } from "./DraggableIssuesWrapper"
import {StatusType} from "../../../../types/status";
import {StoryPointsBadge} from "../../common/StoryPoints/StoryPointsBadge";
import {useColorScheme} from "../../../common/color-scheme";
import {storyPointsAccumulator} from "../../common/StoryPoints/status-accumulator";
import {useCanvasStore} from "../../../lib/Store";

export function SprintsPanel({
sprintsWithIssues,
Expand Down Expand Up @@ -70,6 +71,10 @@ function SprintAccordionControl({
issues: Issue[]
sprint: Sprint
}) {
const { issueStatusByCategory } = useCanvasStore();

const getStatusNamesInCategory = (category: StatusType) => issueStatusByCategory[category]?.map((s) => (s.name)) ?? []

return (
<Accordion.Control>
<Group>
Expand All @@ -83,15 +88,15 @@ function SprintAccordionControl({
<Flex gap={4} p="xs" ml="auto">
<StoryPointsBadge
statusType={StatusType.TODO}
storyPointsEstimate={storyPointsAccumulator(issues, StatusType.TODO)}
storyPointsEstimate={storyPointsAccumulator(issues, getStatusNamesInCategory(StatusType.TODO))}
/>
<StoryPointsBadge
statusType={StatusType.IN_PROGRESS}
storyPointsEstimate={storyPointsAccumulator(issues, StatusType.IN_PROGRESS)}
storyPointsEstimate={storyPointsAccumulator(issues, getStatusNamesInCategory(StatusType.IN_PROGRESS))}
/>
<StoryPointsBadge
statusType={StatusType.DONE}
storyPointsEstimate={storyPointsAccumulator(issues, StatusType.DONE)}
storyPointsEstimate={storyPointsAccumulator(issues, getStatusNamesInCategory(StatusType.DONE))}
/>
</Flex>
</Group>
Expand Down
9 changes: 0 additions & 9 deletions src/components/BacklogView/helpers/backlogHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { Issue, Sprint } from "types"
import { Dispatch, SetStateAction } from "react"
import { StatusType } from "../../../../types/status";

export const storyPointsAccumulator = (issues: Issue[], status: StatusType) =>
issues.reduce((accumulator, currentValue) => {
if (currentValue.storyPointsEstimate && currentValue.status === status) {
return accumulator + currentValue.storyPointsEstimate
}
return accumulator
}, 0)

export const pluralize = (count: number, noun: string, suffix = "s") =>
`${count} ${noun}${count !== 1 ? suffix : ""}`
Expand Down
36 changes: 7 additions & 29 deletions src/components/CreateExport/CreateExportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react"
import { Modal, Stack, Group, Text, Button, Tooltip, Paper, ActionIcon } from "@mantine/core"
import { uniqWith, sortBy } from "lodash";
import { sortBy } from "lodash";
import { useQuery } from "@tanstack/react-query";
import { IconInfoCircle } from "@tabler/icons-react";
import { useCanvasStore } from "../../lib/Store";
Expand All @@ -17,7 +17,7 @@ export function CreateExportModal({
opened: boolean
setOpened: Dispatch<SetStateAction<boolean>>
}) {
const project = useCanvasStore((state) => state.selectedProject);
const { selectedProject: project, issueStatusByCategory, issueTypes, issueStatus } = useCanvasStore();
const boardId = useCanvasStore((state) => state.selectedProjectBoardIds)[0]

const { data: issues } = useQuery<unknown, unknown, Issue[]>({
Expand All @@ -27,29 +27,7 @@ export function CreateExportModal({
initialData: [],
});

const { data: issueTypes } = useQuery({
queryKey: ["issueTypes", project?.key],
queryFn: () => project && window.provider.getIssueTypesByProject(project.key),
enabled: !!project?.key,
initialData: [],
});

const allStatus = sortBy(
uniqWith(
issueTypes?.flatMap((issueType) => issueType.statuses ?? []),
(statusA, statusB) => statusA.id === statusB.id,
),
[
(status) => Object.values(StatusType).indexOf(status.statusCategory.name as StatusType),
'name',
],
)

const allStatusNamesByCategory: { [key: string]: string[] } = {};
allStatus.forEach((status) => {
allStatusNamesByCategory[status.statusCategory.name] ??= [];
allStatusNamesByCategory[status.statusCategory.name].push(status.name);
});
const doneStatusNames = issueStatusByCategory[StatusType.DONE]?.map((s) => s.name) ?? []

const [includedIssueTypes, setIncludedIssueTypes] = useState<string[]>([]);
const [includedIssueStatus, setIncludedIssueStatus] = useState<string[]>([]);
Expand All @@ -61,7 +39,7 @@ export function CreateExportModal({
issues
.filter((issue) => includedIssueTypes.includes(issue.type))
.filter((issue) => includedIssueStatus.includes(issue.status)
&& allStatusNamesByCategory[StatusType.DONE].includes(issue.status)),
&& doneStatusNames.includes(issue.status)),
['issueKey']
)
);
Expand Down Expand Up @@ -126,9 +104,9 @@ export function CreateExportModal({
</Stack>
<Stack align="center">
<Text size="md" fw={450} mt="7%" mb="10%">Include Issue Status</Text>
{allStatus && (
{issueStatus && (
<CheckboxStack
data={allStatus.map((status) => ({
data={issueStatus.map((status) => ({
value: status.name,
label: status.name,
}))}
Expand All @@ -145,7 +123,7 @@ export function CreateExportModal({
<Button
ml="auto"
size="sm"
onClick={() => { exportIssues(issuesToExport, allStatus) }}
onClick={() => { exportIssues(issuesToExport, issueStatus) }}
>
Export CSV
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { StoryPointsBadge } from "../../../common/StoryPoints/StoryPointsBadge";
import { useColorScheme } from "../../../../common/color-scheme";
import { IssueEpicBadge } from "../../../common/IssueEpicBadge";
import { IssueLabelBadge } from "../../../common/IssueLabelBadge";
import {useCanvasStore} from "../../../../lib/Store";

export function ChildIssueCard({
issueKey,
Expand All @@ -42,6 +43,7 @@ export function ChildIssueCard({
const { hovered } = useHover()
const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { issueStatusCategoryByStatusName: statusNameToCategory } = useCanvasStore();

const hoverStyles =
colorScheme === "dark"
Expand Down Expand Up @@ -88,7 +90,7 @@ export function ChildIssueCard({
size="sm"
mr={5}
c="blue"
td={status === StatusType.DONE ? "line-through" : "none"}
td={statusNameToCategory[status] === StatusType.DONE ? "line-through" : "none"}
style={{
":hover": {
textDecoration: "underline",
Expand Down Expand Up @@ -119,7 +121,7 @@ export function ChildIssueCard({
>
<Box style={{ alignSelf: "flex-start" }}>
{storyPointsEstimate &&
<StoryPointsBadge statusType={status as StatusType} storyPointsEstimate={storyPointsEstimate} />}
<StoryPointsBadge statusType={statusNameToCategory[status]} storyPointsEstimate={storyPointsEstimate} />}
</Box>
</Grid.Col>
<Grid.Col
Expand Down
23 changes: 14 additions & 9 deletions src/components/EpicDetailView/EpicDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import { sortIssuesByRank } from "../BacklogView/helpers/backlogHelpers"
import { useCanvasStore } from "../../lib/Store"
import { resizeDivider } from "../BacklogView/helpers/resizeDivider"
import {
inProgressAccumulator,
issueCountAccumulator,
storyPointsAccumulator,
} from "./helpers/storyPointsHelper"
} from "../common/StoryPoints/status-accumulator"
import { StoryPointsHoverCard } from "../common/StoryPoints/StoryPointsHoverCard";
import { CommentSection } from "../DetailView/Components/CommentSection";
import { StatusType } from "../../../types/status";
Expand Down Expand Up @@ -90,8 +90,8 @@ export function EpicDetailView({
timeStyle: "short",
})

const { issueStatusByCategory, selectedProjectBoardIds: boardIds } = useCanvasStore();
const projectKey = useCanvasStore((state) => state.selectedProject?.key)
const boardIds = useCanvasStore((state) => state.selectedProjectBoardIds)
const currentBoardId = boardIds[0]

const [childIssues, setChildIssues] = useState<Issue[]>([])
Expand All @@ -110,9 +110,14 @@ export function EpicDetailView({
},
})

const tasksTodo = inProgressAccumulator(childIssues, StatusType.TODO)
const tasksInProgress = inProgressAccumulator(childIssues, StatusType.IN_PROGRESS)
const tasksDone = inProgressAccumulator(childIssues, StatusType.DONE)
const getStatusNamesInCategory = (category: StatusType) => issueStatusByCategory[category]?.map((s) => (s.name)) ?? []
const validTodoStatus = getStatusNamesInCategory(StatusType.TODO)
const validInProgressStatus = getStatusNamesInCategory(StatusType.IN_PROGRESS)
const validDoneStatus = getStatusNamesInCategory(StatusType.DONE)

const tasksTodo = issueCountAccumulator(childIssues, validTodoStatus)
const tasksInProgress = issueCountAccumulator(childIssues, validInProgressStatus)
const tasksDone = issueCountAccumulator(childIssues, validDoneStatus)
const totalTaskCount = tasksTodo + tasksInProgress + tasksDone

useEffect(() => {
Expand Down Expand Up @@ -196,15 +201,15 @@ export function EpicDetailView({
</Progress.Root>
<StoryPointsHoverCard
statusType={StatusType.TODO}
count={storyPointsAccumulator(childIssues, StatusType.TODO)}
count={storyPointsAccumulator(childIssues, validTodoStatus)}
/>
<StoryPointsHoverCard
statusType={StatusType.IN_PROGRESS}
count={storyPointsAccumulator(childIssues, StatusType.IN_PROGRESS)}
count={storyPointsAccumulator(childIssues, validInProgressStatus)}
/>
<StoryPointsHoverCard
statusType={StatusType.DONE}
count={storyPointsAccumulator(childIssues, StatusType.DONE)}
count={storyPointsAccumulator(childIssues, validDoneStatus)}
/>
</Group>

Expand Down
14 changes: 0 additions & 14 deletions src/components/EpicDetailView/helpers/storyPointsHelper.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/components/ProjectsView/Table/ProjectsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ProjectsTable({ data }: { data: Project[] }) {
const [sortedData, setSortedData] = useState(data)
const [sortBy, setSortBy] = useState<keyof Project | null>(null)
const [reverseSortDirection, setReverseSortDirection] = useState(false)
const { setSelectedProject, setSelectedProjectBoardIds } = useCanvasStore()
const { setSelectedProject, setSelectedProjectBoardIds, setIssueTypes } = useCanvasStore()
const navigate = useNavigate()

useEffect(() => {
Expand Down Expand Up @@ -48,6 +48,7 @@ export function ProjectsTable({ data }: { data: Project[] }) {
const onClickRow = async (row: Project) => {
setSelectedProject(row)
setSelectedProjectBoardIds(await window.provider.getBoardIds(row.key))
setIssueTypes(await window.provider.getIssueTypesByProject(row.key))
navigate("/backlogview")
}

Expand Down
13 changes: 13 additions & 0 deletions src/components/common/StoryPoints/status-accumulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Issue } from "../../../../types"

export const storyPointsAccumulator = (issues: Issue[], validStatus: string[]) =>
issues.reduce(
(accumulator, currentValue) => accumulator + (validStatus.includes(currentValue.status) ? currentValue.storyPointsEstimate ?? 0 : 0) ?? 0,
0,
)

export const issueCountAccumulator = (issues: Issue[], validStatus: string[]) =>
issues.reduce(
(accumulator, currentValue) => accumulator + (validStatus.includes(currentValue.status) ? 1 : 0),
0,
)
32 changes: 30 additions & 2 deletions src/lib/Store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { IssueType, Project } from "types"
import { IssueStatus, IssueType, Project } from "types"
import { create } from "zustand"
import { uniqWith } from "lodash"
import { StatusType } from "../../types/status"

export interface CanvasStore {
projects: Project[]
selectedProject: Project | undefined
selectedProjectBoardIds: number[]
issueTypes: IssueType[]
issueStatus: IssueStatus[]
issueStatusByCategory: Partial<{ [Type in StatusType]: IssueStatus[] }>
issueStatusCategoryByStatusName: { [statusName: string]: StatusType }
setProjects: (projects: Project[]) => void
setSelectedProject: (project: Project) => void
setSelectedProjectBoardIds: (boards: number[]) => void
Expand All @@ -17,10 +22,33 @@ export const useCanvasStore = create<CanvasStore>()((set) => ({
selectedProject: undefined,
selectedProjectBoardIds: [],
issueTypes: [],
issueStatus: [],
issueStatusByCategory: {},
issueStatusCategoryByStatusName: {},
setProjects: (projects: Project[]) => set(() => ({ projects })),
setSelectedProjectBoardIds: (boards: number[]) =>
set(() => ({ selectedProjectBoardIds: boards })),
setSelectedProject: (row: Project | undefined) =>
set(() => ({ selectedProject: row })),
setIssueTypes: (types: IssueType[]) => set(() => ({ issueTypes: types })),
setIssueTypes: (issueTypes: IssueType[]) => set(() => {
const issueStatus = uniqWith(
issueTypes.flatMap((type) => type.statuses ?? []),
(statusA, statusB) => statusA.id === statusB.id,
);

const issueStatusByCategory = {} as { [Type in StatusType]: IssueStatus[] };
const issueStatusCategoryByStatusName = {} as { [statusName: string]: StatusType };
issueStatus.forEach((status) => {
issueStatusByCategory[status.statusCategory.name as StatusType] ??= [];
issueStatusByCategory[status.statusCategory.name as StatusType].push(status);
issueStatusCategoryByStatusName[status.name] = status.statusCategory.name as StatusType;
});

return {
issueTypes,
issueStatus,
issueStatusByCategory,
issueStatusCategoryByStatusName,
};
}),
}))

0 comments on commit ad8aff2

Please sign in to comment.