@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,81 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import { APP_PASSWORD_URLS, discoverSettings } from "./autodiscovery.js";
|
|
4
|
-
import {
|
|
5
|
-
getPresetById,
|
|
6
|
-
PROVIDER_PRESETS,
|
|
7
|
-
type ProviderPreset,
|
|
8
|
-
presetIdForEmail,
|
|
9
|
-
} from "./provider-presets.js";
|
|
10
|
-
|
|
11
|
-
interface ExpectedSettings {
|
|
12
|
-
imap: { host: string; port: number; security: string };
|
|
13
|
-
smtp: { host: string; port: number; security: string };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const EXPECTED: Record<string, ExpectedSettings> = {
|
|
17
|
-
icloud: {
|
|
18
|
-
imap: { host: "imap.mail.me.com", port: 993, security: "tls" },
|
|
19
|
-
smtp: { host: "smtp.mail.me.com", port: 587, security: "starttls" },
|
|
20
|
-
},
|
|
21
|
-
yahoo: {
|
|
22
|
-
imap: { host: "imap.mail.yahoo.com", port: 993, security: "tls" },
|
|
23
|
-
smtp: { host: "smtp.mail.yahoo.com", port: 465, security: "tls" },
|
|
24
|
-
},
|
|
25
|
-
aol: {
|
|
26
|
-
imap: { host: "imap.aol.com", port: 993, security: "tls" },
|
|
27
|
-
smtp: { host: "smtp.aol.com", port: 465, security: "tls" },
|
|
28
|
-
},
|
|
29
|
-
fastmail: {
|
|
30
|
-
imap: { host: "imap.fastmail.com", port: 993, security: "tls" },
|
|
31
|
-
smtp: { host: "smtp.fastmail.com", port: 465, security: "tls" },
|
|
32
|
-
},
|
|
33
|
-
gmail: {
|
|
34
|
-
imap: { host: "imap.gmail.com", port: 993, security: "tls" },
|
|
35
|
-
smtp: { host: "smtp.gmail.com", port: 587, security: "starttls" },
|
|
36
|
-
},
|
|
37
|
-
};
|
|
4
|
+
import { getPresetById, presetIdForEmail } from "./provider-presets.js";
|
|
38
5
|
|
|
39
6
|
describe("provider presets", () => {
|
|
40
|
-
it("exposes exactly the five documented providers", () => {
|
|
41
|
-
const ids = PROVIDER_PRESETS.map((p) => p.id).sort();
|
|
42
|
-
assert.deepEqual(ids, ["aol", "fastmail", "gmail", "icloud", "yahoo"]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
for (const preset of PROVIDER_PRESETS) {
|
|
46
|
-
describe(preset.label, () => {
|
|
47
|
-
const expected = EXPECTED[preset.id];
|
|
48
|
-
|
|
49
|
-
it("matches the exact IMAP settings from the table", () => {
|
|
50
|
-
assert.deepEqual(preset.imap, expected.imap);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("matches the exact SMTP settings from the table", () => {
|
|
54
|
-
assert.deepEqual(preset.smtp, expected.smtp);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("uses the full email as username", () => {
|
|
58
|
-
assert.equal(preset.username, "full-email");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("has password help text", () => {
|
|
62
|
-
assert.ok(preset.passwordHelp.text.length > 0);
|
|
63
|
-
assert.match(preset.passwordHelp.text, /app[\w-]* password/i);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("has a valid https app-password url", () => {
|
|
67
|
-
const url = new URL(preset.passwordHelp.url);
|
|
68
|
-
assert.equal(url.protocol, "https:");
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
it("looks up a preset by id", () => {
|
|
74
|
-
const preset: ProviderPreset | undefined = getPresetById("icloud");
|
|
75
|
-
assert.ok(preset);
|
|
76
|
-
assert.equal(preset.label, "iCloud");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
7
|
it("tells a Gmail user to turn on 2-Step Verification before creating an app password", () => {
|
|
80
8
|
const gmail = getPresetById("gmail");
|
|
81
9
|
assert.ok(gmail);
|
|
@@ -88,10 +16,6 @@ describe("provider presets", () => {
|
|
|
88
16
|
assert.equal(url, APP_PASSWORD_URLS["googlemail.com"]);
|
|
89
17
|
});
|
|
90
18
|
|
|
91
|
-
it("returns undefined for an unknown id", () => {
|
|
92
|
-
assert.equal(getPresetById("custom"), undefined);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
19
|
describe("presetIdForEmail", () => {
|
|
96
20
|
it("pre-selects a preset for a known provider domain", () => {
|
|
97
21
|
assert.equal(presetIdForEmail("alice@icloud.com"), "icloud");
|
|
@@ -3,7 +3,6 @@ import { afterEach, describe, it } from "node:test";
|
|
|
3
3
|
import type { Mutation, Query } from "@tanstack/react-query";
|
|
4
4
|
import { ApiError } from "./api";
|
|
5
5
|
import { __resetFatalError, subscribeFatalError } from "./fatal-error";
|
|
6
|
-
import { NetworkError } from "./network-error";
|
|
7
6
|
import {
|
|
8
7
|
handleMutationCacheError,
|
|
9
8
|
handleQueryCacheError,
|
|
@@ -41,25 +40,6 @@ afterEach(() => {
|
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
describe("handleQueryCacheError (fail-fast contract #1059)", () => {
|
|
44
|
-
it("escalates a 5xx that breaks the INITIAL load", () => {
|
|
45
|
-
const seen = escalations();
|
|
46
|
-
handleQueryCacheError(
|
|
47
|
-
new ApiError("internal", 500),
|
|
48
|
-
fakeQuery(undefined, 0),
|
|
49
|
-
);
|
|
50
|
-
assert.deepEqual(seen, ["internal"]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("escalates a 5xx on a background REFETCH (the deleted dataUpdatedAt guard — the headline bug)", () => {
|
|
54
|
-
const seen = escalations();
|
|
55
|
-
// dataUpdatedAt !== 0 used to swallow this (PR #758). It must now escalate.
|
|
56
|
-
handleQueryCacheError(
|
|
57
|
-
new ApiError("internal", 500),
|
|
58
|
-
fakeQuery(undefined, 123456),
|
|
59
|
-
);
|
|
60
|
-
assert.deepEqual(seen, ["internal"]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
43
|
it("escalates a 5xx EVEN when the query is marked meta.softError (rule 2 wins)", () => {
|
|
64
44
|
const seen = escalations();
|
|
65
45
|
handleQueryCacheError(
|
|
@@ -69,24 +49,6 @@ describe("handleQueryCacheError (fail-fast contract #1059)", () => {
|
|
|
69
49
|
assert.deepEqual(seen, ["internal"]);
|
|
70
50
|
});
|
|
71
51
|
|
|
72
|
-
it("escalates a 4xx by DEFAULT (no opt-out)", () => {
|
|
73
|
-
const seen = escalations();
|
|
74
|
-
handleQueryCacheError(new ApiError("not found", 404), fakeQuery());
|
|
75
|
-
assert.deepEqual(seen, ["not found"]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("does NOT escalate a 4xx the query opted out of via meta.softError", () => {
|
|
79
|
-
let escalated = false;
|
|
80
|
-
subscribeFatalError(() => {
|
|
81
|
-
escalated = true;
|
|
82
|
-
});
|
|
83
|
-
handleQueryCacheError(
|
|
84
|
-
new ApiError("not found", 404),
|
|
85
|
-
fakeQuery({ softError: true }),
|
|
86
|
-
);
|
|
87
|
-
assert.equal(escalated, false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
52
|
it("does NOT escalate an aborted request", () => {
|
|
91
53
|
let escalated = false;
|
|
92
54
|
subscribeFatalError(() => {
|
|
@@ -95,32 +57,9 @@ describe("handleQueryCacheError (fail-fast contract #1059)", () => {
|
|
|
95
57
|
handleQueryCacheError(abortError(), fakeQuery());
|
|
96
58
|
assert.equal(escalated, false);
|
|
97
59
|
});
|
|
98
|
-
|
|
99
|
-
it("does NOT escalate a network blip tagged at the fetch boundary", () => {
|
|
100
|
-
let escalated = false;
|
|
101
|
-
subscribeFatalError(() => {
|
|
102
|
-
escalated = true;
|
|
103
|
-
});
|
|
104
|
-
handleQueryCacheError(
|
|
105
|
-
new NetworkError(new TypeError("Failed to fetch")),
|
|
106
|
-
fakeQuery(),
|
|
107
|
-
);
|
|
108
|
-
assert.equal(escalated, false);
|
|
109
|
-
});
|
|
110
60
|
});
|
|
111
61
|
|
|
112
62
|
describe("handleMutationCacheError (fail-fast contract #1059)", () => {
|
|
113
|
-
it("escalates a mutation 5xx", () => {
|
|
114
|
-
const seen = escalations();
|
|
115
|
-
handleMutationCacheError(
|
|
116
|
-
new ApiError("mutation blew up", 503),
|
|
117
|
-
undefined,
|
|
118
|
-
undefined,
|
|
119
|
-
fakeMutation(),
|
|
120
|
-
);
|
|
121
|
-
assert.deepEqual(seen, ["mutation blew up"]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
63
|
it("escalates a mutation 4xx by DEFAULT", () => {
|
|
125
64
|
const seen = escalations();
|
|
126
65
|
handleMutationCacheError(
|
|
@@ -46,16 +46,16 @@ describe("global query/mutation escalation", () => {
|
|
|
46
46
|
assert.deepEqual(seen, ["internal server error"]);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it("a 5xx on a background REFETCH escalates — the exact #1059 bug (
|
|
49
|
+
it("a 5xx on a background REFETCH escalates — the exact #1059 bug (search-style query that 500s after first rendering data)", async () => {
|
|
50
50
|
const seen: string[] = [];
|
|
51
51
|
subscribeFatalError((fatal) => seen.push(fatal.message));
|
|
52
52
|
const client = makeClient();
|
|
53
|
-
const queryKey = ["
|
|
53
|
+
const queryKey = ["search"];
|
|
54
54
|
let calls = 0;
|
|
55
55
|
const queryFn = async () => {
|
|
56
56
|
calls += 1;
|
|
57
57
|
if (calls === 1) return { items: [] as string[] };
|
|
58
|
-
throw new ApiError("
|
|
58
|
+
throw new ApiError("search exploded", 500);
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// First load succeeds → query now has data (dataUpdatedAt !== 0). Under the
|
|
@@ -63,7 +63,7 @@ describe("global query/mutation escalation", () => {
|
|
|
63
63
|
await client.fetchQuery({ queryKey, queryFn });
|
|
64
64
|
await client.refetchQueries({ queryKey }).catch(() => {});
|
|
65
65
|
|
|
66
|
-
assert.deepEqual(seen, ["
|
|
66
|
+
assert.deepEqual(seen, ["search exploded"]);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it("a first-party 5xx from a mutation escalates to the fatal overlay seam", async () => {
|
package/src/lib/result-count.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResultCount } from "@remit/ui";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Shortest free text a count is asked for.
|
|
4
|
+
* Shortest free text a count is asked for. The search migration set
|
|
5
5
|
* documents that a query under three characters falls out of the trigram index
|
|
6
6
|
* onto a folded `LIKE` scan, so counting one is a mailbox scan per typed
|
|
7
7
|
* character. The list itself still pages — only the count waits.
|
package/src/lib/result-folder.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* says where it came from and spam is held out of the list. Both questions are
|
|
6
6
|
* answered by joining the row's mailbox ids against the mailbox lists the
|
|
7
7
|
* sidebar already loads: the id → folder map here is the only thing either
|
|
8
|
-
* needs, so
|
|
8
|
+
* needs, so the search API does not have to carry a folder.
|
|
9
9
|
*
|
|
10
10
|
* The mailbox row carries the hierarchy delimiter its own server reports, and it
|
|
11
11
|
* is kept alongside the path: a chip labelled by path shows the folder's own
|
|
@@ -52,36 +52,12 @@ describe("createRumTelemetry", () => {
|
|
|
52
52
|
rum = makeRum();
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it("recordPageView delegates to the RUM client", () => {
|
|
56
|
-
const t = createRumTelemetry(rum);
|
|
57
|
-
t.recordPageView("/inbox");
|
|
58
|
-
assert.deepEqual(stubCalls(rum).recordPageView, ["/inbox"]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
55
|
it("recordPageView strips the entire query string", () => {
|
|
62
56
|
const t = createRumTelemetry(rum);
|
|
63
57
|
t.recordPageView("/callback?token=secret&next=/inbox");
|
|
64
58
|
assert.equal(stubCalls(rum).recordPageView[0], "/callback");
|
|
65
59
|
});
|
|
66
60
|
|
|
67
|
-
it("recordPageView strips selectedMessageId query param", () => {
|
|
68
|
-
const t = createRumTelemetry(rum);
|
|
69
|
-
t.recordPageView("/inbox?selectedMessageId=msg_abc123def456");
|
|
70
|
-
assert.equal(stubCalls(rum).recordPageView[0], "/inbox");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("recordPageView strips search query param", () => {
|
|
74
|
-
const t = createRumTelemetry(rum);
|
|
75
|
-
t.recordPageView("/inbox?q=sensitive+search+text");
|
|
76
|
-
assert.equal(stubCalls(rum).recordPageView[0], "/inbox");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("recordPageView strips query string and fragment together", () => {
|
|
80
|
-
const t = createRumTelemetry(rum);
|
|
81
|
-
t.recordPageView("/inbox?q=foo#section");
|
|
82
|
-
assert.equal(stubCalls(rum).recordPageView[0], "/inbox");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
61
|
it("recordPageView strips fragment-only paths", () => {
|
|
86
62
|
const t = createRumTelemetry(rum);
|
|
87
63
|
t.recordPageView("/inbox#fragment");
|
|
@@ -97,13 +73,6 @@ describe("createRumTelemetry", () => {
|
|
|
97
73
|
);
|
|
98
74
|
});
|
|
99
75
|
|
|
100
|
-
it("recordError delegates to the RUM client", () => {
|
|
101
|
-
const t = createRumTelemetry(rum);
|
|
102
|
-
const error = new Error("boom");
|
|
103
|
-
t.recordError(error);
|
|
104
|
-
assert.deepEqual(stubCalls(rum).recordError, [error]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
76
|
it("recordError scrubs message id tokens from error messages", () => {
|
|
108
77
|
const t = createRumTelemetry(rum);
|
|
109
78
|
const error = new Error("failed to load /messages/msg_abc123def456");
|
|
@@ -122,54 +91,6 @@ describe("createRumTelemetry", () => {
|
|
|
122
91
|
const recorded = stubCalls(rum).recordError[0] as Error;
|
|
123
92
|
assert.equal(recorded.name, "TypeError");
|
|
124
93
|
});
|
|
125
|
-
|
|
126
|
-
it("recordError with context emits an additional event", () => {
|
|
127
|
-
const t = createRumTelemetry(rum);
|
|
128
|
-
t.recordError(new Error("boom"), { component: "Inbox" });
|
|
129
|
-
const ctx = stubCalls(rum).recordEvent.find(
|
|
130
|
-
(e) => e.name === "telemetry.error_context",
|
|
131
|
-
);
|
|
132
|
-
assert.ok(ctx, "Expected a telemetry.error_context event");
|
|
133
|
-
assert.deepEqual(ctx.attributes, { component: "Inbox" });
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("recordEvent delegates to the RUM client", () => {
|
|
137
|
-
const t = createRumTelemetry(rum);
|
|
138
|
-
t.recordEvent("thread.open", { mailboxId: "inbox" });
|
|
139
|
-
assert.ok(
|
|
140
|
-
stubCalls(rum).recordEvent.some((e) => e.name === "thread.open"),
|
|
141
|
-
"Expected thread.open event",
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("recordEvent with no attributes passes an empty object", () => {
|
|
146
|
-
const t = createRumTelemetry(rum);
|
|
147
|
-
t.recordEvent("app.load");
|
|
148
|
-
const ev = stubCalls(rum).recordEvent.find((e) => e.name === "app.load");
|
|
149
|
-
assert.ok(ev, "Expected app.load event");
|
|
150
|
-
assert.deepEqual(ev.attributes, {});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("recordTiming emits a telemetry.timing event with name and duration", () => {
|
|
154
|
-
const t = createRumTelemetry(rum);
|
|
155
|
-
t.recordTiming("api.latency", 123);
|
|
156
|
-
const ev = stubCalls(rum).recordEvent.find(
|
|
157
|
-
(e) => e.name === "telemetry.timing",
|
|
158
|
-
);
|
|
159
|
-
assert.ok(ev, "Expected telemetry.timing event");
|
|
160
|
-
assert.equal((ev.attributes as Record<string, string>).name, "api.latency");
|
|
161
|
-
assert.equal((ev.attributes as Record<string, string>).durationMs, "123");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("recordTiming merges extra attributes", () => {
|
|
165
|
-
const t = createRumTelemetry(rum);
|
|
166
|
-
t.recordTiming("api.latency", 50, { route: "/inbox" });
|
|
167
|
-
const ev = stubCalls(rum).recordEvent.find(
|
|
168
|
-
(e) => e.name === "telemetry.timing",
|
|
169
|
-
);
|
|
170
|
-
assert.ok(ev, "Expected telemetry.timing event");
|
|
171
|
-
assert.equal((ev.attributes as Record<string, string>).route, "/inbox");
|
|
172
|
-
});
|
|
173
94
|
});
|
|
174
95
|
|
|
175
96
|
type WindowListener = (event: unknown) => void;
|
|
@@ -202,31 +123,6 @@ describe("initRum", () => {
|
|
|
202
123
|
delete (globalThis as unknown as { window?: unknown }).window;
|
|
203
124
|
});
|
|
204
125
|
|
|
205
|
-
it("returns a no-op telemetry when VITE_RUM_APP_MONITOR_ID is unset", () => {
|
|
206
|
-
globalThis.__REMIT_CONFIG__ = {};
|
|
207
|
-
const t = initRum();
|
|
208
|
-
assert.doesNotThrow(() => t.recordPageView("/"));
|
|
209
|
-
assert.doesNotThrow(() => t.recordError(new Error("x")));
|
|
210
|
-
assert.doesNotThrow(() => t.recordEvent("e"));
|
|
211
|
-
assert.doesNotThrow(() => t.recordTiming("t", 1));
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("creates a RumTelemetry when VITE_RUM_APP_MONITOR_ID is set", () => {
|
|
215
|
-
resetInstances();
|
|
216
|
-
installWindowStub();
|
|
217
|
-
globalThis.__REMIT_CONFIG__ = {
|
|
218
|
-
rum: {
|
|
219
|
-
appMonitorId: "test-monitor-id",
|
|
220
|
-
identityPoolId: "eu-west-1:test-pool",
|
|
221
|
-
region: "eu-west-1",
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
const t = initRum();
|
|
225
|
-
const instance = getLastInstance();
|
|
226
|
-
assert.equal(instance.appMonitorId, "test-monitor-id");
|
|
227
|
-
assert.doesNotThrow(() => t.recordPageView("/inbox"));
|
|
228
|
-
});
|
|
229
|
-
|
|
230
126
|
it("does not enable leaky auto-telemetries", () => {
|
|
231
127
|
resetInstances();
|
|
232
128
|
installWindowStub();
|
|
@@ -7,13 +7,6 @@ describe("normalizeSearchQuery", () => {
|
|
|
7
7
|
assert.equal(normalizeSearchQuery("ALICE"), "alice");
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
test("lowercases a mixed-case query", () => {
|
|
11
|
-
assert.equal(
|
|
12
|
-
normalizeSearchQuery("Alice@Example.COM"),
|
|
13
|
-
"alice@example.com",
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
10
|
test("trims surrounding whitespace", () => {
|
|
18
11
|
assert.equal(normalizeSearchQuery(" Bob "), "bob");
|
|
19
12
|
});
|
|
@@ -22,10 +15,6 @@ describe("normalizeSearchQuery", () => {
|
|
|
22
15
|
assert.equal(normalizeSearchQuery(" "), "");
|
|
23
16
|
});
|
|
24
17
|
|
|
25
|
-
test("leaves an already-lowercase query unchanged", () => {
|
|
26
|
-
assert.equal(normalizeSearchQuery("invoice 2026"), "invoice 2026");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
18
|
test("handles non-ASCII letters via locale-aware lowercasing", () => {
|
|
30
19
|
// `toLocaleLowerCase` folds these where `String.prototype.toLowerCase`
|
|
31
20
|
// would also work for Latin-1, but we exercise it to lock the contract.
|
|
@@ -1,102 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
RemitImapThreadMessageResponse,
|
|
6
|
-
} from "@remit/api-http-client/types.gen.ts";
|
|
7
|
-
import {
|
|
8
|
-
matchedChunkLabel,
|
|
9
|
-
relatedSearchResults,
|
|
10
|
-
semanticToSearchResult,
|
|
11
|
-
threadToSearchResult,
|
|
12
|
-
} from "./search-result.js";
|
|
13
|
-
|
|
14
|
-
function hit(
|
|
15
|
-
overrides: Partial<RemitImapSemanticSearchResult> &
|
|
16
|
-
Pick<RemitImapSemanticSearchResult, "messageId" | "threadId" | "score">,
|
|
17
|
-
): RemitImapSemanticSearchResult {
|
|
18
|
-
return {
|
|
19
|
-
matchedChunkType: "body",
|
|
20
|
-
mailboxIds: [],
|
|
21
|
-
...overrides,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("semanticToSearchResult", () => {
|
|
26
|
-
test("maps display fields and converts epoch seconds to ms", () => {
|
|
27
|
-
const result = semanticToSearchResult(
|
|
28
|
-
hit({
|
|
29
|
-
messageId: "m1",
|
|
30
|
-
threadId: "t1",
|
|
31
|
-
score: 0.9,
|
|
32
|
-
fromName: "Stripe",
|
|
33
|
-
subject: "Invoice ready",
|
|
34
|
-
sentDate: 1_700_000_000,
|
|
35
|
-
}),
|
|
36
|
-
);
|
|
37
|
-
assert.strictEqual(result.id, "m1");
|
|
38
|
-
assert.strictEqual(result.sender, "Stripe");
|
|
39
|
-
assert.strictEqual(result.subject, "Invoice ready");
|
|
40
|
-
assert.notStrictEqual(result.date, "");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("falls back when denormalized display fields are absent", () => {
|
|
44
|
-
const result = semanticToSearchResult(
|
|
45
|
-
hit({ messageId: "m2", threadId: "t2", score: 0.5 }),
|
|
46
|
-
);
|
|
47
|
-
assert.strictEqual(result.sender, "Unknown");
|
|
48
|
-
assert.strictEqual(result.subject, "(No subject)");
|
|
49
|
-
assert.strictEqual(result.date, "");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// The bug: without thread + mailbox the brief could only look the hit up by
|
|
53
|
-
// messageId in its loaded list, where a related hit isn't found — so it must
|
|
54
|
-
// carry both so the conversation opens directly.
|
|
55
|
-
test("carries the thread and first mailbox so the hit opens directly", () => {
|
|
56
|
-
const result = semanticToSearchResult(
|
|
57
|
-
hit({
|
|
58
|
-
messageId: "m3",
|
|
59
|
-
threadId: "t3",
|
|
60
|
-
score: 0.7,
|
|
61
|
-
mailboxIds: ["mb-inbox", "mb-other"],
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
assert.strictEqual(result.threadId, "t3");
|
|
65
|
-
assert.strictEqual(result.mailboxId, "mb-inbox");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("omits mailboxId when the hit carries no mailbox", () => {
|
|
69
|
-
const result = semanticToSearchResult(
|
|
70
|
-
hit({ messageId: "m4", threadId: "t4", score: 0.3, mailboxIds: [] }),
|
|
71
|
-
);
|
|
72
|
-
assert.strictEqual(result.threadId, "t4");
|
|
73
|
-
assert.strictEqual(result.mailboxId, undefined);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("carries the matched-chunk label and score so the row can show why it matched", () => {
|
|
77
|
-
const result = semanticToSearchResult(
|
|
78
|
-
hit({
|
|
79
|
-
messageId: "m5",
|
|
80
|
-
threadId: "t5",
|
|
81
|
-
score: 0.42,
|
|
82
|
-
matchedChunkType: "attachment",
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
assert.strictEqual(result.matchedChunkLabel, "attachment");
|
|
86
|
-
assert.strictEqual(result.score, 0.42);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe("matchedChunkLabel", () => {
|
|
91
|
-
test("labels every known chunk type", () => {
|
|
92
|
-
assert.strictEqual(matchedChunkLabel("body"), "body");
|
|
93
|
-
assert.strictEqual(matchedChunkLabel("subject"), "subject");
|
|
94
|
-
assert.strictEqual(matchedChunkLabel("sender"), "sender");
|
|
95
|
-
assert.strictEqual(matchedChunkLabel("recipient"), "recipient");
|
|
96
|
-
assert.strictEqual(matchedChunkLabel("attachment"), "attachment");
|
|
97
|
-
assert.strictEqual(matchedChunkLabel("entities"), "entities");
|
|
98
|
-
});
|
|
99
|
-
});
|
|
3
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
+
import { threadToSearchResult } from "./search-result.js";
|
|
100
5
|
|
|
101
6
|
describe("threadToSearchResult", () => {
|
|
102
7
|
test("carries thread and mailbox so a literal hit opens directly", () => {
|
|
@@ -113,48 +18,3 @@ describe("threadToSearchResult", () => {
|
|
|
113
18
|
assert.strictEqual(result.mailboxId, "mb1");
|
|
114
19
|
});
|
|
115
20
|
});
|
|
116
|
-
|
|
117
|
-
describe("relatedSearchResults", () => {
|
|
118
|
-
test("orders by score descending", () => {
|
|
119
|
-
const results = relatedSearchResults(
|
|
120
|
-
[
|
|
121
|
-
hit({ messageId: "m1", threadId: "t1", score: 0.2 }),
|
|
122
|
-
hit({ messageId: "m2", threadId: "t2", score: 0.9 }),
|
|
123
|
-
hit({ messageId: "m3", threadId: "t3", score: 0.5 }),
|
|
124
|
-
],
|
|
125
|
-
[],
|
|
126
|
-
);
|
|
127
|
-
assert.deepStrictEqual(
|
|
128
|
-
results.map((r) => r.id),
|
|
129
|
-
["m2", "m3", "m1"],
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("excludes threads already shown under Top matches", () => {
|
|
134
|
-
const results = relatedSearchResults(
|
|
135
|
-
[
|
|
136
|
-
hit({ messageId: "m1", threadId: "shared", score: 0.9 }),
|
|
137
|
-
hit({ messageId: "m2", threadId: "t2", score: 0.5 }),
|
|
138
|
-
],
|
|
139
|
-
["shared"],
|
|
140
|
-
);
|
|
141
|
-
assert.deepStrictEqual(
|
|
142
|
-
results.map((r) => r.id),
|
|
143
|
-
["m2"],
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("collapses multiple hits in the same thread to one row", () => {
|
|
148
|
-
const results = relatedSearchResults(
|
|
149
|
-
[
|
|
150
|
-
hit({ messageId: "m1", threadId: "t1", score: 0.9 }),
|
|
151
|
-
hit({ messageId: "m2", threadId: "t1", score: 0.4 }),
|
|
152
|
-
],
|
|
153
|
-
[],
|
|
154
|
-
);
|
|
155
|
-
assert.deepStrictEqual(
|
|
156
|
-
results.map((r) => r.id),
|
|
157
|
-
["m1"],
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
});
|
package/src/lib/search-result.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Map the row shapes the app carries onto the kit `SearchResult` the search
|
|
3
|
-
* results render
|
|
4
|
-
*
|
|
5
|
-
* third (`RemitImapSemanticSearchResult`) feeds the semantic "Related" section.
|
|
3
|
+
* results render: the per-mailbox `RemitImapThreadMessageResponse` and the
|
|
4
|
+
* brief/flagged `ThreadRowData`.
|
|
6
5
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
RemitImapSemanticSearchChunkType,
|
|
9
|
-
RemitImapSemanticSearchResult,
|
|
10
|
-
RemitImapThreadMessageResponse,
|
|
11
|
-
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
12
7
|
import type { SearchResult, ThreadRowData } from "@remit/ui";
|
|
13
8
|
import { formatEmailDate } from "./format.js";
|
|
14
9
|
import {
|
|
@@ -16,26 +11,6 @@ import {
|
|
|
16
11
|
resolveResultFolder,
|
|
17
12
|
} from "./result-folder.js";
|
|
18
13
|
|
|
19
|
-
/**
|
|
20
|
-
* Plain-language label for the `matched: …` chip, so the user understands why
|
|
21
|
-
* a "Related" hit matched (#428). Falls back to the raw chunk type for any
|
|
22
|
-
* value the backend adds later that the client doesn't know about yet.
|
|
23
|
-
*/
|
|
24
|
-
const MATCHED_CHUNK_LABELS: Record<RemitImapSemanticSearchChunkType, string> = {
|
|
25
|
-
sender: "sender",
|
|
26
|
-
recipient: "recipient",
|
|
27
|
-
subject: "subject",
|
|
28
|
-
attachment: "attachment",
|
|
29
|
-
body: "body",
|
|
30
|
-
entities: "entities",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function matchedChunkLabel(
|
|
34
|
-
chunkType: RemitImapSemanticSearchChunkType,
|
|
35
|
-
): string {
|
|
36
|
-
return MATCHED_CHUNK_LABELS[chunkType] ?? chunkType;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
14
|
export function threadToSearchResult(
|
|
40
15
|
thread: RemitImapThreadMessageResponse,
|
|
41
16
|
folders?: ResultFolderIndex,
|
|
@@ -78,48 +53,3 @@ export function rowToSearchResult(
|
|
|
78
53
|
...(folder ? { folder } : {}),
|
|
79
54
|
};
|
|
80
55
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Map a semantic hit onto a `SearchResult`. Display fields are denormalized at
|
|
84
|
-
* index time and absent for older entries, so each falls back. Read/star state
|
|
85
|
-
* isn't carried by the semantic index, so the row renders neutral. `sentDate` is
|
|
86
|
-
* epoch seconds; `formatEmailDate` takes epoch ms.
|
|
87
|
-
*/
|
|
88
|
-
export function semanticToSearchResult(
|
|
89
|
-
hit: RemitImapSemanticSearchResult,
|
|
90
|
-
folders?: ResultFolderIndex,
|
|
91
|
-
): SearchResult {
|
|
92
|
-
const { mailboxId, folder } = resolveResultFolder(folders, hit.mailboxIds);
|
|
93
|
-
return {
|
|
94
|
-
id: hit.messageId,
|
|
95
|
-
sender: hit.fromName ?? "Unknown",
|
|
96
|
-
subject: hit.subject ?? "(No subject)",
|
|
97
|
-
snippet: "",
|
|
98
|
-
date: hit.sentDate != null ? formatEmailDate(hit.sentDate * 1000) : "",
|
|
99
|
-
threadId: hit.threadId,
|
|
100
|
-
matchedChunkLabel: matchedChunkLabel(hit.matchedChunkType),
|
|
101
|
-
score: hit.score,
|
|
102
|
-
...(mailboxId ? { mailboxId } : {}),
|
|
103
|
-
...(folder ? { folder } : {}),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Build the semantic "Related" section: order by score (most relevant first),
|
|
109
|
-
* collapse to one row per thread, and drop any thread already shown under the
|
|
110
|
-
* literal "Top matches" — literal precedence, so a thread never appears twice.
|
|
111
|
-
*/
|
|
112
|
-
export function relatedSearchResults(
|
|
113
|
-
hits: RemitImapSemanticSearchResult[],
|
|
114
|
-
literalThreadIds: Iterable<string>,
|
|
115
|
-
folders?: ResultFolderIndex,
|
|
116
|
-
): SearchResult[] {
|
|
117
|
-
const seenThreadIds = new Set<string>(literalThreadIds);
|
|
118
|
-
const results: SearchResult[] = [];
|
|
119
|
-
for (const hit of [...hits].sort((a, b) => b.score - a.score)) {
|
|
120
|
-
if (seenThreadIds.has(hit.threadId)) continue;
|
|
121
|
-
seenThreadIds.add(hit.threadId);
|
|
122
|
-
results.push(semanticToSearchResult(hit, folders));
|
|
123
|
-
}
|
|
124
|
-
return results;
|
|
125
|
-
}
|