Skip to content

Commit 6b9ef8f

Browse files
committed
feat: Issues priority
- Sort issues by priority - Show issues priority color - Forbid more than 24 hours as spent time - Reorganized some code
1 parent 25eccbd commit 6b9ef8f

File tree

10 files changed

+131
-25
lines changed

10 files changed

+131
-25
lines changed

src/api/redmine.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { formatISO } from "date-fns";
2-
import { TAccount, TCreateTimeEntry, TIssue, TSearchResult, TTimeEntry, TTimeEntryActivity } from "../types/redmine";
2+
import { TAccount, TCreateTimeEntry, TIssue, TSearchResult, TTimeEntry, TTimeEntryActivity, TissuesPriority } from "../types/redmine";
33
import instance from "./axios.config";
44

55
export const getMyAccount = async (): Promise<TAccount> => {
@@ -35,9 +35,15 @@ export const updateIssue = async (id: number, issue: Partial<Omit<TIssue, "id">>
3535
.then((res) => res.data);
3636
};
3737

38+
export const getIssuePriorities = async (): Promise<TissuesPriority[]> => {
39+
return instance.get("/enumerations/issue_priorities.json").then((res) => res.data.issue_priorities);
40+
};
41+
3842
// Time entries
3943
export const getAllMyTimeEntries = async (from: Date, to: Date, offset = 0, limit = 100): Promise<TTimeEntry[]> => {
40-
return instance.get(`/time_entries.json?offset=${offset}&limit=${limit}&user_id=me&from=${formatISO(from, { representation: "date" })}&to=${formatISO(to, { representation: "date" })}`).then((res) => res.data.time_entries);
44+
return instance
45+
.get(`/time_entries.json?offset=${offset}&limit=${limit}&user_id=me&from=${formatISO(from, { representation: "date" })}&to=${formatISO(to, { representation: "date" })}`)
46+
.then((res) => res.data.time_entries);
4147
};
4248

4349
export const createTimeEntry = async (entry: TCreateTimeEntry) => {

src/components/issues/CreateTimeEntryModal.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
22
import { AxiosError, isAxiosError } from "axios";
33
import { startOfDay } from "date-fns";
44
import { Field, Form, Formik, FormikProps } from "formik";
55
import { useEffect, useRef, useState } from "react";
66
import { FormattedMessage, useIntl } from "react-intl";
77
import * as Yup from "yup";
8-
import { createTimeEntry, getTimeEntryActivities, updateIssue } from "../../api/redmine";
8+
import { createTimeEntry, updateIssue } from "../../api/redmine";
99
import useSettings from "../../hooks/useSettings";
10+
import useTimeEntryActivities from "../../hooks/useTimeEntryActivities";
1011
import { TCreateTimeEntry, TIssue, TRedmineError } from "../../types/redmine";
1112
import { formatHours } from "../../utils/date";
1213
import Button from "../general/Button";
@@ -32,15 +33,11 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
3233

3334
const formik = useRef<FormikProps<TCreateTimeEntry>>(null);
3435

35-
const timeEntryActivitiesQuery = useQuery({
36-
queryKey: ["timeEntryActivities"],
37-
queryFn: getTimeEntryActivities,
38-
refetchOnWindowFocus: false,
39-
});
36+
const timeEntryActivities = useTimeEntryActivities();
4037

4138
useEffect(() => {
42-
formik.current?.setFieldValue("activity_id", timeEntryActivitiesQuery.data?.find((entry) => entry.is_default)?.id ?? undefined);
43-
}, [timeEntryActivitiesQuery.data]);
39+
formik.current?.setFieldValue("activity_id", timeEntryActivities.find((entry) => entry.is_default)?.id ?? undefined);
40+
}, [timeEntryActivities]);
4441

