@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
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./message-body-source";
|
|
7
7
|
|
|
8
8
|
const make = (overrides: Partial<RenderableBodyPart>): RenderableBodyPart => ({
|
|
9
|
+
partPath: "1",
|
|
9
10
|
mediaType: "TEXT",
|
|
10
11
|
mediaSubtype: "PLAIN",
|
|
11
12
|
contentUrl: "https://cdn.test/content/x",
|
|
@@ -75,10 +76,6 @@ describe("pickRenderablePart", () => {
|
|
|
75
76
|
assert.equal(pickRenderablePart([image]), null);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
it("returns null on an empty list", () => {
|
|
79
|
-
assert.equal(pickRenderablePart([]), null);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
79
|
it("treats casing variants of the subtype as equivalent (mailers are inconsistent)", () => {
|
|
83
80
|
const html = make({
|
|
84
81
|
mediaSubtype: "html",
|
|
@@ -95,4 +92,24 @@ describe("pickRenderablePart", () => {
|
|
|
95
92
|
const broken = make({ mediaSubtype: "HTML", contentUrl: "" });
|
|
96
93
|
assert.equal(pickRenderablePart([broken]), null);
|
|
97
94
|
});
|
|
95
|
+
|
|
96
|
+
it("never renders a forwarded message's body as the body carrying it", () => {
|
|
97
|
+
const outer = make({ partPath: "1", contentUrl: "https://cdn.test/outer" });
|
|
98
|
+
const forwarded = make({
|
|
99
|
+
partPath: "2",
|
|
100
|
+
mediaType: "MESSAGE",
|
|
101
|
+
mediaSubtype: "rfc822",
|
|
102
|
+
disposition: "attachment",
|
|
103
|
+
contentUrl: "https://cdn.test/forwarded",
|
|
104
|
+
});
|
|
105
|
+
const innerHtml = make({
|
|
106
|
+
partPath: "2.1",
|
|
107
|
+
mediaSubtype: "HTML",
|
|
108
|
+
contentUrl: "https://cdn.test/inner",
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(pickRenderablePart([outer, forwarded, innerHtml]), {
|
|
111
|
+
kind: "text",
|
|
112
|
+
contentUrl: "https://cdn.test/outer",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
98
115
|
});
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* type without standing up jsdom or DOMPurify.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { ownParts } from "@remit/data-ports/message-parts";
|
|
12
|
+
|
|
11
13
|
export type BodyContentKind = "html" | "text";
|
|
12
14
|
|
|
13
15
|
export interface RenderableBodyPart {
|
|
16
|
+
partPath: string;
|
|
14
17
|
mediaType: string;
|
|
15
18
|
mediaSubtype: string;
|
|
16
19
|
disposition?: string;
|
|
@@ -27,13 +30,13 @@ export interface PickedBodyPart {
|
|
|
27
30
|
* Pick the best body part to render: prefer the first inline `text/html`,
|
|
28
31
|
* fall back to the first inline `text/plain`. Multipart container rows and
|
|
29
32
|
* attachment-disposition parts are ignored — they carry no renderable
|
|
30
|
-
* payload at this position. Returns `null` when nothing renderable is
|
|
33
|
+
* payload at this position — and so are the parts of a forwarded message. Returns `null` when nothing renderable is
|
|
31
34
|
* available so callers can show an empty-body affordance.
|
|
32
35
|
*/
|
|
33
36
|
export const pickRenderablePart = (
|
|
34
37
|
parts: readonly RenderableBodyPart[],
|
|
35
38
|
): PickedBodyPart | null => {
|
|
36
|
-
const renderable = parts.filter(
|
|
39
|
+
const renderable = ownParts(parts).filter(
|
|
37
40
|
(p) =>
|
|
38
41
|
!p.isMultipart &&
|
|
39
42
|
p.disposition !== "attachment" &&
|
|
@@ -45,20 +45,4 @@ describe("completeOnboarding", () => {
|
|
|
45
45
|
"navigate",
|
|
46
46
|
]);
|
|
47
47
|
});
|
|
48
|
-
|
|
49
|
-
it("refreshes the config query", async () => {
|
|
50
|
-
let refetchedKey: unknown;
|
|
51
|
-
await completeOnboarding({
|
|
52
|
-
queryClient: {
|
|
53
|
-
refetchQueries: (filters) => {
|
|
54
|
-
refetchedKey = (filters as { queryKey?: unknown }).queryKey;
|
|
55
|
-
return Promise.resolve();
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
recordCompleted: () => {},
|
|
59
|
-
navigateToInbox: () => {},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
assert.ok(refetchedKey, "refetchQueries called with a query key");
|
|
63
|
-
});
|
|
64
48
|
});
|
|
@@ -19,7 +19,6 @@ const filter = (
|
|
|
19
19
|
scope: "Standing",
|
|
20
20
|
state: "Active",
|
|
21
21
|
disabledReason: "None",
|
|
22
|
-
hasAnchor: false,
|
|
23
22
|
ruleChangedAt: 0,
|
|
24
23
|
actionChangedAt: 0,
|
|
25
24
|
matchOperator: "And",
|
|
@@ -77,21 +76,6 @@ describe("filterToRule", () => {
|
|
|
77
76
|
assert.equal(rule.scope, "until");
|
|
78
77
|
assert.match(rule.until ?? "", /^\d{4}-\d{2}-\d{2}$/);
|
|
79
78
|
});
|
|
80
|
-
|
|
81
|
-
it("shows a semantic anchor as an active widen chip when the deployment can serve it", () => {
|
|
82
|
-
const rule = filterToRule(filter({ hasAnchor: true }));
|
|
83
|
-
assert.ok(rule.widen);
|
|
84
|
-
assert.equal(rule.widen?.inactive, undefined);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("marks the widen inactive on a deployment that cannot evaluate it (D4)", () => {
|
|
88
|
-
const rule = filterToRule(filter({ hasAnchor: true }), true);
|
|
89
|
-
assert.equal(rule.widen?.inactive, true);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("carries no widen when the filter has no anchor", () => {
|
|
93
|
-
assert.equal(filterToRule(filter({ hasAnchor: false })).widen, undefined);
|
|
94
|
-
});
|
|
95
79
|
});
|
|
96
80
|
|
|
97
81
|
describe("expiresAtToPickedDate", () => {
|
|
@@ -109,7 +93,7 @@ describe("expiresAtToPickedDate", () => {
|
|
|
109
93
|
});
|
|
110
94
|
|
|
111
95
|
describe("ruleChangesPredicateOrAction", () => {
|
|
112
|
-
const base = filterToRule(filter(
|
|
96
|
+
const base = filterToRule(filter());
|
|
113
97
|
|
|
114
98
|
it("is false for an identical rule and for a rename only", () => {
|
|
115
99
|
assert.equal(ruleChangesPredicateOrAction(base, base), false);
|
|
@@ -119,7 +103,7 @@ describe("ruleChangesPredicateOrAction", () => {
|
|
|
119
103
|
);
|
|
120
104
|
});
|
|
121
105
|
|
|
122
|
-
it("is true when a clause, operator
|
|
106
|
+
it("is true when a clause, operator or move target changes", () => {
|
|
123
107
|
assert.equal(
|
|
124
108
|
ruleChangesPredicateOrAction({ ...base, matchOperator: "any" }, base),
|
|
125
109
|
true,
|
|
@@ -131,10 +115,6 @@ describe("ruleChangesPredicateOrAction", () => {
|
|
|
131
115
|
),
|
|
132
116
|
true,
|
|
133
117
|
);
|
|
134
|
-
assert.equal(
|
|
135
|
-
ruleChangesPredicateOrAction({ ...base, widen: undefined }, base),
|
|
136
|
-
true,
|
|
137
|
-
);
|
|
138
118
|
assert.equal(
|
|
139
119
|
ruleChangesPredicateOrAction(
|
|
140
120
|
{ ...base, clauses: [{ id: "c", field: "Subject", value: "x" }] },
|
|
@@ -288,14 +268,4 @@ describe("buildUpdateFilterInput", () => {
|
|
|
288
268
|
assert.equal(body.scope, "Temporary");
|
|
289
269
|
assert.match(body.expiresAt ?? "", /^2027-06-01T/);
|
|
290
270
|
});
|
|
291
|
-
|
|
292
|
-
it("carries no anchor field — the editor never has one to send (reader #266)", () => {
|
|
293
|
-
const changed: FilterRule = {
|
|
294
|
-
...original,
|
|
295
|
-
scope: "until",
|
|
296
|
-
until: "2027-03-04",
|
|
297
|
-
};
|
|
298
|
-
const body = buildUpdateFilterInput(changed, original);
|
|
299
|
-
assert.equal("anchorMessageId" in body, false);
|
|
300
|
-
});
|
|
301
271
|
});
|
|
@@ -8,20 +8,15 @@ import { NO_ACTION } from "./organize-model";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The persisted filter as the chip editor's rule (RFC 038 D6). Loads the whole
|
|
11
|
-
* rule the user edits: literal clauses, the match operator, the move action,
|
|
12
|
-
* scope (and, for a Temporary filter, the date it stops on)
|
|
13
|
-
* anchor as a widen chip. The anchor renders inactive when this deployment
|
|
14
|
-
* cannot evaluate it (D4) — the rule then matches by its literal clauses only.
|
|
11
|
+
* rule the user edits: literal clauses, the match operator, the move action, and
|
|
12
|
+
* the scope (and, for a Temporary filter, the date it stops on).
|
|
15
13
|
*
|
|
16
14
|
* The scope maps `Standing` → "standing" and `Temporary` → "until"; a persisted
|
|
17
15
|
* filter is never the one-time "once" scope. `moveMailboxId` and `labelId` both
|
|
18
16
|
* drop the `"None"` sentinel to `undefined` so an empty select reads as "no
|
|
19
17
|
* action" (issue #26).
|
|
20
18
|
*/
|
|
21
|
-
export const filterToRule = (
|
|
22
|
-
filter: RemitImapFilterResponse,
|
|
23
|
-
semanticUnavailable = false,
|
|
24
|
-
): FilterRule => {
|
|
19
|
+
export const filterToRule = (filter: RemitImapFilterResponse): FilterRule => {
|
|
25
20
|
const clauses: RuleClause[] = filter.literalClauses.map((clause, index) => ({
|
|
26
21
|
id: `clause-${index}`,
|
|
27
22
|
field: clause.field,
|
|
@@ -30,9 +25,6 @@ export const filterToRule = (
|
|
|
30
25
|
return {
|
|
31
26
|
clauses,
|
|
32
27
|
matchOperator: filter.matchOperator === "And" ? "all" : "any",
|
|
33
|
-
widen: filter.hasAnchor
|
|
34
|
-
? { anchorCount: 1, ...(semanticUnavailable ? { inactive: true } : {}) }
|
|
35
|
-
: undefined,
|
|
36
28
|
moveMailboxId:
|
|
37
29
|
filter.actionMailboxId !== NO_ACTION ? filter.actionMailboxId : undefined,
|
|
38
30
|
labelId:
|
|
@@ -69,13 +61,12 @@ const predicateActionKey = (rule: FilterRule): string =>
|
|
|
69
61
|
operator: rule.matchOperator,
|
|
70
62
|
move: rule.moveMailboxId ?? null,
|
|
71
63
|
label: rule.labelId ?? null,
|
|
72
|
-
widen: rule.widen ? (rule.widen.inactive ? "inactive" : "active") : "none",
|
|
73
64
|
});
|
|
74
65
|
|
|
75
66
|
/**
|
|
76
67
|
* Whether the edited rule changes the predicate or the action versus the one it
|
|
77
68
|
* was loaded from — the clauses, the match operator, the move target, the
|
|
78
|
-
*
|
|
69
|
+
* or the label target. A change here is what bumps
|
|
79
70
|
* `ruleChangedAt` and offers the re-back-apply (RFC 034 Decision 3.2); the name
|
|
80
71
|
* is deliberately excluded, so a cosmetic rename is not a rule change.
|
|
81
72
|
*/
|
|
@@ -93,7 +84,7 @@ const scopeExpiryKey = (rule: FilterRule): string =>
|
|
|
93
84
|
/**
|
|
94
85
|
* Whether the edited rule changes scope (standing ↔ until-a-date) or the date
|
|
95
86
|
* itself, versus the one it was loaded from (reader #266). Scope and expiry
|
|
96
|
-
* are mutable on an existing filter,
|
|
87
|
+
* are mutable on an existing filter, and a change here
|
|
97
88
|
* bumps `ruleChangedAt` the same way a predicate/action change does — moving a
|
|
98
89
|
* lapsed filter back to Standing (or extending its date) is the same kind of
|
|
99
90
|
* "the user just reasserted this rule" moment, and re-offers the back-apply so
|
|
@@ -110,8 +101,7 @@ export const ruleChangesScopeOrExpiry = (
|
|
|
110
101
|
* untouched (RFC 034 Decision 3.2). A predicate or action change sends the
|
|
111
102
|
* operator, clauses, move target, and label target; a scope or expiry change
|
|
112
103
|
* sends `scope` and, for the `until` scope, `expiresAt` (reader #266) —
|
|
113
|
-
* either bumps `ruleChangedAt`.
|
|
114
|
-
* it never enters the patch; the partial update preserves it.
|
|
104
|
+
* either bumps `ruleChangedAt`.
|
|
115
105
|
*/
|
|
116
106
|
export const buildUpdateFilterInput = (
|
|
117
107
|
rule: FilterRule,
|
|
@@ -53,31 +53,17 @@ describe("hasCommittableAction", () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
describe("buildOrganizeInput", () => {
|
|
56
|
-
it("
|
|
57
|
-
const input = buildOrganizeInput(baseDraft(
|
|
58
|
-
assert.equal(input.anchorMessageId, "msg-1");
|
|
56
|
+
it("defaults both actions to None when neither is set", () => {
|
|
57
|
+
const input = buildOrganizeInput(baseDraft());
|
|
59
58
|
assert.equal(input.actionMailboxId, NO_ACTION);
|
|
60
59
|
assert.equal(input.actionLabelId, NO_ACTION);
|
|
61
60
|
assert.equal(input.matchOperator, "And");
|
|
62
61
|
assert.deepEqual(input.literalClauses, []);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
|
-
it("omits anchorMessageId entirely for a purely-literal input", () => {
|
|
66
|
-
const input = buildOrganizeInput(
|
|
67
|
-
baseDraft({
|
|
68
|
-
literalClauses: [{ field: "From", value: "airbnb.com" }],
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
71
|
-
assert.equal("anchorMessageId" in input, false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
64
|
it("carries the move and label actions independently (issue #26)", () => {
|
|
75
65
|
const input = buildOrganizeInput(
|
|
76
|
-
baseDraft({
|
|
77
|
-
anchorMessageId: "msg-1",
|
|
78
|
-
moveMailboxId: "mbx-9",
|
|
79
|
-
labelId: "lbl-9",
|
|
80
|
-
}),
|
|
66
|
+
baseDraft({ moveMailboxId: "mbx-9", labelId: "lbl-9" }),
|
|
81
67
|
);
|
|
82
68
|
assert.equal(input.actionMailboxId, "mbx-9");
|
|
83
69
|
assert.equal(input.actionLabelId, "lbl-9");
|
|
@@ -87,7 +73,7 @@ describe("buildOrganizeInput", () => {
|
|
|
87
73
|
describe("buildCreateFilterInput", () => {
|
|
88
74
|
it("builds a Standing filter with no expiresAt", () => {
|
|
89
75
|
const input = buildCreateFilterInput(
|
|
90
|
-
baseDraft({
|
|
76
|
+
baseDraft({ moveMailboxId: "mbx-2" }),
|
|
91
77
|
"standing",
|
|
92
78
|
"Travel",
|
|
93
79
|
);
|
|
@@ -96,7 +82,6 @@ describe("buildCreateFilterInput", () => {
|
|
|
96
82
|
assert.equal(input.name, "Travel");
|
|
97
83
|
assert.equal(input.actionMailboxId, "mbx-2");
|
|
98
84
|
assert.equal(input.actionLabelId, NO_ACTION);
|
|
99
|
-
assert.equal(input.anchorMessageId, "msg-1");
|
|
100
85
|
});
|
|
101
86
|
|
|
102
87
|
it("builds a Temporary filter that carries expiresAt", () => {
|
|
@@ -143,8 +128,8 @@ describe("organizeScopeFor", () => {
|
|
|
143
128
|
);
|
|
144
129
|
});
|
|
145
130
|
|
|
146
|
-
it("reads
|
|
147
|
-
assert.equal(organizeScopeFor({ mode: "
|
|
131
|
+
it("reads the properties door at scope once as all-like-these", () => {
|
|
132
|
+
assert.equal(organizeScopeFor({ mode: "properties" }), "all-like-these");
|
|
148
133
|
assert.equal(
|
|
149
134
|
organizeScopeFor({ mode: "properties", ruleScope: "once" }),
|
|
150
135
|
"all-like-these",
|
|
@@ -152,7 +137,7 @@ describe("organizeScopeFor", () => {
|
|
|
152
137
|
});
|
|
153
138
|
|
|
154
139
|
it("reads the two persisting scopes off the scope answer alone", () => {
|
|
155
|
-
for (const mode of ["selected", "
|
|
140
|
+
for (const mode of ["selected", "properties"] as const) {
|
|
156
141
|
assert.equal(
|
|
157
142
|
organizeScopeFor({ mode, ruleScope: "standing" }),
|
|
158
143
|
"standing",
|
|
@@ -196,24 +181,6 @@ describe("buildWizardDraft", () => {
|
|
|
196
181
|
}
|
|
197
182
|
});
|
|
198
183
|
|
|
199
|
-
it("anchors on the ticked messages only while the similar door is matching", () => {
|
|
200
|
-
const anchored = buildWizardDraft({
|
|
201
|
-
mode: "similar",
|
|
202
|
-
anchorMessageId: "msg-1",
|
|
203
|
-
clauses: [],
|
|
204
|
-
matchOperator: "all",
|
|
205
|
-
});
|
|
206
|
-
assert.equal(anchored.anchorMessageId, "msg-1");
|
|
207
|
-
|
|
208
|
-
const literal = buildWizardDraft({
|
|
209
|
-
mode: "properties",
|
|
210
|
-
anchorMessageId: "msg-1",
|
|
211
|
-
clauses,
|
|
212
|
-
matchOperator: "any",
|
|
213
|
-
});
|
|
214
|
-
assert.equal("anchorMessageId" in literal, false);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
184
|
it("maps the wizard's operator onto the API's", () => {
|
|
218
185
|
assert.equal(
|
|
219
186
|
buildWizardDraft({ mode: "properties", clauses, matchOperator: "all" })
|
|
@@ -32,16 +32,23 @@ export type OrganizeScope =
|
|
|
32
32
|
/** The `"None"` sentinel the API uses for an absent label / move action. */
|
|
33
33
|
export const NO_ACTION = "None";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The predicate a preview counts and a commit acts on: the operator and the
|
|
37
|
+
* literal clauses. The set it previews equals the set every commit scope acts on.
|
|
38
|
+
*/
|
|
39
|
+
export type OrganizeMatchPredicate = Pick<
|
|
40
|
+
RemitImapOrganizeInput,
|
|
41
|
+
"matchOperator" | "literalClauses"
|
|
42
|
+
>;
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* The user's in-progress organize decision, independent of which scope they
|
|
37
|
-
* land on. The
|
|
46
|
+
* land on. The predicate drives the match set; `moveMailboxId` and
|
|
38
47
|
* `labelId` are the two committable actions (issue #26) — independent of each
|
|
39
48
|
* other, since a move is exclusive and a label is additive (RFC 034 Decision
|
|
40
49
|
* 3.1).
|
|
41
50
|
*/
|
|
42
51
|
export interface OrganizeDraft {
|
|
43
|
-
/** Semantic anchor — "mail like this one". The first selected message. */
|
|
44
|
-
anchorMessageId?: string;
|
|
45
52
|
/** How `literalClauses` combine. Ignored when there are none. */
|
|
46
53
|
matchOperator: RemitImapFilterMatchOperator;
|
|
47
54
|
/** Literal from/subject/has-words clauses (RFC 031). */
|
|
@@ -90,13 +97,12 @@ export const buildRunFilterInput = (
|
|
|
90
97
|
/**
|
|
91
98
|
* Build the read-only preview / back-apply matcher input. The action fields do
|
|
92
99
|
* not affect which messages match — the preview returns exactly the set a job
|
|
93
|
-
* with the same predicate would apply to — so a
|
|
94
|
-
*
|
|
100
|
+
* with the same predicate would apply to — so a preview can pass this before
|
|
101
|
+
* the user has chosen a folder or a label.
|
|
95
102
|
*/
|
|
96
103
|
export const buildOrganizeInput = (
|
|
97
104
|
draft: OrganizeDraft,
|
|
98
105
|
): RemitImapOrganizeInput => ({
|
|
99
|
-
...(draft.anchorMessageId ? { anchorMessageId: draft.anchorMessageId } : {}),
|
|
100
106
|
matchOperator: draft.matchOperator,
|
|
101
107
|
literalClauses: draft.literalClauses,
|
|
102
108
|
actionLabelId: draft.labelId ?? NO_ACTION,
|
|
@@ -111,9 +117,9 @@ export const buildOrganizeInput = (
|
|
|
111
117
|
export interface WizardCommitAnswers {
|
|
112
118
|
/**
|
|
113
119
|
* The door the match came through. An escalated predicate is not one: it has
|
|
114
|
-
* no clauses to build a rule from
|
|
115
|
-
*
|
|
116
|
-
*
|
|
120
|
+
* no clauses to build a rule from, so a scope reconstructed for it would be a
|
|
121
|
+
* rule matching everything. Typed to the doors so that cannot be written
|
|
122
|
+
* rather than merely not written.
|
|
117
123
|
*/
|
|
118
124
|
mode: MatchDoor;
|
|
119
125
|
/** Absent on the verbs that never reach the scope step — those act once. */
|
|
@@ -123,7 +129,7 @@ export interface WizardCommitAnswers {
|
|
|
123
129
|
/**
|
|
124
130
|
* Which of the four commit scopes the wizard's answers add up to. Reconstructed
|
|
125
131
|
* here and nowhere else, so a driver cannot invent a fifth: the ticked list at
|
|
126
|
-
* scope once is `just-these`,
|
|
132
|
+
* scope once is `just-these`, the properties door at scope once is `all-like-these`,
|
|
127
133
|
* and the two persisting scopes are `standing` and `temporary`.
|
|
128
134
|
*/
|
|
129
135
|
export const organizeScopeFor = ({
|
|
@@ -136,8 +142,6 @@ export const organizeScopeFor = ({
|
|
|
136
142
|
};
|
|
137
143
|
|
|
138
144
|
export interface WizardDraftInput extends WizardCommitAnswers {
|
|
139
|
-
/** The semantic anchor, present only while the similar door is the matcher. */
|
|
140
|
-
anchorMessageId?: string;
|
|
141
145
|
clauses: readonly RuleClause[];
|
|
142
146
|
matchOperator: MatchOperator;
|
|
143
147
|
moveMailboxId?: string;
|
|
@@ -152,15 +156,12 @@ export interface WizardDraftInput extends WizardCommitAnswers {
|
|
|
152
156
|
* instant once there is a draft to carry it.
|
|
153
157
|
*/
|
|
154
158
|
export const buildWizardDraft = ({
|
|
155
|
-
mode,
|
|
156
159
|
ruleScope,
|
|
157
|
-
anchorMessageId,
|
|
158
160
|
clauses,
|
|
159
161
|
matchOperator,
|
|
160
162
|
moveMailboxId,
|
|
161
163
|
until,
|
|
162
164
|
}: WizardDraftInput): OrganizeDraft => ({
|
|
163
|
-
...(mode === "similar" && anchorMessageId ? { anchorMessageId } : {}),
|
|
164
165
|
matchOperator: matchOperator === "all" ? "And" : "Or",
|
|
165
166
|
literalClauses: clauses.map((clause) => ({
|
|
166
167
|
field: clause.field,
|
|
@@ -193,8 +194,5 @@ export const buildCreateFilterInput = (
|
|
|
193
194
|
literalClauses: draft.literalClauses,
|
|
194
195
|
actionLabelId: draft.labelId ?? NO_ACTION,
|
|
195
196
|
actionMailboxId: draft.moveMailboxId ?? NO_ACTION,
|
|
196
|
-
...(draft.anchorMessageId
|
|
197
|
-
? { anchorMessageId: draft.anchorMessageId }
|
|
198
|
-
: {}),
|
|
199
197
|
};
|
|
200
198
|
};
|