@remit/web-client 0.0.315 → 0.0.317

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.315",
3
+ "version": "0.0.317",
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": {
@@ -37,6 +37,7 @@ const suggestion = (
37
37
  supersededByThreadId: "",
38
38
  createdAt: 0,
39
39
  updatedAt: 0,
40
+ answerOvertakenBy: "None",
40
41
  ...overrides,
41
42
  });
42
43
 
@@ -63,7 +63,9 @@ export const Empty: Story = {
63
63
  export const OpenEvent: Story = {
64
64
  args: { url: `${STORY_WEEK}/${ROADMAP_OBJECT}` },
65
65
  play: async () => {
66
- await expect(await page().findByText("Room Zuid")).toBeVisible();
66
+ await expect(
67
+ await page().findByText("Room Zuid", {}, { timeout: 5000 }),
68
+ ).toBeVisible();
67
69
  await expect(
68
70
  await page().findByRole("button", { name: "Edit" }),
69
71
  ).toBeVisible();
@@ -78,6 +78,7 @@ const reading = (
78
78
  supersededByThreadId: "",
79
79
  createdAt: 0,
80
80
  updatedAt: 0,
81
+ answerOvertakenBy: "None",
81
82
  ...overrides,
82
83
  });
83
84
 
@@ -67,6 +67,7 @@ const pending: RemitImapCalendarSuggestionResponse = {
67
67
  supersededByThreadId: "",
68
68
  createdAt: 0,
69
69
  updatedAt: 0,
70
+ answerOvertakenBy: "None",
70
71
  };
71
72
 
72
73
  let harness: DomHarness | undefined;
@@ -153,6 +153,7 @@ const invitation: RemitImapCalendarSuggestionResponse = {
153
153
  supersededByThreadId: "",
154
154
  createdAt: 0,
155
155
  updatedAt: 0,
156
+ answerOvertakenBy: "None",
156
157
  };
157
158
 
158
159
  const sender: IntelligenceData = {
@@ -15,6 +15,7 @@ import {
15
15
  calendarSuggestionActionOperationsAcceptCalendarSuggestionMutation,
16
16
  calendarSuggestionActionOperationsDeclineCalendarSuggestionMutation,
17
17
  calendarSuggestionActionOperationsDismissCalendarSuggestionMutation,
18
+ calendarSuggestionActionOperationsReopenCalendarSuggestionMutation,
18
19
  calendarSuggestionOperationsListCalendarSuggestionsOptions,
19
20
  messageCalendarSuggestionOperationsListMessageCalendarSuggestionsOptions,
20
21
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
@@ -162,6 +163,7 @@ export interface CalendarSuggestionAnswers {
162
163
  suggestionId: string,
163
164
  muteSender: boolean,
164
165
  ) => Promise<SuggestionAnswer>;
166
+ reopen: (suggestionId: string) => Promise<SuggestionAnswer>;
165
167
  isAnswering: boolean;
166
168
  }
167
169
 
@@ -188,6 +190,12 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
188
190
  onSuccess,
189
191
  });
190
192
 
193
+ const reopen = useMutation({
194
+ ...calendarSuggestionActionOperationsReopenCalendarSuggestionMutation(),
195
+ meta: SUGGESTION_META,
196
+ onSuccess,
197
+ });
198
+
191
199
  const acceptSuggestion = useCallback(
192
200
  (suggestionId: string, calendarId: string) =>
193
201
  accept
@@ -231,6 +239,14 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
231
239
  .catch(refused),
232
240
  [dismiss],
233
241
  );
242
+ const reopenSuggestion = useCallback(
243
+ (suggestionId: string) =>
244
+ reopen
245
+ .mutateAsync({ path: { suggestionId } })
246
+ .then(() => ANSWERED)
247
+ .catch(refused),
248
+ [reopen],
249
+ );
234
250
 
235
251
  return useMemo(
236
252
  () => ({
@@ -239,7 +255,12 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
239
255
  acceptEdited: acceptEditedSuggestion,
240
256
  decline: declineSuggestion,
241
257
  dismiss: dismissSuggestion,
242
- isAnswering: accept.isPending || decline.isPending || dismiss.isPending,
258
+ reopen: reopenSuggestion,
259
+ isAnswering:
260
+ accept.isPending ||
261
+ decline.isPending ||
262
+ dismiss.isPending ||
263
+ reopen.isPending,
243
264
  }),
244
265
  [
245
266
  acceptSuggestion,
@@ -247,9 +268,11 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
247
268
  acceptEditedSuggestion,
248
269
  declineSuggestion,
249
270
  dismissSuggestion,
271
+ reopenSuggestion,
250
272
  accept.isPending,
251
273
  decline.isPending,
252
274
  dismiss.isPending,
275
+ reopen.isPending,
253
276
  ],
254
277
  );
255
278
  }
