@remit/web-client 0.0.229 → 0.0.231

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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/components/calendar/CalendarComposePane.stories.tsx +25 -0
  3. package/src/components/calendar/CalendarComposePane.tsx +34 -0
  4. package/src/components/calendar/CalendarEventPane.stories.tsx +81 -0
  5. package/src/components/calendar/CalendarEventPane.tsx +67 -0
  6. package/src/components/calendar/CalendarShell.tsx +83 -0
  7. package/src/components/calendar/CalendarViewPlaceholder.tsx +37 -0
  8. package/src/components/calendar/CalendarWorkspace.stories.tsx +197 -0
  9. package/src/components/calendar/CalendarWorkspace.tsx +162 -0
  10. package/src/components/calendar/OpenCalendarEvent.tsx +39 -0
  11. package/src/components/calendar/calendar-workspace.render.test.ts +193 -0
  12. package/src/components/config-import/ConfigImportWizard.flow.test.ts +338 -0
  13. package/src/components/config-import/ConfigImportWizard.tsx +720 -0
  14. package/src/components/config-import/config-import.fixtures.ts +229 -0
  15. package/src/components/config-import/config-import.stories.tsx +449 -0
  16. package/src/components/config-import/steps.tsx +772 -0
  17. package/src/components/mail/MailSidebarAdapter.tsx +17 -0
  18. package/src/components/onboarding/OnboardingWizard.tsx +64 -390
  19. package/src/components/onboarding/credential-steps.tsx +441 -0
  20. package/src/components/settings/ConfigExportCard.tsx +114 -0
  21. package/src/components/settings/ConfigTransferPanel.tsx +72 -0
  22. package/src/hooks/useCalendarData.ts +72 -0
  23. package/src/lib/calendar-density.ts +34 -0
  24. package/src/lib/calendar-fixtures.ts +207 -0
  25. package/src/lib/calendar-format.ts +29 -0
  26. package/src/lib/calendar-route.test.ts +210 -0
  27. package/src/lib/calendar-route.ts +183 -0
  28. package/src/lib/config-import.test.ts +306 -0
  29. package/src/lib/config-import.ts +492 -0
  30. package/src/lib/json.ts +11 -0
  31. package/src/lib/search-params.test.ts +77 -0
  32. package/src/lib/search-params.ts +59 -0
  33. package/src/routeTree.gen.ts +220 -0
  34. package/src/router.tsx +5 -0
  35. package/src/routes/calendar/$view.$date/$calendarObjectId/$recurrenceId.tsx +26 -0
  36. package/src/routes/calendar/$view.$date/$calendarObjectId/index.tsx +18 -0
  37. package/src/routes/calendar/$view.$date/$calendarObjectId.tsx +15 -0
  38. package/src/routes/calendar/$view.$date/index.tsx +25 -0
  39. package/src/routes/calendar/$view.$date/new.tsx +20 -0
  40. package/src/routes/calendar/$view.$date.tsx +100 -0
  41. package/src/routes/calendar/-calendar-tree.test.ts +267 -0
  42. package/src/routes/calendar/index.tsx +27 -0
  43. package/src/routes/calendar.tsx +98 -0
  44. package/src/routes/import.tsx +51 -0
  45. package/src/routes/onboarding.tsx +9 -2
  46. package/src/routes/settings/accounts.tsx +30 -8
  47. package/src/routes/settings/advanced.tsx +5 -1
  48. package/src/routing/calendar-fragment.test.ts +66 -0
  49. package/src/routing/calendar.ts +139 -0
  50. package/src/routing/fragment.ts +19 -0
  51. package/src/routing/index.ts +10 -0
  52. package/src/routing/nav-selection.ts +2 -0
  53. package/src/routing/sections.ts +3 -1
