Skip to content

Commit

Permalink
Revamp core calculations in issue export (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximilianruesch authored Jan 29, 2024
1 parent 97304a6 commit b465605
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 77 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 23 additions & 23 deletions src/components/CreateExport/CreateExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<unknown, unknown, Issue[]>({
queryKey: ["issues", project?.key],
Expand All @@ -34,29 +35,33 @@ export function CreateExportModal({
initialData: [],
});

const doneStatusNames = issueStatusByCategory[StatusType.DONE]?.map((s) => s.name) ?? [];

const [includedIssueTypes, setIncludedIssueTypes] = useState<string[]>([]);
const [includedIssueStatus, setIncludedIssueStatus] = useState<string[]>([]);
const [issuesToExport, setIssuesToExport] = useState<Issue[]>([]);
const [issuesToExport, setIssuesToExport] = useState<ExportableIssue[]>([]);
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(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"],
),
);
Expand Down Expand Up @@ -126,9 +131,9 @@ export function CreateExportModal({
<Text size="md" fw={450} mt="7%" mb="10%">
Include Issue Status
</Text>
{issueStatus && (
{doneIssueStatus && (
<CheckboxStack
data={issueStatus.map((status) => ({
data={doneIssueStatus.map((status) => ({
value: status.name,
label: status.name,
}))}
Expand All @@ -138,7 +143,7 @@ export function CreateExportModal({
</Stack>
<Stack align="center" mt="xs" w="40%">
<Text size="md" fw={450}>
Creation date range
In progress date range
</Text>
<DateInput
label="Start Date"
Expand Down Expand Up @@ -166,12 +171,7 @@ export function CreateExportModal({
<Button
ml="auto"
size="sm"
onClick={() => {
exportIssues(
issuesToExport,
issueStatus.filter((s) => includedIssueStatus.includes(s.name)),
);
}}
onClick={() => exportIssues(issuesToExport)}
>
Export CSV
</Button>
Expand Down
82 changes: 29 additions & 53 deletions src/components/CreateExport/exportHelper.ts
Original file line number Diff line number Diff line change
@@ -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<Issue, "startDate"> & {
startDate?: Dayjs,
endDate?: Dayjs,
dayjs.extend(dayjsBusinessDays);

export type ExportableIssue = Omit<Issue, "startDate"> & {
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) => {
Expand All @@ -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(","));
Expand Down
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down

0 comments on commit b465605

Please sign in to comment.