@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
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
-
import {
|
|
3
|
-
Badge,
|
|
4
|
-
Button,
|
|
5
|
-
LabelChip,
|
|
6
|
-
type LabelOption,
|
|
7
|
-
WidenChip,
|
|
8
|
-
} from "@remit/ui";
|
|
2
|
+
import { Badge, Button, LabelChip, type LabelOption } from "@remit/ui";
|
|
9
3
|
import { Trash2 } from "lucide-react";
|
|
10
4
|
import type { FilterRunStatus } from "@/lib/organize/filter-run";
|
|
11
5
|
import {
|
|
@@ -47,12 +41,6 @@ interface FiltersListProps {
|
|
|
47
41
|
runFilterId?: string;
|
|
48
42
|
/** How that run stands; a failure is reported above the list. */
|
|
49
43
|
runStatus?: FilterRunStatus;
|
|
50
|
-
/**
|
|
51
|
-
* This deployment ships no vector pipeline (RFC 038 D4). A filter carrying a
|
|
52
|
-
* semantic anchor lists with its widen chip inactive — it matches by its
|
|
53
|
-
* literal clauses only.
|
|
54
|
-
*/
|
|
55
|
-
semanticUnavailable?: boolean;
|
|
56
44
|
/** Injected for deterministic status in tests; defaults to now. */
|
|
57
45
|
now?: number;
|
|
58
46
|
}
|
|
@@ -60,9 +48,7 @@ interface FiltersListProps {
|
|
|
60
48
|
/**
|
|
61
49
|
* The account's standing filters, each opening its rule in the editor (RFC 038
|
|
62
50
|
* D6). Expired temporary filters stay listed and are marked Expired distinctly
|
|
63
|
-
* rather than hidden (RFC 034 Decision 1.2).
|
|
64
|
-
* shows the widen chip — inactive where this deployment cannot evaluate it
|
|
65
|
-
* (D4), so the list says honestly that it matches by literal clauses only.
|
|
51
|
+
* rather than hidden (RFC 034 Decision 1.2).
|
|
66
52
|
*/
|
|
67
53
|
export function FiltersList({
|
|
68
54
|
filters,
|
|
@@ -76,7 +62,6 @@ export function FiltersList({
|
|
|
76
62
|
onRunNow,
|
|
77
63
|
runFilterId,
|
|
78
64
|
runStatus,
|
|
79
|
-
semanticUnavailable = false,
|
|
80
65
|
now = Date.now(),
|
|
81
66
|
}: FiltersListProps) {
|
|
82
67
|
if (filters.length === 0) {
|
|
@@ -156,25 +141,15 @@ export function FiltersList({
|
|
|
156
141
|
{runLine}
|
|
157
142
|
</p>
|
|
158
143
|
)}
|
|
159
|
-
{
|
|
144
|
+
{label && (
|
|
160
145
|
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)}
|
|
169
|
-
{label && (
|
|
170
|
-
<LabelChip
|
|
171
|
-
label={{
|
|
172
|
-
labelId: label.id,
|
|
173
|
-
name: label.name,
|
|
174
|
-
color: label.color,
|
|
175
|
-
}}
|
|
176
|
-
/>
|
|
177
|
-
)}
|
|
146
|
+
<LabelChip
|
|
147
|
+
label={{
|
|
148
|
+
labelId: label.id,
|
|
149
|
+
name: label.name,
|
|
150
|
+
color: label.color,
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
178
153
|
</div>
|
|
179
154
|
)}
|
|
180
155
|
</button>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AttentionDot } from "@remit/ui";
|
|
2
|
+
import { TriangleAlert } from "lucide-react";
|
|
3
|
+
import { useWarningsAttention } from "@/hooks/useWarningsAttention";
|
|
4
|
+
|
|
5
|
+
export function WarningsNavIcon() {
|
|
6
|
+
const attention = useWarningsAttention();
|
|
7
|
+
return (
|
|
8
|
+
<AttentionDot label={attention}>
|
|
9
|
+
<TriangleAlert className="size-4" />
|
|
10
|
+
</AttentionDot>
|
|
11
|
+
);
|
|
12
|
+
}
|
package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts}
RENAMED
|
@@ -13,7 +13,7 @@ import { meOperationsListQuarantineOptions } from "@remit/api-http-client/@tanst
|
|
|
13
13
|
import { createElement } from "react";
|
|
14
14
|
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
15
15
|
import { type HttpMock, httpError, mockFetch } from "../../test-support/http";
|
|
16
|
-
import {
|
|
16
|
+
import { WarningsPanel } from "./WarningsPanel";
|
|
17
17
|
|
|
18
18
|
const ACCOUNT_ID = "acct-1";
|
|
19
19
|
const MAILBOX_ID = "mbx-q3";
|
|
@@ -51,6 +51,8 @@ const QUARANTINE = {
|
|
|
51
51
|
structure: [],
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
|
+
warnings: [],
|
|
55
|
+
warningsTotal: 0,
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
let harness: DomHarness | undefined;
|
|
@@ -76,16 +78,17 @@ const mount = async (
|
|
|
76
78
|
mailboxes: () => unknown,
|
|
77
79
|
settled: (view: DomHarness) => boolean,
|
|
78
80
|
description: string,
|
|
81
|
+
list: () => unknown = () => QUARANTINE,
|
|
79
82
|
): Promise<DomHarness> => {
|
|
80
83
|
http = mockFetch((call) => {
|
|
81
84
|
if (call.path.endsWith("/config")) return CONFIG;
|
|
82
85
|
if (call.path.endsWith("/mailboxes")) return mailboxes();
|
|
83
|
-
if (call.path.endsWith("/quarantine")) return
|
|
86
|
+
if (call.path.endsWith("/quarantine")) return list();
|
|
84
87
|
return {};
|
|
85
88
|
});
|
|
86
89
|
const view = createDomHarness();
|
|
87
90
|
harness = view;
|
|
88
|
-
view.renderApp(createElement(
|
|
91
|
+
view.renderApp(createElement(WarningsPanel));
|
|
89
92
|
// Two waves: the mailbox-list fan-out is keyed on the accounts `/config`
|
|
90
93
|
// returns, so it cannot even start until that first read has settled. How
|
|
91
94
|
// many turns those waves take is the runner's business, so each case names
|
|
@@ -94,7 +97,63 @@ const mount = async (
|
|
|
94
97
|
return view;
|
|
95
98
|
};
|
|
96
99
|
|
|
97
|
-
describe("
|
|
100
|
+
describe("WarningsPanel reads", () => {
|
|
101
|
+
it("lists a body that gave up and a header still retrying beside the set-aside message", async () => {
|
|
102
|
+
const view = await mount(
|
|
103
|
+
() => MAILBOXES,
|
|
104
|
+
(pane) => /Gave up/.test(pane.text()),
|
|
105
|
+
"the warning to be listed",
|
|
106
|
+
() => ({
|
|
107
|
+
...QUARANTINE,
|
|
108
|
+
warnings: [
|
|
109
|
+
{
|
|
110
|
+
syncWarningId: "w-1",
|
|
111
|
+
accountConfigId: "cfg-1",
|
|
112
|
+
accountId: ACCOUNT_ID,
|
|
113
|
+
mailboxId: MAILBOX_ID,
|
|
114
|
+
uidValidity: 1_712_000_000,
|
|
115
|
+
uid: 51002,
|
|
116
|
+
stage: "Body",
|
|
117
|
+
attempts: 3,
|
|
118
|
+
createdAt: Date.parse("2026-10-17T08:12:00Z"),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
syncWarningId: "w-2",
|
|
122
|
+
accountConfigId: "cfg-1",
|
|
123
|
+
accountId: ACCOUNT_ID,
|
|
124
|
+
mailboxId: MAILBOX_ID,
|
|
125
|
+
uidValidity: 1_712_000_000,
|
|
126
|
+
uid: 50874,
|
|
127
|
+
stage: "Header",
|
|
128
|
+
attempts: 5,
|
|
129
|
+
createdAt: Date.parse("2026-10-16T21:40:00Z"),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
warningsTotal: 2,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
assert.match(view.text(), /uid 51002/);
|
|
137
|
+
assert.match(view.text(), /Retrying/);
|
|
138
|
+
assert.match(view.text(), /uid 50874/);
|
|
139
|
+
assert.match(view.text(), /Messages set aside/);
|
|
140
|
+
assert.match(view.text(), /uid 40251/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("shows a 401 on the list as signed out, never as an empty list", async () => {
|
|
144
|
+
const view = await mount(
|
|
145
|
+
() => MAILBOXES,
|
|
146
|
+
(pane) => /Your session has ended/.test(pane.text()),
|
|
147
|
+
"the signed-out notice",
|
|
148
|
+
() => httpError(401),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assert.match(view.html(), /role="alert"/);
|
|
152
|
+
assert.doesNotMatch(view.text(), /Nothing has failed|Nothing is set aside/);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("WarningsPanel folder names", () => {
|
|
98
157
|
it("waits for the account's delimiter rather than cutting on a guess", async () => {
|
|
99
158
|
// The mailbox read never resolves here, so settled means every read that
|
|
100
159
|
// can land has: the entries are in hand and only the delimiter is missing.
|
|
@@ -105,7 +164,7 @@ describe("QuarantinePanel folder names", () => {
|
|
|
105
164
|
"the quarantine list to land while the mailbox read hangs",
|
|
106
165
|
);
|
|
107
166
|
|
|
108
|
-
assert.match(view.text(), /Checking for
|
|
167
|
+
assert.match(view.text(), /Checking for warnings/);
|
|
109
168
|
assert.doesNotMatch(
|
|
110
169
|
view.text(),
|
|
111
170
|
new RegExp(FULL_PATH.replace(/\./g, "\\.")),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import React, { createElement } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { WarningsPanelView, type WarningsRead } from "./WarningsPanel";
|
|
6
|
+
|
|
7
|
+
// See MessageToolbar.render.test.ts: the SSR test loader transpiles remit-ui
|
|
8
|
+
// `.tsx` with the classic JSX runtime, which needs a global `React`.
|
|
9
|
+
(globalThis as { React?: typeof React }).React = React;
|
|
10
|
+
|
|
11
|
+
const render = (read: WarningsRead): string =>
|
|
12
|
+
renderToString(
|
|
13
|
+
createElement(WarningsPanelView, {
|
|
14
|
+
read,
|
|
15
|
+
signIn: createElement("button", { type: "button" }, "Sign in again"),
|
|
16
|
+
}) as never,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const ready = (
|
|
20
|
+
over: Partial<Extract<WarningsRead, { kind: "Ready" }>>,
|
|
21
|
+
): WarningsRead => ({
|
|
22
|
+
kind: "Ready",
|
|
23
|
+
quarantine: [],
|
|
24
|
+
warnings: [],
|
|
25
|
+
warningsTotal: 0,
|
|
26
|
+
...over,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("WarningsPanelView", () => {
|
|
30
|
+
it("says nothing failed and nothing is set aside rather than rendering nothing", () => {
|
|
31
|
+
const html = render(ready({}));
|
|
32
|
+
assert.match(html, /Messages that failed to sync/);
|
|
33
|
+
assert.match(html, /Nothing has failed/);
|
|
34
|
+
assert.match(html, /Messages set aside/);
|
|
35
|
+
assert.match(html, /Nothing is set aside/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("reports a failed read in the pane, with a way to file it", () => {
|
|
39
|
+
const html = render({
|
|
40
|
+
kind: "Failed",
|
|
41
|
+
error: new Error("Failed to fetch"),
|
|
42
|
+
});
|
|
43
|
+
assert.match(html, /role="alert"/);
|
|
44
|
+
assert.match(html, /could not be read/);
|
|
45
|
+
assert.match(html, /Failed to fetch/);
|
|
46
|
+
assert.match(html, /Report this/);
|
|
47
|
+
assert.doesNotMatch(html, /Nothing has failed/);
|
|
48
|
+
assert.doesNotMatch(html, /Nothing is set aside/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("says it is still reading rather than claiming an empty list", () => {
|
|
52
|
+
const html = render({ kind: "Pending" });
|
|
53
|
+
assert.match(html, /Checking for warnings/);
|
|
54
|
+
assert.doesNotMatch(html, /Nothing has failed/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { configOperationsGetConfigOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import type {
|
|
3
|
+
RemitImapQuarantineResponse,
|
|
4
|
+
RemitImapSyncWarningResponse,
|
|
5
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
formatErrorMessage,
|
|
9
|
+
formatQuarantineReport,
|
|
10
|
+
formatSyncWarningReport,
|
|
11
|
+
MessageReportDialog,
|
|
12
|
+
type QuarantineEntry,
|
|
13
|
+
QuarantineSection,
|
|
14
|
+
quarantineIssueTitle,
|
|
15
|
+
quarantineReportSections,
|
|
16
|
+
type SyncWarningEntry,
|
|
17
|
+
SyncWarningSection,
|
|
18
|
+
syncWarningIssueTitle,
|
|
19
|
+
} from "@remit/ui";
|
|
20
|
+
import { useQuery } from "@tanstack/react-query";
|
|
21
|
+
import { Bug } from "lucide-react";
|
|
22
|
+
import { type ReactNode, useMemo, useState } from "react";
|
|
23
|
+
import { SignInAgain } from "@/auth/SignInAgain";
|
|
24
|
+
import { useResultFolderIndex } from "@/hooks/useResultFolderIndex";
|
|
25
|
+
import { useWarningsList } from "@/hooks/useWarningsAttention";
|
|
26
|
+
import { buildBugReportContext, buildGitHubIssueUrl } from "@/lib/bug-report";
|
|
27
|
+
import { isUnauthenticated } from "@/lib/error-classifier";
|
|
28
|
+
import { toQuarantineEntry } from "@/lib/quarantine-entries";
|
|
29
|
+
import type { ResultFolderIndex } from "@/lib/result-folder";
|
|
30
|
+
import { toSyncWarningEntry } from "@/lib/sync-warning-entries";
|
|
31
|
+
|
|
32
|
+
const EMPTY_QUARANTINE: readonly RemitImapQuarantineResponse[] = [];
|
|
33
|
+
const EMPTY_WARNINGS: readonly RemitImapSyncWarningResponse[] = [];
|
|
34
|
+
|
|
35
|
+
interface Reporting {
|
|
36
|
+
report: string;
|
|
37
|
+
issueUrl: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function openIssue(url: string): void {
|
|
41
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function quarantineReporting(entry: QuarantineEntry): Reporting {
|
|
45
|
+
return {
|
|
46
|
+
report: formatQuarantineReport(entry),
|
|
47
|
+
issueUrl: buildGitHubIssueUrl(
|
|
48
|
+
buildBugReportContext({
|
|
49
|
+
quarantine: quarantineReportSections(entry),
|
|
50
|
+
title: quarantineIssueTitle(entry),
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function syncWarningReporting(entry: SyncWarningEntry): Reporting {
|
|
57
|
+
const report = formatSyncWarningReport(entry);
|
|
58
|
+
return {
|
|
59
|
+
report,
|
|
60
|
+
issueUrl: buildGitHubIssueUrl(
|
|
61
|
+
buildBugReportContext({
|
|
62
|
+
syncWarning: report,
|
|
63
|
+
title: syncWarningIssueTitle(entry),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function WarningsSignedOut({ signIn }: { signIn: ReactNode }) {
|
|
70
|
+
return (
|
|
71
|
+
<section role="alert" className="space-y-3">
|
|
72
|
+
<div className="space-y-2 rounded-sm border border-danger/40 bg-danger-soft px-row-inset py-3">
|
|
73
|
+
<p className="text-sm font-medium text-danger">
|
|
74
|
+
Your session has ended.
|
|
75
|
+
</p>
|
|
76
|
+
<p className="text-sm text-fg-muted">
|
|
77
|
+
Sign in again to see the messages that failed to sync or were set
|
|
78
|
+
aside.
|
|
79
|
+
</p>
|
|
80
|
+
{signIn}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function WarningsReadFailure({ error }: { error: unknown }) {
|
|
87
|
+
return (
|
|
88
|
+
<section role="alert" className="space-y-3">
|
|
89
|
+
<div className="space-y-2 rounded-sm border border-danger/40 bg-danger-soft px-row-inset py-3">
|
|
90
|
+
<p className="text-sm font-medium text-danger">
|
|
91
|
+
The warnings could not be read.
|
|
92
|
+
</p>
|
|
93
|
+
<p className="break-words text-sm text-fg-muted">
|
|
94
|
+
{formatErrorMessage(error)}
|
|
95
|
+
</p>
|
|
96
|
+
<p className="text-xs text-fg-muted">
|
|
97
|
+
Mail may still be failing to sync or have been set aside — this page
|
|
98
|
+
cannot say either way until it can read the list. Reporting this gets
|
|
99
|
+
it fixed.
|
|
100
|
+
</p>
|
|
101
|
+
<Button
|
|
102
|
+
variant="secondary"
|
|
103
|
+
size="sm"
|
|
104
|
+
icon={<Bug className="size-3.5" />}
|
|
105
|
+
onClick={() =>
|
|
106
|
+
openIssue(buildGitHubIssueUrl(buildBugReportContext()))
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
Report this
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type WarningsRead =
|
|
117
|
+
| { kind: "Pending" }
|
|
118
|
+
| { kind: "SignedOut" }
|
|
119
|
+
| { kind: "Failed"; error: unknown }
|
|
120
|
+
| {
|
|
121
|
+
kind: "Ready";
|
|
122
|
+
quarantine: readonly QuarantineEntry[];
|
|
123
|
+
warnings: readonly SyncWarningEntry[];
|
|
124
|
+
warningsTotal: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export interface WarningsPanelViewProps {
|
|
128
|
+
read: WarningsRead;
|
|
129
|
+
signIn: ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function WarningsPanelView({ read, signIn }: WarningsPanelViewProps) {
|
|
133
|
+
const [reporting, setReporting] = useState<Reporting | null>(null);
|
|
134
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
135
|
+
|
|
136
|
+
if (read.kind === "Pending") {
|
|
137
|
+
return (
|
|
138
|
+
<p className="animate-pulse text-sm text-fg-subtle">
|
|
139
|
+
Checking for warnings…
|
|
140
|
+
</p>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (read.kind === "SignedOut") return <WarningsSignedOut signIn={signIn} />;
|
|
145
|
+
|
|
146
|
+
if (read.kind === "Failed") return <WarningsReadFailure error={read.error} />;
|
|
147
|
+
|
|
148
|
+
const handleCopy = (report: string) => {
|
|
149
|
+
navigator.clipboard.writeText(report).then(
|
|
150
|
+
() => {
|
|
151
|
+
setCopyFailed(false);
|
|
152
|
+
setReporting(null);
|
|
153
|
+
},
|
|
154
|
+
() => {
|
|
155
|
+
setCopyFailed(true);
|
|
156
|
+
setReporting(null);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="space-y-6">
|
|
163
|
+
{copyFailed && (
|
|
164
|
+
<p role="alert" className="text-sm text-danger">
|
|
165
|
+
The report was not copied — this browser refused clipboard access. Cut
|
|
166
|
+
the bug again and use Open on GitHub, which needs no clipboard.
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
<SyncWarningSection
|
|
170
|
+
entries={read.warnings}
|
|
171
|
+
total={read.warningsTotal}
|
|
172
|
+
onReport={(entry) => setReporting(syncWarningReporting(entry))}
|
|
173
|
+
/>
|
|
174
|
+
<QuarantineSection
|
|
175
|
+
entries={read.quarantine}
|
|
176
|
+
onCutBug={(entry) => setReporting(quarantineReporting(entry))}
|
|
177
|
+
/>
|
|
178
|
+
<MessageReportDialog
|
|
179
|
+
report={reporting?.report ?? null}
|
|
180
|
+
issueUrl={reporting?.issueUrl ?? ""}
|
|
181
|
+
onClose={() => setReporting(null)}
|
|
182
|
+
onCopy={handleCopy}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function delimiterForMailbox(
|
|
189
|
+
folders: ResultFolderIndex,
|
|
190
|
+
mailboxId: string,
|
|
191
|
+
): string {
|
|
192
|
+
return folders.get(mailboxId)?.hierarchyDelimiter ?? "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function WarningsPanel() {
|
|
196
|
+
const list = useWarningsList();
|
|
197
|
+
const config = useQuery(configOperationsGetConfigOptions());
|
|
198
|
+
const accounts = useMemo(() => config.data?.accounts ?? [], [config.data]);
|
|
199
|
+
const folders = useResultFolderIndex(accounts);
|
|
200
|
+
|
|
201
|
+
const wireQuarantine = list.data?.entries ?? EMPTY_QUARANTINE;
|
|
202
|
+
const wireWarnings = list.data?.warnings ?? EMPTY_WARNINGS;
|
|
203
|
+
const quarantine = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
wireQuarantine.map((entry) =>
|
|
206
|
+
toQuarantineEntry(
|
|
207
|
+
entry,
|
|
208
|
+
delimiterForMailbox(folders.index, entry.mailboxId),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
[wireQuarantine, folders.index],
|
|
212
|
+
);
|
|
213
|
+
const warnings = useMemo(
|
|
214
|
+
() =>
|
|
215
|
+
wireWarnings.map((warning) =>
|
|
216
|
+
toSyncWarningEntry(warning, folders.index.get(warning.mailboxId)),
|
|
217
|
+
),
|
|
218
|
+
[wireWarnings, folders.index],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const read = readOf(
|
|
222
|
+
list.error ?? config.error ?? folders.error,
|
|
223
|
+
list.isPending || config.isPending || folders.isPending,
|
|
224
|
+
{
|
|
225
|
+
kind: "Ready",
|
|
226
|
+
quarantine,
|
|
227
|
+
warnings,
|
|
228
|
+
warningsTotal: list.data?.warningsTotal ?? 0,
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return <WarningsPanelView read={read} signIn={<SignInAgain />} />;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readOf(
|
|
236
|
+
error: unknown,
|
|
237
|
+
isPending: boolean,
|
|
238
|
+
ready: WarningsRead,
|
|
239
|
+
): WarningsRead {
|
|
240
|
+
if (isUnauthenticated(error)) return { kind: "SignedOut" };
|
|
241
|
+
if (error) return { kind: "Failed", error };
|
|
242
|
+
if (isPending) return { kind: "Pending" };
|
|
243
|
+
return ready;
|
|
244
|
+
}
|
|
@@ -253,6 +253,24 @@ export const AccountEdit: Story = {
|
|
|
253
253
|
},
|
|
254
254
|
};
|
|
255
255
|
|
|
256
|
+
export const AccountEditRevealPassword: Story = {
|
|
257
|
+
name: "Accounts — edit, reveal password",
|
|
258
|
+
globals: desktop,
|
|
259
|
+
play: async ({ canvasElement }) => {
|
|
260
|
+
const panel = within(await openEditPanel(canvasElement, 0));
|
|
261
|
+
const password = panel.getByLabelText("Password");
|
|
262
|
+
await expect(password).toHaveValue("");
|
|
263
|
+
await expect(password).toHaveAttribute(
|
|
264
|
+
"placeholder",
|
|
265
|
+
"Leave empty to keep current",
|
|
266
|
+
);
|
|
267
|
+
await userEvent.type(password, "new-app-password");
|
|
268
|
+
await userEvent.click(panel.getByRole("button", { name: "Show password" }));
|
|
269
|
+
await expect(password).toHaveAttribute("type", "text");
|
|
270
|
+
await expect(password).toHaveValue("new-app-password");
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
256
274
|
export const AccountEditClosed: Story = {
|
|
257
275
|
name: "Accounts — edit panel closed",
|
|
258
276
|
globals: desktop,
|
|
@@ -1,57 +1,10 @@
|
|
|
1
|
-
import type { RemitImapQuarantineResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
-
import { HttpResponse, http } from "msw";
|
|
4
2
|
import { expect, within } from "storybook/test";
|
|
5
3
|
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
6
4
|
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
7
|
-
import {
|
|
8
|
-
mailboxIdFor,
|
|
9
|
-
mailWorld,
|
|
10
|
-
NOW,
|
|
11
|
-
PERSONAL,
|
|
12
|
-
} from "@/mocks/story-frame/mail-world";
|
|
5
|
+
import { mailWorld } from "@/mocks/story-frame/mail-world";
|
|
13
6
|
import { withRuntimeConfig } from "@/mocks/story-frame/session";
|
|
14
7
|
|
|
15
|
-
const setAside = (
|
|
16
|
-
id: string,
|
|
17
|
-
role: "Inbox" | "Archive",
|
|
18
|
-
failureCode: RemitImapQuarantineResponse["failureCode"],
|
|
19
|
-
failureMessage: string,
|
|
20
|
-
): RemitImapQuarantineResponse => ({
|
|
21
|
-
quarantineId: `q-${id}`,
|
|
22
|
-
accountConfigId: "cfg-1",
|
|
23
|
-
accountId: PERSONAL,
|
|
24
|
-
mailboxId: mailboxIdFor(PERSONAL, role),
|
|
25
|
-
uidValidity: 1_712_000_000,
|
|
26
|
-
uid: 40_217,
|
|
27
|
-
mailboxRole: role,
|
|
28
|
-
mailboxPath: role === "Inbox" ? "INBOX" : "Archive",
|
|
29
|
-
quarantinedAt: NOW,
|
|
30
|
-
attempts: 3,
|
|
31
|
-
failureStage: "BodyParse",
|
|
32
|
-
failureCode,
|
|
33
|
-
failureMessage,
|
|
34
|
-
workerVersion: "worker 1.0.0",
|
|
35
|
-
contentType: "multipart/mixed",
|
|
36
|
-
sizeBytes: 184_233,
|
|
37
|
-
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
38
|
-
createdAt: NOW,
|
|
39
|
-
updatedAt: NOW,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const unreadableBody = setAside(
|
|
43
|
-
"1",
|
|
44
|
-
"Inbox",
|
|
45
|
-
"UnreadableBody",
|
|
46
|
-
"multipart boundary was never closed",
|
|
47
|
-
);
|
|
48
|
-
const unknownCharset = setAside(
|
|
49
|
-
"2",
|
|
50
|
-
"Archive",
|
|
51
|
-
"UnknownCharset",
|
|
52
|
-
"declared charset is not a known encoding",
|
|
53
|
-
);
|
|
54
|
-
|
|
55
8
|
const meta = {
|
|
56
9
|
title: "Playground/Shipped/Settings/Advanced/Overview",
|
|
57
10
|
component: AppStory,
|
|
@@ -68,89 +21,14 @@ type Story = StoryObj<typeof meta>;
|
|
|
68
21
|
export const Advanced: Story = {
|
|
69
22
|
play: async ({ canvasElement }) => {
|
|
70
23
|
const canvas = within(canvasElement);
|
|
71
|
-
await expect(
|
|
72
|
-
|
|
73
|
-
).toBeVisible();
|
|
74
|
-
await expect(canvas.getByText("About")).toBeVisible();
|
|
24
|
+
await expect(await canvas.findByText("About")).toBeVisible();
|
|
25
|
+
await expect(canvas.queryByText("Messages set aside")).toBeNull();
|
|
75
26
|
await expect(
|
|
76
27
|
canvas.queryByRole("link", { name: "Download root certificate" }),
|
|
77
28
|
).toBeNull();
|
|
78
29
|
},
|
|
79
30
|
};
|
|
80
31
|
|
|
81
|
-
export const AdvancedOneQuarantined: Story = {
|
|
82
|
-
parameters: {
|
|
83
|
-
msw: {
|
|
84
|
-
handlers: mailHandlers(mailWorld({ quarantine: [unreadableBody] })),
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
play: async ({ canvasElement }) => {
|
|
88
|
-
const canvas = within(canvasElement);
|
|
89
|
-
await expect(
|
|
90
|
-
await canvas.findByText(/multipart boundary was never closed/),
|
|
91
|
-
).toBeVisible();
|
|
92
|
-
await expect(canvas.queryByText(/messages could not be read/)).toBeNull();
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const AdvancedQuarantineAlert: Story = {
|
|
97
|
-
parameters: {
|
|
98
|
-
msw: {
|
|
99
|
-
handlers: mailHandlers(
|
|
100
|
-
mailWorld({ quarantine: [unreadableBody, unknownCharset] }),
|
|
101
|
-
),
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
play: async ({ canvasElement }) => {
|
|
105
|
-
const canvas = within(canvasElement);
|
|
106
|
-
await expect(
|
|
107
|
-
await canvas.findByText("2 messages could not be read."),
|
|
108
|
-
).toBeVisible();
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export const AdvancedQuarantineLoading: Story = {
|
|
113
|
-
parameters: {
|
|
114
|
-
msw: {
|
|
115
|
-
handlers: [
|
|
116
|
-
http.get(
|
|
117
|
-
"/api/me/quarantine",
|
|
118
|
-
() => new Promise<never>(() => undefined),
|
|
119
|
-
),
|
|
120
|
-
...mailHandlers(mailWorld()),
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
play: async ({ canvasElement }) => {
|
|
125
|
-
const canvas = within(canvasElement);
|
|
126
|
-
await expect(
|
|
127
|
-
await canvas.findByText("Checking for messages set aside…"),
|
|
128
|
-
).toBeVisible();
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
export const AdvancedQuarantineUnreadable: Story = {
|
|
133
|
-
parameters: {
|
|
134
|
-
msw: {
|
|
135
|
-
handlers: [
|
|
136
|
-
http.get("/api/me/quarantine", () => HttpResponse.error()),
|
|
137
|
-
...mailHandlers(mailWorld()),
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
play: async ({ canvasElement }) => {
|
|
142
|
-
const canvas = within(canvasElement);
|
|
143
|
-
await expect(
|
|
144
|
-
await canvas.findByText(
|
|
145
|
-
"The list of set-aside messages could not be read.",
|
|
146
|
-
),
|
|
147
|
-
).toBeVisible();
|
|
148
|
-
await expect(
|
|
149
|
-
canvas.getByRole("button", { name: "Report this" }),
|
|
150
|
-
).toBeVisible();
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
|
|
154
32
|
export const AdvancedTlsInternal: Story = {
|
|
155
33
|
beforeEach: withRuntimeConfig({ tlsMode: "internal" }),
|
|
156
34
|
play: async ({ canvasElement }) => {
|