@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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/auth/SessionRateLimited.stories.tsx +1 -1
  3. package/src/components/calendar/AgendaStrip.stories.tsx +1 -1
  4. package/src/components/calendar/AgendaView.stories.tsx +1 -1
  5. package/src/components/calendar/CalendarComposePane.stories.tsx +18 -1
  6. package/src/components/calendar/CalendarEventPane.stories.tsx +1 -1
  7. package/src/components/calendar/CalendarShell.stories.tsx +1 -1
  8. package/src/components/calendar/CalendarViewPlaceholder.stories.tsx +1 -1
  9. package/src/components/calendar/CalendarWorkspace.stories.tsx +1 -1
  10. package/src/components/calendar/OpenCalendarEvent.stories.tsx +1 -1
  11. package/src/components/calendar/WriteCalendarEvent.stories.tsx +2 -1
  12. package/src/components/calendar/WriteCalendarEvent.tsx +1 -0
  13. package/src/components/compose/FromSelector.stories.tsx +1 -1
  14. package/src/components/config-import/config-import.stories.tsx +1 -1
  15. package/src/components/mail/authenticity-warning.stories.tsx +1 -1
  16. package/src/components/mail/blocked-images-notice.stories.tsx +1 -1
  17. package/src/components/mail/empty-trash-bar.stories.tsx +1 -1
  18. package/src/components/mail/intelligence-drawer.stories.tsx +1 -1
  19. package/src/components/settings/CalendarFeedCard.stories.tsx +1 -1
  20. package/src/components/settings/LabelsList.stories.tsx +1 -1
  21. package/src/components/settings/label-delete-confirm.stories.tsx +1 -1
  22. package/src/components/settings/settings-filter.stories.tsx +1 -1
  23. package/src/components/ui/ErrorBanner.stories.tsx +1 -1
  24. package/src/hooks/calendar/draft.test.ts +152 -5
  25. 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.286",
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: "Screens/SessionRateLimited",
10
+ title: "Playground/Shipped/Sign in/Session rate limited",
11
11
  component: SessionRateLimited,
12
12
  parameters: { layout: "fullscreen" },
13
13
  args: {
@@ -199,7 +199,7 @@ function Strip({
199
199
  }
200
200
 
201
201
  const meta: Meta<typeof Strip> = {
202
- title: "App/Calendar/Agenda",
202
+ title: "Playground/Shipped/Calendar/Agenda",
203
203
  component: Strip,
204
204
  parameters: { layout: "fullscreen" },
205
205
  args: {
@@ -56,7 +56,7 @@ function Agenda({
56
56
  }
57
57
 
58
58
  const meta: Meta<typeof Agenda> = {
59
- title: "App/Calendar/Agenda view",
59
+ title: "Playground/Shipped/Calendar/Agenda view",
60
60
  component: Agenda,
61
61
  parameters: { layout: "fullscreen" },
62
62
  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: "App/Calendar/Compose pane",
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: "App/Calendar/Event pane",
47
+ title: "Playground/Shipped/Calendar/Event pane",
48
48
  component: CalendarEventPane,
49
49
  parameters: { layout: "fullscreen" },
50
50
  args: {
@@ -127,7 +127,7 @@ function Shell({
127
127
  }
128
128
 
129
129
  const meta: Meta<typeof Shell> = {
130
- title: "App/Calendar/Shell",
130
+ title: "Playground/Shipped/Calendar/Shell",
131
131
  component: Shell,
132
132
  parameters: { layout: "fullscreen" },
133
133
  };
@@ -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: "App/Calendar/Not built yet",
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: "App/Calendar/Workspace",
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: "App/Calendar/Open event",
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: "App/Calendar/Write event",
51
+ title: "Playground/Shipped/Calendar/Write event",
51
52
  component: Composer,
52
53
  parameters: { layout: "fullscreen" },
53
54
  };
@@ -37,6 +37,7 @@ export function WriteCalendarEvent({ onClose }: WriteCalendarEventProps) {
37
37
  ...blank,
38
38
  allDay: seed.allDay,
39
39
  startTime: seed.allDay ? "" : seed.startTime,
40
+ endDate: seed.endDate,
40
41
  endTime: seed.allDay ? "" : seed.endTime,
41
42
  };
42
43
  });
@@ -28,7 +28,7 @@ function seededClient(): QueryClient {
28
28
  }
29
29
 
30
30
  const meta: Meta<typeof FromSelector> = {
31
- title: "Components/FromSelector",
31
+ title: "Playground/Shipped/Mail/Compose/FromSelector",
32
32
  component: FromSelector,
33
33
  parameters: { layout: "padded" },
34
34
  decorators: [
@@ -37,7 +37,7 @@ import {
37
37
  } from "./steps";
38
38
 
39
39
  const meta: Meta = {
40
- title: "Flows/Config import",
40
+ title: "Playground/Shipped/Import/Config import",
41
41
  parameters: { layout: "fullscreen" },
42
42
  };
43
43
  export default meta;
@@ -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: "Flows/Reading/Authenticity Warning",
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: "Flows/Mail/Blocked Images Notice",
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: "Flows/Mail/Empty Trash",
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: "Flows/Reading/Intelligence Drawer",
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: "Flows/Settings Calendars/Subscription address",
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: "Flows/Settings Labels/LabelsList",
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: "Flows/Settings Labels/Delete Confirmation",
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: "Flows/Settings Filters",
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: "Components/ErrorBanner",
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 type { EventDraft } from "@remit/ui";
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 time is not after the start time/i,
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 { CalendarEventData, EventDraft } from "@remit/ui";
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) return { start: draft.date, end: addDays(draft.date, 1) };
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.date, draft.endTime, timeZone),
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 (!draft.allDay && draft.endTime <= draft.startTime)
102
- return "The end time is not after the start time. Move one of them.";
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) {