@remit/web-client 0.0.286 → 0.0.288
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/package.json +1 -1
- package/src/auth/SessionRateLimited.stories.tsx +1 -1
- package/src/components/calendar/AgendaStrip.stories.tsx +1 -1
- package/src/components/calendar/AgendaView.stories.tsx +1 -1
- package/src/components/calendar/CalendarComposePane.stories.tsx +18 -1
- package/src/components/calendar/CalendarEventPane.stories.tsx +1 -1
- package/src/components/calendar/CalendarShell.stories.tsx +1 -1
- package/src/components/calendar/CalendarViewPlaceholder.stories.tsx +1 -1
- package/src/components/calendar/CalendarWorkspace.stories.tsx +1 -1
- package/src/components/calendar/OpenCalendarEvent.stories.tsx +1 -1
- package/src/components/calendar/WriteCalendarEvent.stories.tsx +2 -1
- package/src/components/calendar/WriteCalendarEvent.tsx +1 -0
- package/src/components/compose/FromSelector.stories.tsx +1 -1
- package/src/components/config-import/config-import.stories.tsx +1 -1
- package/src/components/mail/authenticity-warning.stories.tsx +1 -1
- package/src/components/mail/blocked-images-notice.stories.tsx +1 -1
- package/src/components/mail/empty-trash-bar.stories.tsx +1 -1
- package/src/components/mail/intelligence-drawer.stories.tsx +1 -1
- package/src/components/settings/CalendarFeedCard.stories.tsx +1 -1
- package/src/components/settings/LabelsList.stories.tsx +1 -1
- package/src/components/settings/label-delete-confirm.stories.tsx +1 -1
- package/src/components/settings/settings-filter.stories.tsx +1 -1
- package/src/components/ui/ErrorBanner.stories.tsx +1 -1
- package/src/hooks/calendar/draft.test.ts +152 -5
- package/src/hooks/calendar/draft.ts +15 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.288",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -7,7 +7,7 @@ import { SessionRateLimited } from "./SessionRateLimited";
|
|
|
7
7
|
* told everyone behind the address that they had been logged out.
|
|
8
8
|
*/
|
|
9
9
|
const meta: Meta<typeof SessionRateLimited> = {
|
|
10
|
-
title: "
|
|
10
|
+
title: "Playground/Shipped/Sign in/Session rate limited",
|
|
11
11
|
component: SessionRateLimited,
|
|
12
12
|
parameters: { layout: "fullscreen" },
|
|
13
13
|
args: {
|
|
@@ -34,6 +34,7 @@ const draft: EventDraft = {
|
|
|
34
34
|
title: "Roadmap review",
|
|
35
35
|
date: "2026-06-10",
|
|
36
36
|
startTime: "10:00",
|
|
37
|
+
endDate: "2026-06-10",
|
|
37
38
|
endTime: "11:30",
|
|
38
39
|
allDay: false,
|
|
39
40
|
calendarId: "cal_work",
|
|
@@ -44,7 +45,7 @@ const draft: EventDraft = {
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const meta: Meta<typeof CalendarComposePane> = {
|
|
47
|
-
title: "
|
|
48
|
+
title: "Playground/Shipped/Calendar/Compose pane",
|
|
48
49
|
component: CalendarComposePane,
|
|
49
50
|
parameters: { layout: "fullscreen" },
|
|
50
51
|
args: {
|
|
@@ -104,6 +105,22 @@ export const EditingOneOccurrence: Story = {
|
|
|
104
105
|
},
|
|
105
106
|
};
|
|
106
107
|
|
|
108
|
+
/** A night shift opens as one night, ending on the next day. */
|
|
109
|
+
export const EditingOvernight: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
title: "Edit event",
|
|
112
|
+
subtitle: "2026-06-10",
|
|
113
|
+
saveLabel: "Save",
|
|
114
|
+
draft: {
|
|
115
|
+
...draft,
|
|
116
|
+
title: "Night shift handover",
|
|
117
|
+
startTime: "22:00",
|
|
118
|
+
endDate: "2026-06-11",
|
|
119
|
+
endTime: "01:00",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
107
124
|
/** The version on screen was replaced elsewhere. Nothing is overwritten. */
|
|
108
125
|
export const Conflicted: Story = {
|
|
109
126
|
args: {
|
|
@@ -44,7 +44,7 @@ const event: CalendarEventData = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const meta: Meta<typeof CalendarEventPane> = {
|
|
47
|
-
title: "
|
|
47
|
+
title: "Playground/Shipped/Calendar/Event pane",
|
|
48
48
|
component: CalendarEventPane,
|
|
49
49
|
parameters: { layout: "fullscreen" },
|
|
50
50
|
args: {
|
|
@@ -11,7 +11,7 @@ import { CalendarViewPlaceholder } from "./CalendarViewPlaceholder";
|
|
|
11
11
|
* and the reader would plan around an empty screen.
|
|
12
12
|
*/
|
|
13
13
|
const meta: Meta<typeof CalendarViewPlaceholder> = {
|
|
14
|
-
title: "
|
|
14
|
+
title: "Playground/Shipped/Calendar/Not built yet",
|
|
15
15
|
component: CalendarViewPlaceholder,
|
|
16
16
|
parameters: { layout: "fullscreen" },
|
|
17
17
|
render: (args) => (
|
|
@@ -151,7 +151,7 @@ function Workspace({
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const meta: Meta<typeof Workspace> = {
|
|
154
|
-
title: "
|
|
154
|
+
title: "Playground/Shipped/Calendar/Workspace",
|
|
155
155
|
component: Workspace,
|
|
156
156
|
parameters: { layout: "fullscreen" },
|
|
157
157
|
args: { initialView: "week", events: week },
|
|
@@ -74,7 +74,7 @@ function Opened({
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const meta: Meta<typeof Opened> = {
|
|
77
|
-
title: "
|
|
77
|
+
title: "Playground/Shipped/Calendar/Open event",
|
|
78
78
|
component: Opened,
|
|
79
79
|
parameters: { layout: "fullscreen" },
|
|
80
80
|
args: { calendarObjectId: ROADMAP_OBJECT, server: answering() },
|
|
@@ -28,6 +28,7 @@ const server: CalendarServer = (request) => {
|
|
|
28
28
|
|
|
29
29
|
const slot: CalendarSlotPick = {
|
|
30
30
|
date: "2026-06-11",
|
|
31
|
+
endDate: "2026-06-11",
|
|
31
32
|
startTime: "14:00",
|
|
32
33
|
endTime: "15:00",
|
|
33
34
|
allDay: false,
|
|
@@ -47,7 +48,7 @@ function Composer({ pick }: { pick?: CalendarSlotPick }) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const meta: Meta<typeof Composer> = {
|
|
50
|
-
title: "
|
|
51
|
+
title: "Playground/Shipped/Calendar/Write event",
|
|
51
52
|
component: Composer,
|
|
52
53
|
parameters: { layout: "fullscreen" },
|
|
53
54
|
};
|
|
@@ -28,7 +28,7 @@ function seededClient(): QueryClient {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const meta: Meta<typeof FromSelector> = {
|
|
31
|
-
title: "
|
|
31
|
+
title: "Playground/Shipped/Mail/Compose/FromSelector",
|
|
32
32
|
component: FromSelector,
|
|
33
33
|
parameters: { layout: "padded" },
|
|
34
34
|
decorators: [
|
|
@@ -15,7 +15,7 @@ import { AuthenticityBanner } from "@/components/mail/AuthenticityBanner";
|
|
|
15
15
|
* body's own size rather than floating off to the right as chrome.
|
|
16
16
|
*/
|
|
17
17
|
const meta: Meta<typeof AuthenticityBanner> = {
|
|
18
|
-
title: "
|
|
18
|
+
title: "Playground/Shipped/Mail/Reading/Authenticity warning",
|
|
19
19
|
component: AuthenticityBanner,
|
|
20
20
|
parameters: { layout: "centered" },
|
|
21
21
|
};
|
|
@@ -12,7 +12,7 @@ import { BlockedImagesNotice } from "@/components/mail/BlockedImagesNotice";
|
|
|
12
12
|
* message that explains nothing is indistinguishable from a broken one.
|
|
13
13
|
*/
|
|
14
14
|
const meta: Meta<typeof BlockedImagesNotice> = {
|
|
15
|
-
title: "
|
|
15
|
+
title: "Playground/Shipped/Mail/Reading/Blocked images notice",
|
|
16
16
|
component: BlockedImagesNotice,
|
|
17
17
|
parameters: { layout: "padded" },
|
|
18
18
|
};
|
|
@@ -13,7 +13,7 @@ import { EmptyTrashBar } from "@/components/mail/EmptyTrashBar";
|
|
|
13
13
|
* lands under the button and stays there until the user repairs it.
|
|
14
14
|
*/
|
|
15
15
|
const meta: Meta<typeof EmptyTrashBar> = {
|
|
16
|
-
title: "
|
|
16
|
+
title: "Playground/Shipped/Mail/Empty trash",
|
|
17
17
|
component: EmptyTrashBar,
|
|
18
18
|
parameters: { layout: "fullscreen" },
|
|
19
19
|
};
|
|
@@ -27,7 +27,7 @@ import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
|
|
|
27
27
|
* mid-width panes both run on.
|
|
28
28
|
*/
|
|
29
29
|
const meta: Meta = {
|
|
30
|
-
title: "
|
|
30
|
+
title: "Playground/Shipped/Mail/Reading/Intelligence drawer",
|
|
31
31
|
parameters: { layout: "centered" },
|
|
32
32
|
};
|
|
33
33
|
export default meta;
|
|
@@ -7,7 +7,7 @@ import { CalendarFeedCard } from "./CalendarFeedCard";
|
|
|
7
7
|
* is legible and the two confirmations that take it away.
|
|
8
8
|
*/
|
|
9
9
|
const meta: Meta<typeof CalendarFeedCard> = {
|
|
10
|
-
title: "
|
|
10
|
+
title: "Playground/Shipped/Settings/Calendars/Subscription address",
|
|
11
11
|
component: CalendarFeedCard,
|
|
12
12
|
parameters: { layout: "padded" },
|
|
13
13
|
args: {
|
|
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
3
3
|
import { LabelsList } from "./LabelsList";
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof LabelsList> = {
|
|
6
|
-
title: "
|
|
6
|
+
title: "Playground/Shipped/Settings/Labels/LabelsList",
|
|
7
7
|
component: LabelsList,
|
|
8
8
|
parameters: { layout: "padded" },
|
|
9
9
|
decorators: [
|
|
@@ -10,7 +10,7 @@ import { deleteLabelConfirmCopy } from "@/lib/organize/label-delete-copy";
|
|
|
10
10
|
* approved surface, not a placeholder for one.
|
|
11
11
|
*/
|
|
12
12
|
const meta: Meta<typeof ConfirmDialog> = {
|
|
13
|
-
title: "
|
|
13
|
+
title: "Playground/Shipped/Settings/Labels/Delete confirmation",
|
|
14
14
|
component: ConfirmDialog,
|
|
15
15
|
parameters: { layout: "centered" },
|
|
16
16
|
};
|
|
@@ -103,7 +103,7 @@ function SheetStage({ children }: { children: ReactNode }) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const meta: Meta = {
|
|
106
|
-
title: "
|
|
106
|
+
title: "Playground/Shipped/Settings/Filters",
|
|
107
107
|
parameters: { layout: "fullscreen" },
|
|
108
108
|
};
|
|
109
109
|
export default meta;
|
|
@@ -12,7 +12,7 @@ import { ErrorBanner } from "./ErrorBanner";
|
|
|
12
12
|
* builds from build-time constants Storybook has no `define` for.
|
|
13
13
|
*/
|
|
14
14
|
const meta: Meta<typeof ErrorBanner> = {
|
|
15
|
-
title: "
|
|
15
|
+
title: "Playground/Shipped/Mail/ErrorBanner",
|
|
16
16
|
component: ErrorBanner,
|
|
17
17
|
parameters: { layout: "padded" },
|
|
18
18
|
args: {
|
|
@@ -9,10 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { describe, it } from "node:test";
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
type CalendarEventData,
|
|
14
|
+
type EventDraft,
|
|
15
|
+
withAllDay,
|
|
16
|
+
withStartDate,
|
|
17
|
+
} from "@remit/ui";
|
|
13
18
|
import {
|
|
14
19
|
type CreateInput,
|
|
15
20
|
createInputFromDraft,
|
|
21
|
+
draftFromEvent,
|
|
16
22
|
emptyDraft,
|
|
17
23
|
patchFromDrafts,
|
|
18
24
|
type UpdatePatch,
|
|
@@ -28,6 +34,30 @@ const draft = (over: Partial<EventDraft> = {}): EventDraft => ({
|
|
|
28
34
|
...over,
|
|
29
35
|
});
|
|
30
36
|
|
|
37
|
+
const STORED = { repeat: "", location: "", notes: "" };
|
|
38
|
+
|
|
39
|
+
const event = (over: Partial<CalendarEventData>): CalendarEventData => ({
|
|
40
|
+
id: "evt_1",
|
|
41
|
+
calendarId: CALENDAR,
|
|
42
|
+
title: "Roadmap review",
|
|
43
|
+
start: "2026-06-10T09:00:00+02:00",
|
|
44
|
+
end: "2026-06-10T10:00:00+02:00",
|
|
45
|
+
allDay: false,
|
|
46
|
+
location: "",
|
|
47
|
+
notes: "",
|
|
48
|
+
attendees: [],
|
|
49
|
+
myRsvp: "accepted",
|
|
50
|
+
threadId: "",
|
|
51
|
+
threadSubject: "",
|
|
52
|
+
timeZone: AMSTERDAM,
|
|
53
|
+
zoneCertainty: "explicit",
|
|
54
|
+
recurrenceRule: "",
|
|
55
|
+
seriesId: "",
|
|
56
|
+
seriesException: false,
|
|
57
|
+
status: "confirmed",
|
|
58
|
+
...over,
|
|
59
|
+
});
|
|
60
|
+
|
|
31
61
|
const refusal = (result: CreateInput | UpdatePatch): string => {
|
|
32
62
|
assert.ok(
|
|
33
63
|
!result.ok,
|
|
@@ -57,6 +87,31 @@ describe("creating an event", () => {
|
|
|
57
87
|
assert.equal(built.input.end, "2026-06-11");
|
|
58
88
|
});
|
|
59
89
|
|
|
90
|
+
it("ends a night that runs past midnight on the next day", () => {
|
|
91
|
+
const built = createInputFromDraft(
|
|
92
|
+
draft({ startTime: "22:00", endDate: "2026-06-11", endTime: "01:00" }),
|
|
93
|
+
AMSTERDAM,
|
|
94
|
+
);
|
|
95
|
+
assert.ok(built.ok);
|
|
96
|
+
assert.equal(built.input.start, "2026-06-10T22:00:00+02:00");
|
|
97
|
+
assert.equal(built.input.end, "2026-06-11T01:00:00+02:00");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ends an all-day event the day after the last day it covers", () => {
|
|
101
|
+
const built = createInputFromDraft(
|
|
102
|
+
draft({
|
|
103
|
+
allDay: true,
|
|
104
|
+
startTime: "",
|
|
105
|
+
endTime: "",
|
|
106
|
+
endDate: "2026-06-12",
|
|
107
|
+
}),
|
|
108
|
+
AMSTERDAM,
|
|
109
|
+
);
|
|
110
|
+
assert.ok(built.ok);
|
|
111
|
+
assert.equal(built.input.start, "2026-06-10");
|
|
112
|
+
assert.equal(built.input.end, "2026-06-13");
|
|
113
|
+
});
|
|
114
|
+
|
|
60
115
|
it("turns a picked repeat sentence into the rule it means", () => {
|
|
61
116
|
const built = createInputFromDraft(
|
|
62
117
|
draft({ repeat: "Every weekday, 09:00" }),
|
|
@@ -86,7 +141,24 @@ describe("a refusal", () => {
|
|
|
86
141
|
AMSTERDAM,
|
|
87
142
|
),
|
|
88
143
|
),
|
|
89
|
-
/end
|
|
144
|
+
/end is not after the start/i,
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("names a last day before the first on an all-day event", () => {
|
|
149
|
+
assert.match(
|
|
150
|
+
refusal(
|
|
151
|
+
createInputFromDraft(
|
|
152
|
+
draft({
|
|
153
|
+
allDay: true,
|
|
154
|
+
startTime: "",
|
|
155
|
+
endTime: "",
|
|
156
|
+
endDate: "2026-06-09",
|
|
157
|
+
}),
|
|
158
|
+
AMSTERDAM,
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
/last day is before the first/i,
|
|
90
162
|
);
|
|
91
163
|
});
|
|
92
164
|
|
|
@@ -153,6 +225,41 @@ describe("editing an event", () => {
|
|
|
153
225
|
assert.equal(patch.patch.recurrenceRule, "");
|
|
154
226
|
});
|
|
155
227
|
|
|
228
|
+
it("saves another field of a night that runs past midnight", () => {
|
|
229
|
+
const before = draftFromEvent(
|
|
230
|
+
event({
|
|
231
|
+
start: "2026-06-10T22:00:00+02:00",
|
|
232
|
+
end: "2026-06-11T01:00:00+02:00",
|
|
233
|
+
}),
|
|
234
|
+
STORED,
|
|
235
|
+
);
|
|
236
|
+
assert.equal(before.endDate, "2026-06-11");
|
|
237
|
+
const patch = patchFromDrafts(
|
|
238
|
+
before,
|
|
239
|
+
{ ...before, title: "Night shift" },
|
|
240
|
+
AMSTERDAM,
|
|
241
|
+
);
|
|
242
|
+
assert.ok(patch.ok);
|
|
243
|
+
assert.deepEqual(patch.patch, { summary: "Night shift" });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("keeps every day of an all-day event it opens", () => {
|
|
247
|
+
const before = draftFromEvent(
|
|
248
|
+
event({ allDay: true, start: "2026-06-10", end: "2026-06-13" }),
|
|
249
|
+
STORED,
|
|
250
|
+
);
|
|
251
|
+
assert.equal(before.date, "2026-06-10");
|
|
252
|
+
assert.equal(before.endDate, "2026-06-12");
|
|
253
|
+
const patch = patchFromDrafts(
|
|
254
|
+
before,
|
|
255
|
+
{ ...before, date: "2026-06-17", endDate: "2026-06-19" },
|
|
256
|
+
AMSTERDAM,
|
|
257
|
+
);
|
|
258
|
+
assert.ok(patch.ok);
|
|
259
|
+
assert.equal(patch.patch.start, "2026-06-17");
|
|
260
|
+
assert.equal(patch.patch.end, "2026-06-20");
|
|
261
|
+
});
|
|
262
|
+
|
|
156
263
|
it("changes nothing when nothing was touched", () => {
|
|
157
264
|
const before = draft();
|
|
158
265
|
const patch = patchFromDrafts(before, { ...before }, AMSTERDAM);
|
|
@@ -185,7 +292,7 @@ describe("an event anchored somewhere other than the device", () => {
|
|
|
185
292
|
const before = draft();
|
|
186
293
|
const patch = patchFromDrafts(
|
|
187
294
|
before,
|
|
188
|
-
{ ...before, date: "2026-06-17" },
|
|
295
|
+
{ ...before, date: "2026-06-17", endDate: "2026-06-17" },
|
|
189
296
|
AMSTERDAM,
|
|
190
297
|
);
|
|
191
298
|
assert.ok(patch.ok);
|
|
@@ -200,7 +307,7 @@ describe("an event anchored somewhere other than the device", () => {
|
|
|
200
307
|
|
|
201
308
|
it("reads the zone's own winter offset rather than one it saw in June", () => {
|
|
202
309
|
const built = createInputFromDraft(
|
|
203
|
-
draft({ date: "2026-01-14" }),
|
|
310
|
+
draft({ date: "2026-01-14", endDate: "2026-01-14" }),
|
|
204
311
|
AMSTERDAM,
|
|
205
312
|
);
|
|
206
313
|
assert.ok(built.ok);
|
|
@@ -231,7 +338,7 @@ describe("an event anchored somewhere other than the device", () => {
|
|
|
231
338
|
const before = draft();
|
|
232
339
|
const patch = patchFromDrafts(
|
|
233
340
|
before,
|
|
234
|
-
{ ...before, date: "2026-06-17" },
|
|
341
|
+
{ ...before, date: "2026-06-17", endDate: "2026-06-17" },
|
|
235
342
|
"",
|
|
236
343
|
);
|
|
237
344
|
assert.ok(patch.ok);
|
|
@@ -239,3 +346,43 @@ describe("an event anchored somewhere other than the device", () => {
|
|
|
239
346
|
assert.equal(patch.patch.start, "2026-06-17T09:00:00+00:00");
|
|
240
347
|
});
|
|
241
348
|
});
|
|
349
|
+
|
|
350
|
+
describe("the form moving its own dates", () => {
|
|
351
|
+
const weekend = draft({
|
|
352
|
+
allDay: true,
|
|
353
|
+
startTime: "",
|
|
354
|
+
endTime: "",
|
|
355
|
+
date: "2026-06-12",
|
|
356
|
+
endDate: "2026-06-14",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("carries the last day along when the first day moves", () => {
|
|
360
|
+
const moved = withStartDate(weekend, "2026-06-19");
|
|
361
|
+
assert.equal(moved.date, "2026-06-19");
|
|
362
|
+
assert.equal(moved.endDate, "2026-06-21");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("keeps the last day while the start date is cleared and retyped", () => {
|
|
366
|
+
const cleared = withStartDate(weekend, "");
|
|
367
|
+
assert.equal(cleared.endDate, "2026-06-14");
|
|
368
|
+
const retyped = withStartDate(cleared, "2026-06-12");
|
|
369
|
+
assert.equal(retyped.date, "2026-06-12");
|
|
370
|
+
assert.equal(retyped.endDate, "2026-06-14");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("makes a night that runs past midnight one all-day date", () => {
|
|
374
|
+
const night = draft({
|
|
375
|
+
startTime: "22:00",
|
|
376
|
+
endDate: "2026-06-11",
|
|
377
|
+
endTime: "01:00",
|
|
378
|
+
});
|
|
379
|
+
const allDay = withAllDay(night, true);
|
|
380
|
+
assert.equal(allDay.allDay, true);
|
|
381
|
+
assert.equal(allDay.endDate, "2026-06-10");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("keeps the days of a timed event that spans them when made all day", () => {
|
|
385
|
+
const span = draft({ endDate: "2026-06-12", endTime: "17:00" });
|
|
386
|
+
assert.equal(withAllDay(span, true).endDate, "2026-06-12");
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
RemitImapCreateCalendarEventInput,
|
|
14
14
|
RemitImapUpdateCalendarEventInput,
|
|
15
15
|
} from "@remit/api-http-client/types.gen.ts";
|
|
16
|
-
import type
|
|
16
|
+
import { type CalendarEventData, type EventDraft, lastDayOf } from "@remit/ui";
|
|
17
17
|
import { rruleFromText } from "./recurrence-rule";
|
|
18
18
|
import { addDays, isoAtInZone } from "./window";
|
|
19
19
|
|
|
@@ -33,6 +33,7 @@ export function emptyDraft(date: string, calendarId: string): EventDraft {
|
|
|
33
33
|
title: "",
|
|
34
34
|
date,
|
|
35
35
|
startTime: "09:00",
|
|
36
|
+
endDate: date,
|
|
36
37
|
endTime: "10:00",
|
|
37
38
|
allDay: false,
|
|
38
39
|
calendarId,
|
|
@@ -56,6 +57,7 @@ export function draftFromEvent(
|
|
|
56
57
|
title: event.title,
|
|
57
58
|
date: event.start.slice(0, 10),
|
|
58
59
|
startTime: event.allDay ? "" : event.start.slice(11, 16),
|
|
60
|
+
endDate: lastDayOf(event),
|
|
59
61
|
endTime: event.allDay ? "" : event.end.slice(11, 16),
|
|
60
62
|
allDay: event.allDay,
|
|
61
63
|
calendarId: event.calendarId,
|
|
@@ -79,10 +81,11 @@ function timesFor(
|
|
|
79
81
|
draft: EventDraft,
|
|
80
82
|
timeZone: string,
|
|
81
83
|
): { start: string; end: string } {
|
|
82
|
-
if (draft.allDay)
|
|
84
|
+
if (draft.allDay)
|
|
85
|
+
return { start: draft.date, end: addDays(draft.endDate, 1) };
|
|
83
86
|
return {
|
|
84
87
|
start: isoAtInZone(draft.date, draft.startTime, timeZone),
|
|
85
|
-
end: isoAtInZone(draft.
|
|
88
|
+
end: isoAtInZone(draft.endDate, draft.endTime, timeZone),
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -95,11 +98,17 @@ function refuse(draft: EventDraft, checkRepeat: boolean): string {
|
|
|
95
98
|
if (draft.title.trim() === "")
|
|
96
99
|
return "Give the event a title before saving it.";
|
|
97
100
|
if (draft.date === "") return "Pick the day the event is on.";
|
|
101
|
+
if (draft.endDate === "") return "Pick the day the event ends on.";
|
|
98
102
|
if (draft.calendarId === "") return "Pick the calendar to save the event in.";
|
|
99
103
|
if (!draft.allDay && (draft.startTime === "" || draft.endTime === ""))
|
|
100
104
|
return "Set a start and an end time, or mark the event all day.";
|
|
101
|
-
if (
|
|
102
|
-
return "The
|
|
105
|
+
if (draft.allDay && draft.endDate < draft.date)
|
|
106
|
+
return "The last day is before the first. Move one of them.";
|
|
107
|
+
if (
|
|
108
|
+
!draft.allDay &&
|
|
109
|
+
`${draft.endDate}T${draft.endTime}` <= `${draft.date}T${draft.startTime}`
|
|
110
|
+
)
|
|
111
|
+
return "The end is not after the start. Move one of them.";
|
|
103
112
|
if (checkRepeat && rruleFromText(draft.repeat) === undefined)
|
|
104
113
|
return "This calendar can't store that repeat rule. Pick one of the offered rules, or turn the repeat off.";
|
|
105
114
|
return "";
|
|
@@ -149,6 +158,7 @@ export function patchFromDrafts(
|
|
|
149
158
|
const moved =
|
|
150
159
|
after.date !== before.date ||
|
|
151
160
|
after.startTime !== before.startTime ||
|
|
161
|
+
after.endDate !== before.endDate ||
|
|
152
162
|
after.endTime !== before.endTime ||
|
|
153
163
|
after.allDay !== before.allDay;
|
|
154
164
|
if (moved) {
|