@@ -0,0 +1,306 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import type {
4
+ RemitImapConfigImportItemReport,
5
+ RemitImapConfigImportReport,
6
+ } from "@remit/api-http-client/types.gen.ts";
7
+ import { ApiError } from "./api.js";
8
+ import {
9
+ countVerdicts,
10
+ groupReportSections,
11
+ MAX_CONFIG_FILE_BYTES,
12
+ pendingFolders,
13
+ readConfigText,
14
+ readConflict,
15
+ readFailure,
16
+ sectionResults,
17
+ writeFailure,
18
+ } from "./config-import.js";
19
+
20
+ const item = (
21
+ section: string,
22
+ key: string,
23
+ verdict: string,
24
+ reason?: string,
25
+ ): RemitImapConfigImportItemReport => ({ section, key, verdict, reason });
26
+
27
+ const reportOf = (
28
+ overrides: Partial<RemitImapConfigImportReport>,
29
+ ): RemitImapConfigImportReport => ({
30
+ valid: true,
31
+ schemaVersion: 1,
32
+ applied: false,
33
+ items: [],
34
+ errors: [],
35
+ warnings: [],
36
+ accountsNeedingCredentials: [],
37
+ ...overrides,
38
+ });
39
+
40
+ test("sections come back in write order, not in the order the report listed them", () => {
41
+ const sections = groupReportSections([
42
+ item("settings", "Density", "updated"),
43
+ item("accounts", "a@example.test", "created"),
44
+ item("filters", "Receipts", "created"),
45
+ ]);
46
+
47
+ assert.deepEqual(
48
+ sections.map((section) => section.id),
49
+ ["accounts", "filters", "settings"],
50
+ );
51
+ assert.deepEqual(
52
+ sections.map((section) => section.title),
53
+ ["Accounts", "Rules & filters", "Folders & appearance"],
54
+ );
55
+ });
56
+
57
+ test("a verdict this client does not know reads as rejected, never as an unmarked row", () => {
58
+ const counts = countVerdicts([
59
+ item("accounts", "a@example.test", "created"),
60
+ item("accounts", "b@example.test", "quarantined"),
61
+ ]);
62
+
63
+ assert.equal(counts.created, 1);
64
+ assert.equal(counts.rejected, 1);
65
+ });
66
+
67
+ test("each read failure gets its own instruction, and carries the server's words", () => {
68
+ const newer = readFailure(
69
+ reportOf({
70
+ valid: false,
71
+ errors: [
72
+ {
73
+ code: "UnsupportedVersion",
74
+ message: "Document schemaVersion 5 was written by a newer reader",
75
+ },
76
+ ],
77
+ }),
78
+ );
79
+ assert.equal(newer?.title, "This file was written by a newer Reader");
80
+ assert.match(newer?.fix ?? "", /Update this Reader/);
81
+ assert.match(newer?.raw ?? "", /UnsupportedVersion/);
82
+
83
+ const credential = readFailure(
84
+ reportOf({
85
+ valid: false,
86
+ errors: [
87
+ {
88
+ code: "CredentialPresent",
89
+ message: "Credential field at accounts[1]",
90
+ },
91
+ ],
92
+ }),
93
+ );
94
+ assert.equal(credential?.title, "The file carries a credential");
95
+ assert.match(credential?.fix ?? "", /change that mailbox's password/);
96
+ });
97
+
98
+ test("an unrecognised error code still says what happened rather than rendering blank", () => {
99
+ const failure = readFailure(
100
+ reportOf({
101
+ valid: false,
102
+ errors: [{ code: "something_new", message: "the store said no" }],
103
+ }),
104
+ );
105
+ assert.equal(failure?.title, "The file could not be imported");
106
+ assert.equal(failure?.explanation, "the store said no");
107
+ });
108
+
109
+ test("a write failure is not a read failure: it routes to the partial screen", () => {
110
+ const report = reportOf({
111
+ applied: true,
112
+ errors: [
113
+ {
114
+ code: "import_write_failed",
115
+ message: "the filter store refused the write",
116
+ details: { section: "filters", key: "Receipts" },
117
+ },
118
+ ],
119
+ });
120
+
121
+ assert.equal(readFailure(report), undefined);
122
+ assert.equal(writeFailure(report)?.code, "import_write_failed");
123
+ });
124
+
125
+ test("a stopped import splits the write order into landed, failed and never reached", () => {
126
+ const results = sectionResults(
127
+ reportOf({
128
+ applied: true,
129
+ items: [
130
+ item("accounts", "a@example.test", "created"),
131
+ item("labels", "Receipts", "created"),
132
+ item("filters", "Receipts", "rejected", "the store refused"),
133
+ ],
134
+ errors: [
135
+ {
136
+ code: "import_write_failed",
137
+ message:
138
+ "the filter store refused the write The items above it were written and remain; the import stopped here.",
139
+ details: { section: "filters", key: "Receipts" },
140
+ },
141
+ ],
142
+ }),
143
+ );
144
+
145
+ const byId = new Map(results.map((result) => [result.section, result]));
146
+ assert.equal(byId.get("accounts")?.state, "landed");
147
+ assert.equal(byId.get("accounts")?.detail, "1 entry written.");
148
+ assert.equal(byId.get("labels")?.state, "landed");
149
+ assert.equal(byId.get("filters")?.state, "failed");
150
+ assert.equal(byId.get("addressFlags")?.state, "not-attempted");
151
+ assert.equal(byId.get("settings")?.state, "not-attempted");
152
+ });
153
+
154
+ test("a pending folder names the setting waiting on it, and repeats are folded", () => {
155
+ const folders = pendingFolders(
156
+ reportOf({
157
+ warnings: [
158
+ {
159
+ code: "folder_not_found_yet",
160
+ message: "not here yet",
161
+ details: { filter: "Waternet", folderPath: "Projecten/Waternet" },
162
+ },
163
+ {
164
+ code: "folder_not_found_yet",
165
+ message: "not here yet",
166
+ details: { filter: "Waternet", folderPath: "Projecten/Waternet" },
167
+ },
168
+ {
169
+ code: "folder_not_found_yet",
170
+ message: "not here yet",
171
+ details: { folderPath: "Raadsstukken", accountId: "acct-1" },
172
+ },
173
+ {
174
+ code: "anchor_not_embedded",
175
+ message: "carried as text",
176
+ details: { filter: "Waternet" },
177
+ },
178
+ ],
179
+ }),
180
+ );
181
+
182
+ assert.deepEqual(
183
+ folders.map((folder) => folder.path),
184
+ ["Projecten/Waternet", "Raadsstukken"],
185
+ );
186
+ assert.equal(folders[0].waitingFor, "Rule “Waternet”");
187
+ assert.equal(folders[1].accountId, "acct-1");
188
+ });
189
+
190
+ test("a file that is not a configuration is refused before anything is sent", async () => {
191
+ const notJson = await readConfigText("photos.zip", 120, "PK");
192
+ assert.equal(notJson.ok, false);
193
+ assert.equal(
194
+ notJson.ok === false && notJson.failure.title,
195
+ "That file is not JSON",
196
+ );
197
+
198
+ const array = await readConfigText("list.json", 20, "[1,2,3]");
199
+ assert.equal(array.ok, false);
200
+ assert.equal(
201
+ array.ok === false && array.failure.title,
202
+ "That is not a Reader config file",
203
+ );
204
+
205
+ const huge = await readConfigText(
206
+ "dump.json",
207
+ MAX_CONFIG_FILE_BYTES + 1,
208
+ "{}",
209
+ );
210
+ assert.equal(huge.ok, false);
211
+ assert.match(
212
+ huge.ok === false ? huge.failure.title : "",
213
+ /too large to be a config file/,
214
+ );
215
+
216
+ const good = await readConfigText(
217
+ "config.json",
218
+ 30,
219
+ '{"kind":"reader.config"}',
220
+ );
221
+ assert.equal(good.ok, true);
222
+ assert.deepEqual(good.ok === true && good.document, {
223
+ kind: "reader.config",
224
+ });
225
+ });
226
+
227
+ test("an unrecognised section gets its own heading rather than being filed under one we know", () => {
228
+ const sections = groupReportSections([
229
+ item("accounts", "a@example.test", "created"),
230
+ item("messageDecisions", "some-message", "created"),
231
+ ]);
232
+
233
+ assert.deepEqual(
234
+ sections.map((section) => section.id),
235
+ ["accounts", "unknown"],
236
+ );
237
+ assert.equal(sections[1].title, "Not recognised");
238
+ assert.match(
239
+ sections[1].entries[0].reason ?? "",
240
+ /does not know the "messageDecisions" section/,
241
+ );
242
+ });
243
+
244
+ test("a write failure that names no section leaves every section unknown, never landed", () => {
245
+ const results = sectionResults(
246
+ reportOf({
247
+ applied: true,
248
+ items: [item("accounts", "a@example.test", "created")],
249
+ errors: [{ code: "import_write_failed", message: "the store refused" }],
250
+ }),
251
+ );
252
+
253
+ assert.deepEqual(
254
+ [...new Set(results.map((result) => result.state))],
255
+ ["unknown"],
256
+ );
257
+ assert.match(results[0].detail, /without naming where/);
258
+ });
259
+
260
+ test("a write failure naming a section this client does not know is also unknown", () => {
261
+ const results = sectionResults(
262
+ reportOf({
263
+ applied: true,
264
+ errors: [
265
+ {
266
+ code: "import_write_failed",
267
+ message: "the store refused",
268
+ details: { section: "messageDecisions" },
269
+ },
270
+ ],
271
+ }),
272
+ );
273
+
274
+ assert.deepEqual(
275
+ [...new Set(results.map((result) => result.state))],
276
+ ["unknown"],
277
+ );
278
+ assert.match(results[0].detail, /"messageDecisions"/);
279
+ });
280
+
281
+ test("the 409 is read off the wrapper the client throws, not off the error itself", () => {
282
+ const body = {
283
+ code: "config_not_empty",
284
+ message: "already holds configuration",
285
+ details: { accounts: "2" },
286
+ };
287
+
288
+ const wrapped = readConflict(new ApiError("already holds", 409, body));
289
+ assert.equal(wrapped?.message, "already holds configuration");
290
+ assert.deepEqual(wrapped?.details, { accounts: "2" });
291
+
292
+ // Belt and braces: the same body thrown unwrapped still reaches the screen.
293
+ assert.equal(readConflict(body)?.message, "already holds configuration");
294
+ });
295
+
296
+ test("only the config conflict reads as one", () => {
297
+ assert.equal(
298
+ readConflict(new ApiError("nope", 409, { code: "other" })),
299
+ undefined,
300
+ );
301
+ assert.equal(
302
+ readConflict(new ApiError("nope", 500, { code: "config_not_empty" })),
303
+ undefined,
304
+ );
305
+ assert.equal(readConflict(new Error("boom")), undefined);
306
+ });