@remit/web-client 0.0.294 → 0.0.295
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/components/calendar/CalendarUnavailable.tsx +56 -0
- package/src/components/calendar/CalendarWaiting.tsx +99 -0
- package/src/components/calendar/CalendarWorkspace.stories.tsx +100 -0
- package/src/components/calendar/CalendarWorkspace.tsx +49 -40
- package/src/components/calendar/PendingSuggestions.tsx +70 -0
- package/src/components/calendar/pending-suggestions.render.test.ts +132 -0
- package/src/components/mail/DailyBrief.tsx +2 -1
- package/src/components/mail/IntelligencePane.tsx +5 -0
- package/src/components/mail/authenticity-warning.stories.tsx +6 -1
- package/src/components/mail/intelligence-drawer.stories.tsx +11 -2
- package/src/components/mail/intelligence-story-calendar.ts +19 -0
- package/src/hooks/calendar/agenda-window.test.ts +216 -132
- package/src/hooks/calendar/index.ts +8 -0
- package/src/hooks/calendar/useCalendarSuggestions.ts +180 -0
- package/src/hooks/calendar/useCalendars.ts +14 -2
- package/src/hooks/intelligence-calendar.render.test.ts +277 -0
- package/src/hooks/useBriefSections.ts +2 -1
- package/src/hooks/useIntelligenceCalendar.ts +347 -0
- package/src/lib/calendar-format.ts +41 -1
- package/src/lib/calendar-report.ts +11 -0
- package/src/lib/calendar-suggestion.test.ts +169 -0
- package/src/lib/calendar-suggestion.ts +251 -0
- package/src/lib/calendar-suggestion.west.test.ts +56 -0
- package/src/lib/starred-rows.ts +4 -2
- package/src/routes/calendar/$view.$date.tsx +2 -0
- package/src/routing/calendar.ts +29 -0
- package/src/routing/index.ts +1 -0
|
@@ -12,6 +12,7 @@ import { expect, userEvent, within } from "storybook/test";
|
|
|
12
12
|
import { Drawer } from "@/components/layout/Drawer";
|
|
13
13
|
import { MessageToolbar } from "@/components/mail/MessageToolbar";
|
|
14
14
|
import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
|
|
15
|
+
import { quietCalendar } from "./intelligence-story-calendar";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The intelligence drawer below the desktop tier (#777, #778).
|
|
@@ -159,7 +160,11 @@ const PhoneReader = () => {
|
|
|
159
160
|
ariaLabel="Message details"
|
|
160
161
|
side="right"
|
|
161
162
|
>
|
|
162
|
-
<IntelligencePanel
|
|
163
|
+
<IntelligencePanel
|
|
164
|
+
data={intelligence}
|
|
165
|
+
calendar={quietCalendar}
|
|
166
|
+
hideCloseButton
|
|
167
|
+
/>
|
|
163
168
|
</Drawer>
|
|
164
169
|
</div>
|
|
165
170
|
<p className="font-mono text-2xs text-fg-subtle">
|
|
@@ -291,7 +296,11 @@ const MidWidthReader = () => {
|
|
|
291
296
|
ariaLabel="Message details"
|
|
292
297
|
side="right"
|
|
293
298
|
>
|
|
294
|
-
<IntelligencePanel
|
|
299
|
+
<IntelligencePanel
|
|
300
|
+
data={intelligence}
|
|
301
|
+
calendar={quietCalendar}
|
|
302
|
+
hideCloseButton
|
|
303
|
+
/>
|
|
295
304
|
</Drawer>
|
|
296
305
|
</div>
|
|
297
306
|
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IntelligenceCalendarSurface } from "@remit/ui";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The calendar half the shipping pane always carries, for a message that says
|
|
5
|
+
* nothing about time. Stories of the Sender half mount it too, so the tab strip
|
|
6
|
+
* they draw is the one the app draws.
|
|
7
|
+
*/
|
|
8
|
+
export const quietCalendar: IntelligenceCalendarSurface = {
|
|
9
|
+
data: { suggestions: [], day: [], dayLabel: "" },
|
|
10
|
+
actions: {
|
|
11
|
+
onAddInvite: () => undefined,
|
|
12
|
+
onDeclineInvite: () => undefined,
|
|
13
|
+
onOfferOtherTimes: () => undefined,
|
|
14
|
+
onToggleSlot: () => undefined,
|
|
15
|
+
onAddSuggestion: () => undefined,
|
|
16
|
+
onDismissSuggestion: () => undefined,
|
|
17
|
+
onSelectEvent: () => undefined,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* free-time half asserts that busy spans the strip did not draw still take the
|
|
8
8
|
* hours they cover — the rule itself is `@remit/ui`'s, and this only decides
|
|
9
9
|
* which spans a day gets.
|
|
10
|
+
*
|
|
11
|
+
* The window a day names is a civil date, and the clock time carried on it is
|
|
12
|
+
* the device's own — so every case here is asserted in more than one zone. A
|
|
13
|
+
* window boundary asserted only in UTC would miss the hour a DST change hides
|
|
14
|
+
* from anything that measures it as elapsed time instead of a calendar date.
|
|
10
15
|
*/
|
|
11
16
|
import assert from "node:assert/strict";
|
|
12
17
|
import { describe, it } from "node:test";
|
|
@@ -48,41 +53,81 @@ const clocks = (
|
|
|
48
53
|
`${Math.floor(stretch.startMinute / 60)}–${Math.floor(stretch.endMinute / 60)}`,
|
|
49
54
|
);
|
|
50
55
|
|
|
56
|
+
/** The days between two civil dates, ignoring whatever offset each one carries. */
|
|
57
|
+
const daysBetween = (from: string, to: string): number =>
|
|
58
|
+
(Date.parse(to.slice(0, 10)) - Date.parse(from.slice(0, 10))) / 86_400_000;
|
|
59
|
+
|
|
60
|
+
/** The zones a reader's device might be on, asserted so a body cannot pass silently. */
|
|
61
|
+
const ZONES = [
|
|
62
|
+
{ name: "UTC", hourAtNoonUtc: 12 },
|
|
63
|
+
{ name: "Europe/Amsterdam", hourAtNoonUtc: 14 },
|
|
64
|
+
{ name: "America/New_York", hourAtNoonUtc: 8 },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function inEveryZone(body: () => void): void {
|
|
68
|
+
const before = process.env.TZ;
|
|
69
|
+
try {
|
|
70
|
+
for (const zone of ZONES) {
|
|
71
|
+
process.env.TZ = zone.name;
|
|
72
|
+
assert.equal(
|
|
73
|
+
new Date("2026-06-11T12:00:00Z").getHours(),
|
|
74
|
+
zone.hourAtNoonUtc,
|
|
75
|
+
`the machine never moved to ${zone.name}`,
|
|
76
|
+
);
|
|
77
|
+
body();
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
if (before === undefined) delete process.env.TZ;
|
|
81
|
+
else process.env.TZ = before;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
describe("the days the strip holds", () => {
|
|
52
86
|
it("opens further ahead than behind, because a diary is read forwards", () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
inEveryZone(() => {
|
|
88
|
+
const range = rangeAround(DATE);
|
|
89
|
+
assert.equal(range.from, "2026-05-31");
|
|
90
|
+
assert.equal(range.to, "2026-07-04");
|
|
91
|
+
assert.equal(datesInRange(range).length, LEAD_IN + LEAD_OUT + 1);
|
|
92
|
+
});
|
|
57
93
|
});
|
|
58
94
|
|
|
59
95
|
it("grows the run at whichever end was reached, keeping the other", () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
inEveryZone(() => {
|
|
97
|
+
const range = rangeAround(DATE);
|
|
98
|
+
assert.deepEqual(extendRangeStart(range), {
|
|
99
|
+
...range,
|
|
100
|
+
from: "2026-05-24",
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(extendRangeEnd(range), { ...range, to: "2026-07-11" });
|
|
103
|
+
assert.equal(
|
|
104
|
+
datesInRange(extendRangeEnd(range)).length,
|
|
105
|
+
datesInRange(range).length + PAGE,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
67
108
|
});
|
|
68
109
|
|
|
69
110
|
it("keeps the run a day is already in, so a scroll refetches nothing", () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
inEveryZone(() => {
|
|
112
|
+
const range = rangeAround(DATE);
|
|
113
|
+
for (const day of ["2026-05-31", DATE, "2026-06-28", "2026-07-04"]) {
|
|
114
|
+
assert.equal(rangeCovering(range, day), range, `${day} moved the run`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
74
117
|
});
|
|
75
118
|
|
|
76
119
|
it("opens a window where a jump landed rather than stretching to it", () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
inEveryZone(() => {
|
|
121
|
+
const range = rangeAround(DATE);
|
|
122
|
+
assert.deepEqual(
|
|
123
|
+
rangeCovering(range, "2026-11-02"),
|
|
124
|
+
rangeAround("2026-11-02"),
|
|
125
|
+
);
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
rangeCovering(range, "2026-01-05"),
|
|
128
|
+
rangeAround("2026-01-05"),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
86
131
|
});
|
|
87
132
|
});
|
|
88
133
|
|
|
@@ -113,41 +158,51 @@ describe("how far the run grows before it asks", () => {
|
|
|
113
158
|
};
|
|
114
159
|
|
|
115
160
|
it("stops a year either way, however often an end is reached", () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
inEveryZone(() => {
|
|
162
|
+
const range = reachEnd(reachStart());
|
|
163
|
+
assert.equal(range.from, addDays(DATE, -CAP_DAYS));
|
|
164
|
+
assert.equal(range.to, addDays(DATE, CAP_DAYS));
|
|
165
|
+
assert.equal(rangeAtFloor(range), true);
|
|
166
|
+
assert.equal(rangeAtCeiling(range), true);
|
|
167
|
+
});
|
|
121
168
|
});
|
|
122
169
|
|
|
123
170
|
it("hands back the run it was given at the cap, so nothing refetches", () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
inEveryZone(() => {
|
|
172
|
+
const capped = reachEnd();
|
|
173
|
+
assert.equal(extendRangeEnd(capped), capped);
|
|
174
|
+
const behind = reachStart();
|
|
175
|
+
assert.equal(extendRangeStart(behind), behind);
|
|
176
|
+
});
|
|
128
177
|
});
|
|
129
178
|
|
|
130
179
|
it("opens the next year where the reader asked, keeping the days held", () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
180
|
+
inEveryZone(() => {
|
|
181
|
+
const capped = reachEnd();
|
|
182
|
+
const more = liftRangeCeiling(capped);
|
|
183
|
+
assert.equal(more.from, capped.from);
|
|
184
|
+
assert.equal(more.to, addDays(capped.to, PAGE));
|
|
185
|
+
assert.equal(rangeAtCeiling(more), false);
|
|
186
|
+
assert.equal(rangeAtFloor(more), false);
|
|
187
|
+
});
|
|
137
188
|
});
|
|
138
189
|
|
|
139
190
|
it("does the same behind, without moving the end ahead", () => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
inEveryZone(() => {
|
|
192
|
+
const capped = reachStart();
|
|
193
|
+
const more = liftRangeFloor(capped);
|
|
194
|
+
assert.equal(more.to, capped.to);
|
|
195
|
+
assert.equal(more.from, addDays(capped.from, -PAGE));
|
|
196
|
+
assert.equal(rangeAtFloor(more), false);
|
|
197
|
+
});
|
|
145
198
|
});
|
|
146
199
|
|
|
147
200
|
it("gives a jump its own year, measured from where it landed", () => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
201
|
+
inEveryZone(() => {
|
|
202
|
+
const jumped = rangeCovering(reachEnd(), "2028-03-01");
|
|
203
|
+
assert.deepEqual(jumped, rangeAround("2028-03-01"));
|
|
204
|
+
assert.equal(rangeAtCeiling(jumped), false);
|
|
205
|
+
});
|
|
151
206
|
});
|
|
152
207
|
});
|
|
153
208
|
|
|
@@ -159,14 +214,16 @@ describe("how far the run grows before it asks", () => {
|
|
|
159
214
|
*/
|
|
160
215
|
describe("the weeks a range is fetched as", () => {
|
|
161
216
|
it("is the grid's own window for each week, once each", () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
217
|
+
inEveryZone(() => {
|
|
218
|
+
const windows = weekWindowsOver(datesInRange(rangeAround(DATE)));
|
|
219
|
+
for (const window of windows) {
|
|
220
|
+
assert.deepEqual(
|
|
221
|
+
window,
|
|
222
|
+
calendarWindow("week", window.from.slice(0, 10)),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
assert.equal(new Set(windows.map((w) => w.from)).size, windows.length);
|
|
226
|
+
});
|
|
170
227
|
});
|
|
171
228
|
|
|
172
229
|
/**
|
|
@@ -175,49 +232,58 @@ describe("the weeks a range is fetched as", () => {
|
|
|
175
232
|
* scrolled to.
|
|
176
233
|
*/
|
|
177
234
|
it("is the six weeks the opening run falls in, and no more", () => {
|
|
178
|
-
|
|
235
|
+
inEveryZone(() => {
|
|
236
|
+
assert.equal(weekWindowsOver(datesInRange(rangeAround(DATE))).length, 6);
|
|
237
|
+
});
|
|
179
238
|
});
|
|
180
239
|
|
|
181
240
|
it("covers every day the strip holds and nothing before or after", () => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
inEveryZone(() => {
|
|
242
|
+
const dates = datesInRange(rangeAround(DATE));
|
|
243
|
+
const held = new Set(weekWindowsOver(dates).map((window) => window.from));
|
|
244
|
+
for (const date of dates) assert.ok(held.has(weekKeyOf(date)));
|
|
245
|
+
});
|
|
185
246
|
});
|
|
186
247
|
|
|
187
248
|
it("names the week a day belongs to, Monday to Monday", () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
249
|
+
inEveryZone(() => {
|
|
250
|
+
// 2026-06-10 is a Wednesday; its week opens on the 8th.
|
|
251
|
+
assert.equal(weekKeyOf("2026-06-10"), startOfDay("2026-06-08"));
|
|
252
|
+
assert.equal(weekKeyOf("2026-06-08"), startOfDay("2026-06-08"));
|
|
253
|
+
assert.equal(weekKeyOf("2026-06-14"), startOfDay("2026-06-08"));
|
|
254
|
+
assert.equal(weekKeyOf("2026-06-15"), startOfDay("2026-06-15"));
|
|
255
|
+
});
|
|
193
256
|
});
|
|
194
257
|
|
|
195
258
|
it("asks for one week however far the reader scrolls", () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
(
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
inEveryZone(() => {
|
|
260
|
+
// Past the server's 366-day ceiling in both directions.
|
|
261
|
+
let range = rangeAround(DATE);
|
|
262
|
+
for (let reach = 0; reach < 30; reach += 1) {
|
|
263
|
+
range = extendRangeEnd(extendRangeStart(range));
|
|
264
|
+
}
|
|
265
|
+
const dates = datesInRange(range);
|
|
266
|
+
assert.ok(dates.length > 366, "the range never grew past the ceiling");
|
|
267
|
+
|
|
268
|
+
for (const window of weekWindowsOver(dates)) {
|
|
269
|
+
const days = daysBetween(window.from, window.to);
|
|
270
|
+
assert.equal(days, 7, `a window covered ${days} days`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
209
273
|
});
|
|
210
274
|
|
|
211
275
|
it("grows by adding weeks rather than by widening one", () => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
276
|
+
inEveryZone(() => {
|
|
277
|
+
const opening = weekWindowsOver(datesInRange(rangeAround(DATE)));
|
|
278
|
+
const wider = weekWindowsOver(
|
|
279
|
+
datesInRange(extendRangeStart(rangeAround(DATE))),
|
|
280
|
+
);
|
|
281
|
+
const kept = new Set(wider.map((window) => window.from));
|
|
282
|
+
for (const window of opening) {
|
|
283
|
+
assert.ok(kept.has(window.from), `${window.from} was given up`);
|
|
284
|
+
}
|
|
285
|
+
assert.ok(wider.length > opening.length);
|
|
286
|
+
});
|
|
221
287
|
});
|
|
222
288
|
});
|
|
223
289
|
|
|
@@ -225,50 +291,62 @@ describe("busy time, split into the days it covers", () => {
|
|
|
225
291
|
const dates = ["2026-06-10", "2026-06-11", "2026-06-12"];
|
|
226
292
|
|
|
227
293
|
it("reads a span as minutes of the day it falls on", () => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
294
|
+
inEveryZone(() => {
|
|
295
|
+
const byDate = busySpansByDate(dates, [
|
|
296
|
+
{ start: at("2026-06-10", "09:00"), end: at("2026-06-10", "10:30") },
|
|
297
|
+
]);
|
|
298
|
+
assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 630 }]);
|
|
299
|
+
assert.equal(byDate.get("2026-06-11"), undefined);
|
|
300
|
+
});
|
|
233
301
|
});
|
|
234
302
|
|
|
235
303
|
it("gives a span crossing midnight one entry per day it touches", () => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
304
|
+
inEveryZone(() => {
|
|
305
|
+
const byDate = busySpansByDate(dates, [
|
|
306
|
+
{ start: at("2026-06-10", "22:00"), end: at("2026-06-11", "02:00") },
|
|
307
|
+
]);
|
|
308
|
+
assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
|
|
309
|
+
assert.deepEqual(byDate.get("2026-06-11"), [{ from: 0, to: 120 }]);
|
|
310
|
+
});
|
|
241
311
|
});
|
|
242
312
|
|
|
243
313
|
it("leaves no entry on the day a span merely ends at midnight of", () => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
314
|
+
inEveryZone(() => {
|
|
315
|
+
const byDate = busySpansByDate(dates, [
|
|
316
|
+
{ start: at("2026-06-10", "22:00"), end: at("2026-06-11", "00:00") },
|
|
317
|
+
]);
|
|
318
|
+
assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
|
|
319
|
+
assert.equal(byDate.get("2026-06-11"), undefined);
|
|
320
|
+
});
|
|
249
321
|
});
|
|
250
322
|
|
|
251
323
|
it("merges spans that run into each other into the hours they cover", () => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
324
|
+
inEveryZone(() => {
|
|
325
|
+
const byDate = busySpansByDate(dates, [
|
|
326
|
+
{ start: at("2026-06-10", "11:00"), end: at("2026-06-10", "12:00") },
|
|
327
|
+
{ start: at("2026-06-10", "09:00"), end: at("2026-06-10", "11:30") },
|
|
328
|
+
]);
|
|
329
|
+
assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 720 }]);
|
|
330
|
+
});
|
|
257
331
|
});
|
|
258
332
|
|
|
259
333
|
it("drops a span that is not a stretch of time at all", () => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
334
|
+
inEveryZone(() => {
|
|
335
|
+
const byDate = busySpansByDate(dates, [
|
|
336
|
+
{ start: at("2026-06-10", "11:00"), end: at("2026-06-10", "11:00") },
|
|
337
|
+
{ start: "not a date", end: at("2026-06-10", "12:00") },
|
|
338
|
+
]);
|
|
339
|
+
assert.equal(byDate.size, 0);
|
|
340
|
+
});
|
|
265
341
|
});
|
|
266
342
|
|
|
267
343
|
it("ignores busy time outside the days on screen", () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
344
|
+
inEveryZone(() => {
|
|
345
|
+
const byDate = busySpansByDate(dates, [
|
|
346
|
+
{ start: at("2026-07-01", "09:00"), end: at("2026-07-01", "10:00") },
|
|
347
|
+
]);
|
|
348
|
+
assert.equal(byDate.size, 0);
|
|
349
|
+
});
|
|
272
350
|
});
|
|
273
351
|
});
|
|
274
352
|
|
|
@@ -276,9 +354,11 @@ describe("free time, off the spans the server merged", () => {
|
|
|
276
354
|
const dates = ["2026-06-10", "2026-06-11"];
|
|
277
355
|
|
|
278
356
|
it("answers every day the strip holds, including the untouched ones", () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
357
|
+
inEveryZone(() => {
|
|
358
|
+
const free = freeStretchesByDate(dates, []);
|
|
359
|
+
assert.deepEqual([...free.keys()], dates);
|
|
360
|
+
assert.equal(free.get("2026-06-11")?.[0].wholeDay, true);
|
|
361
|
+
});
|
|
282
362
|
});
|
|
283
363
|
|
|
284
364
|
/**
|
|
@@ -287,22 +367,26 @@ describe("free time, off the spans the server merged", () => {
|
|
|
287
367
|
* this view must never say.
|
|
288
368
|
*/
|
|
289
369
|
it("takes hours out of a day the strip is drawing nothing on", () => {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
370
|
+
inEveryZone(() => {
|
|
371
|
+
const free = freeStretchesByDate(dates, [
|
|
372
|
+
{ start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
|
|
373
|
+
{ start: at("2026-06-10", "13:00"), end: at("2026-06-10", "16:00") },
|
|
374
|
+
]);
|
|
375
|
+
assert.deepEqual(clocks(free.get("2026-06-10") ?? []), ["16–22"]);
|
|
376
|
+
assert.equal(free.get("2026-06-10")?.[0].wholeDay, false);
|
|
377
|
+
});
|
|
296
378
|
});
|
|
297
379
|
|
|
298
380
|
it("is the rule `@remit/ui` states, applied to those spans", () => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
381
|
+
inEveryZone(() => {
|
|
382
|
+
const spans = [{ from: 9 * 60, to: 12 * 60 }];
|
|
383
|
+
const free = freeStretchesByDate(dates, [
|
|
384
|
+
{ start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
|
|
385
|
+
]);
|
|
386
|
+
assert.deepEqual(
|
|
387
|
+
free.get("2026-06-10"),
|
|
388
|
+
freeStretchesFromSpans("2026-06-10", spans),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
307
391
|
});
|
|
308
392
|
});
|
|
@@ -79,6 +79,14 @@ export {
|
|
|
79
79
|
type CalendarFreeBusyResult,
|
|
80
80
|
useCalendarFreeBusyWeeks,
|
|
81
81
|
} from "./useCalendarFreeBusy";
|
|
82
|
+
export {
|
|
83
|
+
type CalendarSuggestionAnswers,
|
|
84
|
+
type CalendarSuggestionsResult,
|
|
85
|
+
type SuggestionAnswer,
|
|
86
|
+
useCalendarSuggestionAnswers,
|
|
87
|
+
useMessageCalendarSuggestions,
|
|
88
|
+
usePendingCalendarSuggestions,
|
|
89
|
+
} from "./useCalendarSuggestions";
|
|
82
90
|
export { type CalendarsResult, useCalendars } from "./useCalendars";
|
|
83
91
|
export {
|
|
84
92
|
type CalendarWriteOutcome,
|