@marimo-team/islands 0.24.1-dev109 → 0.24.1-dev110
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/dist/{common-qgnc1X0K.js → common-DTE0vt-W.js} +5574 -5471
- package/dist/main.js +6174 -6167
- package/dist/{reveal-component-CPbame7x.js → reveal-component-CW9RWIRA.js} +438 -438
- package/package.json +1 -1
- package/src/components/data-table/__tests__/column-explorer.test.tsx +61 -2
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +130 -1
- package/src/components/data-table/__tests__/useColumnVisibility.test.ts +153 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +54 -25
- package/src/components/data-table/column-visibility-dropdown.tsx +62 -14
- package/src/components/data-table/hooks/use-column-visibility.ts +76 -0
- package/src/components/data-table/show-only-column-button.tsx +58 -0
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getCoreRowModel,
|
|
6
6
|
useReactTable,
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
8
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
9
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
11
11
|
import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer";
|
|
@@ -46,7 +46,7 @@ function PanelHarness({
|
|
|
46
46
|
columns: TEST_COLUMNS,
|
|
47
47
|
getCoreRowModel: getCoreRowModel(),
|
|
48
48
|
locale: "en-US",
|
|
49
|
-
|
|
49
|
+
initialState: {
|
|
50
50
|
columnVisibility: Object.fromEntries(
|
|
51
51
|
initiallyHidden.map((id) => [id, false]),
|
|
52
52
|
),
|
|
@@ -76,6 +76,20 @@ function getSearchInput() {
|
|
|
76
76
|
return screen.getByPlaceholderText("Search columns...");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function getColumnRow(columnName: string): HTMLElement {
|
|
80
|
+
const row = screen.getByText(columnName).closest('[role="option"]');
|
|
81
|
+
if (!row) {
|
|
82
|
+
throw new Error(`No row for column ${columnName}`);
|
|
83
|
+
}
|
|
84
|
+
return row as HTMLElement;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getShowOnlyButton(columnName: string): HTMLElement {
|
|
88
|
+
return within(getColumnRow(columnName)).getByRole("button", {
|
|
89
|
+
name: "Show only this column",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
describe("ColumnExplorerPanel search", () => {
|
|
80
94
|
it("shows all columns when search is empty", () => {
|
|
81
95
|
renderPanel();
|
|
@@ -150,4 +164,49 @@ describe("ColumnExplorerPanel visibility actions", () => {
|
|
|
150
164
|
expect(showAll()).toBeEnabled();
|
|
151
165
|
expect(hideAll()).toBeEnabled();
|
|
152
166
|
});
|
|
167
|
+
|
|
168
|
+
it("show-only icon isolates one column", () => {
|
|
169
|
+
renderPanel();
|
|
170
|
+
fireEvent.click(getShowOnlyButton("customer_name"));
|
|
171
|
+
|
|
172
|
+
expect(
|
|
173
|
+
within(getColumnRow("customer_name")).getByRole("button", {
|
|
174
|
+
name: "Hide column",
|
|
175
|
+
}),
|
|
176
|
+
).toBeInTheDocument();
|
|
177
|
+
expect(
|
|
178
|
+
within(getColumnRow("cust_age")).getByRole("button", {
|
|
179
|
+
name: "Show column",
|
|
180
|
+
}),
|
|
181
|
+
).toBeInTheDocument();
|
|
182
|
+
expect(
|
|
183
|
+
within(getColumnRow("order_total")).getByRole("button", {
|
|
184
|
+
name: "Show column",
|
|
185
|
+
}),
|
|
186
|
+
).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("disables show-only icon when the column is already alone", () => {
|
|
190
|
+
renderPanel({
|
|
191
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
192
|
+
});
|
|
193
|
+
expect(getShowOnlyButton("customer_name")).toHaveAttribute(
|
|
194
|
+
"aria-disabled",
|
|
195
|
+
"true",
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("disabled show-only icon does not toggle explorer row expansion", () => {
|
|
200
|
+
renderPanel({
|
|
201
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
202
|
+
});
|
|
203
|
+
const row = getColumnRow("customer_name");
|
|
204
|
+
expect(row.querySelector(".lucide-chevron-down")).not.toBeNull();
|
|
205
|
+
|
|
206
|
+
const button = getShowOnlyButton("customer_name");
|
|
207
|
+
fireEvent.pointerDown(button, { pointerId: 1, bubbles: true });
|
|
208
|
+
fireEvent.click(button, { bubbles: true });
|
|
209
|
+
|
|
210
|
+
expect(row.querySelector(".lucide-chevron-down")).not.toBeNull();
|
|
211
|
+
});
|
|
153
212
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
8
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
9
9
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
10
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
10
11
|
import { ColumnVisibilityDropdown } from "../column-visibility-dropdown";
|
|
11
12
|
import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
|
|
12
13
|
|
|
@@ -70,11 +71,25 @@ function Harness({ initiallyHidden = [], nonHideable = [] }: HarnessProps) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function renderAndOpen(props?: HarnessProps) {
|
|
73
|
-
const result = render(
|
|
74
|
+
const result = render(
|
|
75
|
+
<TooltipProvider>
|
|
76
|
+
<Harness {...(props ?? {})} />
|
|
77
|
+
</TooltipProvider>,
|
|
78
|
+
);
|
|
74
79
|
fireEvent.click(screen.getByTestId("column-visibility-trigger"));
|
|
75
80
|
return result;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
function getShowOnlyButton(columnName: string): HTMLElement {
|
|
84
|
+
const button = getColumnOption(columnName).querySelector(
|
|
85
|
+
'[aria-label="Show only this column"]',
|
|
86
|
+
);
|
|
87
|
+
if (!button) {
|
|
88
|
+
throw new Error(`No show-only button for column ${columnName}`);
|
|
89
|
+
}
|
|
90
|
+
return button as HTMLElement;
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
function getOptionTexts(): string[] {
|
|
79
94
|
return screen.getAllByRole("option").map((el) => el.textContent ?? "");
|
|
80
95
|
}
|
|
@@ -270,10 +285,124 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
270
285
|
it("offers both bulk actions when matches are mixed", () => {
|
|
271
286
|
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
272
287
|
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
288
|
+
expect(screen.getByText(/Show only 2 matching/)).toBeInTheDocument();
|
|
273
289
|
expect(screen.getByText(/Hide 1 matching/)).toBeInTheDocument();
|
|
274
290
|
expect(screen.getByText(/Show 1 matching/)).toBeInTheDocument();
|
|
275
291
|
});
|
|
276
292
|
|
|
293
|
+
it("lists show-only before hide and show bulk actions while searching", () => {
|
|
294
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
295
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
296
|
+
expect(getOptionTexts().slice(0, 3)).toEqual([
|
|
297
|
+
"Show only 2 matching",
|
|
298
|
+
"Hide 1 matching",
|
|
299
|
+
"Show 1 matching",
|
|
300
|
+
]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("'Show only N matching' isolates matching columns", () => {
|
|
304
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
305
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
306
|
+
fireEvent.click(getColumnOption("Show only 2 matching"));
|
|
307
|
+
fireEvent.change(getSearchInput(), { target: { value: "" } });
|
|
308
|
+
|
|
309
|
+
expect(
|
|
310
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
311
|
+
).toBeNull();
|
|
312
|
+
expect(
|
|
313
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
314
|
+
).toBeNull();
|
|
315
|
+
expect(
|
|
316
|
+
getColumnOption("order_total").querySelector(".lucide-eye-off"),
|
|
317
|
+
).not.toBeNull();
|
|
318
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
319
|
+
expect(getColumnOption("Show only 2 matching")).toHaveAttribute(
|
|
320
|
+
"aria-disabled",
|
|
321
|
+
"true",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("disables 'Show only N matching' when already showing only matches", () => {
|
|
326
|
+
renderAndOpen({
|
|
327
|
+
initiallyHidden: ["order_total"],
|
|
328
|
+
});
|
|
329
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
330
|
+
expect(getColumnOption("Show only 2 matching")).toHaveAttribute(
|
|
331
|
+
"aria-disabled",
|
|
332
|
+
"true",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("omits 'Show only N matching' without search text", () => {
|
|
337
|
+
renderAndOpen();
|
|
338
|
+
expect(
|
|
339
|
+
screen.queryByText(/Show only \d+ matching/),
|
|
340
|
+
).not.toBeInTheDocument();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("omits search bulk actions when only non-hideable columns match", () => {
|
|
344
|
+
renderAndOpen({ nonHideable: ["customer_name"] });
|
|
345
|
+
fireEvent.change(getSearchInput(), { target: { value: "customer" } });
|
|
346
|
+
expect(screen.getByText("customer_name")).toBeInTheDocument();
|
|
347
|
+
expect(
|
|
348
|
+
screen.queryByText(/Show only \d+ matching/),
|
|
349
|
+
).not.toBeInTheDocument();
|
|
350
|
+
expect(screen.queryByText(/Hide \d+ matching/)).not.toBeInTheDocument();
|
|
351
|
+
expect(screen.queryByText(/Show \d+ matching/)).not.toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("show-only icon isolates one column without clearing search", () => {
|
|
355
|
+
renderAndOpen();
|
|
356
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
357
|
+
fireEvent.click(getShowOnlyButton("customer_name"));
|
|
358
|
+
expect(getSearchInput()).toHaveValue("cust");
|
|
359
|
+
fireEvent.change(getSearchInput(), { target: { value: "" } });
|
|
360
|
+
|
|
361
|
+
expect(
|
|
362
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
363
|
+
).toBeNull();
|
|
364
|
+
expect(
|
|
365
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
366
|
+
).not.toBeNull();
|
|
367
|
+
expect(
|
|
368
|
+
getColumnOption("order_total").querySelector(".lucide-eye-off"),
|
|
369
|
+
).not.toBeNull();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("show-only icon does not toggle the row visibility", () => {
|
|
373
|
+
renderAndOpen();
|
|
374
|
+
fireEvent.click(getShowOnlyButton("customer_name"));
|
|
375
|
+
|
|
376
|
+
expect(
|
|
377
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
378
|
+
).toBeNull();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("disables show-only icon when the column is already alone", () => {
|
|
382
|
+
renderAndOpen({
|
|
383
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
384
|
+
});
|
|
385
|
+
expect(getShowOnlyButton("customer_name")).toHaveAttribute(
|
|
386
|
+
"aria-disabled",
|
|
387
|
+
"true",
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("disabled show-only icon blocks pointer activation of the row", () => {
|
|
392
|
+
renderAndOpen({
|
|
393
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
394
|
+
});
|
|
395
|
+
const button = getShowOnlyButton("customer_name");
|
|
396
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
397
|
+
|
|
398
|
+
fireEvent.pointerDown(button, { pointerId: 1, bubbles: true });
|
|
399
|
+
fireEvent.click(button, { bubbles: true });
|
|
400
|
+
|
|
401
|
+
expect(
|
|
402
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
403
|
+
).toBeNull();
|
|
404
|
+
});
|
|
405
|
+
|
|
277
406
|
it("renders non-hideable columns disabled and without an eye toggle", () => {
|
|
278
407
|
renderAndOpen({ nonHideable: ["customer_name"] });
|
|
279
408
|
const option = getColumnOption("customer_name");
|
|
@@ -1,8 +1,62 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
useReactTable,
|
|
7
|
+
} from "@tanstack/react-table";
|
|
3
8
|
import { act, renderHook } from "@testing-library/react";
|
|
4
9
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
applyShowOnlyColumns,
|
|
12
|
+
getShowOnlyColumnState,
|
|
13
|
+
getShowOnlyVisibility,
|
|
14
|
+
isShowingOnly,
|
|
15
|
+
isShowingOnlyColumns,
|
|
16
|
+
useColumnVisibility,
|
|
17
|
+
} from "../hooks/use-column-visibility";
|
|
18
|
+
import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
|
|
19
|
+
|
|
20
|
+
type Row = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
const TEST_COLUMNS: ColumnDef<Row>[] = [
|
|
23
|
+
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID, enableHiding: false },
|
|
24
|
+
{
|
|
25
|
+
id: INDEX_COLUMN_NAME,
|
|
26
|
+
accessorKey: INDEX_COLUMN_NAME,
|
|
27
|
+
enableHiding: false,
|
|
28
|
+
},
|
|
29
|
+
{ id: "customer_name", accessorKey: "customer_name" },
|
|
30
|
+
{ id: "cust_age", accessorKey: "cust_age" },
|
|
31
|
+
{ id: "order_total", accessorKey: "order_total" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function createTestTable({
|
|
35
|
+
initiallyHidden = [],
|
|
36
|
+
nonHideable = [],
|
|
37
|
+
}: {
|
|
38
|
+
initiallyHidden?: string[];
|
|
39
|
+
nonHideable?: string[];
|
|
40
|
+
} = {}) {
|
|
41
|
+
const { result } = renderHook(() =>
|
|
42
|
+
useReactTable<Row>({
|
|
43
|
+
data: [],
|
|
44
|
+
columns: TEST_COLUMNS.map((column) =>
|
|
45
|
+
nonHideable.includes(column.id as string)
|
|
46
|
+
? { ...column, enableHiding: false }
|
|
47
|
+
: column,
|
|
48
|
+
),
|
|
49
|
+
getCoreRowModel: getCoreRowModel(),
|
|
50
|
+
locale: "en-US",
|
|
51
|
+
initialState: {
|
|
52
|
+
columnVisibility: Object.fromEntries(
|
|
53
|
+
initiallyHidden.map((id) => [id, false]),
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
return result.current;
|
|
59
|
+
}
|
|
6
60
|
|
|
7
61
|
describe("useColumnVisibility", () => {
|
|
8
62
|
it("should initialize with correct default values", () => {
|
|
@@ -40,3 +94,101 @@ describe("useColumnVisibility", () => {
|
|
|
40
94
|
expect(result.current.columnVisibility).toEqual({ a: false, c: false });
|
|
41
95
|
});
|
|
42
96
|
});
|
|
97
|
+
|
|
98
|
+
describe("getShowOnlyVisibility", () => {
|
|
99
|
+
it("shows matching hideable columns and hides the rest", () => {
|
|
100
|
+
const table = createTestTable({ initiallyHidden: ["cust_age"] });
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
getShowOnlyVisibility(table, ["customer_name", "order_total"]),
|
|
104
|
+
).toEqual({
|
|
105
|
+
customer_name: true,
|
|
106
|
+
cust_age: false,
|
|
107
|
+
order_total: true,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("ignores non-hideable columns in the input", () => {
|
|
112
|
+
const table = createTestTable({ nonHideable: ["customer_name"] });
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
getShowOnlyVisibility(table, [
|
|
116
|
+
SELECT_COLUMN_ID,
|
|
117
|
+
"customer_name",
|
|
118
|
+
"cust_age",
|
|
119
|
+
]),
|
|
120
|
+
).toEqual({
|
|
121
|
+
cust_age: true,
|
|
122
|
+
order_total: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("hides every hideable column when the input is empty", () => {
|
|
127
|
+
const table = createTestTable();
|
|
128
|
+
|
|
129
|
+
expect(getShowOnlyVisibility(table, [])).toEqual({
|
|
130
|
+
customer_name: false,
|
|
131
|
+
cust_age: false,
|
|
132
|
+
order_total: false,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("isShowingOnly", () => {
|
|
138
|
+
it("returns true when visible hideable columns match the input", () => {
|
|
139
|
+
const table = createTestTable({
|
|
140
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(isShowingOnly(table, ["customer_name"])).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns false when extra hideable columns remain visible", () => {
|
|
147
|
+
const table = createTestTable();
|
|
148
|
+
|
|
149
|
+
expect(isShowingOnly(table, ["customer_name"])).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("ignores non-hideable columns in the input", () => {
|
|
153
|
+
const table = createTestTable({
|
|
154
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
155
|
+
nonHideable: ["customer_name"],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(isShowingOnly(table, [SELECT_COLUMN_ID, "customer_name"])).toBe(
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns true when every hideable column is hidden and the input is empty", () => {
|
|
164
|
+
const table = createTestTable({
|
|
165
|
+
initiallyHidden: ["customer_name", "cust_age", "order_total"],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(isShowingOnly(table, [])).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("isShowingOnlyColumns", () => {
|
|
173
|
+
it("reuses precomputed visible hideable columns", () => {
|
|
174
|
+
const table = createTestTable({
|
|
175
|
+
initiallyHidden: ["cust_age", "order_total"],
|
|
176
|
+
});
|
|
177
|
+
const state = getShowOnlyColumnState(table);
|
|
178
|
+
|
|
179
|
+
expect(isShowingOnlyColumns(state, ["customer_name"])).toBe(true);
|
|
180
|
+
expect(isShowingOnly(table, ["customer_name"])).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("applyShowOnlyColumns", () => {
|
|
185
|
+
it("shows only the requested hideable columns", () => {
|
|
186
|
+
const table = createTestTable({ initiallyHidden: ["cust_age"] });
|
|
187
|
+
|
|
188
|
+
act(() => {
|
|
189
|
+
applyShowOnlyColumns(table, ["customer_name", "order_total"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(isShowingOnly(table, ["customer_name", "order_total"])).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
EyeIcon,
|
|
11
11
|
EyeOffIcon,
|
|
12
12
|
} from "lucide-react";
|
|
13
|
-
import { useState } from "react";
|
|
13
|
+
import { useMemo, useState } from "react";
|
|
14
14
|
import { useLocale } from "react-aria";
|
|
15
15
|
import {
|
|
16
16
|
AddDataframeChart,
|
|
@@ -52,8 +52,12 @@ import type { Column, Table } from "@tanstack/react-table";
|
|
|
52
52
|
import { cn } from "@/utils/cn";
|
|
53
53
|
import {
|
|
54
54
|
getColumnCountForDisplay,
|
|
55
|
+
getShowOnlyColumnState,
|
|
55
56
|
getUserColumnVisibilityCounts,
|
|
57
|
+
isShowingOnlyColumns,
|
|
58
|
+
type ShowOnlyColumnState,
|
|
56
59
|
} from "../hooks/use-column-visibility";
|
|
60
|
+
import { ShowOnlyColumnButton } from "../show-only-column-button";
|
|
57
61
|
|
|
58
62
|
interface ColumnExplorerPanelProps<TData> {
|
|
59
63
|
previewColumn: PreviewColumn;
|
|
@@ -94,6 +98,11 @@ export function ColumnExplorerPanel<TData>({
|
|
|
94
98
|
hiddenColumns: hiddenColumnCount,
|
|
95
99
|
} = getColumnCountForDisplay(table, totalColumns);
|
|
96
100
|
const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
|
|
101
|
+
const columnVisibility = table.getState().columnVisibility;
|
|
102
|
+
const showOnlyColumnState = useMemo(
|
|
103
|
+
() => getShowOnlyColumnState(table),
|
|
104
|
+
[table, columnVisibility],
|
|
105
|
+
);
|
|
97
106
|
|
|
98
107
|
const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
|
|
99
108
|
numRows: totalRows,
|
|
@@ -162,6 +171,8 @@ export function ColumnExplorerPanel<TData>({
|
|
|
162
171
|
dataType={dataType}
|
|
163
172
|
externalType={externalType}
|
|
164
173
|
previewColumn={previewColumn}
|
|
174
|
+
table={table}
|
|
175
|
+
showOnlyColumnState={showOnlyColumnState}
|
|
165
176
|
defaultExpanded={index === 0}
|
|
166
177
|
/>
|
|
167
178
|
);
|
|
@@ -179,6 +190,8 @@ function ColumnItem<TData>({
|
|
|
179
190
|
dataType,
|
|
180
191
|
externalType,
|
|
181
192
|
previewColumn,
|
|
193
|
+
table,
|
|
194
|
+
showOnlyColumnState,
|
|
182
195
|
defaultExpanded = false,
|
|
183
196
|
}: {
|
|
184
197
|
columnName: string;
|
|
@@ -186,6 +199,8 @@ function ColumnItem<TData>({
|
|
|
186
199
|
dataType: DataType;
|
|
187
200
|
externalType: string;
|
|
188
201
|
previewColumn: PreviewColumn;
|
|
202
|
+
table: Table<TData>;
|
|
203
|
+
showOnlyColumnState: ShowOnlyColumnState;
|
|
189
204
|
defaultExpanded?: boolean;
|
|
190
205
|
}) {
|
|
191
206
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
@@ -218,36 +233,50 @@ function ColumnItem<TData>({
|
|
|
218
233
|
)}
|
|
219
234
|
/>
|
|
220
235
|
{column?.getCanHide() && (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
size="icon"
|
|
229
|
-
aria-label={
|
|
230
|
-
column.getIsVisible() ? "Hide column" : "Show column"
|
|
231
|
-
}
|
|
236
|
+
<>
|
|
237
|
+
<ShowOnlyColumnButton
|
|
238
|
+
table={table}
|
|
239
|
+
columnIds={[columnName]}
|
|
240
|
+
disabled={isShowingOnlyColumns(showOnlyColumnState, [
|
|
241
|
+
columnName,
|
|
242
|
+
])}
|
|
232
243
|
className={cn(
|
|
233
|
-
"hover:bg-muted text-muted-foreground hover:text-primary",
|
|
234
244
|
column.getIsVisible()
|
|
235
245
|
? "group-hover:opacity-100 opacity-0"
|
|
236
246
|
: "opacity-100",
|
|
237
247
|
)}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}}
|
|
248
|
+
/>
|
|
249
|
+
<Tooltip
|
|
250
|
+
content={column.getIsVisible() ? "Hide column" : "Show column"}
|
|
251
|
+
delayDuration={400}
|
|
243
252
|
>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
<Button
|
|
254
|
+
type="button"
|
|
255
|
+
variant="text"
|
|
256
|
+
size="icon"
|
|
257
|
+
aria-label={
|
|
258
|
+
column.getIsVisible() ? "Hide column" : "Show column"
|
|
259
|
+
}
|
|
260
|
+
className={cn(
|
|
261
|
+
"hover:bg-muted text-muted-foreground hover:text-primary",
|
|
262
|
+
column.getIsVisible()
|
|
263
|
+
? "group-hover:opacity-100 opacity-0"
|
|
264
|
+
: "opacity-100",
|
|
265
|
+
)}
|
|
266
|
+
onClick={(e) => {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
e.stopPropagation();
|
|
269
|
+
column.toggleVisibility(!column.getIsVisible());
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
{column.getIsVisible() ? (
|
|
273
|
+
<EyeIcon className="h-3 w-3" strokeWidth={2.5} />
|
|
274
|
+
) : (
|
|
275
|
+
<EyeOffIcon className="h-3 w-3" strokeWidth={2.5} />
|
|
276
|
+
)}
|
|
277
|
+
</Button>
|
|
278
|
+
</Tooltip>
|
|
279
|
+
</>
|
|
251
280
|
)}
|
|
252
281
|
<span className="text-xs text-muted-foreground">{externalType}</span>
|
|
253
282
|
</div>
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// https://github.com/TanStack/table/issues/5567
|
|
6
6
|
|
|
7
7
|
import type { Table } from "@tanstack/react-table";
|
|
8
|
-
import { Columns3Icon, EyeIcon, EyeOffIcon } from "lucide-react";
|
|
9
|
-
import React from "react";
|
|
8
|
+
import { Columns3Icon, EyeIcon, EyeOffIcon, ScanEyeIcon } from "lucide-react";
|
|
9
|
+
import React, { useMemo } from "react";
|
|
10
10
|
import { ColumnName } from "@/components/datasources/components";
|
|
11
11
|
import { Button } from "@/components/ui/button";
|
|
12
12
|
import {
|
|
@@ -28,6 +28,11 @@ import { cn } from "@/utils/cn";
|
|
|
28
28
|
import { Events } from "@/utils/events";
|
|
29
29
|
import { smartMatchFilter } from "@/utils/smartMatch";
|
|
30
30
|
import { NAMELESS_COLUMN_PREFIX } from "./columns";
|
|
31
|
+
import {
|
|
32
|
+
applyShowOnlyColumns,
|
|
33
|
+
isShowingOnlyColumns,
|
|
34
|
+
} from "./hooks/use-column-visibility";
|
|
35
|
+
import { ShowOnlyColumnButton } from "./show-only-column-button";
|
|
31
36
|
import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "./types";
|
|
32
37
|
|
|
33
38
|
function getUserColumns<TData>(table: Table<TData>) {
|
|
@@ -95,6 +100,23 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
95
100
|
> =>
|
|
96
101
|
action.kind === "select-matching" || action.kind === "deselect-matching",
|
|
97
102
|
);
|
|
103
|
+
const showOnlyMatchIds =
|
|
104
|
+
list.searchQuery !== ""
|
|
105
|
+
? list.visibleOptions
|
|
106
|
+
.filter((option) => !option.disabled)
|
|
107
|
+
.map((option) => option.value)
|
|
108
|
+
: [];
|
|
109
|
+
const showOnlyColumnState = useMemo(
|
|
110
|
+
() => ({
|
|
111
|
+
visibleHideableColumnIds: hideableIds.filter(
|
|
112
|
+
(id) => !hiddenIds.includes(id),
|
|
113
|
+
),
|
|
114
|
+
hideableColumnIdSet: new Set(hideableIds),
|
|
115
|
+
}),
|
|
116
|
+
[hideableIds, hiddenIds],
|
|
117
|
+
);
|
|
118
|
+
const hasSearchBulkActions =
|
|
119
|
+
showOnlyMatchIds.length > 0 || matchingActions.length > 0;
|
|
98
120
|
|
|
99
121
|
return (
|
|
100
122
|
<Popover open={list.open} onOpenChange={list.setOpen}>
|
|
@@ -145,8 +167,25 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
145
167
|
<CommandSeparator />
|
|
146
168
|
</>
|
|
147
169
|
) : (
|
|
148
|
-
|
|
170
|
+
hasSearchBulkActions && (
|
|
149
171
|
<>
|
|
172
|
+
<CommandItem
|
|
173
|
+
value="__show_only_matching__"
|
|
174
|
+
disabled={
|
|
175
|
+
showOnlyMatchIds.length === 0 ||
|
|
176
|
+
isShowingOnlyColumns(
|
|
177
|
+
showOnlyColumnState,
|
|
178
|
+
showOnlyMatchIds,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
onSelect={() => {
|
|
182
|
+
applyShowOnlyColumns(table, showOnlyMatchIds);
|
|
183
|
+
}}
|
|
184
|
+
className="cursor-pointer"
|
|
185
|
+
>
|
|
186
|
+
<ScanEyeIcon className="w-3 h-3 mr-1.5" />
|
|
187
|
+
Show only {showOnlyMatchIds.length} matching
|
|
188
|
+
</CommandItem>
|
|
150
189
|
{matchingActions.map((action) => (
|
|
151
190
|
<CommandItem
|
|
152
191
|
key={action.kind}
|
|
@@ -194,17 +233,26 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
194
233
|
/>
|
|
195
234
|
)}
|
|
196
235
|
{!option.disabled && (
|
|
197
|
-
<span
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
<span className="ml-auto flex items-center gap-0.5">
|
|
237
|
+
<ShowOnlyColumnButton
|
|
238
|
+
table={table}
|
|
239
|
+
columnIds={[option.value]}
|
|
240
|
+
disabled={isShowingOnlyColumns(showOnlyColumnState, [
|
|
241
|
+
option.value,
|
|
242
|
+
])}
|
|
243
|
+
iconClassName="w-3 h-3"
|
|
244
|
+
/>
|
|
245
|
+
<span
|
|
246
|
+
className={cn(
|
|
247
|
+
hidden ? "text-primary" : "text-muted-foreground",
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
{hidden ? (
|
|
251
|
+
<EyeOffIcon className="w-3 h-3" />
|
|
252
|
+
) : (
|
|
253
|
+
<EyeIcon className="w-3 h-3" />
|
|
254
|
+
)}
|
|
255
|
+
</span>
|
|
208
256
|
</span>
|
|
209
257
|
)}
|
|
210
258
|
</CommandItem>
|