@oneuptime/common 8.0.5289 → 8.0.5300
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +535 -0
- package/UI/Components/DuplicateModel/DuplicateModel.tsx +4 -2
- package/UI/Components/Forms/Fields/FormField.tsx +26 -0
- package/UI/Components/Input/Input.tsx +4 -21
- package/UI/Components/TimePicker/Index.ts +3 -0
- package/UI/Components/TimePicker/TimePicker.tsx +476 -0
- package/build/dist/Tests/UI/Components/TimePicker/TimePicker.test.js +358 -0
- package/build/dist/Tests/UI/Components/TimePicker/TimePicker.test.js.map +1 -0
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js +3 -2
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +4 -23
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/Index.js +3 -0
- package/build/dist/UI/Components/TimePicker/Index.js.map +1 -0
- package/build/dist/UI/Components/TimePicker/TimePicker.js +218 -0
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -0
- package/jest.config.json +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
/*
|
|
3
|
+
* Ensure deterministic timezone for Date#getHours() etc.
|
|
4
|
+
* This must be set before importing the component under test.
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line no-undef
|
|
7
|
+
process.env.TZ = "UTC";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { render, screen, within } from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
import "@testing-library/jest-dom";
|
|
12
|
+
import TimePicker from "../../../../UI/Components/TimePicker/TimePicker";
|
|
13
|
+
import DateUtilities from "../../../../Types/Date";
|
|
14
|
+
|
|
15
|
+
type DateModule = typeof import("../../../../Types/Date");
|
|
16
|
+
type DateLib = DateModule["default"];
|
|
17
|
+
type MockedDateLib = jest.Mocked<DateLib>;
|
|
18
|
+
type UserEventInstance = ReturnType<typeof userEvent.setup>;
|
|
19
|
+
type ChangeHandler = jest.Mock<void, [string | undefined]>;
|
|
20
|
+
type VoidHandler = jest.Mock<void, []>;
|
|
21
|
+
type DialogElement = HTMLElement;
|
|
22
|
+
type ButtonElement = HTMLButtonElement;
|
|
23
|
+
type InputElement = HTMLInputElement;
|
|
24
|
+
type HourMinuteMock = {
|
|
25
|
+
getHours: () => number;
|
|
26
|
+
getMinutes: () => number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Mock OneUptimeDate utilities used by the component
|
|
30
|
+
jest.mock("../../../../Types/Date", () => {
|
|
31
|
+
const real: DateModule = jest.requireActual("../../../../Types/Date");
|
|
32
|
+
// Helper to create a minimal date-like object with getHours/getMinutes
|
|
33
|
+
const makeHM: (h: number, m: number) => HourMinuteMock = (
|
|
34
|
+
h: number,
|
|
35
|
+
m: number,
|
|
36
|
+
): HourMinuteMock => {
|
|
37
|
+
return {
|
|
38
|
+
getHours: () => {
|
|
39
|
+
return h;
|
|
40
|
+
},
|
|
41
|
+
getMinutes: () => {
|
|
42
|
+
return m;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: {
|
|
49
|
+
...real.default,
|
|
50
|
+
getUserPrefers12HourFormat: jest.fn(() => {
|
|
51
|
+
return false;
|
|
52
|
+
}), // default to 24h; tests can override per test
|
|
53
|
+
getCurrentDate: jest.fn(() => {
|
|
54
|
+
return makeHM(13, 45) as unknown as Date;
|
|
55
|
+
}),
|
|
56
|
+
fromString: jest.fn((v: string | Date) => {
|
|
57
|
+
if (!v) {
|
|
58
|
+
return undefined as unknown as Date;
|
|
59
|
+
}
|
|
60
|
+
if (typeof v === "string") {
|
|
61
|
+
const match: RegExpMatchArray | null = v.match(/T(\d{2}):(\d{2})/);
|
|
62
|
+
const hh: number = match ? parseInt(match[1] as string, 10) : 0;
|
|
63
|
+
const mm: number = match ? parseInt(match[2] as string, 10) : 0;
|
|
64
|
+
return makeHM(hh, mm) as unknown as Date;
|
|
65
|
+
}
|
|
66
|
+
// If a Date instance is provided, prefer UTC to avoid env timezone
|
|
67
|
+
const d: Date = v;
|
|
68
|
+
const hasUtcHours: (() => number) | undefined = (
|
|
69
|
+
d as { getUTCHours?: () => number }
|
|
70
|
+
).getUTCHours;
|
|
71
|
+
const hasUtcMinutes: (() => number) | undefined = (
|
|
72
|
+
d as { getUTCMinutes?: () => number }
|
|
73
|
+
).getUTCMinutes;
|
|
74
|
+
const hh: number = hasUtcHours ? hasUtcHours.call(d) : d.getHours();
|
|
75
|
+
const mm: number = hasUtcMinutes
|
|
76
|
+
? hasUtcMinutes.call(d)
|
|
77
|
+
: d.getMinutes();
|
|
78
|
+
return makeHM(hh, mm) as unknown as Date;
|
|
79
|
+
}),
|
|
80
|
+
toString: jest.fn((d: Date) => {
|
|
81
|
+
return d.toISOString();
|
|
82
|
+
}),
|
|
83
|
+
getDateWithCustomTime: jest.fn(
|
|
84
|
+
({
|
|
85
|
+
hours,
|
|
86
|
+
minutes,
|
|
87
|
+
}: {
|
|
88
|
+
hours: number;
|
|
89
|
+
minutes: number;
|
|
90
|
+
seconds?: number;
|
|
91
|
+
}) => {
|
|
92
|
+
const base: Date = new Date("2024-05-15T00:00:00.000Z");
|
|
93
|
+
base.setUTCHours(hours, minutes, 0, 0);
|
|
94
|
+
return base;
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
getCurrentTimezoneString: jest.fn(() => {
|
|
98
|
+
return "UTC";
|
|
99
|
+
}),
|
|
100
|
+
getCurrentTimezone: jest.fn(() => {
|
|
101
|
+
return "Etc/UTC";
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Mock Icon to avoid SVG complexity
|
|
108
|
+
jest.mock("../../../../UI/Components/Icon/Icon", () => {
|
|
109
|
+
return {
|
|
110
|
+
__esModule: true,
|
|
111
|
+
default: ({ className }: { className?: string }) => {
|
|
112
|
+
return <i data-testid="icon" className={className} />;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Mock Modal to render children immediately and expose submit/close
|
|
118
|
+
jest.mock("../../../../UI/Components/Modal/Modal", () => {
|
|
119
|
+
return {
|
|
120
|
+
__esModule: true,
|
|
121
|
+
default: ({
|
|
122
|
+
title,
|
|
123
|
+
description,
|
|
124
|
+
onClose,
|
|
125
|
+
onSubmit,
|
|
126
|
+
children,
|
|
127
|
+
submitButtonText,
|
|
128
|
+
}: any) => {
|
|
129
|
+
return (
|
|
130
|
+
<div role="dialog" aria-label={title}>
|
|
131
|
+
<div>{description}</div>
|
|
132
|
+
<div>{children}</div>
|
|
133
|
+
<button onClick={onSubmit}>{submitButtonText ?? "Apply"}</button>
|
|
134
|
+
<button onClick={onClose}>Close</button>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
ModalWidth: { Medium: "Medium" },
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const getDateLib: () => MockedDateLib = () => {
|
|
143
|
+
return DateUtilities as MockedDateLib;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
describe("TimePicker", () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
// Do not reset implementations provided by jest.mock factory; only clear call history
|
|
149
|
+
jest.clearAllMocks();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders in 24h by default and shows current time", () => {
|
|
153
|
+
const onChange: ChangeHandler = jest.fn();
|
|
154
|
+
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
|
155
|
+
|
|
156
|
+
// Should display HH:mm based on value prop
|
|
157
|
+
expect(screen.getByLabelText("Hours")).toHaveValue("08");
|
|
158
|
+
expect(screen.getByLabelText("Minutes")).toHaveValue("05");
|
|
159
|
+
|
|
160
|
+
// AM/PM buttons are not shown in 24h
|
|
161
|
+
expect(
|
|
162
|
+
screen.queryByRole("button", { name: "AM" }),
|
|
163
|
+
).not.toBeInTheDocument();
|
|
164
|
+
expect(
|
|
165
|
+
screen.queryByRole("button", { name: "PM" }),
|
|
166
|
+
).not.toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("opens modal on click when enabled", async () => {
|
|
170
|
+
const user: UserEventInstance = userEvent.setup();
|
|
171
|
+
render(<TimePicker value="2024-05-15T10:20:00.000Z" />);
|
|
172
|
+
|
|
173
|
+
// Click the field container by clicking on hours input
|
|
174
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
175
|
+
|
|
176
|
+
// Modal should appear
|
|
177
|
+
expect(
|
|
178
|
+
screen.getByRole("dialog", { name: "Select time" }),
|
|
179
|
+
).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByText(/your UTC/i)).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("does not open modal when readOnly or disabled", async () => {
|
|
184
|
+
const user: UserEventInstance = userEvent.setup();
|
|
185
|
+
const { rerender } = render(
|
|
186
|
+
<TimePicker value="2024-05-15T10:20:00.000Z" readOnly />,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
190
|
+
expect(
|
|
191
|
+
screen.queryByRole("dialog", { name: "Select time" }),
|
|
192
|
+
).not.toBeInTheDocument();
|
|
193
|
+
|
|
194
|
+
rerender(<TimePicker value="2024-05-15T10:20:00.000Z" disabled />);
|
|
195
|
+
await user.click(screen.getByLabelText("Minutes"));
|
|
196
|
+
expect(
|
|
197
|
+
screen.queryByRole("dialog", { name: "Select time" }),
|
|
198
|
+
).not.toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("applies changes from modal and emits ISO via onChange (24h)", async () => {
|
|
202
|
+
const user: UserEventInstance = userEvent.setup();
|
|
203
|
+
const onChange: ChangeHandler = jest.fn();
|
|
204
|
+
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
|
205
|
+
|
|
206
|
+
// Open modal
|
|
207
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
208
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
209
|
+
name: "Select time",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Increase hours and minutes using the chevrons
|
|
213
|
+
const incHour: ButtonElement = within(dialog).getByLabelText(
|
|
214
|
+
"Increase hours",
|
|
215
|
+
) as HTMLButtonElement;
|
|
216
|
+
const incMin: ButtonElement = within(dialog).getByLabelText(
|
|
217
|
+
"Increase minutes",
|
|
218
|
+
) as HTMLButtonElement;
|
|
219
|
+
|
|
220
|
+
await user.click(incHour); // 08 -> 09
|
|
221
|
+
await user.click(incMin); // 05 -> 06
|
|
222
|
+
|
|
223
|
+
// Apply
|
|
224
|
+
await user.click(
|
|
225
|
+
within(dialog).getByRole("button", {
|
|
226
|
+
name: "Apply",
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// onChange should be called with ISO string
|
|
231
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
232
|
+
const emittedCall: [string | undefined] | undefined =
|
|
233
|
+
onChange.mock.calls[0];
|
|
234
|
+
expect(emittedCall).toBeDefined();
|
|
235
|
+
const emitted: string = (emittedCall as [string])[0];
|
|
236
|
+
expect(typeof emitted).toBe("string");
|
|
237
|
+
|
|
238
|
+
const lib: MockedDateLib = getDateLib();
|
|
239
|
+
// getDateWithCustomTime uses UTC hours in our mock; 9:06 maps to 09:06:00Z on the chosen date
|
|
240
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
241
|
+
hours: 9,
|
|
242
|
+
minutes: 6,
|
|
243
|
+
seconds: 0,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("supports decrement wrapping for hours and minutes (24h)", async () => {
|
|
248
|
+
const user: UserEventInstance = userEvent.setup();
|
|
249
|
+
const onChange: ChangeHandler = jest.fn();
|
|
250
|
+
render(<TimePicker value="2024-05-15T00:00:00.000Z" onChange={onChange} />);
|
|
251
|
+
|
|
252
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
253
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
254
|
+
name: "Select time",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const decHour: ButtonElement = within(dialog).getByLabelText(
|
|
258
|
+
"Decrease hours",
|
|
259
|
+
) as HTMLButtonElement;
|
|
260
|
+
const decMin: ButtonElement = within(dialog).getByLabelText(
|
|
261
|
+
"Decrease minutes",
|
|
262
|
+
) as HTMLButtonElement;
|
|
263
|
+
|
|
264
|
+
// Minutes 00 -> 59 and hours 00 -> 23 when decreasing
|
|
265
|
+
await user.click(decMin);
|
|
266
|
+
await user.click(decHour);
|
|
267
|
+
|
|
268
|
+
await user.click(
|
|
269
|
+
within(dialog).getByRole("button", {
|
|
270
|
+
name: "Apply",
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const lib: MockedDateLib = getDateLib();
|
|
275
|
+
// dec minute first -> 00 -> 59, hours 0->23, then dec hour -> 22
|
|
276
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
277
|
+
hours: 22,
|
|
278
|
+
minutes: 59,
|
|
279
|
+
seconds: 0,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("renders and operates in 12h mode with AM/PM toggles", async () => {
|
|
284
|
+
const user: UserEventInstance = userEvent.setup();
|
|
285
|
+
const lib: MockedDateLib = getDateLib();
|
|
286
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
|
287
|
+
|
|
288
|
+
const onChange: ChangeHandler = jest.fn();
|
|
289
|
+
render(<TimePicker value="2024-05-15T13:45:00.000Z" onChange={onChange} />);
|
|
290
|
+
|
|
291
|
+
// Displays 01:45 PM
|
|
292
|
+
expect(screen.getByLabelText("Hours")).toHaveValue("01");
|
|
293
|
+
const apButtons: HTMLElement[] = screen.getAllByRole("button", {
|
|
294
|
+
name: "Open time selector for AM/PM",
|
|
295
|
+
});
|
|
296
|
+
expect(apButtons).toHaveLength(2);
|
|
297
|
+
|
|
298
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
299
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
300
|
+
name: "Select time",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Modal description should reflect 12h mode
|
|
304
|
+
expect(
|
|
305
|
+
within(dialog).getByText(/choose hours, minutes, and AM\/PM/i),
|
|
306
|
+
).toBeInTheDocument();
|
|
307
|
+
|
|
308
|
+
// Toggle to AM and change hour input to 12 to map to 00
|
|
309
|
+
await user.click(
|
|
310
|
+
within(dialog).getByRole("button", { name: /^AM$/ }) as HTMLButtonElement,
|
|
311
|
+
);
|
|
312
|
+
const hourInput: InputElement = within(dialog).getByLabelText(
|
|
313
|
+
"Hours",
|
|
314
|
+
) as InputElement;
|
|
315
|
+
// Change to 12
|
|
316
|
+
await user.clear(hourInput);
|
|
317
|
+
await user.type(hourInput, "12");
|
|
318
|
+
|
|
319
|
+
await user.click(
|
|
320
|
+
within(dialog).getByRole("button", {
|
|
321
|
+
name: "Apply",
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Should map to hours 0 in 24h
|
|
326
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
327
|
+
hours: 0,
|
|
328
|
+
minutes: 45,
|
|
329
|
+
seconds: 0,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("AM/PM button mapping inside modal", async () => {
|
|
334
|
+
const user: UserEventInstance = userEvent.setup();
|
|
335
|
+
const lib: MockedDateLib = getDateLib();
|
|
336
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
|
337
|
+
|
|
338
|
+
render(<TimePicker value="2024-05-15T01:10:00.000Z" />);
|
|
339
|
+
|
|
340
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
341
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
342
|
+
name: "Select time",
|
|
343
|
+
});
|
|
344
|
+
// Click PM, should add 12 hours (1 -> 13)
|
|
345
|
+
await user.click(
|
|
346
|
+
within(dialog).getByRole("button", { name: /^PM$/ }) as HTMLButtonElement,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Increase minutes to 11 to ensure state changed
|
|
350
|
+
await user.click(
|
|
351
|
+
within(dialog).getByLabelText("Increase minutes") as HTMLButtonElement,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await user.click(
|
|
355
|
+
within(dialog).getByRole("button", {
|
|
356
|
+
name: "Apply",
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
361
|
+
hours: 13,
|
|
362
|
+
minutes: 11,
|
|
363
|
+
seconds: 0,
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("quick minutes buttons set minutes", async () => {
|
|
368
|
+
const user: UserEventInstance = userEvent.setup();
|
|
369
|
+
const onChange: ChangeHandler = jest.fn();
|
|
370
|
+
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
|
371
|
+
|
|
372
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
373
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
374
|
+
name: "Select time",
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await user.click(
|
|
378
|
+
within(dialog).getByRole("button", { name: "05" }) as HTMLButtonElement,
|
|
379
|
+
);
|
|
380
|
+
await user.click(
|
|
381
|
+
within(dialog).getByRole("button", {
|
|
382
|
+
name: "Apply",
|
|
383
|
+
}) as HTMLButtonElement,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const lib: MockedDateLib = getDateLib();
|
|
387
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
388
|
+
hours: 8,
|
|
389
|
+
minutes: 5,
|
|
390
|
+
seconds: 0,
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("respects placeholder in 24h and 12h modes", () => {
|
|
395
|
+
const lib: MockedDateLib = getDateLib();
|
|
396
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(false);
|
|
397
|
+
const { unmount } = render(<TimePicker placeholder="HH" />);
|
|
398
|
+
expect(screen.getByLabelText("Hours")).toHaveAttribute("placeholder", "HH");
|
|
399
|
+
|
|
400
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
|
401
|
+
unmount();
|
|
402
|
+
render(<TimePicker />);
|
|
403
|
+
expect(screen.getByLabelText("Hours")).toHaveAttribute("placeholder", "hh");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("shows error icon and message when error prop is set", () => {
|
|
407
|
+
render(<TimePicker error="Required" />);
|
|
408
|
+
|
|
409
|
+
expect(screen.getByTestId("error-message")).toHaveTextContent("Required");
|
|
410
|
+
// Error icon rendered
|
|
411
|
+
expect(
|
|
412
|
+
screen.getAllByTestId("icon").some((iconEl: HTMLElement) => {
|
|
413
|
+
return iconEl.className?.includes("text-red-500");
|
|
414
|
+
}),
|
|
415
|
+
).toBeTruthy();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("calls onFocus and onBlur from the hours input", async () => {
|
|
419
|
+
const user: UserEventInstance = userEvent.setup();
|
|
420
|
+
const onFocus: VoidHandler = jest.fn();
|
|
421
|
+
const onBlur: VoidHandler = jest.fn();
|
|
422
|
+
|
|
423
|
+
render(<TimePicker onFocus={onFocus} onBlur={onBlur} />);
|
|
424
|
+
|
|
425
|
+
const hours: InputElement = screen.getByLabelText("Hours") as InputElement;
|
|
426
|
+
await user.click(hours);
|
|
427
|
+
expect(onFocus).toHaveBeenCalled();
|
|
428
|
+
|
|
429
|
+
hours.blur();
|
|
430
|
+
expect(onBlur).toHaveBeenCalled();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("updates when value prop changes", () => {
|
|
434
|
+
// Force 24h mode for this test to avoid bleed from prior tests
|
|
435
|
+
const lib: MockedDateLib = getDateLib();
|
|
436
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(false);
|
|
437
|
+
|
|
438
|
+
const { rerender } = render(
|
|
439
|
+
<TimePicker value="2024-05-15T02:03:00.000Z" />,
|
|
440
|
+
);
|
|
441
|
+
expect(screen.getByLabelText("Hours")).toHaveValue("02");
|
|
442
|
+
expect(screen.getByLabelText("Minutes")).toHaveValue("03");
|
|
443
|
+
|
|
444
|
+
rerender(<TimePicker value="2024-05-15T21:59:00.000Z" />);
|
|
445
|
+
expect(screen.getByLabelText("Hours")).toHaveValue("21");
|
|
446
|
+
expect(screen.getByLabelText("Minutes")).toHaveValue("59");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("clamps and maps hour text edits inside modal for 12h", async () => {
|
|
450
|
+
const user: UserEventInstance = userEvent.setup();
|
|
451
|
+
const lib: MockedDateLib = getDateLib();
|
|
452
|
+
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
|
453
|
+
|
|
454
|
+
render(<TimePicker value="2024-05-15T12:00:00.000Z" />);
|
|
455
|
+
|
|
456
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
457
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
458
|
+
name: "Select time",
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const hourInput: InputElement = within(dialog).getByLabelText(
|
|
462
|
+
"Hours",
|
|
463
|
+
) as InputElement;
|
|
464
|
+
await user.clear(hourInput);
|
|
465
|
+
await user.type(hourInput, "99"); // should clamp to 12 in 12h mode
|
|
466
|
+
|
|
467
|
+
await user.click(
|
|
468
|
+
within(dialog).getByRole("button", {
|
|
469
|
+
name: "Apply",
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// 12 PM stays 12 (i.e., 12 in 24h), with minutes from initial value 00
|
|
474
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
475
|
+
hours: 12,
|
|
476
|
+
minutes: 0,
|
|
477
|
+
seconds: 0,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("minute text edits clamp to 0-59", async () => {
|
|
482
|
+
const user: UserEventInstance = userEvent.setup();
|
|
483
|
+
render(<TimePicker value="2024-05-15T10:10:00.000Z" />);
|
|
484
|
+
|
|
485
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
486
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
487
|
+
name: "Select time",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const minInput: InputElement = within(dialog).getByLabelText(
|
|
491
|
+
"Minutes",
|
|
492
|
+
) as InputElement;
|
|
493
|
+
await user.clear(minInput);
|
|
494
|
+
await user.type(minInput, "99");
|
|
495
|
+
|
|
496
|
+
await user.click(
|
|
497
|
+
within(dialog).getByRole("button", {
|
|
498
|
+
name: "Apply",
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const lib: MockedDateLib = getDateLib();
|
|
503
|
+
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
|
504
|
+
hours: 10,
|
|
505
|
+
minutes: 59,
|
|
506
|
+
seconds: 0,
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("modal Close does not emit change or update main display", async () => {
|
|
511
|
+
const user: UserEventInstance = userEvent.setup();
|
|
512
|
+
const onChange: ChangeHandler = jest.fn();
|
|
513
|
+
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
|
514
|
+
|
|
515
|
+
// Open modal, change something, then close
|
|
516
|
+
await user.click(screen.getByLabelText("Hours"));
|
|
517
|
+
const dialog: DialogElement = screen.getByRole("dialog", {
|
|
518
|
+
name: "Select time",
|
|
519
|
+
});
|
|
520
|
+
await user.click(
|
|
521
|
+
within(dialog).getByLabelText("Increase hours") as HTMLButtonElement,
|
|
522
|
+
);
|
|
523
|
+
await user.click(
|
|
524
|
+
within(dialog).getByRole("button", {
|
|
525
|
+
name: "Close",
|
|
526
|
+
}) as HTMLButtonElement,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// No onChange called
|
|
530
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
531
|
+
|
|
532
|
+
// Still shows original value
|
|
533
|
+
expect(screen.getByLabelText("Hours")).toHaveValue("08");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -17,7 +17,7 @@ import Select from "../../../Types/BaseDatabase/Select";
|
|
|
17
17
|
export interface ComponentProps<TBaseModel extends BaseModel> {
|
|
18
18
|
modelType: { new (): TBaseModel };
|
|
19
19
|
modelId: ObjectID;
|
|
20
|
-
onDuplicateSuccess?: (item: TBaseModel) => void |
|
|
20
|
+
onDuplicateSuccess?: (item: TBaseModel) => Promise<void> | void;
|
|
21
21
|
fieldsToDuplicate: Select<TBaseModel>;
|
|
22
22
|
fieldsToChange: Array<ModelField<TBaseModel>>;
|
|
23
23
|
navigateToOnSuccess?: Route | undefined;
|
|
@@ -78,7 +78,9 @@ const DuplicateModel: <TBaseModel extends BaseModel>(
|
|
|
78
78
|
throw new Error(`Could not create ${model.singularName}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
props.onDuplicateSuccess
|
|
81
|
+
if (props.onDuplicateSuccess) {
|
|
82
|
+
await props.onDuplicateSuccess(newItem.data);
|
|
83
|
+
}
|
|
82
84
|
|
|
83
85
|
if (props.navigateToOnSuccess) {
|
|
84
86
|
Navigation.navigate(
|
|
@@ -9,6 +9,7 @@ import DictionaryForm from "../../Dictionary/Dictionary";
|
|
|
9
9
|
import Dropdown, { DropdownValue } from "../../Dropdown/Dropdown";
|
|
10
10
|
import FilePicker from "../../FilePicker/FilePicker";
|
|
11
11
|
import Input, { InputType } from "../../Input/Input";
|
|
12
|
+
import TimePicker from "../../TimePicker/Index";
|
|
12
13
|
import Link from "../../Link/Link";
|
|
13
14
|
import Modal from "../../Modal/Modal";
|
|
14
15
|
import IDGenerator from "../../ObjectID/IDGenerator";
|
|
@@ -280,6 +281,31 @@ const FormField: <T extends GenericObject>(
|
|
|
280
281
|
)}
|
|
281
282
|
|
|
282
283
|
<div className="mt-2">
|
|
284
|
+
{/* Time Picker */}
|
|
285
|
+
{props.field.fieldType === FormFieldSchemaType.Time && (
|
|
286
|
+
<TimePicker
|
|
287
|
+
autoFocus={!props.disableAutofocus && index === 1}
|
|
288
|
+
tabIndex={index}
|
|
289
|
+
disabled={props.isDisabled || props.field.disabled}
|
|
290
|
+
error={props.touched && props.error ? props.error : undefined}
|
|
291
|
+
dataTestId={props.field.dataTestId}
|
|
292
|
+
onChange={async (value: string) => {
|
|
293
|
+
onChange(value);
|
|
294
|
+
props.setFieldValue(props.fieldName, value);
|
|
295
|
+
}}
|
|
296
|
+
onBlur={async () => {
|
|
297
|
+
props.setFieldTouched(props.fieldName, true);
|
|
298
|
+
}}
|
|
299
|
+
value={
|
|
300
|
+
(props.currentValues &&
|
|
301
|
+
(props.currentValues as any)[props.fieldName]) ||
|
|
302
|
+
(props.field.defaultValue as any) ||
|
|
303
|
+
undefined
|
|
304
|
+
}
|
|
305
|
+
placeholder={props.field.placeholder || undefined}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
|
|
283
309
|
{props.field.fieldType === FormFieldSchemaType.Color && (
|
|
284
310
|
<ColorPicker
|
|
285
311
|
error={props.touched && props.error ? props.error : undefined}
|
|
@@ -70,17 +70,13 @@ const Input: FunctionComponent<ComponentProps> = (
|
|
|
70
70
|
useEffect(() => {
|
|
71
71
|
if (
|
|
72
72
|
props.type === InputType.DATE ||
|
|
73
|
-
props.type === InputType.DATETIME_LOCAL
|
|
74
|
-
props.type === InputType.TIME
|
|
73
|
+
props.type === InputType.DATETIME_LOCAL
|
|
75
74
|
) {
|
|
76
75
|
if (value && (value as unknown) instanceof Date) {
|
|
77
76
|
let dateString: string = "";
|
|
78
77
|
try {
|
|
79
78
|
if (props.type === InputType.DATETIME_LOCAL) {
|
|
80
79
|
dateString = OneUptimeDate.toDateTimeLocalString(value as any);
|
|
81
|
-
} else if (props.type === InputType.TIME) {
|
|
82
|
-
// get time from date
|
|
83
|
-
dateString = OneUptimeDate.toTimeString(value as any);
|
|
84
80
|
} else {
|
|
85
81
|
dateString = OneUptimeDate.asDateForDatabaseQuery(value);
|
|
86
82
|
}
|
|
@@ -99,9 +95,6 @@ const Input: FunctionComponent<ComponentProps> = (
|
|
|
99
95
|
try {
|
|
100
96
|
if (props.type === InputType.DATETIME_LOCAL) {
|
|
101
97
|
dateString = OneUptimeDate.toDateTimeLocalString(date);
|
|
102
|
-
} else if (props.type === InputType.TIME) {
|
|
103
|
-
// get time from date
|
|
104
|
-
dateString = OneUptimeDate.toTimeString(value as any);
|
|
105
98
|
} else {
|
|
106
99
|
dateString = OneUptimeDate.asDateForDatabaseQuery(date);
|
|
107
100
|
}
|
|
@@ -161,25 +154,15 @@ const Input: FunctionComponent<ComponentProps> = (
|
|
|
161
154
|
onFocus={props.onFocus}
|
|
162
155
|
onClick={props.onClick}
|
|
163
156
|
data-testid={props.dataTestId}
|
|
164
|
-
spellCheck={!props.disableSpellCheck
|
|
157
|
+
spellCheck={!props.disableSpellCheck}
|
|
165
158
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
166
|
-
|
|
159
|
+
const value: string | Date = e.target.value;
|
|
167
160
|
|
|
168
161
|
if (
|
|
169
162
|
(props.type === InputType.DATE ||
|
|
170
|
-
props.type === InputType.DATETIME_LOCAL
|
|
171
|
-
props.type === InputType.TIME) &&
|
|
163
|
+
props.type === InputType.DATETIME_LOCAL) &&
|
|
172
164
|
value
|
|
173
165
|
) {
|
|
174
|
-
if (props.type === InputType.TIME) {
|
|
175
|
-
// conver value like "16:00" to date with local timezone
|
|
176
|
-
value = OneUptimeDate.getDateWithCustomTime({
|
|
177
|
-
hours: parseInt(value.split(":")[0]?.toString() || "0"),
|
|
178
|
-
minutes: parseInt(value.split(":")[1]?.toString() || "0"),
|
|
179
|
-
seconds: 0,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
166
|
const date: Date = OneUptimeDate.fromString(value);
|
|
184
167
|
const dateString: string = OneUptimeDate.toString(date);
|
|
185
168
|
setValue(dateString);
|