@remit/web-client 0.0.321 → 0.0.323
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 +4 -4
- package/src/auth/SignInAgain.tsx +18 -0
- package/src/auth/amplify-config.test.ts +1 -107
- package/src/auth/auth-interceptor.test.ts +0 -6
- package/src/auth/cognito/cognito-token.test.ts +2 -4
- package/src/components/calendar/AgendaStrip.tsx +1 -1
- package/src/components/calendar/CalendarWorkspace.tsx +1 -1
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
- package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
- package/src/components/compose/ComposeForm.tsx +11 -6
- package/src/components/compose/FullCompose.tsx +1 -1
- package/src/components/compose/MobileComposeSheet.tsx +1 -2
- package/src/components/compose/compose.stories.tsx +18 -0
- package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
- package/src/components/config-import/config-import.fixtures.ts +8 -9
- package/src/components/config-import/config-import.stories.tsx +2 -2
- package/src/components/layout/MailTopBar.tsx +3 -0
- package/src/components/mail/BriefPane.tsx +0 -3
- package/src/components/mail/ConversationView.tsx +1 -3
- package/src/components/mail/DailyBrief.tsx +5 -28
- package/src/components/mail/FlaggedList.tsx +3 -4
- package/src/components/mail/FlaggedPane.tsx +0 -3
- package/src/components/mail/IntelligenceDrawer.tsx +0 -3
- package/src/components/mail/IntelligencePane.render.test.ts +0 -1
- package/src/components/mail/IntelligencePane.test.ts +1 -95
- package/src/components/mail/IntelligencePane.tsx +3 -93
- package/src/components/mail/MailListHeader.tsx +12 -43
- package/src/components/mail/MailNav.tsx +6 -2
- package/src/components/mail/MailViewChrome.tsx +1 -11
- package/src/components/mail/MailboxPane.tsx +4 -34
- package/src/components/mail/MessageActionMenu.tsx +6 -6
- package/src/components/mail/MessageAttachments.tsx +35 -0
- package/src/components/mail/MessageBody.tsx +1 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
- package/src/components/mail/MessageCard.tsx +16 -4
- package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
- package/src/components/mail/MessageList.tsx +23 -11
- package/src/components/mail/MessageRow.render.test.ts +0 -7
- package/src/components/mail/MoveToTrigger.tsx +1 -1
- package/src/components/mail/RawMessageView.tsx +1 -1
- package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
- package/src/components/mail/SelectionWizardHost.tsx +16 -102
- package/src/components/mail/authenticity-warning.stories.tsx +0 -21
- package/src/components/mail/daily-brief.stories.tsx +31 -17
- package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
- package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
- package/src/components/mail/message-gone.stories.tsx +44 -0
- package/src/components/mail/offer-times-reply.render.test.ts +25 -1
- package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
- package/src/components/overlay-escape.render.test.ts +7 -3
- package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
- package/src/components/settings/AccountFormPanel.test.ts +1 -36
- package/src/components/settings/AccountFormPanel.tsx +5 -25
- package/src/components/settings/AccountServices.tsx +1 -1
- package/src/components/settings/AdvancedNavIcon.tsx +3 -3
- package/src/components/settings/CalendarFeedCard.tsx +2 -1
- package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
- package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
- package/src/components/settings/FilterActionError.tsx +1 -1
- package/src/components/settings/FilterEditor.render.test.ts +3 -27
- package/src/components/settings/FilterEditor.tsx +4 -24
- package/src/components/settings/FilterEditorSurface.tsx +0 -3
- package/src/components/settings/FiltersList.render.test.ts +5 -24
- package/src/components/settings/FiltersList.stories.tsx +0 -1
- package/src/components/settings/FiltersList.tsx +10 -35
- package/src/components/settings/WarningsNavIcon.tsx +12 -0
- package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
- package/src/components/settings/WarningsPanel.render.test.ts +56 -0
- package/src/components/settings/WarningsPanel.tsx +244 -0
- package/src/components/settings/accounts.stories.tsx +18 -0
- package/src/components/settings/advanced.stories.tsx +3 -125
- package/src/components/settings/settings-filter.stories.tsx +2 -42
- package/src/components/settings/warnings.stories.tsx +290 -0
- package/src/components/ui/ErrorBannerProvider.tsx +5 -3
- package/src/components/ui/error-banners.test.ts +1 -22
- package/src/components/ui/error-banners.ts +9 -20
- package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
- package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
- package/src/hooks/calendar/useCalendarWrites.ts +2 -1
- package/src/hooks/intelligence-calendar.render.test.ts +0 -1
- package/src/hooks/useDeleteMessages.test.ts +1 -42
- package/src/hooks/useFilters.test.ts +0 -8
- package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
- package/src/hooks/useIntelligenceData.test.ts +9 -47
- package/src/hooks/useIntelligenceData.ts +7 -100
- package/src/hooks/useLayoutTier.test.ts +0 -48
- package/src/hooks/useReportSpam.ts +1 -1
- package/src/hooks/useRescueCandidates.test.ts +0 -23
- package/src/hooks/useRulePreview.ts +8 -7
- package/src/hooks/useSearchTokenContext.ts +2 -3
- package/src/hooks/useStaleAccountSync.test.ts +0 -17
- package/src/hooks/useStarredThreads.test.ts +0 -10
- package/src/hooks/useTriggerSync.test.ts +0 -6
- package/src/hooks/useVisualViewport.render.test.ts +63 -0
- package/src/hooks/useWarningsAttention.ts +37 -0
- package/src/lib/account-order.test.ts +0 -10
- package/src/lib/adjacent-message.test.ts +0 -9
- package/src/lib/app-info.ts +0 -3
- package/src/lib/autodiscovery.test.ts +0 -47
- package/src/lib/bug-report.test.ts +38 -0
- package/src/lib/bug-report.ts +10 -7
- package/src/lib/bulk-action-copy.ts +1 -1
- package/src/lib/calendar-route.test.ts +0 -5
- package/src/lib/config-import.test.ts +3 -39
- package/src/lib/dedupe-thread-messages.test.ts +0 -4
- package/src/lib/desktop-breakpoint.test.ts +0 -8
- package/src/lib/drafts.test.ts +0 -23
- package/src/lib/fatal-error.test.ts +0 -19
- package/src/lib/folder-roles.test.ts +0 -4
- package/src/lib/hot-sync-window.test.ts +0 -5
- package/src/lib/list-header-chrome.ts +1 -1
- package/src/lib/mail-context.ts +1 -1
- package/src/lib/message-attachments.test.ts +77 -0
- package/src/lib/message-attachments.ts +31 -15
- package/src/lib/message-body-source.test.ts +21 -4
- package/src/lib/message-body-source.ts +5 -2
- package/src/lib/onboarding-completion.test.ts +0 -16
- package/src/lib/organize/filter-edit-model.test.ts +2 -32
- package/src/lib/organize/filter-edit-model.ts +6 -16
- package/src/lib/organize/organize-model.test.ts +7 -40
- package/src/lib/organize/organize-model.ts +16 -18
- package/src/lib/organize/rule-model.test.ts +24 -202
- package/src/lib/organize/rule-model.ts +19 -166
- package/src/lib/organize/search-to-rule.test.ts +2 -36
- package/src/lib/organize/search-to-rule.ts +1 -16
- package/src/lib/provider-presets.test.ts +1 -77
- package/src/lib/query-error-handler.test.ts +0 -61
- package/src/lib/query-escalation.integration.test.ts +4 -4
- package/src/lib/result-count.ts +1 -1
- package/src/lib/result-folder.ts +1 -1
- package/src/lib/rum-adapter.test.ts +0 -104
- package/src/lib/search-query.test.ts +0 -11
- package/src/lib/search-result.test.ts +2 -142
- package/src/lib/search-result.ts +3 -73
- package/src/lib/search-scope.test.ts +1 -92
- package/src/lib/search-scope.ts +0 -29
- package/src/lib/search-surface.ts +2 -2
- package/src/lib/search-token-index.test.ts +0 -8
- package/src/lib/search-tokens.ts +15 -16
- package/src/lib/starred-rows.test.ts +0 -4
- package/src/lib/sync-warning-entries.ts +33 -0
- package/src/lib/wizard-selection.ts +1 -1
- package/src/mocks/story-frame/mail-handlers.ts +12 -8
- package/src/mocks/story-frame/mail-world.ts +5 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/calendar/$view.$date/index.tsx +1 -1
- package/src/routes/calendar.tsx +2 -1
- package/src/routes/mail/brief.tsx +1 -1
- package/src/routes/mail/flagged.tsx +1 -1
- package/src/routes/mail.tsx +5 -3
- package/src/routes/settings/-accounts.test.ts +0 -5
- package/src/routes/settings/-filters.test.ts +0 -1
- package/src/routes/settings/-shared/help-copy.tsx +15 -0
- package/src/routes/settings/accounts.tsx +1 -1
- package/src/routes/settings/advanced.tsx +1 -4
- package/src/routes/settings/filters.tsx +1 -2
- package/src/routes/settings/folders.tsx +1 -1
- package/src/routes/settings/labels.tsx +1 -1
- package/src/routes/settings/senders.tsx +1 -1
- package/src/routes/settings/warnings.tsx +36 -0
- package/src/routes/settings.tsx +7 -0
- package/src/routing/wizard-history.test.ts +1 -45
- package/src/runtime-config.ts +5 -0
- package/src/test-support/fixtures.ts +1 -1
- package/src/components/compose/draft-discard-banner.test.ts +0 -76
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
- package/src/components/mail/SpamRescue.render.test.ts +0 -35
- package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
- package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
- package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
- package/src/components/settings/QuarantinePanel.tsx +0 -178
- package/src/components/ui/DropdownMenu.tsx +0 -99
- package/src/components/ui/EmptyState.tsx +0 -11
- package/src/components/ui/ErrorBanner.render.test.ts +0 -104
- package/src/components/ui/ErrorBanner.stories.tsx +0 -132
- package/src/components/ui/ErrorBanner.tsx +0 -106
- package/src/components/ui/ErrorBannerStack.tsx +0 -35
- package/src/components/ui/ErrorState.render.test.ts +0 -82
- package/src/components/ui/ErrorState.tsx +0 -80
- package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
- package/src/hooks/useMoveMessages.test.ts +0 -63
- package/src/hooks/useOrganizeWiden.test.ts +0 -217
- package/src/hooks/useOrganizeWiden.ts +0 -112
- package/src/hooks/useSemanticSearch.ts +0 -132
- package/src/hooks/useSemanticSearchEnabled.ts +0 -23
- package/src/hooks/useVisualViewport.test.ts +0 -126
- package/src/lib/organize/sender-fallback.test.ts +0 -52
- package/src/lib/organize/sender-fallback.ts +0 -38
- package/src/lib/rescue-telemetry.test.ts +0 -75
- package/src/router.test.ts +0 -81
|
@@ -170,7 +170,6 @@ const sender: IntelligenceData = {
|
|
|
170
170
|
summary: "Signed by example.test.",
|
|
171
171
|
},
|
|
172
172
|
category: { value: "personal" },
|
|
173
|
-
similar: [],
|
|
174
173
|
};
|
|
175
174
|
|
|
176
175
|
let harness: DomHarness | undefined;
|
|
@@ -264,6 +263,28 @@ const creates = (): HttpCall[] =>
|
|
|
264
263
|
(call) => call.method === "POST" && call.path.endsWith("/outbox"),
|
|
265
264
|
);
|
|
266
265
|
|
|
266
|
+
const draftWrites = (): HttpCall[] => [
|
|
267
|
+
...creates(),
|
|
268
|
+
...(http?.calls ?? []).filter(
|
|
269
|
+
(call) =>
|
|
270
|
+
call.method === "PATCH" && call.path.endsWith(`/outbox/${DRAFT_ID}`),
|
|
271
|
+
),
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// An autosave still waiting out its debounce outlives the test that scheduled
|
|
275
|
+
// it and lands on the next test's server, so a test that writes waits for it.
|
|
276
|
+
const savedHolding = (mounted: DomHarness, text: string): Promise<void> =>
|
|
277
|
+
mounted.waitFor(
|
|
278
|
+
() =>
|
|
279
|
+
draftWrites().some(
|
|
280
|
+
(call) =>
|
|
281
|
+
(call.body as { textBody?: string }).textBody?.includes(text) ===
|
|
282
|
+
true,
|
|
283
|
+
),
|
|
284
|
+
`the draft to be saved holding ${JSON.stringify(text)}`,
|
|
285
|
+
AUTOSAVE_DEBOUNCE_MS + 3000,
|
|
286
|
+
);
|
|
287
|
+
|
|
267
288
|
const mount = async (
|
|
268
289
|
href: string,
|
|
269
290
|
{
|
|
@@ -445,6 +466,7 @@ describe("offering other times from an invitation", () => {
|
|
|
445
466
|
assert.match(plainBody(mounted)?.value ?? "", /^Hi/);
|
|
446
467
|
assert.equal(router.state.location.pathname, `${MESSAGE_PATH}/reply`);
|
|
447
468
|
assert.equal(seedInAddress(router), undefined);
|
|
469
|
+
await savedHolding(mounted, picked);
|
|
448
470
|
});
|
|
449
471
|
|
|
450
472
|
it("keeps an open reply-all a reply-all and adds the times to it", async () => {
|
|
@@ -465,6 +487,7 @@ describe("offering other times from an invitation", () => {
|
|
|
465
487
|
assert.ok(composerText(mounted).includes("See you then"));
|
|
466
488
|
assert.equal(router.state.location.pathname, href);
|
|
467
489
|
assert.equal(seedInAddress(router), undefined);
|
|
490
|
+
await savedHolding(mounted, picked);
|
|
468
491
|
});
|
|
469
492
|
|
|
470
493
|
it("adds the times to a forward of the same message already open", async () => {
|
|
@@ -507,6 +530,7 @@ describe("offering other times from an invitation", () => {
|
|
|
507
530
|
router.state.location.pathname,
|
|
508
531
|
`/mail/${MAILBOX_ID}/${THREAD_ID}/${OTHER_ID}/reply`,
|
|
509
532
|
);
|
|
533
|
+
await savedHolding(mounted, picked);
|
|
510
534
|
});
|
|
511
535
|
|
|
512
536
|
it("adds the times on top of a draft still loading when they were picked", async () => {
|
|
@@ -65,9 +65,7 @@ describe("the reclassify picker on a sender that already carries an override", (
|
|
|
65
65
|
|
|
66
66
|
http = mockFetch((call) => {
|
|
67
67
|
if (call.path.endsWith("/config")) {
|
|
68
|
-
return makeConfig([makeAccount({ accountId: "acc-1" })]
|
|
69
|
-
semanticSearchEnabled: false,
|
|
70
|
-
});
|
|
68
|
+
return makeConfig([makeAccount({ accountId: "acc-1" })]);
|
|
71
69
|
}
|
|
72
70
|
if (call.path.endsWith("/addresses/search")) {
|
|
73
71
|
return { items: [stored] };
|
|
@@ -115,9 +113,7 @@ describe("the reclassify picker on a sender that already carries an override", (
|
|
|
115
113
|
it("offers no removal on a sender that carries no override", async () => {
|
|
116
114
|
http = mockFetch((call) => {
|
|
117
115
|
if (call.path.endsWith("/config")) {
|
|
118
|
-
return makeConfig([makeAccount({ accountId: "acc-1" })]
|
|
119
|
-
semanticSearchEnabled: false,
|
|
120
|
-
});
|
|
116
|
+
return makeConfig([makeAccount({ accountId: "acc-1" })]);
|
|
121
117
|
}
|
|
122
118
|
if (call.path.endsWith("/addresses/search")) {
|
|
123
119
|
return { items: [addressWith({})] };
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
import "@remit/test-dom";
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
ConfirmDialog,
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
KeyboardShortcutsModal,
|
|
19
|
+
useTriageKeyboard,
|
|
20
|
+
} from "@remit/ui";
|
|
15
21
|
import { act, createElement, type ReactNode } from "react";
|
|
16
22
|
import { createRoot, type Root } from "react-dom/client";
|
|
17
23
|
import { Drawer } from "./layout/Drawer";
|
|
18
|
-
import { DropdownMenu, DropdownMenuItem } from "./ui/DropdownMenu";
|
|
19
|
-
import { KeyboardShortcutsModal } from "./ui/KeyboardShortcutsModal";
|
|
20
24
|
|
|
21
25
|
let root: Root;
|
|
22
26
|
let container: HTMLElement;
|
|
@@ -202,9 +202,13 @@ describe("AccountFormPanel — provider presets", () => {
|
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
describe("AccountFormPanel — editing an account", () => {
|
|
205
|
-
it("
|
|
205
|
+
it("starts the password blank and keeps the stored one on save", async () => {
|
|
206
206
|
const dom = mount({ account });
|
|
207
|
-
assert.equal(field(dom, "account-password").value, "
|
|
207
|
+
assert.equal(field(dom, "account-password").value, "");
|
|
208
|
+
assert.equal(
|
|
209
|
+
field(dom, "account-password").placeholder,
|
|
210
|
+
"Leave empty to keep current",
|
|
211
|
+
);
|
|
208
212
|
|
|
209
213
|
await submit(dom);
|
|
210
214
|
|
|
@@ -223,7 +227,26 @@ describe("AccountFormPanel — editing an account", () => {
|
|
|
223
227
|
assert.equal(body(saved()).password, "new-app-password");
|
|
224
228
|
});
|
|
225
229
|
|
|
226
|
-
it("
|
|
230
|
+
it("reveals the password the user types, not a mask", () => {
|
|
231
|
+
const dom = mount({ account });
|
|
232
|
+
dom.type(field(dom, "account-password"), "new-app-password");
|
|
233
|
+
|
|
234
|
+
dom.click(dom.byLabel("Show password"));
|
|
235
|
+
|
|
236
|
+
const input = field(dom, "account-password");
|
|
237
|
+
assert.equal(input.getAttribute("type"), "text");
|
|
238
|
+
assert.equal(input.value, "new-app-password");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("reveals an empty field before anything is typed", () => {
|
|
242
|
+
const dom = mount({ account });
|
|
243
|
+
|
|
244
|
+
dom.click(dom.byLabel("Show password"));
|
|
245
|
+
|
|
246
|
+
assert.equal(field(dom, "account-password").value, "");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("tests the connection against the stored password while the field is blank", async () => {
|
|
227
250
|
const dom = mount({ account });
|
|
228
251
|
dom.click(dom.byText("button", "Test IMAP Connection"));
|
|
229
252
|
await dom.flush();
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
deriveUseDifferentSmtpCreds,
|
|
5
|
-
isOAuthMicrosoftAccount,
|
|
6
|
-
} from "./AccountFormPanel.tsx";
|
|
3
|
+
import { deriveUseDifferentSmtpCreds } from "./AccountFormPanel.tsx";
|
|
7
4
|
|
|
8
5
|
describe("deriveUseDifferentSmtpCreds", () => {
|
|
9
6
|
test("returns true when smtpUsername differs from username", () => {
|
|
@@ -22,22 +19,6 @@ describe("deriveUseDifferentSmtpCreds", () => {
|
|
|
22
19
|
assert.strictEqual(result, false);
|
|
23
20
|
});
|
|
24
21
|
|
|
25
|
-
test("returns false when smtpUsername is undefined", () => {
|
|
26
|
-
const result = deriveUseDifferentSmtpCreds({
|
|
27
|
-
username: "alice@example.com",
|
|
28
|
-
smtpUsername: undefined,
|
|
29
|
-
});
|
|
30
|
-
assert.strictEqual(result, false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("returns false when smtpUsername is empty string", () => {
|
|
34
|
-
const result = deriveUseDifferentSmtpCreds({
|
|
35
|
-
username: "alice@example.com",
|
|
36
|
-
smtpUsername: "",
|
|
37
|
-
});
|
|
38
|
-
assert.strictEqual(result, false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
22
|
test("returns false when smtpUsername is whitespace", () => {
|
|
42
23
|
const result = deriveUseDifferentSmtpCreds({
|
|
43
24
|
username: "alice@example.com",
|
|
@@ -54,19 +35,3 @@ describe("deriveUseDifferentSmtpCreds", () => {
|
|
|
54
35
|
assert.strictEqual(result, false);
|
|
55
36
|
});
|
|
56
37
|
});
|
|
57
|
-
|
|
58
|
-
describe("isOAuthMicrosoftAccount", () => {
|
|
59
|
-
test("returns true for oauthMicrosoft authType", () => {
|
|
60
|
-
assert.strictEqual(
|
|
61
|
-
isOAuthMicrosoftAccount({ authType: "oauthMicrosoft" }),
|
|
62
|
-
true,
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("returns false for password authType", () => {
|
|
67
|
-
assert.strictEqual(
|
|
68
|
-
isOAuthMicrosoftAccount({ authType: "password" }),
|
|
69
|
-
false,
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -45,9 +45,6 @@ export const isOAuthMicrosoftAccount = (
|
|
|
45
45
|
account: Pick<RemitImapAccountResponse, "authType">,
|
|
46
46
|
): boolean => account.authType === "oauthMicrosoft";
|
|
47
47
|
|
|
48
|
-
// Placeholder shown when editing - indicates password exists but isn't shown
|
|
49
|
-
const PASSWORD_PLACEHOLDER = "••••••••••";
|
|
50
|
-
|
|
51
48
|
/**
|
|
52
49
|
* Derive the initial state of the "use different SMTP credentials" checkbox
|
|
53
50
|
* from a persisted account. Returns true when the account stores an SMTP
|
|
@@ -103,9 +100,6 @@ export const AccountFormPanel = ({
|
|
|
103
100
|
const queryClient = useQueryClient();
|
|
104
101
|
const isEditing = !!account;
|
|
105
102
|
|
|
106
|
-
// Track if user has modified the password field
|
|
107
|
-
const [passwordModified, setPasswordModified] = useState(false);
|
|
108
|
-
|
|
109
103
|
// Selected provider preset (empty string = "Custom / other", manual entry)
|
|
110
104
|
const [presetId, setPresetId] = useState("");
|
|
111
105
|
// When a preset is selected its server fields are read-only until the
|
|
@@ -143,7 +137,7 @@ export const AccountFormPanel = ({
|
|
|
143
137
|
email: account.email,
|
|
144
138
|
displayName: account.displayName || "",
|
|
145
139
|
username: account.username || "",
|
|
146
|
-
password:
|
|
140
|
+
password: "",
|
|
147
141
|
imapHost: account.imapHost,
|
|
148
142
|
imapPort: account.imapPort,
|
|
149
143
|
imapTls: account.imapTls,
|
|
@@ -156,7 +150,6 @@ export const AccountFormPanel = ({
|
|
|
156
150
|
smtpPassword: "",
|
|
157
151
|
useDifferentSmtpCreds,
|
|
158
152
|
});
|
|
159
|
-
setPasswordModified(false);
|
|
160
153
|
} else {
|
|
161
154
|
form.reset({
|
|
162
155
|
email: "",
|
|
@@ -173,7 +166,6 @@ export const AccountFormPanel = ({
|
|
|
173
166
|
smtpStartTls: true,
|
|
174
167
|
useDifferentSmtpCreds: false,
|
|
175
168
|
});
|
|
176
|
-
setPasswordModified(false);
|
|
177
169
|
}
|
|
178
170
|
}, [account, form]);
|
|
179
171
|
|
|
@@ -203,8 +195,7 @@ export const AccountFormPanel = ({
|
|
|
203
195
|
|
|
204
196
|
const handleTestImap = () => {
|
|
205
197
|
const values = form.getValues();
|
|
206
|
-
|
|
207
|
-
const useStoredPassword = isEditing && !passwordModified;
|
|
198
|
+
const useStoredPassword = isEditing && !values.password;
|
|
208
199
|
testMutation.mutate({
|
|
209
200
|
body: {
|
|
210
201
|
accountId: useStoredPassword ? account?.accountId : undefined,
|
|
@@ -220,8 +211,7 @@ export const AccountFormPanel = ({
|
|
|
220
211
|
|
|
221
212
|
const handleTestSmtp = () => {
|
|
222
213
|
const values = form.getValues();
|
|
223
|
-
|
|
224
|
-
const useStoredPassword = isEditing && !passwordModified;
|
|
214
|
+
const useStoredPassword = isEditing && !values.password;
|
|
225
215
|
testMutation.mutate({
|
|
226
216
|
body: {
|
|
227
217
|
accountId: useStoredPassword ? account?.accountId : undefined,
|
|
@@ -298,8 +288,7 @@ export const AccountFormPanel = ({
|
|
|
298
288
|
if (isEditing && account) {
|
|
299
289
|
const body = {
|
|
300
290
|
...baseBody,
|
|
301
|
-
|
|
302
|
-
password: passwordModified ? values.password : undefined,
|
|
291
|
+
password: values.password || undefined,
|
|
303
292
|
};
|
|
304
293
|
updateMutation.mutate({ path: { accountId: account.accountId }, body });
|
|
305
294
|
} else {
|
|
@@ -611,16 +600,7 @@ export const AccountFormPanel = ({
|
|
|
611
600
|
</label>
|
|
612
601
|
<PasswordInput
|
|
613
602
|
id="account-password"
|
|
614
|
-
{...form.register("password"
|
|
615
|
-
onChange: () => setPasswordModified(true),
|
|
616
|
-
})}
|
|
617
|
-
onFocus={() => {
|
|
618
|
-
// Clear placeholder when user focuses the field
|
|
619
|
-
if (isEditing && !passwordModified) {
|
|
620
|
-
form.setValue("password", "");
|
|
621
|
-
setPasswordModified(true);
|
|
622
|
-
}
|
|
623
|
-
}}
|
|
603
|
+
{...form.register("password")}
|
|
624
604
|
placeholder={isEditing ? "Leave empty to keep current" : ""}
|
|
625
605
|
/>
|
|
626
606
|
{form.formState.errors.password && (
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
type AccountServiceChange,
|
|
11
11
|
AccountServiceChangeDialog,
|
|
12
12
|
type AccountServiceIntent,
|
|
13
|
+
formatErrorMessage,
|
|
13
14
|
} from "@remit/ui";
|
|
14
15
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
15
16
|
import { useState } from "react";
|
|
16
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
17
17
|
import {
|
|
18
18
|
REDIRECT_STALL_MESSAGE,
|
|
19
19
|
useRedirectEnded,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* update gets outside the pane — no count, no copy, no action. Everywhere else
|
|
6
6
|
* stays silent.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { AttentionDot } from "@remit/ui";
|
|
9
9
|
import { Wrench } from "lucide-react";
|
|
10
10
|
import { useOptionalSelfUpdate } from "@/hooks/use-system-update";
|
|
11
11
|
|
|
@@ -16,8 +16,8 @@ export function AdvancedNavIcon() {
|
|
|
16
16
|
selfUpdate.surface.section.status === "available";
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<
|
|
19
|
+
<AttentionDot label={updateAvailable ? "Update available" : undefined}>
|
|
20
20
|
<Wrench className="size-4" />
|
|
21
|
-
</
|
|
21
|
+
</AttentionDot>
|
|
22
22
|
);
|
|
23
23
|
}
|
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
CardHeader,
|
|
16
16
|
CardTitle,
|
|
17
17
|
ConfirmDialog,
|
|
18
|
+
ErrorState,
|
|
19
|
+
formatErrorMessage,
|
|
18
20
|
} from "@remit/ui";
|
|
19
21
|
import { Check, Copy, Link2, RefreshCw, Trash2 } from "lucide-react";
|
|
20
22
|
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
|
|
21
|
-
import { ErrorState, formatErrorMessage } from "@/components/ui/ErrorState";
|
|
22
23
|
import type { CalendarFeedState } from "@/hooks/calendar/useCalendarFeed";
|
|
23
24
|
import {
|
|
24
25
|
revokeFeedConfirmCopy,
|
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
CardBody,
|
|
13
13
|
CardHeader,
|
|
14
14
|
CardTitle,
|
|
15
|
+
formatErrorMessage,
|
|
15
16
|
Input,
|
|
16
17
|
} from "@remit/ui";
|
|
17
18
|
import { CalendarPlus } from "lucide-react";
|
|
18
19
|
import { type FormEvent, useId, useState } from "react";
|
|
19
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
20
20
|
|
|
21
21
|
export interface CalendarSubscribeCardProps {
|
|
22
22
|
isBusy: boolean;
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* The address itself is not drawn. For a Google feed it is the whole
|
|
7
7
|
* credential, so only its host is named.
|
|
8
8
|
*/
|
|
9
|
-
import { Badge, Banner, Button } from "@remit/ui";
|
|
9
|
+
import { Badge, Banner, Button, formatErrorMessage } from "@remit/ui";
|
|
10
10
|
import { Pause, Play } from "lucide-react";
|
|
11
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
12
11
|
import { subscriptionHostOf } from "@/lib/calendar-subscription";
|
|
13
12
|
import { formatDatePreset } from "@/lib/format";
|
|
14
13
|
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* The contract these tests pin: a predicate or action change patches the
|
|
7
7
|
* predicate fields (so the server bumps `ruleChangedAt`) and offers a
|
|
8
8
|
* re-back-apply carrying exactly the previewed predicate; a cosmetic rename
|
|
9
|
-
* patches the name alone and offers nothing (RFC 034 Decision 3.2).
|
|
10
|
-
* filter lists its widen inactive and lets the user take it off.
|
|
9
|
+
* patches the name alone and offers nothing (RFC 034 Decision 3.2).
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import assert from "node:assert/strict";
|
|
@@ -48,7 +47,6 @@ const filterFixture = (
|
|
|
48
47
|
scope: "Standing",
|
|
49
48
|
state: "Active",
|
|
50
49
|
disabledReason: "None",
|
|
51
|
-
hasAnchor: false,
|
|
52
50
|
ruleChangedAt: 100,
|
|
53
51
|
actionChangedAt: 100,
|
|
54
52
|
matchOperator: "And",
|
|
@@ -77,9 +75,8 @@ const RULE_ASSERTION_FIELDS = [
|
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
77
|
* A backend that echoes the real update contract: the patch merges over the
|
|
80
|
-
* stored filter, `
|
|
81
|
-
*
|
|
82
|
-
* patch touches a predicate or action field.
|
|
78
|
+
* stored filter, and `ruleChangedAt` advances only when the patch touches a
|
|
79
|
+
* predicate or action field.
|
|
83
80
|
*/
|
|
84
81
|
const backend = (
|
|
85
82
|
filter: RemitImapFilterResponse,
|
|
@@ -95,7 +92,6 @@ const backend = (
|
|
|
95
92
|
return {
|
|
96
93
|
...filter,
|
|
97
94
|
...body,
|
|
98
|
-
hasAnchor: filter.hasAnchor,
|
|
99
95
|
ruleChangedAt: bumped ? filter.ruleChangedAt + 1 : filter.ruleChangedAt,
|
|
100
96
|
};
|
|
101
97
|
}
|
|
@@ -118,7 +114,6 @@ const backend = (
|
|
|
118
114
|
const mount = (
|
|
119
115
|
filter: RemitImapFilterResponse,
|
|
120
116
|
responder?: Responder,
|
|
121
|
-
semanticUnavailable = false,
|
|
122
117
|
): DomHarness => {
|
|
123
118
|
http = mockFetch(responder ?? backend(filter));
|
|
124
119
|
harness = createDomHarness();
|
|
@@ -128,7 +123,6 @@ const mount = (
|
|
|
128
123
|
filter,
|
|
129
124
|
folders: FOLDERS,
|
|
130
125
|
labels: [],
|
|
131
|
-
semanticUnavailable,
|
|
132
126
|
onClose: () => undefined,
|
|
133
127
|
}),
|
|
134
128
|
);
|
|
@@ -248,24 +242,6 @@ describe("FilterEditor — rule change offers the re-back-apply", () => {
|
|
|
248
242
|
});
|
|
249
243
|
});
|
|
250
244
|
|
|
251
|
-
describe("FilterEditor — degraded semantic filter (RFC 038 D4)", () => {
|
|
252
|
-
it("lists the widen inactive and display-only — the anchor is fixed at creation", async () => {
|
|
253
|
-
const dom = mount(filterFixture({ hasAnchor: true }), undefined, true);
|
|
254
|
-
await settlePreview(dom);
|
|
255
|
-
|
|
256
|
-
// The anchor this deployment cannot evaluate lists inactive.
|
|
257
|
-
assert.match(dom.text(), /not available here/i);
|
|
258
|
-
|
|
259
|
-
// The update endpoint carries no anchor, so the chip cannot be removed here:
|
|
260
|
-
// no remove affordance, and a note says the anchor is fixed at creation.
|
|
261
|
-
assert.equal(
|
|
262
|
-
dom.query('[aria-label="Remove the similar-mail widen"]'),
|
|
263
|
-
null,
|
|
264
|
-
);
|
|
265
|
-
assert.match(dom.text(), /similar-mail match is fixed to the message/i);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
245
|
describe("FilterEditor — scope and expiry are editable (reader #266)", () => {
|
|
270
246
|
it("keeps the scope toggle and date input live, minus the once option", async () => {
|
|
271
247
|
const dom = mount(
|
|
@@ -38,26 +38,17 @@ interface FilterEditorProps {
|
|
|
38
38
|
/** The provider hierarchy separator the destination tree nests on. */
|
|
39
39
|
delimiter?: string;
|
|
40
40
|
labels: LabelOption[];
|
|
41
|
-
/**
|
|
42
|
-
* This deployment ships no vector pipeline, so a semantic anchor cannot be
|
|
43
|
-
* evaluated (RFC 038 D4). The filter's widen chip lists inactive and the rule
|
|
44
|
-
* matches by its literal clauses only.
|
|
45
|
-
*/
|
|
46
|
-
semanticUnavailable?: boolean;
|
|
47
41
|
onClose: () => void;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
/**
|
|
51
45
|
* Editing a standing filter in the same chip editor the Organize surface uses
|
|
52
46
|
* (RFC 038 D6). The row's persisted rule opens in the editor — clauses, match
|
|
53
|
-
* operator, move action, scope, expiry,
|
|
54
|
-
* chip. Saving a predicate, action, scope, or expiry change bumps
|
|
47
|
+
* operator, move action, scope, and expiry. Saving a predicate, action, scope, or expiry change bumps
|
|
55
48
|
* `ruleChangedAt` and offers, never runs, a re-back-apply over existing mail;
|
|
56
49
|
* a cosmetic rename does neither (RFC 034 Decision 3.2, reader #266). The
|
|
57
50
|
* re-apply carries exactly the previewed predicate and is held behind the
|
|
58
|
-
* same settled-count commit gate as creation.
|
|
59
|
-
* creation regardless — repointing it would silently change what the filter
|
|
60
|
-
* matches, which deserves a new filter instead.
|
|
51
|
+
* same settled-count commit gate as creation.
|
|
61
52
|
*/
|
|
62
53
|
export function FilterEditor({
|
|
63
54
|
accountId,
|
|
@@ -65,13 +56,9 @@ export function FilterEditor({
|
|
|
65
56
|
folders,
|
|
66
57
|
delimiter,
|
|
67
58
|
labels,
|
|
68
|
-
semanticUnavailable = false,
|
|
69
59
|
onClose,
|
|
70
60
|
}: FilterEditorProps) {
|
|
71
|
-
const original = useMemo(
|
|
72
|
-
() => filterToRule(filter, semanticUnavailable),
|
|
73
|
-
[filter, semanticUnavailable],
|
|
74
|
-
);
|
|
61
|
+
const original = useMemo(() => filterToRule(filter), [filter]);
|
|
75
62
|
const [rule, setRule] = useState<FilterRule>(original);
|
|
76
63
|
const [clauseEdit, setClauseEdit] = useState<ClauseEditState | undefined>();
|
|
77
64
|
const [offerReapply, setOfferReapply] = useState(false);
|
|
@@ -225,15 +212,8 @@ export function FilterEditor({
|
|
|
225
212
|
delimiter={delimiter}
|
|
226
213
|
labels={labels}
|
|
227
214
|
preview={preview}
|
|
228
|
-
// The update endpoint carries no anchor field at all (reader #266), so a
|
|
229
|
-
// widen can be neither added nor removed here: the "…and similar" add is
|
|
230
|
-
// never offered, and the existing chip is display-only (no
|
|
231
|
-
// onRemoveWiden — anchorLocked enforces that regardless).
|
|
232
|
-
// `semanticUnavailable` only drives the chip's inactive styling via
|
|
233
|
-
// `filterToRule`.
|
|
234
|
-
semanticAvailable={false}
|
|
235
215
|
clauseFields={SUPPORTED_CLAUSE_FIELDS}
|
|
236
|
-
|
|
216
|
+
persisted
|
|
237
217
|
clauseEdit={clauseEdit}
|
|
238
218
|
onStartAddClause={startAddClause}
|
|
239
219
|
onStartEditClause={startEditClause}
|
|
@@ -14,7 +14,6 @@ interface FilterEditorSurfaceProps {
|
|
|
14
14
|
folders: readonly FolderTreeNode[];
|
|
15
15
|
delimiter?: string;
|
|
16
16
|
labels: LabelOption[];
|
|
17
|
-
semanticUnavailable?: boolean;
|
|
18
17
|
onClose: () => void;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -30,7 +29,6 @@ export function FilterEditorSurface({
|
|
|
30
29
|
folders,
|
|
31
30
|
delimiter,
|
|
32
31
|
labels,
|
|
33
|
-
semanticUnavailable,
|
|
34
32
|
onClose,
|
|
35
33
|
}: FilterEditorSurfaceProps) {
|
|
36
34
|
const isDesktop = useIsDesktop();
|
|
@@ -42,7 +40,6 @@ export function FilterEditorSurface({
|
|
|
42
40
|
folders={folders}
|
|
43
41
|
delimiter={delimiter}
|
|
44
42
|
labels={labels}
|
|
45
|
-
semanticUnavailable={semanticUnavailable}
|
|
46
43
|
onClose={onClose}
|
|
47
44
|
/>
|
|
48
45
|
);
|
|
@@ -23,7 +23,6 @@ const filter = (
|
|
|
23
23
|
scope: "Standing",
|
|
24
24
|
state: "Active",
|
|
25
25
|
disabledReason: "None",
|
|
26
|
-
hasAnchor: true,
|
|
27
26
|
ruleChangedAt: 0,
|
|
28
27
|
actionChangedAt: 0,
|
|
29
28
|
matchOperator: "And",
|
|
@@ -37,7 +36,6 @@ const filter = (
|
|
|
37
36
|
|
|
38
37
|
const render = (
|
|
39
38
|
filters: RemitImapFilterResponse[],
|
|
40
|
-
semanticUnavailable = false,
|
|
41
39
|
labelById: Map<string, LabelOption> = new Map(),
|
|
42
40
|
run: { runFilterId?: string; runStatus?: FilterRunStatus } = {},
|
|
43
41
|
) =>
|
|
@@ -51,7 +49,6 @@ const render = (
|
|
|
51
49
|
onToggle: () => undefined,
|
|
52
50
|
onRunNow: () => undefined,
|
|
53
51
|
...run,
|
|
54
|
-
semanticUnavailable,
|
|
55
52
|
now: NOW,
|
|
56
53
|
}) as never,
|
|
57
54
|
);
|
|
@@ -74,27 +71,11 @@ describe("FiltersList", () => {
|
|
|
74
71
|
assert.match(html, /Edit filter Travel/);
|
|
75
72
|
});
|
|
76
73
|
|
|
77
|
-
it("shows the widen chip for an anchored filter where the deployment can serve it", () => {
|
|
78
|
-
const html = render([filter({ hasAnchor: true })]);
|
|
79
|
-
assert.match(html, /and anything similar/i);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("lists the widen chip inactive on a deployment that cannot evaluate it (D4)", () => {
|
|
83
|
-
const html = render([filter({ hasAnchor: true })], true);
|
|
84
|
-
assert.match(html, /not available here/i);
|
|
85
|
-
assert.doesNotMatch(html, /and anything similar/i);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("shows no widen chip for a purely literal filter", () => {
|
|
89
|
-
const html = render([filter({ hasAnchor: false })]);
|
|
90
|
-
assert.doesNotMatch(html, /similar/i);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
74
|
it("shows the applied label's chip when the filter has a label action (issue #26)", () => {
|
|
94
75
|
const labelById = new Map<string, LabelOption>([
|
|
95
76
|
["lbl-1", { id: "lbl-1", name: "Receipts", color: "Blue" }],
|
|
96
77
|
]);
|
|
97
|
-
const html = render([filter({ actionLabelId: "lbl-1" })],
|
|
78
|
+
const html = render([filter({ actionLabelId: "lbl-1" })], labelById);
|
|
98
79
|
assert.match(html, /Receipts/);
|
|
99
80
|
});
|
|
100
81
|
|
|
@@ -185,7 +166,7 @@ describe("FiltersList", () => {
|
|
|
185
166
|
});
|
|
186
167
|
|
|
187
168
|
it("says the run is going on the filter that was run, and holds the button", () => {
|
|
188
|
-
const html = render([filter({})],
|
|
169
|
+
const html = render([filter({})], new Map(), {
|
|
189
170
|
runFilterId: "f-1",
|
|
190
171
|
runStatus: { kind: "running" },
|
|
191
172
|
});
|
|
@@ -195,7 +176,7 @@ describe("FiltersList", () => {
|
|
|
195
176
|
});
|
|
196
177
|
|
|
197
178
|
it("reports what the run filed once the job is done", () => {
|
|
198
|
-
const html = render([filter({})],
|
|
179
|
+
const html = render([filter({})], new Map(), {
|
|
199
180
|
runFilterId: "f-1",
|
|
200
181
|
runStatus: { kind: "done", matched: 3, applied: 2 },
|
|
201
182
|
});
|
|
@@ -203,7 +184,7 @@ describe("FiltersList", () => {
|
|
|
203
184
|
});
|
|
204
185
|
|
|
205
186
|
it("says so when nothing in the inbox matched", () => {
|
|
206
|
-
const html = render([filter({})],
|
|
187
|
+
const html = render([filter({})], new Map(), {
|
|
207
188
|
runFilterId: "f-1",
|
|
208
189
|
runStatus: { kind: "done", matched: 0, applied: 0 },
|
|
209
190
|
});
|
|
@@ -211,7 +192,7 @@ describe("FiltersList", () => {
|
|
|
211
192
|
});
|
|
212
193
|
|
|
213
194
|
it("puts no stale status on the row when the run failed", () => {
|
|
214
|
-
const html = render([filter({})],
|
|
195
|
+
const html = render([filter({})], new Map(), {
|
|
215
196
|
runFilterId: "f-1",
|
|
216
197
|
runStatus: { kind: "failed", error: new Error("turned off") },
|
|
217
198
|
});
|