-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support export of issues in backlog view (#115)
Co-authored-by: ValriRod <32778515+ValsiRod@users.noreply.github.com> Co-authored-by: Julian Rupprecht <rupprecht.julian@web.de> Co-authored-by: Benedict Teutsch <bene210@web.de> Co-authored-by: ayman <ayman.kallouz@gmail.com> Co-authored-by: ayman <118556179+aymka@users.noreply.github.com>
- Loading branch information
1 parent
5699856
commit ce7b86e
Showing
10 changed files
with
350 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import fs from "fs"; | ||
import * as Electron from "electron"; | ||
|
||
export const enum ExportStatus { | ||
SUCCESS = 'success', | ||
CANCELED = 'canceled', | ||
ERROR = 'error', | ||
} | ||
|
||
export type ExportReply = { status: ExportStatus, error?: string } | ||
|
||
export const getExportIssuesHandler = | ||
(electron: typeof Electron, mainWindow: Electron.BrowserWindow) => | ||
(event: Electron.IpcMainEvent, data: string) => { | ||
electron.dialog.showSaveDialog(mainWindow!, { | ||
title: "Export issues to CSV", | ||
defaultPath: electron.app.getPath("downloads"), | ||
filters: [{ name: 'CSV file', extensions: ['csv'] }], | ||
properties: [] | ||
}) | ||
.then(file=> { | ||
if (file.canceled) { | ||
event.reply("exportIssuesReply", { status: ExportStatus.CANCELED }); | ||
} else { | ||
fs.writeFile( | ||
file.filePath?.toString() ?? electron.app.getPath('downloads'), | ||
data, | ||
(err) => { if(err) throw err; }, | ||
) | ||
event.reply("exportIssuesReply", { status: ExportStatus.SUCCESS }); | ||
} | ||
}) | ||
.catch((e) => event.reply("exportIssuesReply", { status: ExportStatus.ERROR, error: e.toString })); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { useState } from "react" | ||
import { Divider, Stack, Checkbox, MantineProvider, createTheme } from "@mantine/core" | ||
import { isEqual } from "lodash"; | ||
|
||
export function CheckboxStack({ | ||
data, | ||
onChange, | ||
}: { | ||
data: { | ||
label: string, | ||
value: string, | ||
active?: boolean, | ||
}[], | ||
onChange: (activeValues: string[]) => void | ||
}) { | ||
const theme = createTheme({ cursorType: 'pointer' }); | ||
const [includedValues, setIncludedValues] = useState<string[]>( | ||
data | ||
.filter((d) => d.active) | ||
.map((d) => d.value) | ||
); | ||
|
||
const allValues = data.map((d) => d.value) | ||
const allValuesSelected = isEqual(includedValues.sort(), allValues.sort()) | ||
|
||
const changeValues = (newValues: string[]) => { | ||
setIncludedValues(newValues) | ||
onChange(newValues); | ||
} | ||
|
||
function toggleValue(value: string) { | ||
if (includedValues.indexOf(value) < 0) changeValues([...includedValues, value]); | ||
else changeValues(includedValues.filter(containedValue => containedValue !== value)); | ||
} | ||
|
||
return ( | ||
<Stack mt="-12%"> | ||
<MantineProvider theme={theme}> | ||
<Checkbox | ||
{...(!allValuesSelected && { c: "dimmed" })} | ||
label="Select All" | ||
checked={allValuesSelected} | ||
indeterminate={!allValuesSelected && includedValues.length > 0} | ||
onChange={() => changeValues(allValuesSelected ? [] : allValues)} | ||
/> | ||
<Divider mt="-8%" /> | ||
{data && data.map((d) => ( | ||
<Checkbox | ||
{...(!includedValues.includes(d.value) && { c: "dimmed" })} | ||
key={d.value} | ||
label={d.label} | ||
checked={includedValues.includes(d.value)} | ||
onChange={() => toggleValue(d.value)} | ||
/> | ||
))} | ||
</MantineProvider> | ||
</Stack> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
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 { useQuery } from "@tanstack/react-query"; | ||
import { IconInfoCircle } from "@tabler/icons-react"; | ||
import { useCanvasStore } from "../../lib/Store"; | ||
import { Issue } from "../../../types"; | ||
import { exportIssues } from "./exportHelper"; | ||
import { getIssuesByProject } from "../BacklogView/helpers/queryFetchers"; | ||
import { StatusType } from "../../../types/status"; | ||
import { CheckboxStack } from "./CheckboxStack"; | ||
|
||
export function CreateExportModal({ | ||
opened, | ||
setOpened, | ||
}: { | ||
opened: boolean | ||
setOpened: Dispatch<SetStateAction<boolean>> | ||
}) { | ||
const project = useCanvasStore((state) => state.selectedProject); | ||
const boardId = useCanvasStore((state) => state.selectedProjectBoardIds)[0] | ||
|
||
const { data: issues } = useQuery<unknown, unknown, Issue[]>({ | ||
queryKey: ["issues", project?.key], | ||
queryFn: () => project && getIssuesByProject(project.key, boardId), | ||
enabled: !!project?.key, | ||
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 [includedIssueTypes, setIncludedIssueTypes] = useState<string[]>([]); | ||
const [includedIssueStatus, setIncludedIssueStatus] = useState<string[]>([]); | ||
const [issuesToExport, setIssuesToExport] = useState<Issue[]>([]); | ||
|
||
function calculateIssuesToExport() { | ||
setIssuesToExport( | ||
sortBy( | ||
issues | ||
.filter((issue) => includedIssueTypes.includes(issue.type)) | ||
.filter((issue) => includedIssueStatus.includes(issue.status) | ||
&& allStatusNamesByCategory[StatusType.DONE].includes(issue.status)), | ||
['issueKey'] | ||
) | ||
); | ||
} | ||
|
||
useEffect(() => { | ||
calculateIssuesToExport(); | ||
}, [includedIssueTypes, includedIssueStatus]); | ||
|
||
return ( | ||
<Modal | ||
opened={opened} | ||
onClose={() => { | ||
setIncludedIssueTypes([]); | ||
setIncludedIssueStatus([]); | ||
setOpened(false); | ||
}} | ||
centered | ||
withCloseButton={false} | ||
size="40vw" | ||
> | ||
<Stack | ||
align="left" | ||
gap={0} | ||
style={{ | ||
minHeight: "100%", | ||
minWidth: "100%", | ||
}}> | ||
<Group c="dimmed" mb="5%"> | ||
<Text>{project?.name}</Text> | ||
<Tooltip | ||
withArrow | ||
multiline | ||
w={150} | ||
fz={14} | ||
fw={500} | ||
openDelay={200} | ||
closeDelay={200} | ||
ta="center" | ||
color="primaryBlue" | ||
variant="filled" | ||
label="Only issues with corresponding types and a 'Done' status are exported. The remaining status influence the date calculations." | ||
> | ||
<ActionIcon variant="subtle" ml="auto"> | ||
<IconInfoCircle /> | ||
</ActionIcon> | ||
</Tooltip> | ||
</Group> | ||
<Paper shadow="md" radius="md" withBorder mb="5%"> | ||
<Group align ="top" justify="center" mb="5%"> | ||
<Stack align="center" mr="5%"> | ||
<Text size="md" fw={450} mt="7%" mb="10%" >Include Issue Types</Text> | ||
{issueTypes && ( | ||
<CheckboxStack | ||
data={issueTypes.map((issueType) => ({ | ||
value: issueType.name!, | ||
label: issueType.name!, | ||
}))} | ||
onChange={(includedTypes) => setIncludedIssueTypes(includedTypes)} | ||
/> | ||
)} | ||
</Stack> | ||
<Stack align="center"> | ||
<Text size="md" fw={450} mt="7%" mb="10%">Include Issue Status</Text> | ||
{allStatus && ( | ||
<CheckboxStack | ||
data={allStatus.map((status) => ({ | ||
value: status.name, | ||
label: status.name, | ||
}))} | ||
onChange={(includedStatus) => setIncludedIssueStatus(includedStatus)} | ||
/> | ||
)} | ||
</Stack> | ||
</Group> | ||
</Paper> | ||
<Group> | ||
<Text size="90%" c="dimmed"> | ||
Issues to export: {issuesToExport.length} | ||
</Text> | ||
<Button | ||
ml="auto" | ||
size="sm" | ||
onClick={() => { exportIssues(issuesToExport) }} | ||
> | ||
Export CSV | ||
</Button> | ||
</Group> | ||
</Stack> | ||
</Modal> | ||
) | ||
} |
Oops, something went wrong.