Skip to content

Commit

Permalink
Add epic list view (#77)
Browse files Browse the repository at this point in the history
Co-authored-by: Maximilian Rüsch <maximilian.ruesch@stud.tu-darmstadt.de>
  • Loading branch information
trueliquid and maximilianruesch authored Dec 6, 2023
1 parent 41281cc commit 23f3e02
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Layout } from "./components/layout"
import { Login } from "./components/Login"
import { ProjectsView } from "./components/ProjectsView"
import { BacklogView } from "./components/BacklogView"
import { EpicView } from "./components/EpicView"
import { StoryMapView } from "./components/StoryMapView"
import { StoryMapDashboard } from "./components/StoryMapView/StoryMapDashboard"

Expand All @@ -13,6 +14,7 @@ export function App() {
<Route element={<Layout />}>
<Route path="projectsview" element={<ProjectsView />} />
<Route path="backlogview" element={<BacklogView />} />
<Route path="epicview" element={<EpicView />} />
<Route path="storymapview">
<Route index element={<StoryMapDashboard />} />
<Route path=":storyMapId" element={<StoryMapView />} />
Expand Down
1 change: 1 addition & 0 deletions src/components/CreateIssue/CreateIssueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function CreateIssueModal({
color: "green",
})
queryClient.invalidateQueries({ queryKey: ["issues"] })
queryClient.invalidateQueries({ queryKey: ["epics"] })
setOpened(false)
form.reset()
},
Expand Down
194 changes: 194 additions & 0 deletions src/components/EpicView/EpicCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {Issue} from "types";
import {
Avatar,
Badge,
Box,
Center,
Grid,
Group,
Modal,
Stack,
Text, ThemeIcon,
Tooltip,
useMantineTheme
} from "@mantine/core";
import {useHover} from "@mantine/hooks";
import {useState} from "react";
import {useQueryClient} from "@tanstack/react-query";
import {IconBolt} from "@tabler/icons";
import {DeleteButton} from "../BacklogView/Issue/DeleteButton";

export function EpicCard ({
issueKey,
summary,
status,
storyPointsEstimate,
epic,
labels,
assignee,
}: Issue) {
let storyPointsColor: string
const [opened, setOpened] = useState(false)
const queryClient = useQueryClient()
const {hovered} = useHover()
const theme = useMantineTheme()
const hoverStyles =
theme.colorScheme === "dark"
? {
backgroundColor: theme.colors.dark[8],
transition: "background-color .1s ease-in",
}
: {
backgroundColor: theme.colors.gray[1],
transition: "background-color .1s ease-in",
}
switch (status) {
case "To Do":
storyPointsColor = "gray.6"
break
case "In Progress":
storyPointsColor = "blue.8"
break
case "Done":
storyPointsColor = "green.9"
break
default:
storyPointsColor = "gray.6"
}
return (
<>
<DeleteButton mounted={hovered} issueKey={issueKey} />
<Grid
columns={100}
p={3}
sx={{
borderRadius: theme.radius.sm,
margin: 0,
boxShadow: theme.shadows.xs,
transition: "background-color .8s ease-out",
":hover": hoverStyles,
}}
>
<Grid.Col
span={8}
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<Center>
<ThemeIcon
size="sm"
variant="gradient"
gradient={{ from: "violet", to: "white", deg: 105 }}
>
<IconBolt/>
</ThemeIcon>
</Center>
</Grid.Col>
<Grid.Col span={74}>
<Stack spacing={0}>
<Group spacing={2}>
<Text
size="sm"
mr={5}
color="blue"
td={status === "Done" ? "line-through" : "none"}
sx={{
":hover": {
textDecoration: "underline",
cursor: "pointer",
},
}}
>
{issueKey}
</Text>
{epic && (
<Badge mr={5} color="violet">
{epic}
</Badge>
)}
{labels?.length !== 0 &&
labels.map((label) => (
<Badge
mr={2}
key={`${issueKey}-${label}`}
color="yellow"
>
{label}
</Badge>
))}
</Group>
<Text size="lg">{summary}</Text>
</Stack>
</Grid.Col>
<Grid.Col
span={8}
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<Tooltip
label={
assignee?.displayName !== undefined
? assignee.displayName
: "unassigned"
}
>
{assignee?.avatarUrls !== undefined ? (
<Avatar
src={assignee.avatarUrls["24x24"]}
size="sm"
radius="xl"
ml={4}
mr={4}
/>
) : (
<Avatar
radius="xl"
variant="outline"
size="sm"
ml={4}
mr={4}
/>
)}
</Tooltip>
</Grid.Col>
<Grid.Col span={3}>
<Box sx={{ alignSelf: "flex-start" }}>
<Badge
w="24px"
p="0px"
bg={
storyPointsEstimate !== undefined &&
storyPointsEstimate !== null
? storyPointsColor
: "transparent"
}
variant="filled"
>
{storyPointsEstimate}
</Badge>
</Box>
</Grid.Col>
</Grid>
<Modal
opened={opened}
onClose={() => {
setOpened(false)
queryClient.invalidateQueries({ queryKey: ["issues"] })
}}
size="90vw"
overflow="outside"
overlayOpacity={0.55}
overlayBlur={3}
withCloseButton={false}
>
{/* TODO open Epic Detail View */}
</Modal>
</>
)
}
122 changes: 122 additions & 0 deletions src/components/EpicView/EpicView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {Group, Stack, Text, Title, ScrollArea, Box, Button, Center, Loader} from "@mantine/core";
import {useNavigate} from "react-router-dom";
import {useState} from "react";
import {useQuery} from "@tanstack/react-query";
import {useCanvasStore} from "../../lib/Store";
import {CreateIssueModal} from "../CreateIssue/CreateIssueModal";
import {Issue} from "../../../types";
import {EpicWrapper} from "./EpicWrapper";
import {getEpics} from "./helpers/queryFetchers";


