Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update tanstack-query monorepo to v5 (major) #117

Merged
merged 9 commits into from
Jan 18, 2024
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"@mantine/hooks": "^7.0.0",
"@mantine/notifications": "^7.0.0",
"@tabler/icons-react": "^2.44.0",
"@tanstack/react-query": "^4.23.0",
"@tanstack/react-query-devtools": "^4.23.0",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-devtools": "^5.0.0",
"@types/file-saver": "^2.0.5",
"@types/lodash": "^4.14.202",
"axios": "^1.6.1",
Expand Down
170 changes: 67 additions & 103 deletions src/components/BacklogView/BacklogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import {
Title,
} from "@mantine/core"
import { IconSearch } from "@tabler/icons-react"
import { useQueries, useQuery } from "@tanstack/react-query"
import { ChangeEvent, useEffect, useState } from "react"
import {QueriesResults, useQueries, useQuery} from "@tanstack/react-query"
import { ChangeEvent, useEffect, useMemo, useState } from "react"
import { DragDropContext } from "@hello-pangea/dnd"
import { useNavigate } from "react-router-dom"
import { Issue, Sprint } from "types"
import { UseQueryOptions } from "@tanstack/react-query/src/types";
import { useCanvasStore } from "../../lib/Store"
import { CreateIssueModal } from "../CreateIssue/CreateIssueModal"
import { CreateExportModal } from "../CreateExport/CreateExportModal"
import { CreateSprint } from "./CreateSprint/CreateSprint"
import { searchIssuesFilter, sortIssuesByRank } from "./helpers/backlogHelpers"
import { BacklogKey, IssuesState, searchMatchesIssue, sortIssuesByRank } from "./helpers/backlogHelpers"
import { onDragEnd } from "./helpers/draggingHelpers"
import {
getBacklogIssues,
Expand All @@ -46,93 +47,68 @@ export function BacklogView() {
const [search, setSearch] = useState("")
const [createExportModalOpened, setCreateExportModalOpened] = useState(false)

const [issuesWrappers, setIssuesWrappers] = useState(
new Map<string, { issues: Issue[]; sprint?: Sprint }>()
)
const [searchedissuesWrappers, setSearchedissuesWrappers] = useState(
new Map<string, { issues: Issue[]; sprint?: Sprint }>()
)
const updateIssuesWrapper = (
key: string,
value: { issues: Issue[]; sprint?: Sprint }
) => {
setIssuesWrappers((map) => new Map(map.set(key, value)))
setSearchedissuesWrappers((map) => new Map(map.set(key, value)))
}

const { data: sprints, isError: isErrorSprints } = useQuery({
queryKey: ["sprints", currentBoardId],
queryFn: () => getSprints(currentBoardId),
enabled: !!currentBoardId,
select: (fetchedSprints: Sprint[]) => Object.fromEntries(fetchedSprints.map((s) => [s.id, s])),
initialData: [],
})

const sprintsIssuesResults = useQueries({
queries:
!isErrorSprints && sprints
? sprints?.map((sprint) => ({
queryKey: ["issues", "sprints", projectKey, sprints, sprint.id],
queryFn: () => getIssuesBySprint(sprint.id),
enabled: !!projectKey && !!sprints,
onSuccess: (issues: Issue[]) => {
updateIssuesWrapper(sprint.name, {
sprint,
issues: issues
.filter(
(issue: Issue) =>
issue.type !== "Epic" && issue.type !== "Subtask"
)
.sort((issueA: Issue, issueB: Issue) =>
sortIssuesByRank(issueA, issueB)
),
})
searchIssuesFilter(
search,
issuesWrappers,
searchedissuesWrappers,
setSearchedissuesWrappers
)
},
}))
: [],
})
const isErrorSprintsIssues = sprintsIssuesResults.some(
({ isError }) => isError
)

const { isLoading: isLoadingBacklogIssues, isError: isErrorBacklogIssues } =
useQuery({
queryKey: ["issues", projectKey, currentBoardId],
queryFn: () => getBacklogIssues(projectKey, currentBoardId),
enabled: !!projectKey,
onSuccess: (backlogIssues) => {
updateIssuesWrapper("Backlog", {
sprint: undefined,
issues:
backlogIssues && backlogIssues instanceof Array
? backlogIssues
.filter(
(issue: Issue) =>
issue.type !== "Subtask"
)
.sort((issueA: Issue, issueB: Issue) =>
sortIssuesByRank(issueA, issueB)
)
: [],
})
searchIssuesFilter(
search,
issuesWrappers,
searchedissuesWrappers,
setSearchedissuesWrappers
)
const issueQueries = useQueries<Array<UseQueryOptions<Issue[], unknown, [string, IssuesState]>>>({
queries: [
{
queryKey: ["issues", projectKey, currentBoardId], // IMPROVE: Change this issue key to contain "backlog"
queryFn: () => getBacklogIssues(projectKey, currentBoardId),
enabled: !!projectKey,
select: (backlogIssues: Issue[]): [string, IssuesState] => [
BacklogKey,
{
issues: backlogIssues
.filter((issue: Issue) => issue.type !== "Epic" && issue.type !== "Subtask")
.sort(sortIssuesByRank),
sprintId: undefined
},
],
initialData: [],
},
})
...(Object.values(sprints).map((sprint): UseQueryOptions<Issue[], unknown, [string, IssuesState]> => ({
queryKey: ["issues", "sprints", projectKey, Object.keys(sprints), sprint.id], // IMPROVE: Change this issue key to not contain sprints
queryFn: () => getIssuesBySprint(sprint.id),
enabled: !!projectKey && !!sprints && !isErrorSprints,
select: (issues: Issue[]) => [
sprint.name,
{
issues: issues
.filter((issue: Issue) => issue.type !== "Epic" && issue.type !== "Subtask")
.sort(sortIssuesByRank),
sprintId: sprint.id
},
],
initialData: [],
}))),
],
combine: (results: QueriesResults<Array<UseQueryOptions<Issue[], unknown, [string, IssuesState]>>>) =>
results.map(result => result)
})

const [issuesWrapper, setIssuesWrapper] = useState(new Map<string, IssuesState>());
useEffect(() => {
resizeDivider()
}, [isLoadingBacklogIssues])
// Generally, using useEffect to sync state should be avoided. But since we need our state to be assignable AND
// reactive AND derivable, we found no other solution than to use useEffect.
setIssuesWrapper(new Map<string, IssuesState>(issueQueries.map((query) => query.data!)))
}, [issueQueries])
const updateIssuesWrapper = (key: string, newState: IssuesState) => setIssuesWrapper(new Map(issuesWrapper.set(key, newState)))
const searchedIssuesWrapper = useMemo(() => new Map<string, Issue[]>(
Array.from(issuesWrapper.keys()).map((key) => [
key,
issuesWrapper.get(key)!.issues.filter((i) => searchMatchesIssue(search, i)),
]),
), [issuesWrapper, search])

useEffect(resizeDivider, [issueQueries]);

if (isErrorSprints || isErrorBacklogIssues || isErrorSprintsIssues)
if (isErrorSprints || issueQueries.some(query => query.isError))
return (
<Center style={{ width: "100%", height: "100%" }}>
<Text w="300">
Expand All @@ -144,18 +120,8 @@ export function BacklogView() {
</Center>
)

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const currentSearch = event.currentTarget.value
setSearch(currentSearch)
searchIssuesFilter(
currentSearch,
issuesWrappers,
searchedissuesWrappers,
setSearchedissuesWrappers
)
}

if (isLoadingBacklogIssues)
// This check might be broken. It does not trigger everytime we think it does. Might need to force a rerender.
if (issueQueries.some(query => query.isPending))
return (
<Center style={{ width: "100%", height: "100%" }}>
{projectKey ? (
Expand Down Expand Up @@ -206,7 +172,7 @@ export function BacklogView() {
placeholder="Search by issue summary, key, epic, labels, creator or assignee.."
leftSection={<IconSearch size={14} stroke={1.5} />}
value={search}
onChange={handleSearchChange}
onChange={(event: ChangeEvent<HTMLInputElement>) => { setSearch(event.currentTarget.value) }}
/>
</Stack>

Expand All @@ -215,7 +181,7 @@ export function BacklogView() {
onDragEnd={(dropResult) =>
onDragEnd({
...dropResult,
issuesWrappers,
issuesWrapper,
updateIssuesWrapper,
})
}
Expand All @@ -229,11 +195,11 @@ export function BacklogView() {
minWidth: "260px",
}}
>
{searchedissuesWrappers.get("Backlog") && (
{searchedIssuesWrapper.get(BacklogKey) && ( // IMPROVE: Maybe this check can be removed entirely, please evaluate
<Box mr="xs">
<DraggableIssuesWrapper
id="Backlog"
issues={searchedissuesWrappers.get("Backlog")!.issues.filter(issue => issue.type !== "Epic")}
issues={searchedIssuesWrapper.get(BacklogKey)!}
/>
</Box>
)}
Expand Down Expand Up @@ -284,13 +250,11 @@ export function BacklogView() {
}}
>
<SprintsPanel
sprintsWithIssues={
Array.from(searchedissuesWrappers.values()).filter(
(issuesWrapper) => issuesWrapper.sprint !== undefined
) as unknown as {
issues: Issue[]
sprint: Sprint
}[]
sprints={sprints}
issueWrapper={
Object.fromEntries(Array.from(searchedIssuesWrapper.keys())
.filter((key) => key !== BacklogKey)
.map((key) => [issuesWrapper.get(key)!.sprintId, searchedIssuesWrapper.get(key)!]))
}
/>
<CreateSprint />
Expand Down
16 changes: 10 additions & 6 deletions src/components/BacklogView/IssuesWrapper/SprintsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import {storyPointsAccumulator} from "../../common/StoryPoints/status-accumulato
import {useCanvasStore} from "../../../lib/Store";

export function SprintsPanel({
sprintsWithIssues,
sprints,
issueWrapper,
}: {
sprintsWithIssues: { issues: Issue[]; sprint: Sprint }[]
sprints: { [_: string]: Sprint }
issueWrapper: { [_: string]: Issue[] }
}) {
const colorScheme = useColorScheme()

Expand Down Expand Up @@ -43,10 +45,12 @@ export function SprintsPanel({
},
})}
>
{sprintsWithIssues
.sort(({ sprint: sprintA }, { sprint: sprintB }) =>
sortSprintsByActive(sprintA, sprintB)
)
{Object.keys(issueWrapper)
.map((sprintId) => ({
issues: issueWrapper[sprintId],
sprint: sprints[sprintId]
}))
.sort(({ sprint: sprintA }, { sprint: sprintB }) => sortSprintsByActive(sprintA, sprintB))
.map(({ issues, sprint }) => (
<Accordion.Item
key={`accordion-item-key-${sprint.name}`}
Expand Down
67 changes: 11 additions & 56 deletions src/components/BacklogView/helpers/backlogHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Issue, Sprint } from "types"
import { Dispatch, SetStateAction } from "react"

export const BacklogKey = "Backlog";
export type IssuesState = { issues: Issue[], sprintId?: number };

export const pluralize = (count: number, noun: string, suffix = "s") =>
`${count} ${noun}${count !== 1 ? suffix : ""}`
Expand All @@ -17,58 +19,11 @@ export const sortSprintsByActive = (sprintA: Sprint, sprintB: Sprint) => {
export const sortIssuesByRank = (issueA: Issue, issueB: Issue) =>
issueA.rank.localeCompare(issueB.rank)

export const searchIssuesFilter = (
currentSearch: string,
issuesWrappers: Map<
string,
{
issues: Issue[]
sprint?: Sprint | undefined
}
>,
searchedissueWrapper: Map<
string,
{
issues: Issue[]
sprint?: Sprint | undefined
}
>,
setSearchedissueWrapper: Dispatch<
SetStateAction<
Map<
string,
{
issues: Issue[]
sprint?: Sprint | undefined
}
>
>
>
) => {
searchedissueWrapper.forEach((issueWrapper, issueWrapperKey) => {
const newIssueWrapper: {
issues: Issue[]
sprint?: Sprint | undefined
} = { issues: [], sprint: issueWrapper.sprint }
newIssueWrapper.sprint = issueWrapper.sprint
newIssueWrapper.issues = issuesWrappers
.get(issueWrapperKey)!
.issues.filter(
(issue: Issue) =>
issue.summary.toLowerCase().includes(currentSearch.toLowerCase()) ||
issue.epic.summary?.toLowerCase().includes(currentSearch.toLowerCase()) ||
issue.assignee?.displayName
?.toLowerCase()
.includes(currentSearch.toLowerCase()) ||
issue.issueKey.toLowerCase().includes(currentSearch.toLowerCase()) ||
issue.creator?.toLowerCase().includes(currentSearch.toLowerCase()) ||
issue.labels?.some((label: string) =>
label.toLowerCase().includes(currentSearch.toLowerCase())
) ||
currentSearch === ""
)
setSearchedissueWrapper(
(map) => new Map(map.set(issueWrapperKey, newIssueWrapper))
)
})
}
export const searchMatchesIssue = (search: string, issue: Issue) =>
search === "" ||
issue.summary.toLowerCase().includes(search.toLowerCase()) ||
issue.epic.summary?.toLowerCase().includes(search.toLowerCase()) ||
issue.assignee?.displayName?.toLowerCase().includes(search.toLowerCase()) ||
issue.issueKey.toLowerCase().includes(search.toLowerCase()) ||
issue.creator?.toLowerCase().includes(search.toLowerCase()) ||
issue.labels?.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
Loading