@remit/web-client 0.0.275 → 0.0.276

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.275",
3
+ "version": "0.0.276",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -10,7 +10,7 @@ import type {
10
10
  import type { NavAccount, NavLinkComponent, NavMailboxRole } from "@remit/ui";
11
11
  import { NavSidebar } from "@remit/ui";
12
12
  import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
13
- import { useCallback, useMemo, useState } from "react";
13
+ import { useCallback, useMemo } from "react";
14
14
  import { useTranslation } from "react-i18next";
15
15
  import {
16
16
  buildMailboxRoleMap,
@@ -19,12 +19,7 @@ import {
19
19
  } from "@/lib/folder-roles";
20
20
  import { useMailContext } from "@/lib/mail-context";
21
21
  import { isOutboxListRow } from "@/lib/outbox-status";
22
- import {
23
- loadSavedSearches,
24
- removeSavedSearch,
25
- saveSearch,
26
- } from "@/lib/saved-searches";
27
- import { NavLink, useSearchEverything, useSelectedNavId } from "@/routing";
22
+ import { NavLink, useSelectedNavId } from "@/routing";
28
23
 
29
24
  interface MailSidebarAdapterProps {
30
25
  accounts: RemitImapAccountResponse[];
@@ -123,10 +118,8 @@ export function MailSidebarAdapter({
123
118
  variant = "desktop",
124
119
  }: MailSidebarAdapterProps) {
125
120
  const queryClient = useQueryClient();
126
- const searchEverything = useSearchEverything();
127
121
  const selectedNavId = useSelectedNavId();
128
- const { searchInput, onSearchChange } = useMailContext();
129
- const [savedSearches, setSavedSearches] = useState(loadSavedSearches);
122
+ const { onSearchChange } = useMailContext();
130
123
  const { t } = useTranslation("mail", { useSuspense: false });
131
124
  const translator: Translator = useCallback(
132
125
  (key, fallback) => t(key, { defaultValue: fallback }),
@@ -311,37 +304,6 @@ export function MailSidebarAdapter({
311
304
  onMailboxSelect?.();
312
305
  };
313
306
 
314
- // Saved searches (#428 follow-up, local-only MVP — see
315
- // doc/design/flows/06-search.md). A saved query is the raw typed text; the
316
- // existing token parser re-derives its chips on reselect, so there's nothing
317
- // else to persist.
318
- const trimmedSearchInput = searchInput.trim();
319
- const saveableQuery =
320
- trimmedSearchInput.length > 0 && !savedSearches.includes(trimmedSearchInput)
321
- ? trimmedSearchInput
322
- : undefined;
323
-
324
- const handleSaveCurrentSearch = useCallback(() => {
325
- if (!saveableQuery) return;
326
- setSavedSearches(saveSearch(saveableQuery));
327
- }, [saveableQuery]);
328
-
329
- const handleRemoveSavedSearch = useCallback((query: string) => {
330
- setSavedSearches(removeSavedSearch(query));
331
- }, []);
332
-
333
- // Running a saved search re-uses the daily brief as the search surface (the
334
- // cross-account default view) — the same destination the search field
335
- // itself lands results in once a query is active.
336
- const handleSelectSavedSearch = useCallback(
337
- (query: string) => {
338
- onSearchChange(query);
339
- searchEverything(query);
340
- onMailboxSelect?.();
341
- },
342
- [onSearchChange, searchEverything, onMailboxSelect],
343
- );
344
-
345
307
  return (
346
308
  <NavSidebar
347
309
  accounts={navAccounts}
@@ -350,11 +312,6 @@ export function MailSidebarAdapter({
350
312
  linkComponent={linkComponent}
351
313
  calendarNav="shown"
352
314
  variant={variant}
353
- savedSearches={savedSearches}
354
- saveableQuery={saveableQuery}
355
- onSaveCurrentSearch={handleSaveCurrentSearch}
356
- onSelectSavedSearch={handleSelectSavedSearch}
357
- onRemoveSavedSearch={handleRemoveSavedSearch}
358
315
  />
359
316
  );
360
317
  }
@@ -38,7 +38,7 @@ describe("searchInputForView", () => {
38
38
  );
39
39
  });
40
40
 
41
- it("seeds from the destination's own query (deep link, saved search)", () => {
41
+ it("seeds from the destination's own query (deep link)", () => {
42
42
  assert.equal(
43
43
  searchInputForView(
44
44
  "/mail/$mailboxId:inbox-1",
@@ -184,7 +184,7 @@ describe("an address that drifts from the settled query", () => {
184
184
  });
185
185
 
186
186
  it("does not fight a query arriving mid-debounce", async () => {
187
- // A deep link or a saved search lands while the field is still catching
187
+ // A deep link lands while the field is still catching
188
188
  // up. The committed query is the previous one, so the mirror has nothing
189
189
  // settled to write and must not strip what just arrived.
190
190
  const router = buildRouter(`${LIST_PATH}?q=receipts`);
@@ -5,10 +5,9 @@ import { useCallback } from "react";
5
5
  * Search everything, carrying the query.
6
6
  *
7
7
  * "Everything" is the daily brief — the cross-account view whose scope is
8
- * nothing — so dropping the scope chip and running a saved search are the same
9
- * movement, and both are a navigation rather than an edit of the query text:
10
- * the chip mirrors the route (see `lib/search-scope.ts`), so editing the text
11
- * would leave the chip up and the list still narrowed.
8
+ * nothing — so dropping the scope chip is a navigation rather than an edit of
9
+ * the query text: the chip mirrors the route (see `lib/search-scope.ts`), so
10
+ * editing the text would leave the chip up and the list still narrowed.
12
11
  */
13
12
  export function useSearchEverything(): (query: string) => void {
14
13
  const navigate = useNavigate();
@@ -1,80 +0,0 @@
1
- import assert from "node:assert";
2
- import { beforeEach, describe, test } from "node:test";
3
- import {
4
- isSearchSaved,
5
- loadSavedSearches,
6
- removeSavedSearch,
7
- saveSearch,
8
- } from "./saved-searches.js";
9
-
10
- function installMemoryStorage(): void {
11
- const store = new Map<string, string>();
12
- globalThis.localStorage = {
13
- getItem: (k: string) => store.get(k) ?? null,
14
- setItem: (k: string, v: string) => void store.set(k, v),
15
- removeItem: (k: string) => void store.delete(k),
16
- clear: () => store.clear(),
17
- key: () => null,
18
- length: 0,
19
- } as Storage;
20
- }
21
-
22
- describe("saved-searches", () => {
23
- beforeEach(installMemoryStorage);
24
-
25
- test("starts empty", () => {
26
- assert.deepEqual(loadSavedSearches(), []);
27
- });
28
-
29
- test("saves and loads a query", () => {
30
- assert.deepEqual(saveSearch("from:alice has:attachment"), [
31
- "from:alice has:attachment",
32
- ]);
33
- assert.deepEqual(loadSavedSearches(), ["from:alice has:attachment"]);
34
- });
35
-
36
- test("most recent save first, deduplicated", () => {
37
- saveSearch("alice");
38
- saveSearch("bob");
39
- assert.deepEqual(saveSearch("alice"), ["alice", "bob"]);
40
- });
41
-
42
- test("ignores blank queries", () => {
43
- saveSearch("alice");
44
- assert.deepEqual(saveSearch(" "), ["alice"]);
45
- });
46
-
47
- test("trims before storing", () => {
48
- assert.deepEqual(saveSearch(" bob "), ["bob"]);
49
- });
50
-
51
- test("survives corrupt storage", () => {
52
- globalThis.localStorage.setItem("remit.savedSearches", "{not json");
53
- assert.deepEqual(loadSavedSearches(), []);
54
- });
55
-
56
- test("removeSavedSearch drops the exact query", () => {
57
- saveSearch("alice");
58
- saveSearch("bob");
59
- assert.deepEqual(removeSavedSearch("alice"), ["bob"]);
60
- assert.deepEqual(loadSavedSearches(), ["bob"]);
61
- });
62
-
63
- test("removeSavedSearch is a no-op for a query that isn't saved", () => {
64
- saveSearch("alice");
65
- assert.deepEqual(removeSavedSearch("nonexistent"), ["alice"]);
66
- });
67
-
68
- test("isSearchSaved reflects the persisted set, trimmed", () => {
69
- saveSearch("alice");
70
- assert.equal(isSearchSaved("alice"), true);
71
- assert.equal(isSearchSaved(" alice "), true);
72
- assert.equal(isSearchSaved("bob"), false);
73
- });
74
-
75
- test("caps at MAX_SAVED entries", () => {
76
- for (let i = 0; i < 30; i++) saveSearch(`query-${i}`);
77
- assert.equal(loadSavedSearches().length, 25);
78
- assert.deepEqual(loadSavedSearches()[0], "query-29");
79
- });
80
- });
@@ -1,52 +0,0 @@
1
- /**
2
- * Saved searches for the mail search bar, persisted in localStorage (#428
3
- * follow-up, local-only MVP — see doc/design/flows/06-search.md; no backend
4
- * preference storage yet). Mirrors `recent-searches.ts`: a query already
5
- * carries its filter chips (`parseSearchTokens` re-derives them), so saving
6
- * the raw query text is enough to reproduce the full search. Best-effort:
7
- * storage failures (private mode, quota) degrade to an empty list rather than
8
- * crashing the search surface.
9
- */
10
- const SAVED_SEARCHES_KEY = "remit.savedSearches";
11
- const MAX_SAVED = 25;
12
-
13
- export function loadSavedSearches(): string[] {
14
- try {
15
- const raw = localStorage.getItem(SAVED_SEARCHES_KEY);
16
- if (!raw) return [];
17
- const parsed: unknown = JSON.parse(raw);
18
- if (!Array.isArray(parsed)) return [];
19
- return parsed.filter((q): q is string => typeof q === "string");
20
- } catch {
21
- return [];
22
- }
23
- }
24
-
25
- export function saveSearch(query: string): string[] {
26
- const trimmed = query.trim();
27
- if (!trimmed) return loadSavedSearches();
28
- const next = [
29
- trimmed,
30
- ...loadSavedSearches().filter((q) => q !== trimmed),
31
- ].slice(0, MAX_SAVED);
32
- try {
33
- localStorage.setItem(SAVED_SEARCHES_KEY, JSON.stringify(next));
34
- } catch {
35
- return next;
36
- }
37
- return next;
38
- }
39
-
40
- export function removeSavedSearch(query: string): string[] {
41
- const next = loadSavedSearches().filter((q) => q !== query);
42
- try {
43
- localStorage.setItem(SAVED_SEARCHES_KEY, JSON.stringify(next));
44
- } catch {
45
- return next;
46
- }
47
- return next;
48
- }
49
-
50
- export function isSearchSaved(query: string): boolean {
51
- return loadSavedSearches().includes(query.trim());
52
- }