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

Revamp core calculations in issue export #128

Merged
merged 4 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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