@@ -101,6 +101,7 @@ const invitation = (
101
101
  supersededByThreadId: "",
102
102
  createdAt: 0,
103
103
  updatedAt: 0,
104
+ answerOvertakenBy: "None",
104
105
  });
105
106
 
106
107
  const Harness = () => {
@@ -264,19 +265,63 @@ describe("the invitation beside the open message", () => {
264
265
  press("Stop offering invitations from Alice");
265
266
 
266
267
  await harness?.waitFor(
267
- () => harness?.text().includes("Add to calendar") === false,
268
- "the card to leave once the server dismissed it",
269
- );
270
- assert.match(
271
- harness?.text() ?? "",
272
- /Nothing in this message is about a time/,
273
- "answering the last card keeps the Calendar tab",
268
+ () => harness?.text().includes("You dismissed this") === true,
269
+ "the card to read back the dismissed state",
274
270
  );
271
+ assert.doesNotMatch(harness?.text() ?? "", /Add to calendar/);
275
272
  const dismissed =
276
273
  http?.to(`/calendar-suggestions/${SUGGESTION}/dismiss`) ?? [];
277
274
  assert.equal(dismissed.length, 1);
278
275
  assert.deepEqual(dismissed[0].body, { muteSender: true });
279
276
  });
277
+
278
+ for (const [label, state] of [
279
+ ["Add to calendar", "Accepted"],
280
+ ["Maybe", "Tentative"],
281
+ ["Decline", "Declined"],
282
+ ["Stop offering invitations from Alice", "Dismissed"],
283
+ ] as const) {
284
+ it(`takes a ${state} answer back through the API, and asks again`, async () => {
285
+ await mount(
286
+ server((call) => (call.path.endsWith("/reopen") ? "Pending" : state)),
287
+ );
288
+ press(label);
289
+ await harness?.waitFor(
290
+ () => harness?.text().includes("Change") === true,
291
+ "the card to offer Change once answered",
292
+ );
293
+
294
+ press("Change");
295
+
296
+ await harness?.waitFor(
297
+ () => harness?.text().includes("Add to calendar") === true,
298
+ "the answers to be offered again once the server reopened it",
299
+ );
300
+ assert.equal(
301
+ http?.to(`/calendar-suggestions/${SUGGESTION}/reopen`).length,
302
+ 1,
303
+ );
304
+ });
305
+ }
306
+
307
+ it("offers no Change on an answer its cancellation was answered over", async () => {
308
+ await mount((call) => {
309
+ if (call.path.endsWith("/calendars")) return { items: calendars };
310
+ if (call.path.endsWith("/messages/msg-1/calendar-suggestions"))
311
+ return {
312
+ items: [
313
+ { ...invitation("Accepted"), answerOvertakenBy: "Cancellation" },
314
+ ],
315
+ };
316
+ return { items: [] };
317
+ });
318
+
319
+ await harness?.waitFor(
320
+ () => harness?.text().includes("On your calendar") === true,
321
+ "the card to read back the accepted state",
322
+ );
323
+ assert.equal(harness?.text().includes("Change"), false);
324
+ });
280
325
  });
281
326
 
282
327
  (globalThis as { self?: typeof globalThis }).self ??= globalThis;