4542
const createTimeEntryMutation = useMutation({
4643
mutationFn: (entry: TCreateTimeEntry) => createTimeEntry(entry),
@@ -72,7 +69,8 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
7269
spent_on: Yup.date().max(new Date(), formatMessage({ id: "issues.modal.add-spent-time.date.validation.in-future" })),
7370
hours: Yup.number()
7471
.required(formatMessage({ id: "issues.modal.add-spent-time.hours.validation.required" }))
75-
.min(0.01, formatMessage({ id: "issues.modal.add-spent-time.hours.validation.greater-than-zero" })),
72+
.min(0.01, formatMessage({ id: "issues.modal.add-spent-time.hours.validation.greater-than-zero" }))
73+
.max(24, formatMessage({ id: "issues.modal.add-spent-time.hours.validation.less-than-24" })),
7674
activity_id: Yup.number().required(formatMessage({ id: "issues.modal.add-spent-time.activity.validation.required" })),
7775
})}
7876
onSubmit={async (values, { setSubmitting }) => {
@@ -116,10 +114,11 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
116114
placeholder={formatMessage({ id: "issues.modal.add-spent-time.hours" })}
117115
min="0"
118116
step="0.01"
117+
max="24"
119118
required
120119
as={InputField}
121120
size="sm"
122-
extraText={formatHours(values.hours) + " h"}
121+
extraText={values.hours >= 0 && values.hours <= 24 ? formatHours(values.hours) + " h" : undefined}
123122
error={touched.hours && errors.hours}
124123
autoComplete="off"
125124
/>
@@ -143,7 +142,7 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
143142
size="sm"
144143
error={touched.activity_id && errors.activity_id}
145144
>
146-
{timeEntryActivitiesQuery.data?.map((activity) => (
145+
{timeEntryActivities.map((activity) => (
147146
<>
148147
<option key={activity.id} value={activity.id}>
149148
{activity.name}

src/components/issues/Issue.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { faArrowUpRightFromSquare, faBan, faBookmark, faCircleUser, faEdit, faPa
33
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
44
import clsx from "clsx";
55
import { useRef, useState } from "react";
6-
import { FormattedMessage, useIntl } from "react-intl";
6+
import { FormattedMessage, PrimitiveType, useIntl } from "react-intl";
77
import { Tooltip } from "react-tooltip";
88
import useSettings from "../../hooks/useSettings";
99
import { TIssue } from "../../types/redmine";
10+
import { clsxm } from "../../utils/clsxm";
1011
import ContextMenu from "../general/ContextMenu";
1112
import KBD from "../general/KBD";
1213
import CreateTimeEntryModal from "./CreateTimeEntryModal";
@@ -22,14 +23,15 @@ export type IssueActions = {
2223

2324
type PropTypes = {
2425
issue: TIssue;
26+
priorityType: PrimitiveType;
2527
timerData: IssueTimerData;
2628
assignedToMe: boolean;
2729
pinned: boolean;
2830
remembered: boolean;
2931
} & Omit<TimerActions, "onDoneTimer"> &
3032
IssueActions;
3133

32-
const Issue = ({ issue, timerData, assignedToMe, pinned, remembered, onStart, onPause, onStop, onOverrideTime, onRemember, onForget, onPin, onUnpin }: PropTypes) => {
34+
const Issue = ({ issue, priorityType, timerData, assignedToMe, pinned, remembered, onStart, onPause, onStop, onOverrideTime, onRemember, onForget, onPin, onUnpin }: PropTypes) => {
3335
const { formatMessage } = useIntl();
3436

3537
const { settings } = useSettings();
@@ -108,9 +110,21 @@ const Issue = ({ issue, timerData, assignedToMe, pinned, remembered, onStart, on
108110
]}
109111
>
110112
<div
111-
className={clsx(
112-
"block w-full p-1 bg-white border border-gray-200 rounded-lg shadow-sm dark:shadow-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 relative",
113-
"focus:ring-4 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800"
113+
className={clsxm(
114+
"block w-full p-1 rounded-lg shadow-sm dark:shadow-gray-700 relative",
115+
"focus:ring-4 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
116+
"bg-white hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700",
117+
{
118+
//"bg-[#eaf7ff] border-[#add7f3] hover:bg-[#f2faff]": priorityType === "lowest",
119+
//"bg-white border-gray-200 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700": priorityType === "normal",
120+
//"bg-[#fee] border-[#fcc] hover:bg-[#fff2f2]": priorityType === "high",
121+
//"bg-[#ffc4c4] border-[#ffb4b4] hover:bg-[#ffd4d4]": priorityType === "higher" || priorityType === "highest",
122+
123+
"border-2 border-[#add7f3] dark:border-[#4973f3]/40": priorityType === "lowest",
124+
"border border-gray-200 dark:border-gray-700": priorityType === "normal",
125+
"border-2 border-[#fcc] dark:border-[#ff6868]/40": priorityType === "high",
126+
"border-2 border-[#ffb4b4] dark:border-[#ff5050]/40": priorityType === "higher" || priorityType === "highest",
127+
}
114128
)}
115129
tabIndex={1}
116130
/**
@@ -136,6 +150,9 @@ const Issue = ({ issue, timerData, assignedToMe, pinned, remembered, onStart, on
136150
className={clsx("mb-1 truncate", {
137151
"me-4": (pinned && assignedToMe) || (!pinned && !assignedToMe),
138152
"me-9": pinned && !assignedToMe,
153+
"text-[#559] dark:text-[#9393ed]": priorityType === "lowest",
154+
"text-[#900] dark:text-[#fa7070]": priorityType === "high" || priorityType === "higher",
155+
"text-[#900] dark:text-[#fa7070] font-bold": priorityType === "highest",
139156
})}
140157
>
141158
<a href={`${settings.redmineURL}/issues/${issue.id}`} target="_blank" tabIndex={-1} className="text-blue-500 hover:underline" data-tooltip-id={`tooltip-issue-${issue.id}`}>

src/components/issues/IssuesList.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
33
import { Fragment } from "react";
44
import { FormattedMessage } from "react-intl";
5+
import useIssuePriorities from "../../hooks/useIssuePriorities";
56
import useSettings from "../../hooks/useSettings";
67
import useStorage from "../../hooks/useStorage";
78
import { TAccount, TIssue, TReference } from "../../types/redmine";
@@ -27,12 +28,18 @@ type PropTypes = {
2728
const IssuesList = ({ account, issues: rawIssues, issuesData: { data: issuesData, setData: setIssuesData }, onSearchInProject }: PropTypes) => {
2829
const { settings } = useSettings();
2930

30-
const sortedIssues = rawIssues.sort((a, b) => {
31-
const pinnedA = issuesData[a.id]?.pinned;
32-
const pinnedB = issuesData[b.id]?.pinned;
33-
if (pinnedA && pinnedB) return new Date(a.updated_on).getTime() - new Date(a.updated_on).getTime();
34-
return pinnedA ? -1 : 1;
35-
});
31+
const issuePriorities = useIssuePriorities();
32+
33+
const issuePrioritiesIndices = issuePriorities.data.reduce((result, priority, index) => {
34+
result[priority.id] = index;
35+
return result;
36+
}, {} as Record<number, number>);
37+
const sortedIssues = rawIssues.sort(
38+
(a, b) =>
39+
(issuesData[b.id]?.pinned ? 1 : 0) - (issuesData[a.id]?.pinned ? 1 : 0) ||
40+
issuePrioritiesIndices[b.priority.id] - issuePrioritiesIndices[a.priority.id] ||
41+
new Date(a.updated_on).getTime() - new Date(a.updated_on).getTime()
42+
);
3643

3744
const groupedIssues = Object.values(
3845
sortedIssues.reduce(
@@ -84,6 +91,7 @@ const IssuesList = ({ account, issues: rawIssues, issuesData: { data: issuesData
8491
<Issue
8592
key={issue.id}
8693
issue={issue}
94+
priorityType={issuePriorities.getPriorityType(issue)}
8795
timerData={{ active: data.active, start: data.start, time: data.time }}
8896
assignedToMe={issue.assigned_to?.id === account?.id ?? false}
8997
pinned={data.pinned}

src/hooks/useIssuePriorities.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { getIssuePriorities } from "../api/redmine";
3+
import { TIssue } from "../types/redmine";
4+
5+
export type PriorityType = "highest" | "higher" | "high" | "normal" | "lowest";
6+
7+
const useIssuePriorities = () => {
8+
const issuePrioritiesQuery = useQuery({
9+
queryKey: ["issuePriorities"],
10+
queryFn: getIssuePriorities,
11+
refetchOnWindowFocus: false,
12+
});
13+
14+
const priorities = issuePrioritiesQuery.data?.filter((priority) => priority.active) ?? [];
15+
16+
/**
17+
* Find priority types
18+
*
19+
* Examples:
20+
* ["lowest", "normal", "normal", "normal/default", "high", "higher", "higher", "higher", "highest"]
21+
* ["low", "normal/default", "high", "higher", "highest"]
22+
* ["normal/default", "high", "highest"]
23+
* ["low", "normal/default", "highest"]
24+
*/
25+
const normal = priorities.find((p) => p.is_default);
26+
const normalIdx = priorities.findIndex((p) => p.is_default);
27+
const lowest = normalIdx > 0 ? priorities[0] : undefined;
28+
const high = normalIdx >= 0 && normalIdx < priorities.length - 2 ? priorities[normalIdx + 1] : undefined;
29+
const higher = normalIdx >= 0 ? priorities.slice(normalIdx + 2, priorities.length - 1) : [];
30+
const highest = normalIdx < priorities.length - 1 ? priorities[priorities.length - 1] : undefined;
31+
32+
const getPriorityType = (issue: TIssue): PriorityType => {
33+
switch (issue.priority.id) {
34+
case normal?.id:
35+
return "normal";
36+
case lowest?.id:
37+
return "lowest";
38+
case high?.id:
39+
return "high";
40+
case highest?.id:
41+
return "highest";
42+
default:
43+
if (higher.find((p) => p.id === issue.priority.id)) return "higher";
44+
}
45+
return "normal";
46+
};
47+
48+
return {
49+
data: priorities,
50+
getPriorityType,
51+
};
52+
};
53+
export default useIssuePriorities;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { getTimeEntryActivities } from "../api/redmine";
3+
4+
const useTimeEntryActivities = () => {
5+
const timeEntryActivitiesQuery = useQuery({
6+
queryKey: ["timeEntryActivities"],
7+
queryFn: getTimeEntryActivities,
8+
refetchOnWindowFocus: false,
9+
});
10+
return timeEntryActivitiesQuery.data?.filter((activity) => activity.active) ?? [];
11+
};
12+
export default useTimeEntryActivities;

src/lang/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"issues.modal.add-spent-time.hours": "Stunden",
4040
"issues.modal.add-spent-time.hours.validation.required": "Stunden wird benötigt",
4141
"issues.modal.add-spent-time.hours.validation.greater-than-zero": "Muss größer als 0 sein",
42+
"issues.modal.add-spent-time.hours.validation.less-than-24": "Muss weniger als ein Tag sein",
4243
"issues.modal.add-spent-time.comments": "Kommentar",
4344
"issues.modal.add-spent-time.activity": "Aktivität ",
4445
"issues.modal.add-spent-time.activity.validation.required": "Aktivität wird benötigt",

src/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"issues.modal.add-spent-time.hours": "Hours",
4040
"issues.modal.add-spent-time.hours.validation.required": "Hours is required",
4141
"issues.modal.add-spent-time.hours.validation.greater-than-zero": "Must be greater than 0",
42+
"issues.modal.add-spent-time.hours.validation.less-than-24": "Must be less than one day",
4243
"issues.modal.add-spent-time.comments": "Comments",
4344
"issues.modal.add-spent-time.activity": "Activity",
4445
"issues.modal.add-spent-time.activity.validation.required": "Activity is required",

src/types/redmine.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export type TIssue = {
2727
closed_on?: string;
2828
};
2929

30+
export type TissuesPriority = {
31+
id: number;
32+
name: string;
33+
is_default: boolean;
34+
active: boolean;
35+
};
36+
3037
export type TTimeEntry = {
3138
id: number;
3239
project: TReference;

tailwind.config.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/** @type {import('tailwindcss').Config} */
2+
// eslint-disable-next-line no-undef
23
module.exports = {
34
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
45
darkMode: "media",
@@ -22,5 +23,6 @@ module.exports = {
2223
},
2324
},
2425
},
26+
// eslint-disable-next-line no-undef
2527
plugins: [require("tailwindcss-animate")],
2628
};

0 commit comments

Comments
 (0)