Skip to content

Commit 22b9a26

Browse files
atrakhConvex, Inc.
authored andcommitted
design-system: support "all" selected state for MultiSelectCombobox (#36699)
When we use MultiSelectCombobox, we currently have to maintain the list of selected values all the time, even when the intent is to just select them all (the default state in all current scenarios). If the list of available values change, we would need to have logic to keep track of which values need to be added when all values are selected. Instead, have MultiSelectCombobox and it's instantiating components be aware of an "all selected" state that means all values should be selected. GitOrigin-RevId: b5d22ec4fbff31269b01a5a7414080d164f8fb40
1 parent 04b017a commit 22b9a26

File tree

9 files changed

+265
-167
lines changed

9 files changed

+265
-167
lines changed

npm-packages/@convex-dev/design-system/src/MultiSelectCombobox.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { Button } from "@ui/Button";
1313
import { createPortal } from "react-dom";
1414
import { usePopper } from "react-popper";
1515

16+
export type MultiSelectValue = string[] | "all";
17+
1618
export function MultiSelectCombobox({
1719
options,
1820
selectedOptions,
@@ -26,8 +28,8 @@ export function MultiSelectCombobox({
2628
processFilterOption = (option) => option,
2729
}: {
2830
options: string[];
29-
selectedOptions: string[];
30-
setSelectedOptions(newValue: string[]): void;
31+
selectedOptions: MultiSelectValue;
32+
setSelectedOptions(newValue: MultiSelectValue): void;
3133
unit: string;
3234
unitPlural: string;
3335
label: string;
@@ -79,9 +81,16 @@ export function MultiSelectCombobox({
7981
? options
8082
: options.filter((option) => test(query, processFilterOption(option)));
8183

82-
const count = selectedOptions.filter((name) => name !== "_other").length;
84+
// Convert to internal array representation for Combobox
85+
const selectedArray = selectedOptions === "all" ? options : selectedOptions;
86+
87+
const count =
88+
selectedOptions === "all"
89+
? options.length
90+
: selectedOptions.filter((name) => name !== "_other").length;
91+
8392
const displayValue =
84-
selectedOptions.length === options.length
93+
selectedOptions === "all"
8594
? `All ${unitPlural}`
8695
: `${count} ${count !== 1 ? unitPlural : unit}`;
8796

@@ -92,11 +101,26 @@ export function MultiSelectCombobox({
92101
}
93102
}, [isOpen, update]);
94103

104+
const handleSelectAll = () => {
105+
if (selectedOptions === "all") {
106+
setSelectedOptions([]);
107+
} else {
108+
setSelectedOptions("all");
109+
}
110+
};
111+
95112
return (
96113
<Combobox
97114
as="div"
98-
value={selectedOptions}
99-
onChange={setSelectedOptions}
115+
value={selectedArray}
116+
onChange={(newSelection) => {
117+
// Check if all options are selected and convert to "all" state
118+
if (newSelection.length === options.length) {
119+
setSelectedOptions("all");
120+
} else {
121+
setSelectedOptions(newSelection);
122+
}
123+
}}
100124
multiple
101125
>
102126
{({ open }) => {
@@ -175,15 +199,9 @@ export function MultiSelectCombobox({
175199
<button
176200
type="button"
177201
className="w-full cursor-pointer p-2 pl-7 text-left text-content-primary hover:bg-background-tertiary"
178-
onClick={() =>
179-
setSelectedOptions(
180-
options.length === selectedOptions.length
181-
? []
182-
: [...options],
183-
)
184-
}
202+
onClick={handleSelectAll}
185203
>
186-
{options.length === selectedOptions.length
204+
{selectedOptions === "all"
187205
? "Deselect all"
188206
: "Select all"}
189207
</button>

npm-packages/dashboard-common/src/features/functions/components/FunctionLogs.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Button } from "@ui/Button";
1313
import { ExternalLinkIcon } from "@radix-ui/react-icons";
1414
import { useRouter } from "next/router";
1515
import { DeploymentInfoContext } from "@common/lib/deploymentContext";
16+
import { MultiSelectValue } from "@ui/MultiSelectCombobox";
1617

1718
type LogLevel = "success" | "failure" | "DEBUG" | "INFO" | "WARN" | "ERROR";
1819

@@ -49,10 +50,21 @@ export function FunctionLogs({
4950
"",
5051
);
5152
const [innerFilter, setInnerFilter] = useState(filter ?? "");
52-
const [selectedLevels, setSelectedLevels] = useLocalStorage<LogLevel[]>(
53-
`function-logs/${functionId}/selected-levels`,
54-
DEFAULT_LOG_LEVELS,
55-
);
53+
const [selectedLevelsStorage, setSelectedLevelsStorage] = useLocalStorage<
54+
LogLevel[] | "all"
55+
>(`function-logs/${functionId}/selected-levels`, "all");
56+
57+
// Convert the stored levels to MultiSelectValue type
58+
const selectedLevels: MultiSelectValue =
59+
selectedLevelsStorage === "all"
60+
? "all"
61+
: ((selectedLevelsStorage || []) as string[]);
62+
const setSelectedLevels = (newLevels: MultiSelectValue) => {
63+
// Store in localStorage
64+
setSelectedLevelsStorage(
65+
newLevels === "all" ? "all" : (newLevels as LogLevel[]),
66+
);
67+
};
5668

5769
useDebounce(
5870
() => {
@@ -104,9 +116,9 @@ export function FunctionLogs({
104116
functions={[functionId]}
105117
selectedFunctions={[functionId]}
106118
setSelectedFunctions={(_functions) => {}}
107-
selectedLevels={selectedLevels ?? DEFAULT_LOG_LEVELS}
108-
setSelectedLevels={(levels) => setSelectedLevels(levels as LogLevel[])}
109-
selectedNents={selectedNent ? [selectedNent.path] : []}
119+
selectedLevels={selectedLevels}
120+
setSelectedLevels={setSelectedLevels}
121+
selectedNents={selectedNent ? [selectedNent.path] : "all"}
110122
setSelectedNents={() => {}}
111123
hideFunctionFilter
112124
firstItem={
@@ -134,7 +146,7 @@ export function FunctionLogs({
134146
logs={logs}
135147
filteredLogs={filterLogs(
136148
{
137-
logTypes: selectedLevels ?? DEFAULT_LOG_LEVELS,
149+
logTypes: selectedLevels,
138150
functions: [functionId],
139151
selectedFunctions: [functionId],
140152
filter: filter ?? "",

npm-packages/dashboard-common/src/features/logs/components/LogList.tsx

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,11 @@ import {
66
InfoCircledIcon,
77
QuestionMarkCircledIcon,
88
} from "@radix-ui/react-icons";
9-
import {
10-
Fragment,
11-
memo,
12-
useCallback,
13-
useEffect,
14-
useRef,
15-
useState,
16-
} from "react";
9+
import { Fragment, memo, useCallback, useRef, useState } from "react";
1710
import { FixedSizeList, ListOnScrollProps, areEqual } from "react-window";
18-
import { useDebounce, useMeasure, usePrevious } from "react-use";
11+
import { useDebounce, useMeasure } from "react-use";
1912
import { Transition, Dialog } from "@headlessui/react";
2013
import isEqual from "lodash/isEqual";
21-
import difference from "lodash/difference";
2214
import { PauseCircleIcon, PlayCircleIcon } from "@heroicons/react/24/outline";
2315
import { DeploymentEventListItem } from "@common/features/logs/components/DeploymentEventListItem";
2416
import {
@@ -41,6 +33,7 @@ import { Button } from "@ui/Button";
4133
import { ClosePanelButton } from "@ui/ClosePanelButton";
4234
import { CopyTextButton } from "@common/elements/CopyTextButton";
4335
import { TextInput } from "@ui/TextInput";
36+
import { MultiSelectValue } from "@ui/MultiSelectCombobox";
4437

4538
export type LogListProps = {
4639
logs?: UdfLog[];
@@ -306,29 +299,10 @@ function RequestIdLogs({
306299
}),
307300
),
308301
);
309-
const previousFunctions = usePrevious(functions);
310302
const [selectedFunctions, setSelectedFunctions] =
311-
useState<string[]>(functions);
312-
313-
// If there are new functions in the list of logs, add them to the selected functions.
314-
useEffect(() => {
315-
if (functions.length === previousFunctions?.length) {
316-
return;
317-
}
318-
const newFunctions = previousFunctions
319-
? difference(functions, previousFunctions)
320-
: [];
321-
setSelectedFunctions((current) => [...current, ...newFunctions]);
322-
}, [functions, previousFunctions]);
303+
useState<MultiSelectValue>("all");
323304

324-
const [selectedLevels, setSelectedLevels] = useState<string[]>([
325-
"success",
326-
"failure",
327-
"DEBUG",
328-
"INFO",
329-
"WARN",
330-
"ERROR",
331-
]);
305+
const [selectedLevels, setSelectedLevels] = useState<MultiSelectValue>("all");
332306

333307
const filters = {
334308
logTypes: selectedLevels,

npm-packages/dashboard-common/src/features/logs/components/LogToolbar.test.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,26 @@ describe("selectNentOption", () => {
2828
functions,
2929
newNents: [],
3030
expectedSelectedNents: [],
31-
expectedSelectedFunctions: [],
31+
expectedSelectedFunctions: "all",
3232
},
3333
{
3434
name: "removing two nents removes functions related to those nents",
3535
nents,
3636
functions,
3737
newNents: ["_App"],
3838
expectedSelectedNents: ["_App"],
39-
expectedSelectedFunctions: [functions[0]],
40-
},
41-
{
42-
name: "adding two nents adds functions related to those nents",
43-
nents: [],
44-
functions: [],
45-
newNents: ["_App", "nent2"],
46-
expectedSelectedNents: ["_App", "nent2"],
47-
expectedSelectedFunctions: [functions[0], functions[3]],
39+
expectedSelectedFunctions: "all",
4840
},
4941
{
5042
name: "adding a nent does not add functions related to other nents",
5143
nents: ["_App"],
5244
functions: [],
5345
newNents: ["_App", "nent2"],
5446
expectedSelectedNents: ["_App", "nent2"],
55-
expectedSelectedFunctions: [functions[3]],
47+
expectedSelectedFunctions: [
48+
functionIdentifierValue("func1"),
49+
functionIdentifierValue("func4", "nent2", "id2"),
50+
],
5651
},
5752
];
5853

@@ -95,9 +90,9 @@ describe("functionsForSelectedNents", () => {
9590

9691
const testCases = [
9792
{
98-
name: "returns no functions when no nents are selected",
93+
name: "returns all functions when no nents are selected",
9994
nents: [],
100-
expectedFunctions: [],
95+
expectedFunctions: functions,
10196
},
10297
{
10398
name: "returns only functions related to selected nents",
@@ -120,7 +115,7 @@ describe("functionsForSelectedNents", () => {
120115
expectedFunctions: [functions[0]],
121116
},
122117
{
123-
name: "returns nent functions when only _App is selected",
118+
name: "returns nent functions when only nent1 and nent2 are selected",
124119
nents: ["nent1", "nent2"],
125120
expectedFunctions: functions.slice(1),
126121
},

0 commit comments

Comments
 (0)