@paubox/ui 2.0.0 → 3.0.1
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/index.esm.js +53 -29
- package/package.json +1 -1
- package/src/lib/SearchOmnibox/SearchOmnibox.d.ts +43 -10
package/index.esm.js
CHANGED
|
@@ -30939,12 +30939,12 @@ function _templateObject9() {
|
|
|
30939
30939
|
}
|
|
30940
30940
|
function _templateObject10() {
|
|
30941
30941
|
var data = _tagged_template_literal$5([
|
|
30942
|
-
"\n
|
|
30943
|
-
";\n ",
|
|
30942
|
+
"\n ",
|
|
30944
30943
|
"\n color: ",
|
|
30945
|
-
";\n html.dark & {\n
|
|
30946
|
-
";\n
|
|
30947
|
-
";\n
|
|
30944
|
+
";\n html.dark & {\n color: ",
|
|
30945
|
+
";\n }\n /* Divider above the footer only when there are suggestion rows above\n it. When the footer is the only item in the dropdown (fetchSuggestions\n false, no candidates, non-empty query), the static border-top would\n bleed past the rounded corners of the dropdown container as a stray\n line at the top of the menu. */\n &:not(:first-child) {\n border-top: 1px solid ",
|
|
30946
|
+
";\n html.dark & {\n border-top-color: ",
|
|
30947
|
+
";\n }\n }\n"
|
|
30948
30948
|
]);
|
|
30949
30949
|
_templateObject10 = function _templateObject() {
|
|
30950
30950
|
return data;
|
|
@@ -30992,7 +30992,7 @@ var DropdownRow = styled.button(_templateObject6$1(), spacing(1), spacing(1), sp
|
|
|
30992
30992
|
var RowIcon = styled.span(_templateObject7$1(), neutral500$1);
|
|
30993
30993
|
var RowText = styled.span(_templateObject8$1());
|
|
30994
30994
|
var RowMeta = styled.span(_templateObject9(), $paragraph300Regular, neutral600$1, neutral300$1);
|
|
30995
|
-
var FooterRow = styled(DropdownRow)(_templateObject10(),
|
|
30995
|
+
var FooterRow = styled(DropdownRow)(_templateObject10(), $paragraph200Regular, neutral700$1, neutral300$1, neutral200$1, surfaceRaised);
|
|
30996
30996
|
var PressEnter = styled.span(_templateObject11(), $paragraph300Regular, neutral500$1);
|
|
30997
30997
|
var DeleteHandle = styled.span(_templateObject12());
|
|
30998
30998
|
// ─── Inline icons (kept local to avoid bloating @icons) ─────────────────────
|
|
@@ -31069,11 +31069,20 @@ var formatDate = function(iso) {
|
|
|
31069
31069
|
// with non-memoized callbacks from the parent) drove an infinite render
|
|
31070
31070
|
// loop on pages that don't pass `localCandidates`.
|
|
31071
31071
|
var EMPTY_CANDIDATES = [];
|
|
31072
|
+
// Default for the optional `decodeHeader` prop — pass-through.
|
|
31073
|
+
var identityDecode = function(s) {
|
|
31074
|
+
return s;
|
|
31075
|
+
};
|
|
31072
31076
|
// ─── Component ──────────────────────────────────────────────────────────────
|
|
31073
31077
|
var SearchOmnibox = function(param) {
|
|
31074
31078
|
var bar = param.bar, _param_placeholder = param.placeholder, placeholder = _param_placeholder === void 0 ? 'Search' : _param_placeholder, _param_pbSearchBaseUrl = param.pbSearchBaseUrl, pbSearchBaseUrl = _param_pbSearchBaseUrl === void 0 ? '' : _param_pbSearchBaseUrl, _param_getAuthToken = param.getAuthToken, getAuthToken = _param_getAuthToken === void 0 ? function() {
|
|
31075
31079
|
return '';
|
|
31076
|
-
} : _param_getAuthToken,
|
|
31080
|
+
} : _param_getAuthToken, _param_fetchSuggestions = param.fetchSuggestions, fetchSuggestions = _param_fetchSuggestions === void 0 ? true : _param_fetchSuggestions, _param_withHistory = param.withHistory, withHistory = _param_withHistory === void 0 ? true : _param_withHistory, onResultClick = param.onResultClick, onCommit = param.onCommit, onLiveChange = param.onLiveChange, _param_localCandidates = param.localCandidates, localCandidates = _param_localCandidates === void 0 ? EMPTY_CANDIDATES : _param_localCandidates, decodeHeader = param.decodeHeader, _param_testId = param.testId, testId = _param_testId === void 0 ? 'search-omnibox' : _param_testId, _param_initialQuery = param.initialQuery, initialQuery = _param_initialQuery === void 0 ? '' : _param_initialQuery;
|
|
31081
|
+
// Local identity fallback for the optional decoder. Wrapping at use time
|
|
31082
|
+
// (instead of defaulting via destructure) lets us keep the prop strictly
|
|
31083
|
+
// optional and skip an extra closure allocation on every render when the
|
|
31084
|
+
// caller doesn't pass one.
|
|
31085
|
+
var decode = decodeHeader !== null && decodeHeader !== void 0 ? decodeHeader : identityDecode;
|
|
31077
31086
|
var _useState = _sliced_to_array$4(useState(initialQuery), 2), query = _useState[0], setQuery = _useState[1];
|
|
31078
31087
|
var _useState1 = _sliced_to_array$4(useState(initialQuery), 2), debouncedQuery = _useState1[0], setDebouncedQuery = _useState1[1];
|
|
31079
31088
|
var _useState2 = _sliced_to_array$4(useState(false), 2), focused = _useState2[0], setFocused = _useState2[1];
|
|
@@ -31217,7 +31226,7 @@ var SearchOmnibox = function(param) {
|
|
|
31217
31226
|
]);
|
|
31218
31227
|
// Remote autocomplete fetch
|
|
31219
31228
|
useEffect(function() {
|
|
31220
|
-
if (
|
|
31229
|
+
if (!fetchSuggestions || !debouncedQuery) {
|
|
31221
31230
|
setResults([]);
|
|
31222
31231
|
return;
|
|
31223
31232
|
}
|
|
@@ -31272,7 +31281,7 @@ var SearchOmnibox = function(param) {
|
|
|
31272
31281
|
}, [
|
|
31273
31282
|
debouncedQuery,
|
|
31274
31283
|
bar,
|
|
31275
|
-
|
|
31284
|
+
fetchSuggestions,
|
|
31276
31285
|
apiFetch
|
|
31277
31286
|
]);
|
|
31278
31287
|
// Tracks the last query value we've notified the parent about via the
|
|
@@ -31321,7 +31330,7 @@ var SearchOmnibox = function(param) {
|
|
|
31321
31330
|
// re-render even when the previous state was also `[]`, because React
|
|
31322
31331
|
// bails out only on Object.is equality.
|
|
31323
31332
|
useEffect(function() {
|
|
31324
|
-
if (
|
|
31333
|
+
if (fetchSuggestions) return;
|
|
31325
31334
|
var q = debouncedQuery.trim().toLowerCase();
|
|
31326
31335
|
if (!q) {
|
|
31327
31336
|
setLocalMatches(function(prev) {
|
|
@@ -31339,13 +31348,13 @@ var SearchOmnibox = function(param) {
|
|
|
31339
31348
|
}
|
|
31340
31349
|
}, [
|
|
31341
31350
|
debouncedQuery,
|
|
31342
|
-
|
|
31351
|
+
fetchSuggestions,
|
|
31343
31352
|
localCandidates
|
|
31344
31353
|
]);
|
|
31345
31354
|
// Live-change notification: fires on debounced typing in both modes.
|
|
31346
31355
|
// Pages that want to live-filter their table (or fetch as the user
|
|
31347
31356
|
// types) pass `onLiveChange`; pages that only care about explicit
|
|
31348
|
-
// commits leave it out. Independent of `
|
|
31357
|
+
// commits leave it out. Independent of `fetchSuggestions`.
|
|
31349
31358
|
useEffect(function() {
|
|
31350
31359
|
notifyLiveChange(debouncedQuery);
|
|
31351
31360
|
}, [
|
|
@@ -31482,20 +31491,24 @@ var SearchOmnibox = function(param) {
|
|
|
31482
31491
|
});
|
|
31483
31492
|
}
|
|
31484
31493
|
if (dropdownState === 'results') {
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
+
// Per PRD §1.5 every typing-state dropdown ends in the
|
|
31495
|
+
// "All search results for X — Press ENTER" footer row, regardless
|
|
31496
|
+
// of how many (or whether any) suggestion rows precede it. Bars
|
|
31497
|
+
// with fetchSuggestions=false and no localCandidates therefore show
|
|
31498
|
+
// only the footer when the user has typed text, which is the only
|
|
31499
|
+
// mobile-friendly affordance for committing the search.
|
|
31500
|
+
var suggestionRows = !fetchSuggestions ? localMatches.map(function(s) {
|
|
31501
|
+
return {
|
|
31502
|
+
kind: 'local',
|
|
31503
|
+
key: s
|
|
31504
|
+
};
|
|
31505
|
+
}) : results.slice(0, 5).map(function(r) {
|
|
31494
31506
|
return {
|
|
31495
31507
|
kind: 'result',
|
|
31496
31508
|
key: r.message_id
|
|
31497
31509
|
};
|
|
31498
|
-
})
|
|
31510
|
+
});
|
|
31511
|
+
return _to_consumable_array$2(suggestionRows).concat([
|
|
31499
31512
|
{
|
|
31500
31513
|
kind: 'footer',
|
|
31501
31514
|
key: '__footer__'
|
|
@@ -31508,7 +31521,7 @@ var SearchOmnibox = function(param) {
|
|
|
31508
31521
|
recent,
|
|
31509
31522
|
results,
|
|
31510
31523
|
localMatches,
|
|
31511
|
-
|
|
31524
|
+
fetchSuggestions
|
|
31512
31525
|
]);
|
|
31513
31526
|
// Reset highlight whenever the item set changes — including same-length
|
|
31514
31527
|
// swaps (e.g. one page of 5 results replaced by a different 5 + footer).
|
|
@@ -31807,17 +31820,28 @@ var SearchOmnibox = function(param) {
|
|
|
31807
31820
|
children: /*#__PURE__*/ jsx(Close, {})
|
|
31808
31821
|
})
|
|
31809
31822
|
]
|
|
31810
|
-
}, "recent-".concat(item.key));
|
|
31823
|
+
}, "recent-".concat(idx, "-").concat(item.key));
|
|
31811
31824
|
}
|
|
31812
31825
|
if (item.kind === 'result') {
|
|
31813
31826
|
var result = results.find(function(r) {
|
|
31814
31827
|
return r.message_id === item.key;
|
|
31815
31828
|
});
|
|
31816
31829
|
if (!result) return null;
|
|
31830
|
+
// Decode RFC 2047 encoded-words ("=?UTF-8?Q?..?=") and other
|
|
31831
|
+
// wire-format escapes before rendering. Backend stores the
|
|
31832
|
+
// raw header bytes; presentation is the caller's concern via
|
|
31833
|
+
// the optional `decodeHeader` prop. When the prop is
|
|
31834
|
+
// unset (non-mail bars), `decode` is identity and this is a
|
|
31835
|
+
// no-op. Highlight markup is left as-is — it's a snippet
|
|
31836
|
+
// composed server-side from the indexed (already-tokenized)
|
|
31837
|
+
// text and shouldn't be re-decoded.
|
|
31838
|
+
var decodedSubject = decode(result.subject);
|
|
31839
|
+
var decodedFrom = decode(result.from);
|
|
31817
31840
|
var _result_to;
|
|
31841
|
+
var decodedTo = ((_result_to = result.to) !== null && _result_to !== void 0 ? _result_to : []).map(decode);
|
|
31818
31842
|
var participants = [
|
|
31819
|
-
|
|
31820
|
-
].concat(_to_consumable_array$2(
|
|
31843
|
+
decodedFrom
|
|
31844
|
+
].concat(_to_consumable_array$2(decodedTo)).filter(Boolean).join(', ');
|
|
31821
31845
|
return /*#__PURE__*/ jsxs(DropdownRow, {
|
|
31822
31846
|
id: optionId,
|
|
31823
31847
|
role: "option",
|
|
@@ -31841,7 +31865,7 @@ var SearchOmnibox = function(param) {
|
|
|
31841
31865
|
__html: sanitizeHighlight(result.highlight_subject)
|
|
31842
31866
|
}
|
|
31843
31867
|
}) : /*#__PURE__*/ jsx("strong", {
|
|
31844
|
-
children:
|
|
31868
|
+
children: decodedSubject
|
|
31845
31869
|
}),
|
|
31846
31870
|
/*#__PURE__*/ jsx("span", {
|
|
31847
31871
|
style: {
|
|
@@ -31856,7 +31880,7 @@ var SearchOmnibox = function(param) {
|
|
|
31856
31880
|
children: formatDate(result.timestamp)
|
|
31857
31881
|
})
|
|
31858
31882
|
]
|
|
31859
|
-
}, "result-".concat(item.key));
|
|
31883
|
+
}, "result-".concat(idx, "-").concat(item.key));
|
|
31860
31884
|
}
|
|
31861
31885
|
if (item.kind === 'local') {
|
|
31862
31886
|
return /*#__PURE__*/ jsx(DropdownRow, {
|
|
@@ -31874,7 +31898,7 @@ var SearchOmnibox = function(param) {
|
|
|
31874
31898
|
children: /*#__PURE__*/ jsx(RowText, {
|
|
31875
31899
|
children: item.key
|
|
31876
31900
|
})
|
|
31877
|
-
}, "local-".concat(item.key));
|
|
31901
|
+
}, "local-".concat(idx, "-").concat(item.key));
|
|
31878
31902
|
}
|
|
31879
31903
|
// footer
|
|
31880
31904
|
return /*#__PURE__*/ jsxs(FooterRow, {
|
package/package.json
CHANGED
|
@@ -25,19 +25,38 @@ export interface SearchOmniboxProps {
|
|
|
25
25
|
placeholder?: string;
|
|
26
26
|
/**
|
|
27
27
|
* Base URL of the pb_search service (no trailing slash).
|
|
28
|
-
* Required when `
|
|
28
|
+
* Required when `fetchSuggestions !== false` or `withHistory !== false`. Ignored otherwise.
|
|
29
29
|
*/
|
|
30
30
|
pbSearchBaseUrl?: string;
|
|
31
31
|
/**
|
|
32
32
|
* Returns the current Bearer token to send to pb_search.
|
|
33
|
-
* Required when `
|
|
33
|
+
* Required when `fetchSuggestions !== false` or `withHistory !== false`. Ignored otherwise.
|
|
34
34
|
*/
|
|
35
35
|
getAuthToken?: () => Promise<string> | string;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Whether to fetch suggestion rows from pb_search as the user types.
|
|
38
|
+
* Defaults to `true`.
|
|
39
|
+
*
|
|
40
|
+
* - `true` — fires `GET /search/autocomplete` against pb_search on
|
|
41
|
+
* debounced typing and renders the top-5 hits above the footer row.
|
|
42
|
+
* Use on bars whose data is indexed in pb_search (every mail bar
|
|
43
|
+
* today).
|
|
44
|
+
* - `false` — never fetches `/search/autocomplete`. If `localCandidates`
|
|
45
|
+
* is passed, those are filtered client-side and shown as suggestions;
|
|
46
|
+
* otherwise only the footer row appears. Use on bars whose data
|
|
47
|
+
* isn't indexed in pb_search (rulesets, relays, settings, etc.).
|
|
48
|
+
*
|
|
49
|
+
* Independent of:
|
|
50
|
+
* - `withHistory`, which gates recent-search behavior (Redis), and
|
|
51
|
+
* - the Tab-accept ghost text, which is sourced from recent search
|
|
52
|
+
* history and is always on when `withHistory` is enabled.
|
|
53
|
+
*
|
|
54
|
+
* Named `fetchSuggestions` rather than `autocomplete` to avoid
|
|
55
|
+
* collision with the Tab-accept ghost-text autocomplete feature —
|
|
56
|
+
* both could reasonably be called "autocomplete" but they're
|
|
57
|
+
* independent.
|
|
39
58
|
*/
|
|
40
|
-
|
|
59
|
+
fetchSuggestions?: boolean;
|
|
41
60
|
/**
|
|
42
61
|
* Whether to use Redis-backed recent-search history. Defaults to `true`.
|
|
43
62
|
*
|
|
@@ -47,12 +66,13 @@ export interface SearchOmniboxProps {
|
|
|
47
66
|
* - Ghost-text tab autocomplete is disabled (it's sourced from recent searches)
|
|
48
67
|
*
|
|
49
68
|
* Set this to `false` for config / list pages that don't need per-user history.
|
|
50
|
-
* Pairs naturally with `
|
|
51
|
-
* from pb_search.
|
|
69
|
+
* Pairs naturally with `fetchSuggestions={false}` to keep the component
|
|
70
|
+
* fully decoupled from pb_search.
|
|
52
71
|
*/
|
|
53
72
|
withHistory?: boolean;
|
|
54
73
|
/** Called when the user clicks a result row in the live-results dropdown
|
|
55
|
-
* (
|
|
74
|
+
* (only fires when `fetchSuggestions !== false`; the dropdown is a
|
|
75
|
+
* pb_search-fed top-5 list). */
|
|
56
76
|
onResultClick?: (result: SearchOmniboxResult) => void;
|
|
57
77
|
/** Called when the user **commits** a search — explicit action.
|
|
58
78
|
* Triggers: Enter on the input (no row highlighted), click on the
|
|
@@ -65,11 +85,24 @@ export interface SearchOmniboxProps {
|
|
|
65
85
|
* Fires in both `local` and `remote` modes. Optional — pages that
|
|
66
86
|
* only care about explicit commits should just not pass this. */
|
|
67
87
|
onLiveChange?: (query: string) => void;
|
|
68
|
-
/**
|
|
88
|
+
/** Optional decoder applied to header fields (`from`, `to`, `subject`)
|
|
89
|
+
* on suggestion rows before they're rendered. Use this to handle
|
|
90
|
+
* RFC 2047 encoded-words ("=?UTF-8?Q?Re:_=E2=9A=A1?=") or other
|
|
91
|
+
* on-the-wire encodings the backend hasn't already unwrapped.
|
|
92
|
+
*
|
|
93
|
+
* Kept as a prop (rather than hardcoded) so the component stays
|
|
94
|
+
* mail-agnostic; mail-bar callers wire in `decodeMimeHeader` from
|
|
95
|
+
* paubox-next's `utils/mimeHeader`, non-mail bars can leave it
|
|
96
|
+
* undefined. Defaults to identity. */
|
|
97
|
+
decodeHeader?: (value: string) => string;
|
|
98
|
+
/** Optional candidate strings to filter client-side when
|
|
99
|
+
* `fetchSuggestions={false}` (e.g. ruleset names, usernames). Ignored
|
|
100
|
+
* when `fetchSuggestions` is truthy — pb_search drives the suggestions
|
|
101
|
+
* instead. */
|
|
69
102
|
localCandidates?: string[];
|
|
70
103
|
/** Test id prefix; defaults to `search-omnibox`. */
|
|
71
104
|
testId?: string;
|
|
72
105
|
/** Optional initial query (e.g. when restoring from URL state). */
|
|
73
106
|
initialQuery?: string;
|
|
74
107
|
}
|
|
75
|
-
export declare const SearchOmnibox: ({ bar, placeholder, pbSearchBaseUrl, getAuthToken,
|
|
108
|
+
export declare const SearchOmnibox: ({ bar, placeholder, pbSearchBaseUrl, getAuthToken, fetchSuggestions, withHistory, onResultClick, onCommit, onLiveChange, localCandidates, decodeHeader, testId, initialQuery, }: SearchOmniboxProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|