@@ -9,6 +9,7 @@
9
9
  * outlives the message it is about.
10
10
  */
11
11
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
12
+ import { CalendarAnswerOvertaken } from "@remit/domain-enums";
12
13
  import type {
13
14
  CalendarDayEntry,
14
15
  CalendarSlotPick,
@@ -261,6 +262,9 @@ export function useIntelligenceCalendar(
261
262
  };
262
263
  const pickedText =
263
264
  date === "" ? "" : slotsAsText(date, slots, working.picked);
265
+ const overtaken =
266
+ invitation !== undefined &&
267
+ invitation.answerOvertakenBy !== CalendarAnswerOvertaken.None;
264
268
 
265
269
  const actions: IntelligenceCalendarActions = {
266
270
  onAddInvite: () => {
@@ -295,7 +299,12 @@ export function useIntelligenceCalendar(
295
299
  answer(invitationId, "keep this on your calendar", () =>
296
300
  answers.dismiss(invitationId, false),
297
301
  )
298
- : undefined,
302
+ : overtaken
303
+ ? undefined
304
+ : () =>
305
+ answer(invitationId, "take this answer back", () =>
306
+ answers.reopen(invitationId),
307
+ ),
299
308
  onOpenNewerInvite:
300
309
  openThread && invitation && invitation.supersededByThreadId !== ""
301
310
  ? () =>
@@ -40,6 +40,7 @@ const suggestion = (
40
40
  supersededByThreadId: "",
41
41
  createdAt: 0,
42
42
  updatedAt: 0,
43
+ answerOvertakenBy: "None",
43
44
  ...over,
44
45
  });
45
46
 
@@ -70,8 +71,15 @@ describe("inviteStanding", () => {
70
71
  });
71
72
  });
72
73
 
73
- it("draws no card for one the reader waved away", () => {
74
- assert.equal(inviteStanding(suggestion({ state: "Dismissed" })), undefined);
74
+ it("keeps a card the reader waved away, so it can be taken back", () => {
75
+ assert.deepEqual(inviteStanding(suggestion({ state: "Dismissed" })), {
76
+ state: "dismissed",
77
+ rsvp: "noReply",
78
+ });
79
+ assert.equal(
80
+ inviteStanding(suggestion({ method: "Cancel", state: "Dismissed" })),
81
+ undefined,
82
+ );
75
83
  });
76
84
 
77
85
  it("asks about a cancellation only until it is taken or kept", () => {
@@ -61,18 +61,20 @@ export interface InviteStanding {
61
61
  /**
62
62
  * What a suggestion's state means for its card.
63
63
  *
64
- * A dismissed card was waved away and is gone. A cancellation asks for an
65
- * answer only while it is pending: once taken or kept, the event it named is
66
- * settled and there is nothing left to say beside the message.
64
+ * A dismissed card was waved away without an answer, and stays so it can be
65
+ * taken back. A cancellation asks for an answer only while it is pending: once
66
+ * taken or kept, the event it named is settled and there is nothing left to
67
+ * say beside the message.
67
68
  */
68
69
  export function inviteStanding(
69
70
  suggestion: CalendarSuggestion,
70
71
  ): InviteStanding | undefined {
71
- if (suggestion.state === "Dismissed") return undefined;
72
72
  if (suggestion.method === "Cancel")
73
73
  return suggestion.state === "Pending"
74
74
  ? { state: "cancelled", rsvp: "noReply" }
75
75
  : undefined;
76
+ if (suggestion.state === "Dismissed")
77
+ return { state: "dismissed", rsvp: "noReply" };
76
78
  if (suggestion.state === "Superseded")
77
79
  return { state: "superseded", rsvp: "noReply" };
78
80
  if (suggestion.state === "Accepted")
@@ -35,6 +35,7 @@ const allDay = {
35
35
  supersededByThreadId: "",
36
36
  createdAt: 0,
37
37
  updatedAt: 0,
38
+ answerOvertakenBy: "None",
38
39
  } as const;
39
40
 
40
41
  describe("an all-day invitation, west of where it was sent", () => {