@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { meOperationsListQuarantineOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import type { RemitImapQuarantineListResponse } from "@remit/api-http-client/types.gen.ts";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { isUnauthenticated, softErrorStatuses } from "@/lib/error-classifier";
|
|
5
|
+
|
|
6
|
+
const WARNINGS_LIST_META = softErrorStatuses(401);
|
|
7
|
+
|
|
8
|
+
export function useWarningsList() {
|
|
9
|
+
return useQuery({
|
|
10
|
+
...meOperationsListQuarantineOptions(),
|
|
11
|
+
meta: WARNINGS_LIST_META,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const WARNINGS_ATTENTION = {
|
|
16
|
+
listed: "Sync warnings",
|
|
17
|
+
unread: "Warnings could not be read",
|
|
18
|
+
signedOut: "Session ended",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export function warningsAttentionOf(
|
|
22
|
+
data: RemitImapQuarantineListResponse | undefined,
|
|
23
|
+
error: unknown,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
if (isUnauthenticated(error)) return WARNINGS_ATTENTION.signedOut;
|
|
26
|
+
if (error) return WARNINGS_ATTENTION.unread;
|
|
27
|
+
if (!data) return undefined;
|
|
28
|
+
if ((data.entries?.length ?? 0) > 0 || (data.warnings?.length ?? 0) > 0) {
|
|
29
|
+
return WARNINGS_ATTENTION.listed;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useWarningsAttention(): string | undefined {
|
|
35
|
+
const list = useWarningsList();
|
|
36
|
+
return warningsAttentionOf(list.data, list.error);
|
|
37
|
+
}
|
|
@@ -39,12 +39,6 @@ describe("compareAccounts", () => {
|
|
|
39
39
|
assert.ok(compareAccounts(a, b) < 0);
|
|
40
40
|
assert.ok(compareAccounts(b, a) > 0);
|
|
41
41
|
});
|
|
42
|
-
|
|
43
|
-
test("identical accounts compare equal", () => {
|
|
44
|
-
const a = account({ accountId: "x", createdAt: 1000 });
|
|
45
|
-
const b = account({ accountId: "x", createdAt: 1000 });
|
|
46
|
-
assert.strictEqual(compareAccounts(a, b), 0);
|
|
47
|
-
});
|
|
48
42
|
});
|
|
49
43
|
|
|
50
44
|
describe("sortAccountsByCreatedAt", () => {
|
|
@@ -77,8 +71,4 @@ describe("sortAccountsByCreatedAt", () => {
|
|
|
77
71
|
["a", "m", "z"],
|
|
78
72
|
);
|
|
79
73
|
});
|
|
80
|
-
|
|
81
|
-
test("empty input returns empty array", () => {
|
|
82
|
-
assert.deepStrictEqual(sortAccountsByCreatedAt([]), []);
|
|
83
|
-
});
|
|
84
74
|
});
|
|
@@ -31,13 +31,4 @@ describe("adjacentMessageId", () => {
|
|
|
31
31
|
test("returns null when there is no current id", () => {
|
|
32
32
|
assert.strictEqual(adjacentMessageId(ids, undefined, "next"), null);
|
|
33
33
|
});
|
|
34
|
-
|
|
35
|
-
test("returns null for a single-item list at both ends", () => {
|
|
36
|
-
assert.strictEqual(adjacentMessageId(["only"], "only", "next"), null);
|
|
37
|
-
assert.strictEqual(adjacentMessageId(["only"], "only", "previous"), null);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("returns null for an empty list", () => {
|
|
41
|
-
assert.strictEqual(adjacentMessageId([], "a", "next"), null);
|
|
42
|
-
});
|
|
43
34
|
});
|
package/src/lib/app-info.ts
CHANGED
|
@@ -30,6 +30,3 @@ export const APP_SHA_IS_COMMIT = /^[0-9a-f]{40}$/.test(APP_SHA);
|
|
|
30
30
|
export const GITHUB_COMMIT_URL: string | undefined = APP_SHA_IS_COMMIT
|
|
31
31
|
? `https://github.com/remit-mail/reader/commit/${APP_SHA}`
|
|
32
32
|
: undefined;
|
|
33
|
-
|
|
34
|
-
export const GITHUB_NEW_ISSUE_URL =
|
|
35
|
-
"https://github.com/remit-mail/reader/issues/new";
|
|
@@ -8,53 +8,6 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// Provider table unit tests for top 5 providers (no network calls)
|
|
10
10
|
describe("autodiscovery provider table", () => {
|
|
11
|
-
it("resolves Gmail", async () => {
|
|
12
|
-
const result = await discoverSettings("user@gmail.com");
|
|
13
|
-
assert.ok(result, "should find Gmail");
|
|
14
|
-
assert.equal(result.source, "provider-table");
|
|
15
|
-
assert.equal(result.imap.host, "imap.gmail.com");
|
|
16
|
-
assert.equal(result.imap.port, 993);
|
|
17
|
-
assert.equal(result.imap.security, "tls");
|
|
18
|
-
assert.equal(result.smtp.host, "smtp.gmail.com");
|
|
19
|
-
assert.equal(result.smtp.port, 587);
|
|
20
|
-
assert.equal(result.smtp.security, "starttls");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("resolves iCloud", async () => {
|
|
24
|
-
const result = await discoverSettings("user@icloud.com");
|
|
25
|
-
assert.ok(result, "should find iCloud");
|
|
26
|
-
assert.equal(result.source, "provider-table");
|
|
27
|
-
assert.equal(result.imap.host, "imap.mail.me.com");
|
|
28
|
-
assert.equal(result.imap.port, 993);
|
|
29
|
-
assert.equal(result.smtp.security, "starttls");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("resolves Fastmail", async () => {
|
|
33
|
-
const result = await discoverSettings("user@fastmail.com");
|
|
34
|
-
assert.ok(result, "should find Fastmail");
|
|
35
|
-
assert.equal(result.source, "provider-table");
|
|
36
|
-
assert.equal(result.imap.host, "imap.fastmail.com");
|
|
37
|
-
assert.equal(result.smtp.host, "smtp.fastmail.com");
|
|
38
|
-
assert.equal(result.smtp.port, 465);
|
|
39
|
-
assert.equal(result.smtp.security, "tls");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("resolves Outlook", async () => {
|
|
43
|
-
const result = await discoverSettings("user@outlook.com");
|
|
44
|
-
assert.ok(result, "should find Outlook");
|
|
45
|
-
assert.equal(result.source, "provider-table");
|
|
46
|
-
assert.equal(result.imap.host, "outlook.office365.com");
|
|
47
|
-
assert.equal(result.smtp.host, "smtp.office365.com");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("resolves Yahoo", async () => {
|
|
51
|
-
const result = await discoverSettings("user@yahoo.com");
|
|
52
|
-
assert.ok(result, "should find Yahoo");
|
|
53
|
-
assert.equal(result.source, "provider-table");
|
|
54
|
-
assert.equal(result.imap.host, "imap.mail.yahoo.com");
|
|
55
|
-
assert.equal(result.smtp.security, "tls");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
11
|
it("returns heuristic for unknown domain", async () => {
|
|
59
12
|
// Use a clearly fake domain — network calls will fail/timeout
|
|
60
13
|
// In the test environment there's no network, so it falls to heuristic
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
formatSyncWarningReport,
|
|
4
5
|
QUARANTINE_REPORT_DISCLAIMER,
|
|
5
6
|
type QuarantineEntry,
|
|
6
7
|
quarantineDemoEntries,
|
|
7
8
|
quarantineIssueTitle,
|
|
8
9
|
quarantineReportSections,
|
|
10
|
+
syncWarningDemoEntries,
|
|
11
|
+
syncWarningIssueTitle,
|
|
9
12
|
} from "@remit/ui";
|
|
10
13
|
import type { BugReportContext } from "./bug-report";
|
|
11
14
|
import { buildBugReportDetails, buildGitHubIssueUrl } from "./bug-report";
|
|
@@ -45,6 +48,20 @@ describe("buildGitHubIssueUrl", () => {
|
|
|
45
48
|
);
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
it("points at the issues URL the runtime config names", () => {
|
|
52
|
+
globalThis.__REMIT_CONFIG__ = {
|
|
53
|
+
issuesUrl: "https://github.com/remit-mail/reader-cloudflare/issues/new",
|
|
54
|
+
};
|
|
55
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
56
|
+
globalThis.__REMIT_CONFIG__ = undefined;
|
|
57
|
+
assert.ok(
|
|
58
|
+
url.startsWith(
|
|
59
|
+
"https://github.com/remit-mail/reader-cloudflare/issues/new?",
|
|
60
|
+
),
|
|
61
|
+
`Expected the configured issues URL, got: ${url}`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
48
65
|
it("prefills a title param", () => {
|
|
49
66
|
const url = buildGitHubIssueUrl(baseCtx);
|
|
50
67
|
assert.ok(url.includes("title="), "Expected a title param");
|
|
@@ -447,3 +464,24 @@ describe("quarantine reports", () => {
|
|
|
447
464
|
);
|
|
448
465
|
});
|
|
449
466
|
});
|
|
467
|
+
|
|
468
|
+
describe("buildGitHubIssueUrl for a sync warning", () => {
|
|
469
|
+
const entry = {
|
|
470
|
+
...syncWarningDemoEntries[1],
|
|
471
|
+
folderLabel: "Tax return 2026",
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
it("files the stage, state and attempts without the folder name", () => {
|
|
475
|
+
const decoded = decode(
|
|
476
|
+
buildGitHubIssueUrl({
|
|
477
|
+
...baseCtx,
|
|
478
|
+
title: syncWarningIssueTitle(entry),
|
|
479
|
+
syncWarning: formatSyncWarningReport(entry),
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
assert.ok(decoded.includes("Message failed to sync: Retrying at Header"));
|
|
483
|
+
assert.ok(decoded.includes("**Attempts**: 7"));
|
|
484
|
+
assert.ok(decoded.includes("abcdef1"));
|
|
485
|
+
assert.ok(!decoded.includes("Tax return 2026"));
|
|
486
|
+
});
|
|
487
|
+
});
|
package/src/lib/bug-report.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { QuarantineReportSections } from "@remit/ui";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
APP_SHORT_SHA,
|
|
5
|
-
GITHUB_COMMIT_URL,
|
|
6
|
-
GITHUB_NEW_ISSUE_URL,
|
|
7
|
-
} from "./app-info";
|
|
2
|
+
import { getRuntimeConfig } from "../runtime-config";
|
|
3
|
+
import { APP_BUILD_TIME, APP_SHORT_SHA, GITHUB_COMMIT_URL } from "./app-info";
|
|
8
4
|
import { getRecentErrors } from "./console-errors";
|
|
9
5
|
import {
|
|
10
6
|
getFailingRequest,
|
|
@@ -53,6 +49,7 @@ export interface BugReportContext {
|
|
|
53
49
|
* @remit/ui so the MIME tree is fenced after truncation, never before.
|
|
54
50
|
*/
|
|
55
51
|
quarantine?: QuarantineReportSections;
|
|
52
|
+
syncWarning?: string;
|
|
56
53
|
/** Overrides the derived title, e.g. for a quarantine report. */
|
|
57
54
|
title?: string;
|
|
58
55
|
}
|
|
@@ -63,6 +60,7 @@ export interface BugReportSeed {
|
|
|
63
60
|
stack?: string;
|
|
64
61
|
componentStack?: string;
|
|
65
62
|
quarantine?: QuarantineReportSections;
|
|
63
|
+
syncWarning?: string;
|
|
66
64
|
/** Overrides the derived title, e.g. for a quarantine report. */
|
|
67
65
|
title?: string;
|
|
68
66
|
}
|
|
@@ -85,6 +83,7 @@ export function buildBugReportContext(seed?: BugReportSeed): BugReportContext {
|
|
|
85
83
|
stack: seed?.stack,
|
|
86
84
|
componentStack: seed?.componentStack,
|
|
87
85
|
quarantine: seed?.quarantine,
|
|
86
|
+
syncWarning: seed?.syncWarning,
|
|
88
87
|
title: seed?.title,
|
|
89
88
|
};
|
|
90
89
|
}
|
|
@@ -194,6 +193,10 @@ function buildIssueBody(
|
|
|
194
193
|
);
|
|
195
194
|
}
|
|
196
195
|
|
|
196
|
+
if (ctx.syncWarning) {
|
|
197
|
+
lines.push(ctx.syncWarning, "");
|
|
198
|
+
}
|
|
199
|
+
|
|
197
200
|
const resolvedStack = overrides?.stack ?? ctx.stack;
|
|
198
201
|
if (resolvedStack) {
|
|
199
202
|
lines.push("## Stacktrace", fencedBlock(resolvedStack), "");
|
|
@@ -236,7 +239,7 @@ function issueUrl(ctx: BugReportContext, overrides?: BodyOverrides): string {
|
|
|
236
239
|
title: buildIssueTitle(ctx),
|
|
237
240
|
body: buildIssueBody(ctx, overrides),
|
|
238
241
|
});
|
|
239
|
-
return `${
|
|
242
|
+
return `${getRuntimeConfig().issuesUrl}?${params.toString()}`;
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ErrorBannerSeverity } from "
|
|
1
|
+
import type { ErrorBannerSeverity } from "@remit/ui";
|
|
2
2
|
import type { BulkRunOutcome } from "@/lib/bulk-actions";
|
|
3
3
|
import { type DeleteOutcome, deleteExpunges, formatNumber } from "@/lib/format";
|
|
4
4
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import { describe, it } from "node:test";
|
|
8
|
-
import { CALENDAR_DENSITY_KEY } from "./calendar-density.js";
|
|
9
8
|
import {
|
|
10
9
|
calendarSearchSchema,
|
|
11
10
|
calendarViewMountsAgenda,
|
|
@@ -219,10 +218,6 @@ describe("calendarSearchSchema", () => {
|
|
|
219
218
|
});
|
|
220
219
|
|
|
221
220
|
describe("density is not URL state", () => {
|
|
222
|
-
it("is stored against the device, under a key of its own", () => {
|
|
223
|
-
assert.equal(CALENDAR_DENSITY_KEY, "remit:calendar-density");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
221
|
it("has no reading in the query", () => {
|
|
227
222
|
assert.deepEqual(calendarSearchSchema.parse({ density: "compact" }), {});
|
|
228
223
|
});
|
|
@@ -135,7 +135,7 @@ test("a stopped import splits the write order into landed, failed and never reac
|
|
|
135
135
|
{
|
|
136
136
|
code: "import_write_failed",
|
|
137
137
|
message:
|
|
138
|
-
"the filter store refused the write The items above it were written and remain; the
|
|
138
|
+
"the filter store refused the write The items above it were written and remain; import the same file again with merge to finish.",
|
|
139
139
|
details: { section: "filters", key: "Receipts" },
|
|
140
140
|
},
|
|
141
141
|
],
|
|
@@ -151,38 +151,7 @@ test("a stopped import splits the write order into landed, failed and never reac
|
|
|
151
151
|
assert.equal(byId.get("settings")?.state, "not-attempted");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("
|
|
155
|
-
const results = sectionResults(
|
|
156
|
-
reportOf({
|
|
157
|
-
applied: false,
|
|
158
|
-
items: [
|
|
159
|
-
item("filters", "Receipts", "rejected", "the store refused the write"),
|
|
160
|
-
],
|
|
161
|
-
errors: [
|
|
162
|
-
{
|
|
163
|
-
code: "import_write_failed",
|
|
164
|
-
message:
|
|
165
|
-
"the store refused the write Nothing was written; the import stopped here.",
|
|
166
|
-
details: { section: "filters", key: "Receipts" },
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
}),
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const byId = new Map(results.map((result) => [result.section, result]));
|
|
173
|
-
assert.equal(byId.get("accounts")?.state, "not-landed");
|
|
174
|
-
assert.match(
|
|
175
|
-
byId.get("accounts")?.detail ?? "",
|
|
176
|
-
/Nothing from this section was written/,
|
|
177
|
-
);
|
|
178
|
-
assert.equal(byId.get("labels")?.state, "not-landed");
|
|
179
|
-
assert.equal(byId.get("filters")?.state, "failed");
|
|
180
|
-
assert.equal(byId.get("addressFlags")?.state, "not-attempted");
|
|
181
|
-
assert.equal(byId.get("settings")?.state, "not-attempted");
|
|
182
|
-
assert.equal(results.filter((result) => result.state === "landed").length, 0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("a first write that fails on a store with no transaction lands nothing either", () => {
|
|
154
|
+
test("a first write that fails lands nothing", () => {
|
|
186
155
|
const results = sectionResults(
|
|
187
156
|
reportOf({
|
|
188
157
|
applied: false,
|
|
@@ -193,7 +162,7 @@ test("a first write that fails on a store with no transaction lands nothing eith
|
|
|
193
162
|
{
|
|
194
163
|
code: "import_write_failed",
|
|
195
164
|
message:
|
|
196
|
-
"the store refused The items above it were written and remain; the
|
|
165
|
+
"the store refused The items above it were written and remain; import the same file again with merge to finish.",
|
|
197
166
|
details: { section: "accounts", key: "a@example.test" },
|
|
198
167
|
},
|
|
199
168
|
],
|
|
@@ -239,11 +208,6 @@ test("a pending folder names the setting waiting on it, and repeats are folded",
|
|
|
239
208
|
message: "not here yet",
|
|
240
209
|
details: { folderPath: "Raadsstukken", accountId: "acct-1" },
|
|
241
210
|
},
|
|
242
|
-
{
|
|
243
|
-
code: "anchor_not_embedded",
|
|
244
|
-
message: "carried as text",
|
|
245
|
-
details: { filter: "Waternet" },
|
|
246
|
-
},
|
|
247
211
|
],
|
|
248
212
|
}),
|
|
249
213
|
);
|
|
@@ -73,12 +73,4 @@ describe("the lg variant and the desktop hooks gate on the same condition", () =
|
|
|
73
73
|
);
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
it("excludes a coarse pointer held upright", () => {
|
|
78
|
-
assert.match(DESKTOP_MEDIA_QUERY, /min-width: 1024px/);
|
|
79
|
-
assert.match(
|
|
80
|
-
DESKTOP_MEDIA_QUERY,
|
|
81
|
-
/not \(\(orientation: portrait\) and \(pointer: coarse\)\)/,
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
76
|
});
|
package/src/lib/drafts.test.ts
CHANGED
|
@@ -187,29 +187,6 @@ describe("groupDraftSections", () => {
|
|
|
187
187
|
assert.deepStrictEqual(sections, []);
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
test("section labels match spec", () => {
|
|
191
|
-
const draft = outboxMessage({
|
|
192
|
-
outboxMessageId: "d1",
|
|
193
|
-
accountId: "acc-1",
|
|
194
|
-
status: "draft",
|
|
195
|
-
});
|
|
196
|
-
const thread = threadMessage({
|
|
197
|
-
threadMessageId: "tm1",
|
|
198
|
-
threadId: "t1",
|
|
199
|
-
messageId: "m1",
|
|
200
|
-
accountConfigId: "acc-1",
|
|
201
|
-
});
|
|
202
|
-
const sections = groupDraftSections({
|
|
203
|
-
outboxMessages: [draft],
|
|
204
|
-
accountId: "acc-1",
|
|
205
|
-
imapThreads: [thread],
|
|
206
|
-
});
|
|
207
|
-
assert.deepStrictEqual(
|
|
208
|
-
sections.map((s) => s.label),
|
|
209
|
-
["Not yet sent (Remit)", "On the server"],
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
190
|
test("Remit-drafts section row id equals outboxMessageId", () => {
|
|
214
191
|
const draft = outboxMessage({
|
|
215
192
|
outboxMessageId: "outbox-abc",
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
__resetFatalError,
|
|
7
7
|
getCurrentFatalError,
|
|
8
8
|
reportFatalError,
|
|
9
|
-
setFatalErrorTelemetry,
|
|
10
9
|
subscribeFatalError,
|
|
11
10
|
} from "./fatal-error";
|
|
12
11
|
|
|
@@ -39,24 +38,6 @@ describe("reportFatalError", () => {
|
|
|
39
38
|
assert.equal(getCurrentFatalError(), fatal);
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
it("emits a telemetry fatal-error event through the registered sink", () => {
|
|
43
|
-
const recorded: Array<{ message: string; ctx?: Record<string, string> }> =
|
|
44
|
-
[];
|
|
45
|
-
setFatalErrorTelemetry({
|
|
46
|
-
recordPageView: () => {},
|
|
47
|
-
recordError: (error, ctx) =>
|
|
48
|
-
recorded.push({ message: error.message, ctx }),
|
|
49
|
-
recordEvent: () => {},
|
|
50
|
-
recordTiming: () => {},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
reportFatalError(new Error("telemetry please"));
|
|
54
|
-
|
|
55
|
-
assert.equal(recorded.length, 1);
|
|
56
|
-
assert.equal(recorded[0].message, "telemetry please");
|
|
57
|
-
assert.equal(recorded[0].ctx?.fatal, "true");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
41
|
it("unsubscribe stops further notifications", () => {
|
|
61
42
|
let count = 0;
|
|
62
43
|
const unsubscribe = subscribeFatalError(() => {
|
|
@@ -36,10 +36,6 @@ describe("buildMailboxRoleMap", () => {
|
|
|
36
36
|
]);
|
|
37
37
|
assert.equal(map.get("mb-1"), "inbox");
|
|
38
38
|
});
|
|
39
|
-
|
|
40
|
-
test("empty appointments produce an empty map", () => {
|
|
41
|
-
assert.equal(buildMailboxRoleMap([]).size, 0);
|
|
42
|
-
});
|
|
43
39
|
});
|
|
44
40
|
|
|
45
41
|
describe("labelForMailbox", () => {
|
|
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { afterEach, describe, test } from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
__resetHotSyncWindow,
|
|
5
|
-
HOT_POLL_INTERVAL_MS,
|
|
6
5
|
HOT_WINDOW_MS,
|
|
7
6
|
isHotSyncWindowActive,
|
|
8
7
|
startHotSyncWindow,
|
|
@@ -57,8 +56,4 @@ describe("hot sync window", () => {
|
|
|
57
56
|
startHotSyncWindow(NOW);
|
|
58
57
|
assert.equal(presses, 1);
|
|
59
58
|
});
|
|
60
|
-
|
|
61
|
-
test("the hot cadence is thirty seconds", () => {
|
|
62
|
-
assert.equal(HOT_POLL_INTERVAL_MS, 30_000);
|
|
63
|
-
});
|
|
64
59
|
});
|
|
@@ -28,7 +28,7 @@ export interface ListHeaderChrome {
|
|
|
28
28
|
/** The expanded field, which takes the title's place for as long as it is up. */
|
|
29
29
|
searchField: ReactNode;
|
|
30
30
|
/**
|
|
31
|
-
* The read-only
|
|
31
|
+
* The read-only results panel, when a query is being typed and
|
|
32
32
|
* this view answers it with the panel rather than its own rows. The body
|
|
33
33
|
* renders it in place of its list; `MailListHeader` cannot swap the body
|
|
34
34
|
* out from above, because the header is inside it.
|
package/src/lib/mail-context.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface MailContextValue {
|
|
|
21
21
|
/**
|
|
22
22
|
* Name indexes `parseSearchTokens` resolves `in:`/`account:` chips against
|
|
23
23
|
* (#428 follow-up). Computed once here so every consumer — chip rendering,
|
|
24
|
-
* the brief/flagged token filters
|
|
24
|
+
* the brief/flagged token filters — agrees on the same
|
|
25
25
|
* resolution. See `lib/search-token-index.ts`.
|
|
26
26
|
*/
|
|
27
27
|
mailboxNameIndex: ReadonlyMap<string, string>;
|
|
@@ -8,6 +8,7 @@ const part = (
|
|
|
8
8
|
): RemitImapBodyPartResponse =>
|
|
9
9
|
({
|
|
10
10
|
bodyPartId: "part-1",
|
|
11
|
+
partPath: "2",
|
|
11
12
|
mediaType: "APPLICATION",
|
|
12
13
|
mediaSubtype: "PDF",
|
|
13
14
|
sizeOctets: 2048,
|
|
@@ -83,4 +84,80 @@ describe("selectMessageAttachments", () => {
|
|
|
83
84
|
]);
|
|
84
85
|
assert.equal(attachment.typeLabel, "FILE");
|
|
85
86
|
});
|
|
87
|
+
|
|
88
|
+
describe("a forwarded message", () => {
|
|
89
|
+
const forwarded = part({
|
|
90
|
+
bodyPartId: "forwarded",
|
|
91
|
+
partPath: "2",
|
|
92
|
+
mediaType: "MESSAGE",
|
|
93
|
+
mediaSubtype: "rfc822",
|
|
94
|
+
dispositionFilename: "forwarded.eml",
|
|
95
|
+
});
|
|
96
|
+
const inner = [
|
|
97
|
+
part({
|
|
98
|
+
bodyPartId: "inner-root",
|
|
99
|
+
partPath: "2.0",
|
|
100
|
+
mediaType: "MULTIPART",
|
|
101
|
+
mediaSubtype: "mixed",
|
|
102
|
+
isMultipart: true,
|
|
103
|
+
disposition: undefined,
|
|
104
|
+
}),
|
|
105
|
+
part({
|
|
106
|
+
bodyPartId: "inner-text",
|
|
107
|
+
partPath: "2.1",
|
|
108
|
+
mediaType: "TEXT",
|
|
109
|
+
mediaSubtype: "plain",
|
|
110
|
+
disposition: undefined,
|
|
111
|
+
}),
|
|
112
|
+
part({
|
|
113
|
+
bodyPartId: "inner-pdf",
|
|
114
|
+
partPath: "2.2",
|
|
115
|
+
dispositionFilename: "original.pdf",
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
it("is listed as a message, and the attachment inside it is not", () => {
|
|
120
|
+
const attachments = selectMessageAttachments([
|
|
121
|
+
forwarded,
|
|
122
|
+
...inner,
|
|
123
|
+
part({ bodyPartId: "outer-pdf", partPath: "3" }),
|
|
124
|
+
]);
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
attachments.map((a) => [a.bodyPartId, a.kind, a.filename]),
|
|
127
|
+
[
|
|
128
|
+
["forwarded", "message", "forwarded.eml"],
|
|
129
|
+
["outer-pdf", "file", "report.pdf"],
|
|
130
|
+
],
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("is listed whatever disposition the sender gave it", () => {
|
|
135
|
+
const [attachment] = selectMessageAttachments([
|
|
136
|
+
{ ...forwarded, disposition: undefined },
|
|
137
|
+
]);
|
|
138
|
+
assert.equal(attachment?.kind, "message");
|
|
139
|
+
assert.equal(attachment?.filename, "forwarded.eml");
|
|
140
|
+
assert.equal(attachment?.typeLabel, "MESSAGE");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("lists its own attachment once it is the message being read", () => {
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
selectMessageAttachments(inner).map((a) => a.bodyPartId),
|
|
146
|
+
["inner-pdf"],
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("is not confused with a delivery report", () => {
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
selectMessageAttachments([
|
|
153
|
+
part({
|
|
154
|
+
mediaType: "MESSAGE",
|
|
155
|
+
mediaSubtype: "delivery-status",
|
|
156
|
+
disposition: undefined,
|
|
157
|
+
}),
|
|
158
|
+
]),
|
|
159
|
+
[],
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
86
163
|
});
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { RemitImapBodyPartResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import {
|
|
3
|
+
isEncapsulatedMessage,
|
|
4
|
+
ownParts,
|
|
5
|
+
} from "@remit/data-ports/message-parts";
|
|
2
6
|
import { ContentDisposition } from "@remit/domain-enums";
|
|
3
|
-
import { sanitizeAttachmentFilename } from "@remit/ui";
|
|
7
|
+
import { type AttachmentKind, sanitizeAttachmentFilename } from "@remit/ui";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* The attachment-disposition body parts of a message, in the order the message
|
|
7
11
|
* declares them. `pickRenderablePart` (message-body-source.ts) drops exactly
|
|
8
12
|
* these parts so the renderer never treats a PDF as a body; this is the other
|
|
9
|
-
* half of that split.
|
|
13
|
+
* half of that split. A forwarded message is listed whatever its disposition,
|
|
14
|
+
* and the parts inside it are its own, not the carrying message's.
|
|
10
15
|
*/
|
|
11
16
|
export interface MessageAttachment {
|
|
12
17
|
bodyPartId: string;
|
|
18
|
+
partPath: string;
|
|
19
|
+
kind: AttachmentKind;
|
|
13
20
|
/** Display and save name — sanitized once, used for both. */
|
|
14
21
|
filename: string;
|
|
15
22
|
typeLabel: string;
|
|
@@ -17,7 +24,8 @@ export interface MessageAttachment {
|
|
|
17
24
|
contentUrl: string;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
const typeLabelFor = (mediaSubtype: string): string => {
|
|
27
|
+
const typeLabelFor = (kind: AttachmentKind, mediaSubtype: string): string => {
|
|
28
|
+
if (kind === "message") return "MESSAGE";
|
|
21
29
|
const label = mediaSubtype.toUpperCase();
|
|
22
30
|
return label === "OCTET-STREAM" ? "FILE" : label;
|
|
23
31
|
};
|
|
@@ -35,20 +43,28 @@ const fallbackFilenameFor = (mediaSubtype: string): string => {
|
|
|
35
43
|
export const selectMessageAttachments = (
|
|
36
44
|
parts: readonly RemitImapBodyPartResponse[],
|
|
37
45
|
): MessageAttachment[] =>
|
|
38
|
-
parts
|
|
46
|
+
ownParts(parts)
|
|
39
47
|
.filter(
|
|
40
48
|
(part) =>
|
|
41
49
|
!part.isMultipart &&
|
|
42
|
-
part.disposition === ContentDisposition.Attachment
|
|
50
|
+
(part.disposition === ContentDisposition.Attachment ||
|
|
51
|
+
isEncapsulatedMessage(part)) &&
|
|
43
52
|
part.contentUrl.length > 0,
|
|
44
53
|
)
|
|
45
|
-
.map((part) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
.map((part) => {
|
|
55
|
+
const kind: AttachmentKind = isEncapsulatedMessage(part)
|
|
56
|
+
? "message"
|
|
57
|
+
: "file";
|
|
58
|
+
return {
|
|
59
|
+
bodyPartId: part.bodyPartId,
|
|
60
|
+
partPath: part.partPath,
|
|
61
|
+
kind,
|
|
62
|
+
filename: sanitizeAttachmentFilename(
|
|
63
|
+
part.dispositionFilename ?? "",
|
|
64
|
+
fallbackFilenameFor(part.mediaSubtype),
|
|
65
|
+
),
|
|
66
|
+
typeLabel: typeLabelFor(kind, part.mediaSubtype),
|
|
67
|
+
sizeOctets: part.sizeOctets,
|
|
68
|
+
contentUrl: part.contentUrl,
|
|
69
|
+
};
|
|
70
|
+
});
|