Skip to content

Commit 25eccbd

Browse files
committed
feat: Confirm save changes on cancel edit timer
1 parent e6dca00 commit 25eccbd

File tree

6 files changed

+174
-119
lines changed

6 files changed

+174
-119
lines changed

src/components/general/Button.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import clsx from "clsx";
2+
import React from "react";
3+
4+
interface PropTypes extends React.ComponentProps<"button"> {
5+
variant?: "primary" | "outline";
6+
size?: "sm" | "md";
7+
}
8+
9+
const Button = ({ variant = "primary", type = "button", size = "md", children, className, ...props }: PropTypes) => {
10+
return (
11+
<button
12+
type={type}
13+
className={clsx(
14+
"font-medium rounded-lg",
15+
{
16+
"text-xs px-3 py-2": size === "sm",
17+
"text-sm px-5 py-2.5": size === "md",
18+
"text-white bg-primary-700 hover:bg-primary-800 dark:bg-primary-600 dark:hover:bg-primary-700": variant === "primary",
19+
"text-primary-700 hover:bg-primary-700/20 dark:hover:bg-primary-700/30 ring-2": variant === "outline",
20+
},
21+
"focus:ring-4 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
22+
className
23+
)}
24+
{...props}
25+
>
26+
{children}
27+
</button>
28+
);
29+
};
30+
31+
export default Button;

src/components/issues/CreateTimeEntryModal.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { AxiosError, isAxiosError } from "axios";
3-
import clsx from "clsx";
43
import { startOfDay } from "date-fns";
54
import { Field, Form, Formik, FormikProps } from "formik";
65
import { useEffect, useRef, useState } from "react";
@@ -10,6 +9,7 @@ import { createTimeEntry, getTimeEntryActivities, updateIssue } from "../../api/
109
import useSettings from "../../hooks/useSettings";
1110
import { TCreateTimeEntry, TIssue, TRedmineError } from "../../types/redmine";
1211
import { formatHours } from "../../utils/date";
12+
import Button from "../general/Button";
1313
import DateField from "../general/DateField";
1414
import InputField from "../general/InputField";
1515
import Modal from "../general/Modal";
@@ -152,16 +152,9 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
152152
))}
153153
</Field>
154154

155-
<button
156-
type="submit"
157-
className={clsx(
158-
"text-white bg-primary-700 hover:bg-primary-800 font-medium rounded-lg text-sm px-5 py-1.5 mt-2 dark:bg-primary-600 dark:hover:bg-primary-700",
159-
"focus:ring-4 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800"
160-
)}
161-
disabled={isSubmitting}
162-
>
155+
<Button type="submit" disabled={isSubmitting}>
163156
<FormattedMessage id="issues.modal.add-spent-time.submit" />
164-
</button>
157+
</Button>
165158
</div>
166159
</Form>
167160
</>

src/components/issues/EditTimer.tsx

Lines changed: 127 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,151 @@
11
import clsx from "clsx";
22
import { useState } from "react";
3+
import { FormattedMessage, useIntl } from "react-intl";
34
import useHotKey from "../../hooks/useHotkey";
5+
import Button from "../general/Button";
6+
import Modal from "../general/Modal";
47

58
type PropTypes = {
69
initTime: number;
710
onOverrideTime: (time: number) => void;
811
onCancel: () => void;
912
};
1013

