From b465605db3cf5413a534d9f3c8e85142389019b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= <78490564+maximilianruesch@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:33:31 +0100 Subject: [PATCH] Revamp core calculations in issue export (#128) --- package.json | 1 + .../CreateExport/CreateExportModal.tsx | 46 +++++------ src/components/CreateExport/exportHelper.ts | 82 +++++++------------ yarn.lock | 9 +- 4 files changed, 61 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index db8443d5..43fb23cc 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "axios": "^1.6.1", "cross-fetch": "^4.0.0", "dayjs": "^1.11.7", + "dayjs-business-days2": "^1.2.2", "dotenv": "^16.0.3", "electron-fetch": "^1.9.1", "electron-squirrel-startup": "^1.0.0", diff --git a/src/components/CreateExport/CreateExportModal.tsx b/src/components/CreateExport/CreateExportModal.tsx index 53593edd..fddf009f 100644 --- a/src/components/CreateExport/CreateExportModal.tsx +++ b/src/components/CreateExport/CreateExportModal.tsx @@ -7,7 +7,7 @@ import { DateInput } from "@mantine/dates"; import dayjs from "dayjs"; import { useCanvasStore } from "../../lib/Store"; import { Issue } from "../../../types"; -import { exportIssues } from "./exportHelper"; +import { addExportedTimeProperties, ExportableIssue, exportIssues } from "./exportHelper"; import { getIssuesByProject } from "../BacklogView/helpers/queryFetchers"; import { StatusType } from "../../../types/status"; import { CheckboxStack } from "./CheckboxStack"; @@ -26,6 +26,7 @@ export function CreateExportModal({ issueStatus, } = useCanvasStore(); const boardId = useCanvasStore((state) => state.selectedProjectBoardIds)[0]; + const doneIssueStatus = issueStatus.filter((status) => issueStatusByCategory[StatusType.DONE]?.includes(status)); const { data: issues } = useQuery({ queryKey: ["issues", project?.key], @@ -34,29 +35,33 @@ export function CreateExportModal({ initialData: [], }); - const doneStatusNames = issueStatusByCategory[StatusType.DONE]?.map((s) => s.name) ?? []; - const [includedIssueTypes, setIncludedIssueTypes] = useState([]); const [includedIssueStatus, setIncludedIssueStatus] = useState([]); - const [issuesToExport, setIssuesToExport] = useState([]); + const [issuesToExport, setIssuesToExport] = useState([]); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); function calculateIssuesToExport() { + if (!startDate || !endDate) { + setIssuesToExport([]); + } + + const inProgressStatusNames = issueStatus + .filter((status) => status.statusCategory.name === StatusType.IN_PROGRESS) + .map((status) => status.name); + const doneStatusNames = issueStatus + .filter((status) => status.statusCategory.name === StatusType.DONE) + .map((status) => status.name); + setIssuesToExport( sortBy( issues .filter((issue) => includedIssueTypes.includes(issue.type)) - .filter( - (issue) => includedIssueStatus.includes(issue.status) - && doneStatusNames.includes(issue.status), - ) - .filter( - (issue) => !startDate || dayjs(startDate).isBefore(dayjs(issue.created)), - ) - .filter( - (issue) => !endDate || dayjs(endDate).isAfter(dayjs(issue.created)), - ), + .filter((issue) => includedIssueStatus.includes(issue.status)) + .map((issue) => addExportedTimeProperties(issue, inProgressStatusNames, doneStatusNames)) + .filter((issue) => issue !== undefined) + .filter((issue) => dayjs(startDate).isBefore(issue!.startDate)) + .filter((issue) => dayjs(endDate).isAfter(issue!.endDate)) as ExportableIssue[], ["issueKey"], ), ); @@ -126,9 +131,9 @@ export function CreateExportModal({ Include Issue Status - {issueStatus && ( + {doneIssueStatus && ( ({ + data={doneIssueStatus.map((status) => ({ value: status.name, label: status.name, }))} @@ -138,7 +143,7 @@ export function CreateExportModal({ - Creation date range + In progress date range { - exportIssues( - issuesToExport, - issueStatus.filter((s) => includedIssueStatus.includes(s.name)), - ); - }} + onClick={() => exportIssues(issuesToExport)} > Export CSV diff --git a/src/components/CreateExport/exportHelper.ts b/src/components/CreateExport/exportHelper.ts index b8c28401..de46411d 100644 --- a/src/components/CreateExport/exportHelper.ts +++ b/src/components/CreateExport/exportHelper.ts @@ -1,20 +1,23 @@ import { ipcRenderer } from "electron"; import { showNotification } from "@mantine/notifications"; import dayjs, { Dayjs } from "dayjs"; -import { ChangelogHistoryItem, Issue, IssueStatus } from "../../../types"; +import dayjsBusinessDays from "dayjs-business-days2"; +import { ChangelogHistoryItem, Issue } from "../../../types"; import { ExportReply, ExportStatus } from "../../../electron/export-issues"; -import { StatusType } from "../../../types/status"; -type ExportableIssue = Omit & { - startDate?: Dayjs, - endDate?: Dayjs, +dayjs.extend(dayjsBusinessDays); + +export type ExportableIssue = Omit & { + startDate: Dayjs, + endDate: Dayjs, workingDays: number, }; -const addExportedTimeProperties = ( +export const addExportedTimeProperties = ( issue: Issue, inProgressStatusNames: string[], -): ExportableIssue => { + doneStatusNames: string[], +): ExportableIssue | undefined => { const statusItems = issue.changelog.histories .reverse() .map((history) => { @@ -39,69 +42,42 @@ const addExportedTimeProperties = ( (item) => item.fromString && inProgressStatusNames.includes(item.fromString) && item.toString - && !inProgressStatusNames.includes(item.toString), + && doneStatusNames.includes(item.toString), ); - if (enterEdges.length !== leaveEdges.length) { - throw new Error( - `Inconsistent in-progress changelog history encountered. Enter edge count: ${enterEdges.length}. Leave edge count: ${leaveEdges.length}`, - ); - } if (enterEdges.length === 0) { - return { - ...issue, - startDate: undefined, - endDate: undefined, - workingDays: 0, - }; + return undefined; } - let workingDays = 0; - for (let i = 0; i < enterEdges.length; i += 1) { - const enterDate = dayjs(enterEdges[i].date); - const leaveDate = dayjs(leaveEdges[i].date); + const startDate = dayjs(enterEdges[0].date); + const endDate = dayjs(leaveEdges[leaveEdges.length - 1].date); - workingDays += Math.ceil(leaveDate.diff(enterDate, "day", true)); - } + // Determines if the time of the endDate is after the time of the startDate manually as there is no dayjs API for this + // In the case the start time is before the end time, we need to add one working day to accommodate for the start day + const currentDayIncluded = endDate.hour() > startDate.hour() || (endDate.hour() === startDate.hour() + && (endDate.minute() > startDate.minute() || (endDate.minute() === startDate.minute() + && (endDate.second() > startDate.second() || (endDate.second() === startDate.second() + && endDate.millisecond() > startDate.millisecond()))))); return { ...issue, - startDate: dayjs(enterEdges[0].date), - endDate: dayjs(leaveEdges[leaveEdges.length - 1].date), - workingDays, + startDate, + endDate, + workingDays: currentDayIncluded ? endDate.businessDiff(startDate) : endDate.businessDiff(startDate) + 1, }; }; -export const exportIssues = ( - issues: Issue[], - includedStatus: IssueStatus[], -) => { +export const exportIssues = (issues: ExportableIssue[]) => { const header = ["ID", "Name", "Start Date", "End Date", "Working days"]; const data = [header.map((h) => `"${h}"`).join(",")]; - const inProgressStatusNames = includedStatus - .filter((status) => status.statusCategory.name === StatusType.IN_PROGRESS) - .map((status) => status.name); - issues.forEach((issue) => { - const exportableIssue = addExportedTimeProperties( - issue, - inProgressStatusNames, - ); const exportedValues = [ - `"${exportableIssue.issueKey}"`, - `"${exportableIssue.summary}"`, - `${ - exportableIssue.startDate - ? `"${exportableIssue.startDate?.toISOString()}"` - : "" - }`, - `${ - exportableIssue.endDate - ? `"${exportableIssue.endDate?.toISOString()}"` - : "" - }`, - exportableIssue.workingDays, + `"${issue.issueKey}"`, + `"${issue.summary}"`, + `"${issue.startDate?.toISOString()}"`, + `"${issue.endDate?.toISOString()}"`, + issue.workingDays, ]; data.push(exportedValues.join(",")); diff --git a/yarn.lock b/yarn.lock index c700243c..f4a3ad9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3234,7 +3234,14 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -dayjs@^1.11.7: +dayjs-business-days2@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/dayjs-business-days2/-/dayjs-business-days2-1.2.2.tgz#eb3739ec011ca9d292f7e28caef0fa552735d0d3" + integrity sha512-tYwNKeMxuNEpGw2k5j/KTcH0c1lV+41wfqkTN21OvP2hwZFnpM4dH2biaOI2gElRmJOQQxkKByuH5bZPlea/Jg== + dependencies: + dayjs "^1.11.10" + +dayjs@^1.11.10, dayjs@^1.11.7: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==