From ad8aff28c262f36fb4d6954c73f7c80841b1a98b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maximilian=20R=C3=BCsch?=
<78490564+maximilianruesch@users.noreply.github.com>
Date: Wed, 17 Jan 2024 01:09:53 +0100
Subject: [PATCH] Support custom status in every story points accumulation
(#118)
---
.../BacklogView/Issue/IssueCard.tsx | 6 ++--
.../IssuesWrapper/SprintsPanel.tsx | 13 ++++---
.../BacklogView/helpers/backlogHelpers.ts | 9 -----
.../CreateExport/CreateExportModal.tsx | 36 ++++---------------
.../Components/ChildIssue/ChildIssueCard.tsx | 6 ++--
.../EpicDetailView/EpicDetailView.tsx | 23 +++++++-----
.../helpers/storyPointsHelper.ts | 14 --------
.../ProjectsView/Table/ProjectsTable.tsx | 3 +-
.../common/StoryPoints/status-accumulator.ts | 13 +++++++
src/lib/Store.ts | 32 +++++++++++++++--
10 files changed, 83 insertions(+), 72 deletions(-)
delete mode 100644 src/components/EpicDetailView/helpers/storyPointsHelper.ts
create mode 100644 src/components/common/StoryPoints/status-accumulator.ts
diff --git a/src/components/BacklogView/Issue/IssueCard.tsx b/src/components/BacklogView/Issue/IssueCard.tsx
index 05441cc9..4448dd2b 100644
--- a/src/components/BacklogView/Issue/IssueCard.tsx
+++ b/src/components/BacklogView/Issue/IssueCard.tsx
@@ -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,
@@ -40,6 +41,7 @@ export function IssueCard({
const queryClient = useQueryClient()
const theme = useMantineTheme()
const colorScheme = useColorScheme()
+ const { issueStatusCategoryByStatusName: statusNameToCategory } = useCanvasStore();
const hoverStyles =
colorScheme === "dark"
@@ -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",
@@ -151,7 +153,7 @@ export function IssueCard({
{storyPointsEstimate &&
- }
+ }
diff --git a/src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx b/src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx
index 31c54663..cf86cea8 100644
--- a/src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx
+++ b/src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx
@@ -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,
@@ -70,6 +71,10 @@ function SprintAccordionControl({
issues: Issue[]
sprint: Sprint
}) {
+ const { issueStatusByCategory } = useCanvasStore();
+
+ const getStatusNamesInCategory = (category: StatusType) => issueStatusByCategory[category]?.map((s) => (s.name)) ?? []
+
return (
@@ -83,15 +88,15 @@ function SprintAccordionControl({
diff --git a/src/components/BacklogView/helpers/backlogHelpers.ts b/src/components/BacklogView/helpers/backlogHelpers.ts
index 29d898b7..6a7ebf55 100644
--- a/src/components/BacklogView/helpers/backlogHelpers.ts
+++ b/src/components/BacklogView/helpers/backlogHelpers.ts
@@ -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 : ""}`
diff --git a/src/components/CreateExport/CreateExportModal.tsx b/src/components/CreateExport/CreateExportModal.tsx
index e8f391e5..c88b7325 100644
--- a/src/components/CreateExport/CreateExportModal.tsx
+++ b/src/components/CreateExport/CreateExportModal.tsx
@@ -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";
@@ -17,7 +17,7 @@ export function CreateExportModal({
opened: boolean
setOpened: Dispatch>
}) {
- const project = useCanvasStore((state) => state.selectedProject);
+ const { selectedProject: project, issueStatusByCategory, issueTypes, issueStatus } = useCanvasStore();
const boardId = useCanvasStore((state) => state.selectedProjectBoardIds)[0]
const { data: issues } = useQuery({
@@ -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([]);
const [includedIssueStatus, setIncludedIssueStatus] = useState([]);
@@ -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']
)
);
@@ -126,9 +104,9 @@ export function CreateExportModal({
Include Issue Status
- {allStatus && (
+ {issueStatus && (
({
+ data={issueStatus.map((status) => ({
value: status.name,
label: status.name,
}))}
@@ -145,7 +123,7 @@ export function CreateExportModal({
diff --git a/src/components/EpicDetailView/Components/ChildIssue/ChildIssueCard.tsx b/src/components/EpicDetailView/Components/ChildIssue/ChildIssueCard.tsx
index 17227e66..ff211bc4 100644
--- a/src/components/EpicDetailView/Components/ChildIssue/ChildIssueCard.tsx
+++ b/src/components/EpicDetailView/Components/ChildIssue/ChildIssueCard.tsx
@@ -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,
@@ -42,6 +43,7 @@ export function ChildIssueCard({
const { hovered } = useHover()
const theme = useMantineTheme()
const colorScheme = useColorScheme()
+ const { issueStatusCategoryByStatusName: statusNameToCategory } = useCanvasStore();
const hoverStyles =
colorScheme === "dark"
@@ -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",
@@ -119,7 +121,7 @@ export function ChildIssueCard({
>
{storyPointsEstimate &&
- }
+ }
state.selectedProject?.key)
- const boardIds = useCanvasStore((state) => state.selectedProjectBoardIds)
const currentBoardId = boardIds[0]
const [childIssues, setChildIssues] = useState([])
@@ -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(() => {
@@ -196,15 +201,15 @@ export function EpicDetailView({
diff --git a/src/components/EpicDetailView/helpers/storyPointsHelper.ts b/src/components/EpicDetailView/helpers/storyPointsHelper.ts
deleted file mode 100644
index fa90e594..00000000
--- a/src/components/EpicDetailView/helpers/storyPointsHelper.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Issue } from "../../../../types"
-import { StatusType } from "../../../../types/status";
-
-export const storyPointsAccumulator = (issues: Issue[], status: StatusType) =>
- issues.reduce(
- (accumulator, currentValue) => accumulator + (currentValue.status === status ? currentValue.storyPointsEstimate ?? 0 : 0) ?? 0,
- 0,
- )
-
-export const inProgressAccumulator = (issues: Issue[], status: StatusType) =>
- issues.reduce(
- (accumulator, currentValue) => accumulator + (currentValue.status === status ? 1 : 0),
- 0,
- )
diff --git a/src/components/ProjectsView/Table/ProjectsTable.tsx b/src/components/ProjectsView/Table/ProjectsTable.tsx
index 3ed41b2b..97ab03f7 100644
--- a/src/components/ProjectsView/Table/ProjectsTable.tsx
+++ b/src/components/ProjectsView/Table/ProjectsTable.tsx
@@ -19,7 +19,7 @@ export function ProjectsTable({ data }: { data: Project[] }) {
const [sortedData, setSortedData] = useState(data)
const [sortBy, setSortBy] = useState(null)
const [reverseSortDirection, setReverseSortDirection] = useState(false)
- const { setSelectedProject, setSelectedProjectBoardIds } = useCanvasStore()
+ const { setSelectedProject, setSelectedProjectBoardIds, setIssueTypes } = useCanvasStore()
const navigate = useNavigate()
useEffect(() => {
@@ -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")
}
diff --git a/src/components/common/StoryPoints/status-accumulator.ts b/src/components/common/StoryPoints/status-accumulator.ts
new file mode 100644
index 00000000..827fcca1
--- /dev/null
+++ b/src/components/common/StoryPoints/status-accumulator.ts
@@ -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,
+ )
diff --git a/src/lib/Store.ts b/src/lib/Store.ts
index 81e26c86..c6214b24 100644
--- a/src/lib/Store.ts
+++ b/src/lib/Store.ts
@@ -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
@@ -17,10 +22,33 @@ export const useCanvasStore = create()((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,
+ };
+ }),
}))