11-
const EditTimer = ({ initTime, onOverrideTime, onCancel }: PropTypes) => {
14+
const EditTimer = ({ initTime, onOverrideTime, onCancel: onConfirmCancel }: PropTypes) => {
15+
const { formatMessage } = useIntl();
16+
1217
const [h, setH] = useState(Math.floor(initTime / 1000 / 60 / 60).toString());
1318
const [m, setM] = useState(to2Digit(Math.floor((initTime / 1000 / 60) % 60)));
1419
const [s, setS] = useState(to2Digit(Math.floor((initTime / 1000) % 60)));
1520
const updatedTime = (Number(h) * 60 * 60 + Number(m) * 60 + Number(s)) * 1000;
1621

22+
const [confirmCancelModal, setConfirmCancelModal] = useState(false);
23+
const onCancel = () => setConfirmCancelModal(true);
1724
/**
1825
* On "Escape" => cancel
1926
*/
2027
useHotKey(onCancel, { key: "Escape" });
2128

2229
return (
23-
<div className="flex items-center gap-x-0">
24-
<input
25-
type="number"
26-
value={h}
27-
min={0}
28-
max={100}
29-
className={clsx(
30-
"text-lg rounded-md w-4 text-center appearance-none",
31-
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
32-
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
33-
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
34-
)}
35-
/**
36-
* auto focus & select input on focus
37-
*/
38-
autoFocus
39-
onFocus={(e) => e.target.select()}
40-
onChange={(e) => {
41-
const { value, min, max } = e.target;
42-
setH(Math.max(Number(min), Math.min(Number(max), Number(value))).toString());
43-
}}
44-
/**
45-
* On "Enter" => override time
46-
*/
47-
onKeyDown={(e) => {
48-
if (e.key === "Enter") {
49-
onOverrideTime(updatedTime);
50-
e.preventDefault();
51-
}
52-
}}
53-
/**
54-
* On loose focus, check if next target not a input => cancel
55-
*/
56-
onBlur={(e) => {
57-
if (!(e.relatedTarget?.localName === "input")) onCancel();
58-
}}
59-
/>
60-
:
61-
<input
62-
type="number"
63-
value={m}
64-
min={0}
65-
max={59}
66-
className={clsx(
67-
"text-lg rounded-md w-6 text-center appearance-none",
68-
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
69-
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
70-
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
71-
)}
72-
onChange={(e) => {
73-
const { value, min, max } = e.target;
74-
setM(to2Digit(Math.max(Number(min), Math.min(Number(max), Number(value)))));
75-
}}
76-
/**
77-
* On "Enter" => override time
78-
*/
79-
onKeyDown={(e) => {
80-
if (e.key === "Enter") {
81-
onOverrideTime(updatedTime);
82-
e.preventDefault();
83-
}
84-
}}
85-
/>
86-
:
87-
<input
88-
type="number"
89-
value={s}
90-
min={0}
91-
max={59}
92-
className={clsx(
93-
"text-lg rounded-md w-6 text-center appearance-none",
94-
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
95-
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
96-
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
97-
)}
98-
onChange={(e) => {
99-
const { value, min, max } = e.target;
100-
setS(to2Digit(Math.max(Number(min), Math.min(Number(max), Number(value)))));
101-
}}
102-
/**
103-
* On "Enter" => override time
104-
*/
105-
onKeyDown={(e) => {
106-
if (e.key === "Enter") {
107-
onOverrideTime(updatedTime);
108-
e.preventDefault();
109-
}
110-
}}
111-
/**
112-
* On loose focus, check if next target not a input => cancel
113-
*/
114-
onBlur={(e) => {
115-
if (!(e.relatedTarget?.localName === "input")) onCancel();
116-
}}
117-
/>
118-
</div>
30+
<>
31+
<div className="flex items-center gap-x-0">
32+
<input
33+
type="number"
34+
value={h}
35+
min={0}
36+
max={100}
37+
className={clsx(
38+
"text-lg rounded-md w-4 text-center appearance-none",
39+
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
40+
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
41+
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
42+
)}
43+
/**
44+
* auto focus & select input on focus
45+
*/
46+
autoFocus
47+
onFocus={(e) => e.target.select()}
48+
onChange={(e) => {
49+
const { value, min, max } = e.target;
50+
setH(Math.max(Number(min), Math.min(Number(max), Number(value))).toString());
51+
}}
52+
/**
53+
* On "Enter" => override time
54+
*/
55+
onKeyDown={(e) => {
56+
if (e.key === "Enter") {
57+
onOverrideTime(updatedTime);
58+
e.preventDefault();
59+
}
60+
}}
61+
/**
62+
* On loose focus, check if next target not a input => cancel
63+
*/
64+
onBlur={(e) => {
65+
if (!(e.relatedTarget?.localName === "input")) onCancel();
66+
}}
67+
/>
68+
:
69+
<input
70+
type="number"
71+
value={m}
72+
min={0}
73+
max={59}
74+
className={clsx(
75+
"text-lg rounded-md w-6 text-center appearance-none",
76+
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
77+
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
78+
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
79+
)}
80+
onChange={(e) => {
81+
const { value, min, max } = e.target;
82+
setM(to2Digit(Math.max(Number(min), Math.min(Number(max), Number(value)))));
83+
}}
84+
/**
85+
* On "Enter" => override time
86+
*/
87+
onKeyDown={(e) => {
88+
if (e.key === "Enter") {
89+
onOverrideTime(updatedTime);
90+
e.preventDefault();
91+
}
92+
}}
93+
/**
94+
* On loose focus, check if next target not a input => cancel
95+
*/
96+
onBlur={(e) => {
97+
if (!(e.relatedTarget?.localName === "input")) onCancel();
98+
}}
99+
/>
100+
:
101+
<input
102+
type="number"
103+
value={s}
104+
min={0}
105+
max={59}
106+
className={clsx(
107+
"text-lg rounded-md w-6 text-center appearance-none",
108+
"bg-gray-50 border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400",
109+
"focus:ring-2 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800",
110+
initTime > 0 ? "text-yellow-500" : "text-gray-700 dark:text-gray-500"
111+
)}
112+
onChange={(e) => {
113+
const { value, min, max } = e.target;
114+
setS(to2Digit(Math.max(Number(min), Math.min(Number(max), Number(value)))));
115+
}}
116+
/**
117+
* On "Enter" => override time
118+
*/
119+
onKeyDown={(e) => {
120+
if (e.key === "Enter") {
121+
onOverrideTime(updatedTime);
122+
e.preventDefault();
123+
}
124+
}}
125+
/**
126+
* On loose focus, check if next target not a input => cancel
127+
*/
128+
onBlur={(e) => {
129+
if (!(e.relatedTarget?.localName === "input")) onCancel();
130+
}}
131+
/>
132+
</div>
133+
{confirmCancelModal && (
134+
<Modal title={formatMessage({ id: "issues.modal.save-changes.title" })} onClose={onConfirmCancel}>
135+
<p className="mb-5">
136+
<FormattedMessage id="issues.modal.save-changes.message" />
137+
</p>
138+
<div className="flex justify-between items-end">
139+
<Button size="sm" variant="outline" onClick={onConfirmCancel}>
140+
<FormattedMessage id="issues.modal.save-changes.cancel" />
141+
</Button>
142+
<Button size="sm" onClick={() => onOverrideTime(updatedTime)} autoFocus>
143+
<FormattedMessage id="issues.modal.save-changes.save" />
144+
</Button>
145+
</div>
146+
</Modal>
147+
)}
148+
</>
119149
);
120150
};
121151

src/lang/de.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"issues.context-menu.open-in-redmine": "In Redmine öffnen",
1414
"issues.context-menu.timer.start": "Timer starten",
1515
"issues.context-menu.timer.pause": "Timer pausieren",
16-
"issues.context-menu.timer.stop": "Timer stoppen",
16+
"issues.context-menu.timer.stop": "Timer beenden",
1717
"issues.context-menu.timer.edit": "Timer bearbeiten",
1818
"issues.context-menu.pin": "Ticket anheften",
1919
"issues.context-menu.unpin": "Ticket lösen",
@@ -25,7 +25,7 @@
2525
"issues.timer.action.edit.tooltip": "Doppelklicken zum Bearbeiten",
2626
"issues.timer.action.start.tooltip": "Klicken, um Timer zu starten",
2727
"issues.timer.action.pause.tooltip": "Klicken, um Timer zu pausieren",
28-
"issues.timer.action.stop.tooltip": "Klicken, um Timer zu stoppen",
28+
"issues.timer.action.stop.tooltip": "Klicken, um Timer zu beenden",
2929
"issues.timer.action.add-spent-time.tooltip": "Klicken, um Zeitaufwand hinzuzufügen",
3030
"issues.info-tooltip.status": "Status",
3131
"issues.info-tooltip.priority": "Priorität",
@@ -43,6 +43,10 @@
4343
"issues.modal.add-spent-time.activity": "Aktivität ",
4444
"issues.modal.add-spent-time.activity.validation.required": "Aktivität wird benötigt",
4545
"issues.modal.add-spent-time.submit": "Erstellen",
46+
"issues.modal.save-changes.title": "Änderung speichern?",
47+
"issues.modal.save-changes.message": "Möchten Sie Ihre Änderungen speichern?",
48+
"issues.modal.save-changes.cancel": "Nein, abbrechen",
49+
"issues.modal.save-changes.save": "Ja, speichern",
4650
"issues.error.fail-to-load-issues": "Fehler beim Laden der Tickets",
4751

4852
"time.error.fail-to-load-data": "Fehler beim Laden der Daten",

src/lang/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
"issues.modal.add-spent-time.activity": "Activity",
4444
"issues.modal.add-spent-time.activity.validation.required": "Activity is required",
4545
"issues.modal.add-spent-time.submit": "Create",
46+
"issues.modal.save-changes.title": "Save changes?",
47+
"issues.modal.save-changes.message": "Do you want to save your changes?",
48+
"issues.modal.save-changes.cancel": "No, cancel",
49+
"issues.modal.save-changes.save": "Yes, Save",
4650
"issues.error.fail-to-load-issues": "Failed to load issues",
4751

4852
"time.error.fail-to-load-data": "Failed to load data",

src/pages/SettingsPage.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useQueryClient } from "@tanstack/react-query";
2-
import clsx from "clsx";
32
import { Field, Form, Formik, FormikProps } from "formik";
43
import { useEffect, useRef, useState } from "react";
54
import { FormattedMessage, useIntl } from "react-intl";
65
import * as Yup from "yup";
76
import { LANGUAGES } from "../IntlProvider";
7+
import Button from "../components/general/Button";
88
import CheckBox from "../components/general/CheckBox";
99
import Indicator from "../components/general/Indicator";
1010
import InputField from "../components/general/InputField";
@@ -170,16 +170,9 @@ const SettingsPage = () => {
170170
</div>
171171
</fieldset>
172172

173-
<button
174-
type="button"
175-
className={clsx(
176-
"text-white bg-primary-700 hover:bg-primary-800 font-medium rounded-lg text-sm px-5 py-2.5 mt-2 dark:bg-primary-600 dark:hover:bg-primary-700",
177-
"focus:ring-4 focus:ring-primary-300 focus:outline-none dark:focus:ring-primary-800"
178-
)}
179-
onClick={submitForm}
180-
>
173+
<Button onClick={submitForm} className="mt-2">
181174
<FormattedMessage id="settings.save-settings" />
182-
</button>
175+
</Button>
183176
</div>
184177
</Form>
185178
</>

0 commit comments

Comments
 (0)