export function EpicView() {
const navigate = useNavigate()
const projectName = useCanvasStore((state) => state.selectedProject?.name)
const [createIssueModalOpened, setCreateIssueModalOpened] = useState(false)
const projectKey = useCanvasStore((state) => state.selectedProject?.key)
const [EpicWrappers, setEpicWrappers] = useState(
new Map<string, { issues: Issue[]}>()
)

const updateEpicWrapper = (
key: string,
value: { issues: Issue[]}
) => {
setEpicWrappers((map) => new Map(map.set(key, value)))
}

const {isLoading: isLoadingEpics} =
useQuery({
queryKey: ["epics", projectKey],
queryFn: () => getEpics(projectKey),
enabled: !!projectKey,
onSuccess: (epics) => {
updateEpicWrapper("EpicView", {
issues:
epics && epics instanceof Array ? epics : []
})
},
})
if (isLoadingEpics)
return (
<Center style={{ width: "100%", height: "100%" }}>
{projectKey ? (
<Loader />
) : (
<Stack align="center">
<Title>No Project has been selected!</Title>
<Text>
Please go back to the Projects View section and select a project
</Text>
<Button onClick={() => navigate("/projectsview")}>Go back</Button>
</Stack>
)}
</Center>
)
return (
<Stack sx={{ minHeight: "100%"}}>
<Stack align="left" spacing={0}>
<Group>
<Group spacing="xs" c="dimmed">
<Text
onClick={() => navigate("/projectsview")}
sx={{
":hover": {
textDecoration: "underline",
cursor: "pointer",
},
}}
>
Projects
</Text>
<Text>/</Text>
<Text>{projectName}</Text>
</Group>
</Group>
<Title mb="sm">Epics</Title>
</Stack>
<ScrollArea.Autosize
className="main-panel"
maxHeight="calc(100vh - 230px)"
w="100%"
p="sm"
sx={{
minWidth: "260px",
}}
>
{EpicWrappers.get("EpicView") &&(
<Box mr="xs">
<EpicWrapper epics={EpicWrappers.get("EpicView")!.issues}/>
</Box>
)}
<Box mr="xs">
<Button
mt="sm"
mb="xl"
variant="subtle"
color="gray"
radius="sm"
display="flex"
fullWidth
onClick={() => setCreateIssueModalOpened(true)}
sx={(theme) => ({
justifyContent: "left",
":hover": {
background:
theme.colorScheme === "dark"
? theme.colors.dark[4]
: theme.colors.gray[4],
},
})}
>
+ Create Epic
</Button>
</Box>
<CreateIssueModal
opened={createIssueModalOpened}
setOpened={setCreateIssueModalOpened}
/>
</ScrollArea.Autosize>
</Stack>
)
}
18 changes: 18 additions & 0 deletions src/components/EpicView/EpicWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Stack} from "@mantine/core";
import {Issue} from "../../../types";
import {EpicCard} from "./EpicCard";


export function EpicWrapper({
epics,
}: {
epics: Issue[]
}){
return (
<Stack spacing="sm">
{epics.map((epic: Issue) => (
<EpicCard {...epic} key={epic.issueKey} />
))}
</Stack>
)
}
6 changes: 6 additions & 0 deletions src/components/EpicView/helpers/queryFetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Issue} from "../../../../types";

export const getEpics = (
projectKey: string | undefined
): Promise<Issue[]> =>
window.provider.getEpicsByProject(projectKey || "")
1 change: 1 addition & 0 deletions src/components/EpicView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./EpicView"
6 changes: 6 additions & 0 deletions src/components/layout/LayoutHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export function LayoutHeader() {
>
Backlog
</Anchor>
<Anchor
component="button"
type="button"
onClick={() => navigate("/epicview")}>
Epics
</Anchor>
<StoryMapMenu />
<Button onClick={() => setCreateIssueModalOpened(true)}>
Create
Expand Down

0 comments on commit 23f3e02

Please sign in to comment.