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, + }; + }), }))