@remit/web-client 0.0.85 → 0.0.87
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 +1 -1
- package/src/auth/BetterAuthShell.tsx +2 -2
- package/src/components/compose/AddressField.tsx +60 -77
- package/src/components/mail/AutoMovedIndicator.tsx +7 -9
- package/src/components/mail/BriefPane.tsx +4 -2
- package/src/components/mail/DailyBrief.selection.test.ts +49 -0
- package/src/components/mail/DailyBrief.tsx +484 -76
- package/src/components/mail/FlaggedList.tsx +3 -2
- package/src/components/mail/FlaggedPane.tsx +4 -2
- package/src/components/mail/MailListHeader.tsx +155 -34
- package/src/components/mail/MailViewChrome.tsx +18 -1
- package/src/components/mail/MailboxPane.tsx +55 -20
- package/src/components/mail/MessageCard.tsx +0 -1
- package/src/components/mail/MessageList.tsx +70 -15
- package/src/components/mail/MessageListItem.tsx +4 -21
- package/src/components/mail/MessageRow.tsx +13 -31
- package/src/components/mail/SwipeableMessageRow.tsx +13 -6
- package/src/components/mail/ThreadListInteraction.tsx +51 -6
- package/src/components/mail/organize/OrganizeRuleEditor.tsx +57 -2
- package/src/components/mail/organize/SearchFilterDialog.render.test.ts +13 -2
- package/src/components/mail/organize/SearchFilterDialog.tsx +3 -1
- package/src/components/mail/organize/SearchFilterEditor.render.test.ts +37 -3
- package/src/components/mail/organize/SearchFilterEditor.tsx +8 -2
- package/src/components/mail/organize/smart-organize.stories.tsx +86 -5
- package/src/components/mail/useModifierSelect.render.test.ts +247 -0
- package/src/components/mail/useModifierSelect.ts +109 -0
- package/src/components/onboarding/OnboardingWizard.tsx +53 -9
- package/src/components/settings/AccountFormPanel.tsx +10 -5
- package/src/components/settings/TlsRootCaDownload.render.test.ts +37 -0
- package/src/components/settings/TlsRootCaDownload.tsx +33 -0
- package/src/hooks/bulk-chunking.render.test.ts +157 -0
- package/src/hooks/useApplyLabel.ts +6 -3
- package/src/hooks/useClauseSuggestions.ts +57 -0
- package/src/hooks/useDeleteMessages.ts +6 -3
- package/src/hooks/useFollowFocusOpen.render.test.ts +155 -0
- package/src/hooks/useFollowFocusOpen.ts +81 -0
- package/src/hooks/useInitialSyncProgress.render.test.ts +280 -0
- package/src/hooks/useInitialSyncProgress.ts +134 -0
- package/src/hooks/useListCursor.ts +36 -5
- package/src/hooks/useMarkAsRead.ts +6 -3
- package/src/hooks/useMoveMessages.ts +6 -3
- package/src/hooks/useRulePreview.ts +11 -1
- package/src/hooks/useSearchFilterSeed.render.test.ts +50 -15
- package/src/hooks/useSearchFilterSeed.ts +21 -3
- package/src/hooks/useSearchSuggestions.ts +126 -0
- package/src/hooks/useSelectedSubjects.ts +0 -0
- package/src/hooks/useSemanticSearch.ts +26 -7
- package/src/hooks/useThreadActions.ts +11 -2
- package/src/lib/brief.test.ts +67 -0
- package/src/lib/brief.ts +11 -1
- package/src/lib/bulk-actions.ts +29 -0
- package/src/lib/drafts.ts +1 -1
- package/src/lib/organize/clause-suggestions.test.ts +113 -0
- package/src/lib/organize/clause-suggestions.ts +90 -0
- package/src/lib/organize/property-prefill.test.ts +173 -0
- package/src/lib/organize/property-prefill.ts +151 -0
- package/src/lib/organize/rule-model.test.ts +57 -0
- package/src/lib/organize/rule-model.ts +110 -31
- package/src/lib/organize/search-to-rule.ts +12 -4
- package/src/lib/organize/sender-fallback.ts +11 -1
- package/src/lib/search-result.ts +2 -0
- package/src/lib/search-suggestions.test.ts +249 -0
- package/src/lib/search-suggestions.ts +296 -0
- package/src/lib/search-token-index.test.ts +99 -0
- package/src/lib/search-token-index.ts +67 -1
- package/src/lib/search-tokens.test.ts +236 -0
- package/src/lib/search-tokens.ts +303 -36
- package/src/lib/thread-cache.ts +1 -1
- package/src/lib/thread-search-tokens.test.ts +193 -0
- package/src/lib/thread-search-tokens.ts +148 -0
- package/src/routes/onboarding.tsx +9 -4
- package/src/routes/settings/advanced.tsx +2 -0
- package/src/runtime-config.test.ts +26 -0
- package/src/runtime-config.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.87",
|
|
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": {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Banner,
|
|
6
6
|
Button,
|
|
7
7
|
Input,
|
|
8
|
+
PasswordInput,
|
|
8
9
|
} from "@remit/ui";
|
|
9
10
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
10
11
|
import { AppShellSkeleton } from "@/components/layout/AppShellSkeleton";
|
|
@@ -161,9 +162,8 @@ export const SignInForm = () => {
|
|
|
161
162
|
<label htmlFor="ba-password" className="text-fg-muted">
|
|
162
163
|
Password
|
|
163
164
|
</label>
|
|
164
|
-
<
|
|
165
|
+
<PasswordInput
|
|
165
166
|
id="ba-password"
|
|
166
|
-
type="password"
|
|
167
167
|
autoComplete={
|
|
168
168
|
activeMode === "signUp" ? "new-password" : "current-password"
|
|
169
169
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { addressOperationsSearchAddressesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
2
|
import type { RemitImapAddressResponse } from "@remit/api-http-client/types.gen.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AddressTag,
|
|
5
|
+
type Suggestion,
|
|
6
|
+
SuggestList,
|
|
7
|
+
useSuggestList,
|
|
8
|
+
} from "@remit/ui";
|
|
4
9
|
import { useQuery } from "@tanstack/react-query";
|
|
5
|
-
import { useCallback,
|
|
10
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
6
11
|
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
12
|
|
|
9
13
|
export interface AddressEntry {
|
|
10
14
|
email: string;
|
|
@@ -18,12 +22,8 @@ interface AddressFieldProps {
|
|
|
18
22
|
placeholder?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return `${addr.displayName} <${addr.normalizedEmail}>`;
|
|
24
|
-
}
|
|
25
|
-
return addr.normalizedEmail;
|
|
26
|
-
};
|
|
25
|
+
/** Beyond Enter, the keys that take the highlighted suggestion in a chips field. */
|
|
26
|
+
const ACCEPT_KEYS = ["Tab", ","] as const;
|
|
27
27
|
|
|
28
28
|
const isValidEmail = (value: string): boolean =>
|
|
29
29
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
@@ -50,10 +50,7 @@ export const AddressField = ({
|
|
|
50
50
|
placeholder,
|
|
51
51
|
}: AddressFieldProps) => {
|
|
52
52
|
const [inputValue, setInputValue] = useState("");
|
|
53
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
54
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
55
53
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
56
|
-
const listRef = useRef<HTMLUListElement>(null);
|
|
57
54
|
|
|
58
55
|
const debouncedQuery = useDebouncedValue(inputValue, 200);
|
|
59
56
|
|
|
@@ -66,21 +63,18 @@ export const AddressField = ({
|
|
|
66
63
|
|
|
67
64
|
const suggestions = suggestionsData?.items ?? [];
|
|
68
65
|
const existingEmails = new Set(addresses.map((a) => a.email.toLowerCase()));
|
|
69
|
-
const filteredSuggestions =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
setIsOpen(filteredSuggestions.length > 0 && inputValue.length >= 2);
|
|
76
|
-
}, [filteredSuggestions.length, inputValue.length]);
|
|
66
|
+
const filteredSuggestions =
|
|
67
|
+
inputValue.length >= 2
|
|
68
|
+
? suggestions.filter(
|
|
69
|
+
(s) => !existingEmails.has(s.normalizedEmail.toLowerCase()),
|
|
70
|
+
)
|
|
71
|
+
: [];
|
|
77
72
|
|
|
78
73
|
const addAddress = useCallback(
|
|
79
74
|
(entry: AddressEntry) => {
|
|
80
75
|
if (existingEmails.has(entry.email.toLowerCase())) return;
|
|
81
76
|
onChange([...addresses, entry]);
|
|
82
77
|
setInputValue("");
|
|
83
|
-
setIsOpen(false);
|
|
84
78
|
},
|
|
85
79
|
[addresses, existingEmails, onChange],
|
|
86
80
|
);
|
|
@@ -110,6 +104,24 @@ export const AddressField = ({
|
|
|
110
104
|
}
|
|
111
105
|
}, [inputValue, addAddress]);
|
|
112
106
|
|
|
107
|
+
// The open state, the highlight, and the arrow/Enter/Escape handling are the
|
|
108
|
+
// app's one typeahead behaviour, shared with the filter-rule value field.
|
|
109
|
+
const suggest = useSuggestList({
|
|
110
|
+
count: filteredSuggestions.length,
|
|
111
|
+
acceptKeys: ACCEPT_KEYS,
|
|
112
|
+
onAccept: (index) => selectSuggestion(filteredSuggestions[index]),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const options = useMemo<Suggestion[]>(
|
|
116
|
+
() =>
|
|
117
|
+
filteredSuggestions.map((suggestion) => ({
|
|
118
|
+
value: suggestion.normalizedEmail,
|
|
119
|
+
label: suggestion.displayName ?? suggestion.normalizedEmail,
|
|
120
|
+
...(suggestion.displayName ? { hint: suggestion.normalizedEmail } : {}),
|
|
121
|
+
})),
|
|
122
|
+
[filteredSuggestions],
|
|
123
|
+
);
|
|
124
|
+
|
|
113
125
|
const handleKeyDown = useCallback(
|
|
114
126
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
115
127
|
if (e.key === "Backspace" && inputValue === "" && addresses.length > 0) {
|
|
@@ -117,48 +129,20 @@ export const AddressField = ({
|
|
|
117
129
|
return;
|
|
118
130
|
}
|
|
119
131
|
|
|
132
|
+
if (suggest.handleKeyDown(e)) return;
|
|
133
|
+
|
|
120
134
|
if (e.key === "Enter" || e.key === "Tab" || e.key === ",") {
|
|
121
|
-
if (highlightedIndex >= 0 && filteredSuggestions[highlightedIndex]) {
|
|
122
|
-
e.preventDefault();
|
|
123
|
-
selectSuggestion(filteredSuggestions[highlightedIndex]);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
135
|
if (inputValue.trim()) {
|
|
127
136
|
e.preventDefault();
|
|
128
137
|
commitInput();
|
|
129
138
|
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (e.key === "ArrowDown" && isOpen) {
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
setHighlightedIndex((prev) =>
|
|
136
|
-
prev < filteredSuggestions.length - 1 ? prev + 1 : 0,
|
|
137
|
-
);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (e.key === "ArrowUp" && isOpen) {
|
|
142
|
-
e.preventDefault();
|
|
143
|
-
setHighlightedIndex((prev) =>
|
|
144
|
-
prev > 0 ? prev - 1 : filteredSuggestions.length - 1,
|
|
145
|
-
);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (e.key === "Escape") {
|
|
150
|
-
setIsOpen(false);
|
|
151
|
-
setHighlightedIndex(-1);
|
|
152
139
|
}
|
|
153
140
|
},
|
|
154
141
|
[
|
|
155
142
|
inputValue,
|
|
156
143
|
addresses.length,
|
|
157
|
-
highlightedIndex,
|
|
158
|
-
filteredSuggestions,
|
|
159
|
-
isOpen,
|
|
160
144
|
removeAddress,
|
|
161
|
-
|
|
145
|
+
suggest.handleKeyDown,
|
|
162
146
|
commitInput,
|
|
163
147
|
],
|
|
164
148
|
);
|
|
@@ -166,9 +150,9 @@ export const AddressField = ({
|
|
|
166
150
|
const handleBlur = useCallback(() => {
|
|
167
151
|
setTimeout(() => {
|
|
168
152
|
commitInput();
|
|
169
|
-
|
|
153
|
+
suggest.dismiss();
|
|
170
154
|
}, 150);
|
|
171
|
-
}, [commitInput]);
|
|
155
|
+
}, [commitInput, suggest.dismiss]);
|
|
172
156
|
|
|
173
157
|
return (
|
|
174
158
|
<div className="relative">
|
|
@@ -200,37 +184,36 @@ export const AddressField = ({
|
|
|
200
184
|
id={`address-field-${label}`}
|
|
201
185
|
type="text"
|
|
202
186
|
value={inputValue}
|
|
203
|
-
onChange={(e) =>
|
|
187
|
+
onChange={(e) => {
|
|
188
|
+
suggest.reopen();
|
|
189
|
+
setInputValue(e.target.value);
|
|
190
|
+
}}
|
|
204
191
|
onKeyDown={handleKeyDown}
|
|
205
192
|
onBlur={handleBlur}
|
|
206
193
|
placeholder={addresses.length === 0 ? placeholder : ""}
|
|
207
194
|
className="flex-1 min-w-[120px] bg-transparent outline-none text-sm py-0.5"
|
|
195
|
+
autoComplete="off"
|
|
196
|
+
{...suggest.comboboxProps}
|
|
208
197
|
/>
|
|
209
198
|
</div>
|
|
210
199
|
</div>
|
|
211
200
|
|
|
212
|
-
{
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
onMouseEnter={() => setHighlightedIndex(index)}
|
|
229
|
-
>
|
|
230
|
-
{formatSuggestion(suggestion)}
|
|
231
|
-
</li>
|
|
232
|
-
))}
|
|
233
|
-
</ul>
|
|
201
|
+
{suggest.open && (
|
|
202
|
+
<SuggestList
|
|
203
|
+
id={suggest.listId}
|
|
204
|
+
suggestions={options}
|
|
205
|
+
activeIndex={suggest.activeIndex}
|
|
206
|
+
optionId={suggest.optionId}
|
|
207
|
+
onPick={(option) => {
|
|
208
|
+
const picked = filteredSuggestions.find(
|
|
209
|
+
(suggestion) => suggestion.normalizedEmail === option.value,
|
|
210
|
+
);
|
|
211
|
+
if (picked) selectSuggestion(picked);
|
|
212
|
+
}}
|
|
213
|
+
onHighlight={suggest.setActiveIndex}
|
|
214
|
+
label={`${label} suggestions`}
|
|
215
|
+
className="absolute left-12 right-0 z-50 mt-1 max-h-[200px] shadow-lg"
|
|
216
|
+
/>
|
|
234
217
|
)}
|
|
235
218
|
</div>
|
|
236
219
|
);
|
|
@@ -8,15 +8,15 @@ interface AutoMovedIndicatorProps {
|
|
|
8
8
|
threadId: string;
|
|
9
9
|
mailboxId: string;
|
|
10
10
|
autoMoved: RemitImapAutoMovedInfo | undefined;
|
|
11
|
-
/** `md` (reading view) adds the inline actions (Undo, and the Manage-filter link for a filter move); `sm` (list row) shows the icon + label. */
|
|
12
|
-
size?: "sm" | "md";
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
|
-
* Renders the "auto-moved by Remit" badge
|
|
17
|
-
* effect (current mailbox matches the verdict's implied
|
|
18
|
-
* `useAutoMovedBadge`.
|
|
19
|
-
*
|
|
14
|
+
* Renders the "auto-moved by Remit" badge on the open message only while the
|
|
15
|
+
* move is still in effect (current mailbox matches the verdict's implied
|
|
16
|
+
* destination) — see `useAutoMovedBadge`. Mailbox list rows deliberately do not
|
|
17
|
+
* show it: there the move is noise, and here it carries the Undo and
|
|
18
|
+
* Manage-filter actions. Mount this only when `autoMoved` is present (callers
|
|
19
|
+
* gate with `threadMessage.autoMoved &&`) so messages without a move never pay
|
|
20
20
|
* for the Inbox/Junk mailbox lookups.
|
|
21
21
|
*/
|
|
22
22
|
export function AutoMovedIndicator({
|
|
@@ -25,7 +25,6 @@ export function AutoMovedIndicator({
|
|
|
25
25
|
threadId,
|
|
26
26
|
mailboxId,
|
|
27
27
|
autoMoved,
|
|
28
|
-
size = "sm",
|
|
29
28
|
}: AutoMovedIndicatorProps) {
|
|
30
29
|
const badge = useAutoMovedBadge({
|
|
31
30
|
accountId,
|
|
@@ -40,10 +39,9 @@ export function AutoMovedIndicator({
|
|
|
40
39
|
return (
|
|
41
40
|
<AutoMovedBadge
|
|
42
41
|
label={badge.label}
|
|
43
|
-
size={size}
|
|
44
42
|
onUndo={badge.onUndo}
|
|
45
43
|
undoLabel={badge.isUndoing ? "Undoing…" : "Undo"}
|
|
46
|
-
filtersHref={
|
|
44
|
+
filtersHref={badge.filtersHref}
|
|
47
45
|
/>
|
|
48
46
|
);
|
|
49
47
|
}
|
|
@@ -33,6 +33,7 @@ import { ConversationView } from "@/components/mail/ConversationView";
|
|
|
33
33
|
import { DailyBrief } from "@/components/mail/DailyBrief";
|
|
34
34
|
import { IntelligencePane } from "@/components/mail/IntelligencePane";
|
|
35
35
|
import { MessageToolbar } from "@/components/mail/MessageToolbar";
|
|
36
|
+
import type { OpenMessageOptions } from "@/components/mail/ThreadListInteraction";
|
|
36
37
|
import { useDeleteMessages } from "@/hooks/useDeleteMessages";
|
|
37
38
|
import { useToggleReadFor } from "@/hooks/useMarkAsRead";
|
|
38
39
|
import { type ThreadActions, useThreadActions } from "@/hooks/useThreadActions";
|
|
@@ -56,7 +57,7 @@ interface BriefPaneContextValue {
|
|
|
56
57
|
selectedThread: RemitImapThreadMessageResponse | undefined;
|
|
57
58
|
/** The conversation to open — the loaded thread, or a tapped "Related" hit. */
|
|
58
59
|
conversation: ConversationTarget | undefined;
|
|
59
|
-
onSelectMessage: (id: string) => void;
|
|
60
|
+
onSelectMessage: (id: string, options?: OpenMessageOptions) => void;
|
|
60
61
|
onSelectSearchResult: (result: SearchResult) => void;
|
|
61
62
|
onCloseThread: () => void;
|
|
62
63
|
/**
|
|
@@ -120,7 +121,7 @@ function BriefPaneProvider({ selectedMessageId, children }: BriefPaneProps) {
|
|
|
120
121
|
);
|
|
121
122
|
|
|
122
123
|
const handleSelectMessage = useCallback(
|
|
123
|
-
(id: string) => {
|
|
124
|
+
(id: string, options?: OpenMessageOptions) => {
|
|
124
125
|
navigate({
|
|
125
126
|
to: "/mail",
|
|
126
127
|
search: (prev) => ({
|
|
@@ -129,6 +130,7 @@ function BriefPaneProvider({ selectedMessageId, children }: BriefPaneProps) {
|
|
|
129
130
|
selectedThreadId: undefined,
|
|
130
131
|
selectedMailboxId: undefined,
|
|
131
132
|
}),
|
|
133
|
+
replace: options?.replace,
|
|
132
134
|
});
|
|
133
135
|
},
|
|
134
136
|
[navigate],
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The brief raises two selection surfaces — the desktop toolbar and the touch
|
|
9
|
+
* sheet — and they offer the same verbs. Select-all was wired into the sheet
|
|
10
|
+
* only, so the same list could be selected whole on a phone and not on a
|
|
11
|
+
* desktop (#453).
|
|
12
|
+
*
|
|
13
|
+
* As with `MessageList.selection.test.ts`, `DailyBrief` weaves routing, the
|
|
14
|
+
* cursor and several data hooks together, so the rule is enforced by reading
|
|
15
|
+
* the source. `resolveBriefSelectionScope` carries the decisions that can be
|
|
16
|
+
* unit-tested on their own.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const source = readFileSync(resolve(here, "DailyBrief.tsx"), "utf8");
|
|
21
|
+
|
|
22
|
+
const chromeBody = (): string => {
|
|
23
|
+
const start = source.indexOf("function BriefSelectionChrome(");
|
|
24
|
+
assert.notEqual(start, -1, "BriefSelectionChrome is defined");
|
|
25
|
+
const next = source.indexOf("\nfunction ", start + 1);
|
|
26
|
+
return source.slice(start, next === -1 ? undefined : next);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("the brief's select-all", () => {
|
|
30
|
+
it("reaches the desktop toolbar and the touch sheet from one derivation", () => {
|
|
31
|
+
const body = chromeBody();
|
|
32
|
+
const derivations = body.match(/const selectAll = useMemo\(/g) ?? [];
|
|
33
|
+
assert.equal(
|
|
34
|
+
derivations.length,
|
|
35
|
+
1,
|
|
36
|
+
"one select-all, so the two surfaces cannot drift",
|
|
37
|
+
);
|
|
38
|
+
const wired = body.match(/selectAll=\{selectAll\}/g) ?? [];
|
|
39
|
+
assert.equal(
|
|
40
|
+
wired.length,
|
|
41
|
+
2,
|
|
42
|
+
"both SelectionToolbar and SelectionSheet take it",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is offered only when there are rows to select", () => {
|
|
47
|
+
assert.match(chromeBody(), /orderedIds\.length > 0/);
|
|
48
|
+
});
|
|
49
|
+
});
|