@marimo-team/islands 0.23.9-dev4 → 0.23.9-dev5
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/{code-visibility-VZebNmSs.js → code-visibility-DY2PjKMU.js} +783 -705
- package/dist/main.js +842 -840
- package/dist/{reveal-component-DZtPMEoM.js → reveal-component-Bq1iwOTq.js} +1 -1
- package/package.json +1 -1
- package/src/components/data-table/TableBottomBar.tsx +30 -6
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +73 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +52 -1
- package/src/components/data-table/__tests__/header-items.test.tsx +47 -1
- package/src/components/data-table/__tests__/useColumnVisibility.test.ts +42 -0
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +7 -1
- package/src/components/data-table/column-header.tsx +2 -0
- package/src/components/data-table/columns.tsx +3 -4
- package/src/components/data-table/data-table.tsx +30 -0
- package/src/components/data-table/header-items.tsx +18 -1
- package/src/components/data-table/hooks/use-column-visibility.ts +42 -0
- package/src/components/data-table/pagination.tsx +16 -3
- package/src/plugins/impl/DataTablePlugin.tsx +4 -0
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { ct as kioskModeAtom } from "./html-to-image-CiSinpSR.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BNjes6Yx.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-C1BYNeCR.js";
|
|
12
|
-
import { G as PanelGroup, Gt as Code, Ht as Expand, K as PanelResizeHandle, Vt as EyeOff, W as Panel, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-
|
|
12
|
+
import { G as PanelGroup, Gt as Code, Ht as Expand, K as PanelResizeHandle, Vt as EyeOff, W as Panel, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-DY2PjKMU.js";
|
|
13
13
|
import { q as useDebouncedCallback } from "./input-CZD2z6X2.js";
|
|
14
14
|
import "./toDate-ZVVIBmdk.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
package/package.json
CHANGED
|
@@ -7,8 +7,13 @@ import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
|
|
|
7
7
|
import { cn } from "@/utils/cn";
|
|
8
8
|
import { Events } from "@/utils/events";
|
|
9
9
|
import { prettyNumber } from "@/utils/numbers";
|
|
10
|
+
import {
|
|
11
|
+
PANEL_TYPES,
|
|
12
|
+
type PanelType,
|
|
13
|
+
} from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
|
|
10
14
|
import { Button } from "../ui/button";
|
|
11
15
|
import { toast } from "../ui/use-toast";
|
|
16
|
+
import { getUserColumnVisibilityCounts } from "./hooks/use-column-visibility";
|
|
12
17
|
import { DataTablePagination, prettifyRowColumnCount } from "./pagination";
|
|
13
18
|
import { CellSelectionStats } from "./range-focus/cell-selection-stats";
|
|
14
19
|
import type { DataTableSelection } from "./types";
|
|
@@ -22,6 +27,7 @@ interface TableBottomBarProps<TData> {
|
|
|
22
27
|
getRowIds?: GetRowIds;
|
|
23
28
|
showPageSizeSelector?: boolean;
|
|
24
29
|
tableLoading?: boolean;
|
|
30
|
+
togglePanel?: (panelType: PanelType) => void;
|
|
25
31
|
part?: string;
|
|
26
32
|
className?: string;
|
|
27
33
|
}
|
|
@@ -35,6 +41,7 @@ export const TableBottomBar = <TData,>({
|
|
|
35
41
|
getRowIds,
|
|
36
42
|
showPageSizeSelector,
|
|
37
43
|
tableLoading,
|
|
44
|
+
togglePanel,
|
|
38
45
|
part,
|
|
39
46
|
className,
|
|
40
47
|
}: TableBottomBarProps<TData>) => {
|
|
@@ -140,13 +147,30 @@ export const TableBottomBar = <TData,>({
|
|
|
140
147
|
);
|
|
141
148
|
}
|
|
142
149
|
|
|
150
|
+
const counts = getUserColumnVisibilityCounts(table);
|
|
151
|
+
// When columns are clipped, the table instance only has the rendered
|
|
152
|
+
// subset, so the visible/hidden math must use that subset's total. The
|
|
153
|
+
// dataset-wide `totalColumns` prop is only correct for the no-hidden
|
|
154
|
+
// "N columns" label.
|
|
155
|
+
const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
|
|
156
|
+
numRows: table.getRowCount(),
|
|
157
|
+
totalColumns: counts.hidden > 0 ? counts.total : totalColumns,
|
|
158
|
+
hiddenColumns: counts.hidden,
|
|
159
|
+
locale,
|
|
160
|
+
});
|
|
161
|
+
|
|
143
162
|
return (
|
|
144
|
-
<span>
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
<span className="flex items-center gap-1">
|
|
164
|
+
<span>{rowsAndColumns}</span>
|
|
165
|
+
{hiddenSuffix && (
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className="text-xs underline-offset-2 hover:underline cursor-pointer"
|
|
169
|
+
onClick={() => togglePanel?.(PANEL_TYPES.COLUMN_EXPLORER)}
|
|
170
|
+
>
|
|
171
|
+
{hiddenSuffix}
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
150
174
|
</span>
|
|
151
175
|
);
|
|
152
176
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
|
+
import { CellSelectionProvider } from "../range-focus/provider";
|
|
9
|
+
import { TableBottomBar } from "../TableBottomBar";
|
|
10
|
+
|
|
11
|
+
function renderWithTable(opts: {
|
|
12
|
+
totalColumns: number;
|
|
13
|
+
hiddenColumns?: string[];
|
|
14
|
+
togglePanel?: (panelType: string) => void;
|
|
15
|
+
}) {
|
|
16
|
+
const Wrapper = () => {
|
|
17
|
+
const table = useReactTable({
|
|
18
|
+
data: [] as Array<Record<string, unknown>>,
|
|
19
|
+
columns: Array.from({ length: opts.totalColumns }, (_, i) => ({
|
|
20
|
+
id: `col${i}`,
|
|
21
|
+
enableHiding: true,
|
|
22
|
+
})),
|
|
23
|
+
getCoreRowModel: getCoreRowModel(),
|
|
24
|
+
locale: "en-US",
|
|
25
|
+
state: {
|
|
26
|
+
columnVisibility: Object.fromEntries(
|
|
27
|
+
(opts.hiddenColumns ?? []).map((c) => [c, false]),
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TableBottomBar
|
|
34
|
+
pagination={false}
|
|
35
|
+
totalColumns={opts.totalColumns}
|
|
36
|
+
table={table}
|
|
37
|
+
togglePanel={opts.togglePanel}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return render(
|
|
43
|
+
<TooltipProvider>
|
|
44
|
+
<CellSelectionProvider>
|
|
45
|
+
<Wrapper />
|
|
46
|
+
</CellSelectionProvider>
|
|
47
|
+
</TooltipProvider>,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("TableBottomBar — hidden column count", () => {
|
|
52
|
+
it("does not render '(n hidden)' when no columns are hidden", () => {
|
|
53
|
+
renderWithTable({ totalColumns: 3 });
|
|
54
|
+
expect(screen.queryByText(/hidden/)).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders 'X visible (n hidden)' when columns are hidden", () => {
|
|
58
|
+
renderWithTable({ totalColumns: 3, hiddenColumns: ["col1"] });
|
|
59
|
+
expect(screen.getByText(/2 visible/)).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText(/\(1 hidden\)/)).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("invokes togglePanel('column-explorer') when '(n hidden)' is clicked", () => {
|
|
64
|
+
const togglePanel = vi.fn();
|
|
65
|
+
renderWithTable({
|
|
66
|
+
totalColumns: 3,
|
|
67
|
+
hiddenColumns: ["col1"],
|
|
68
|
+
togglePanel,
|
|
69
|
+
});
|
|
70
|
+
screen.getByText(/\(1 hidden\)/).click();
|
|
71
|
+
expect(togglePanel).toHaveBeenCalledWith("column-explorer");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
RowSelectionState,
|
|
6
6
|
SortingState,
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
|
-
import { render, screen, within } from "@testing-library/react";
|
|
8
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
9
|
import { describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
11
11
|
import { DataTable } from "../data-table";
|
|
@@ -251,3 +251,54 @@ describe("DataTable", () => {
|
|
|
251
251
|
expect(within(updatedRows[3]).getByText("pending")).toBeTruthy();
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
describe("DataTable — all-hidden banner", () => {
|
|
256
|
+
interface Row {
|
|
257
|
+
a: number;
|
|
258
|
+
b: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const columns: ColumnDef<Row>[] = [
|
|
262
|
+
{ accessorKey: "a", header: "A" },
|
|
263
|
+
{ accessorKey: "b", header: "B" },
|
|
264
|
+
];
|
|
265
|
+
const data: Row[] = [{ a: 1, b: 2 }];
|
|
266
|
+
|
|
267
|
+
const renderWithVisibility = (hiddenColumns: string[]) =>
|
|
268
|
+
render(
|
|
269
|
+
<TooltipProvider>
|
|
270
|
+
<DataTable
|
|
271
|
+
data={data}
|
|
272
|
+
columns={columns}
|
|
273
|
+
selection={null}
|
|
274
|
+
totalRows={1}
|
|
275
|
+
totalColumns={2}
|
|
276
|
+
pagination={false}
|
|
277
|
+
hiddenColumns={hiddenColumns}
|
|
278
|
+
/>
|
|
279
|
+
</TooltipProvider>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
it("renders banner when every user column is hidden", () => {
|
|
283
|
+
renderWithVisibility(["a", "b"]);
|
|
284
|
+
expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
|
|
285
|
+
expect(screen.getByText(/Unhide all/i)).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("does not render the banner when at least one column is visible", () => {
|
|
289
|
+
renderWithVisibility(["a"]);
|
|
290
|
+
expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("does not render the banner when no columns are hidden", () => {
|
|
294
|
+
renderWithVisibility([]);
|
|
295
|
+
expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("'Unhide all' restores columns hidden via the Python kwarg", () => {
|
|
299
|
+
renderWithVisibility(["a", "b"]);
|
|
300
|
+
expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
|
|
301
|
+
fireEvent.click(screen.getByText(/Unhide all/i));
|
|
302
|
+
expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type { SortingState } from "@tanstack/react-table";
|
|
3
|
+
import type { Column, SortingState } from "@tanstack/react-table";
|
|
4
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@/components/ui/dropdown-menu";
|
|
11
|
+
import { HideColumn } from "../header-items";
|
|
5
12
|
|
|
6
13
|
describe("multi-column sorting logic", () => {
|
|
7
14
|
// Extract the core sorting logic to test in isolation
|
|
@@ -146,3 +153,42 @@ describe("multi-column sorting logic", () => {
|
|
|
146
153
|
// After removal, dept should move from priority 3 to priority 2
|
|
147
154
|
});
|
|
148
155
|
});
|
|
156
|
+
|
|
157
|
+
describe("HideColumn", () => {
|
|
158
|
+
const makeColumn = ({
|
|
159
|
+
canHide = true,
|
|
160
|
+
toggleVisibility = vi.fn(),
|
|
161
|
+
}: {
|
|
162
|
+
canHide?: boolean;
|
|
163
|
+
toggleVisibility?: (value?: boolean) => void;
|
|
164
|
+
} = {}) =>
|
|
165
|
+
({
|
|
166
|
+
getCanHide: () => canHide,
|
|
167
|
+
toggleVisibility,
|
|
168
|
+
}) as unknown as Column<unknown, unknown>;
|
|
169
|
+
|
|
170
|
+
const renderInMenu = (node: React.ReactNode) =>
|
|
171
|
+
render(
|
|
172
|
+
<DropdownMenu open={true}>
|
|
173
|
+
<DropdownMenuTrigger />
|
|
174
|
+
<DropdownMenuContent>{node}</DropdownMenuContent>
|
|
175
|
+
</DropdownMenu>,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
it("renders 'Hide column' when canHide is true", () => {
|
|
179
|
+
renderInMenu(<HideColumn column={makeColumn()} />);
|
|
180
|
+
expect(screen.getByText("Hide column")).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns null when getCanHide is false", () => {
|
|
184
|
+
renderInMenu(<HideColumn column={makeColumn({ canHide: false })} />);
|
|
185
|
+
expect(screen.queryByText("Hide column")).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("calls toggleVisibility(false) on click", () => {
|
|
189
|
+
const toggleVisibility = vi.fn();
|
|
190
|
+
renderInMenu(<HideColumn column={makeColumn({ toggleVisibility })} />);
|
|
191
|
+
fireEvent.click(screen.getByText("Hide column"));
|
|
192
|
+
expect(toggleVisibility).toHaveBeenCalledWith(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { useColumnVisibility } from "../hooks/use-column-visibility";
|
|
6
|
+
|
|
7
|
+
describe("useColumnVisibility", () => {
|
|
8
|
+
it("should initialize with correct default values", () => {
|
|
9
|
+
const { result } = renderHook(() => useColumnVisibility());
|
|
10
|
+
expect(result.current.columnVisibility).toEqual({});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should seed hidden columns as { name: false }", () => {
|
|
14
|
+
const { result } = renderHook(() => useColumnVisibility(["a", "b"]));
|
|
15
|
+
expect(result.current.columnVisibility).toEqual({ a: false, b: false });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should treat empty hidden list as a no-op", () => {
|
|
19
|
+
const { result } = renderHook(() => useColumnVisibility([]));
|
|
20
|
+
expect(result.current.columnVisibility).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should update visibility state via setter", () => {
|
|
24
|
+
const { result } = renderHook(() => useColumnVisibility(["a"]));
|
|
25
|
+
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.setColumnVisibility({ a: true, b: false });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.current.columnVisibility).toEqual({ a: true, b: false });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle functional updates", () => {
|
|
34
|
+
const { result } = renderHook(() => useColumnVisibility(["a"]));
|
|
35
|
+
|
|
36
|
+
act(() => {
|
|
37
|
+
result.current.setColumnVisibility((prev) => ({ ...prev, c: false }));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.current.columnVisibility).toEqual({ a: false, c: false });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -71,10 +71,16 @@ export const ColumnExplorerPanel = ({
|
|
|
71
71
|
return columnName.toLowerCase().includes(searchValue.toLowerCase());
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
const rowColumnHiddenStr = prettifyRowColumnCount({
|
|
75
|
+
numRows: totalRows,
|
|
76
|
+
totalColumns,
|
|
77
|
+
locale,
|
|
78
|
+
}).rowsAndColumns;
|
|
79
|
+
|
|
74
80
|
return (
|
|
75
81
|
<div className="mb-3">
|
|
76
82
|
<span className="text-xs font-semibold ml-2 flex">
|
|
77
|
-
{
|
|
83
|
+
{rowColumnHiddenStr}
|
|
78
84
|
<CopyClipboardIcon
|
|
79
85
|
tooltip="Copy column names"
|
|
80
86
|
value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
|
|
@@ -17,6 +17,7 @@ import { useFilterEditor } from "./filter-editor-context";
|
|
|
17
17
|
import { EDITABLE_FILTER_TYPES, isMembershipFilterType } from "./filters";
|
|
18
18
|
import {
|
|
19
19
|
ClearFilterMenuItem,
|
|
20
|
+
HideColumn,
|
|
20
21
|
renderColumnPinning,
|
|
21
22
|
renderColumnWrapping,
|
|
22
23
|
renderCopyColumn,
|
|
@@ -124,6 +125,7 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
124
125
|
{renderColumnPinning(column)}
|
|
125
126
|
{renderColumnWrapping(column)}
|
|
126
127
|
{renderFormatOptions(column, locale)}
|
|
128
|
+
<HideColumn column={column} />
|
|
127
129
|
{canEditFilter && <DropdownMenuSeparator />}
|
|
128
130
|
{canEditFilter && (
|
|
129
131
|
<DropdownMenuItem
|
|
@@ -44,9 +44,10 @@ import { detectSentinel, splitLeadingTrailingWhitespace } from "./utils";
|
|
|
44
44
|
import { uniformSample } from "./uniformSample";
|
|
45
45
|
import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
|
|
46
46
|
|
|
47
|
+
export const NAMELESS_COLUMN_PREFIX = "__m_column__";
|
|
47
48
|
// Artificial limit to display long strings
|
|
49
|
+
export const SELECT_ID = "__select__";
|
|
48
50
|
const MAX_STRING_LENGTH = 50;
|
|
49
|
-
const SELECT_ID = "__select__";
|
|
50
51
|
|
|
51
52
|
function inferDataType(value: unknown): [type: DataType, displayType: string] {
|
|
52
53
|
if (typeof value === "string") {
|
|
@@ -106,8 +107,6 @@ export function inferFieldTypes<T>(items: T[]): FieldTypesWithExternalType {
|
|
|
106
107
|
return Objects.entries(fieldTypes);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
export const NAMELESS_COLUMN_PREFIX = "__m_column__";
|
|
110
|
-
|
|
111
110
|
export function generateColumns<T>({
|
|
112
111
|
rowHeaders,
|
|
113
112
|
selection,
|
|
@@ -192,7 +191,7 @@ export function generateColumns<T>({
|
|
|
192
191
|
accessorFn: (row) => {
|
|
193
192
|
return row[key as keyof T];
|
|
194
193
|
},
|
|
195
|
-
|
|
194
|
+
enableHiding: !rowHeadersSet.has(key) && key !== "",
|
|
196
195
|
header: ({ column, table }) => {
|
|
197
196
|
const stats = chartSpecModel?.getColumnStats(key);
|
|
198
197
|
const dtype = column.columnDef.meta?.dtype;
|
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
import React, { memo } from "react";
|
|
23
23
|
import { useLocale } from "react-aria";
|
|
24
24
|
|
|
25
|
+
import { Button } from "@/components/ui/button";
|
|
25
26
|
import { Table } from "@/components/ui/table";
|
|
27
|
+
import { Banner } from "@/plugins/impl/common/error-banner";
|
|
26
28
|
import type {
|
|
27
29
|
CalculateTopKRows,
|
|
28
30
|
GetRowIds,
|
|
@@ -63,6 +65,10 @@ import {
|
|
|
63
65
|
type TooManyRows,
|
|
64
66
|
} from "./types";
|
|
65
67
|
import { getStableRowId } from "./utils";
|
|
68
|
+
import {
|
|
69
|
+
getUserColumnVisibilityCounts,
|
|
70
|
+
useColumnVisibility,
|
|
71
|
+
} from "./hooks/use-column-visibility";
|
|
66
72
|
|
|
67
73
|
interface DataTableProps<TData> extends Partial<ExportActionProps> {
|
|
68
74
|
wrapperClassName?: string;
|
|
@@ -107,6 +113,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
|
|
|
107
113
|
// Columns
|
|
108
114
|
freezeColumnsLeft?: string[];
|
|
109
115
|
freezeColumnsRight?: string[];
|
|
116
|
+
hiddenColumns?: string[];
|
|
110
117
|
toggleDisplayHeader?: () => void;
|
|
111
118
|
// Row viewer panel
|
|
112
119
|
viewedRowIdx?: number;
|
|
@@ -158,6 +165,7 @@ const DataTableInternal = <TData,>({
|
|
|
158
165
|
reloading,
|
|
159
166
|
freezeColumnsLeft,
|
|
160
167
|
freezeColumnsRight,
|
|
168
|
+
hiddenColumns,
|
|
161
169
|
toggleDisplayHeader,
|
|
162
170
|
showChartBuilder,
|
|
163
171
|
isChartBuilderOpen,
|
|
@@ -176,6 +184,8 @@ const DataTableInternal = <TData,>({
|
|
|
176
184
|
freezeColumnsLeft,
|
|
177
185
|
freezeColumnsRight,
|
|
178
186
|
);
|
|
187
|
+
const { columnVisibility, setColumnVisibility } =
|
|
188
|
+
useColumnVisibility(hiddenColumns);
|
|
179
189
|
|
|
180
190
|
// Show loading bar only after a short delay to prevent flickering
|
|
181
191
|
React.useEffect(() => {
|
|
@@ -267,6 +277,8 @@ const DataTableInternal = <TData,>({
|
|
|
267
277
|
enableMultiCellSelection: selection === "multi-cell",
|
|
268
278
|
// pinning
|
|
269
279
|
onColumnPinningChange: setColumnPinning,
|
|
280
|
+
// col visibility
|
|
281
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
270
282
|
// focus row
|
|
271
283
|
enableFocusRow: true,
|
|
272
284
|
onFocusRowChange: onViewedRowChange,
|
|
@@ -284,6 +296,7 @@ const DataTableInternal = <TData,>({
|
|
|
284
296
|
{ pagination: { pageIndex: 0, pageSize: data.length } }),
|
|
285
297
|
rowSelection: rowSelection ?? {},
|
|
286
298
|
cellSelection: cellSelection ?? [],
|
|
299
|
+
columnVisibility,
|
|
287
300
|
cellStyling,
|
|
288
301
|
columnPinning: columnPinning,
|
|
289
302
|
cellHoverTemplate: hoverTemplate,
|
|
@@ -317,6 +330,10 @@ const DataTableInternal = <TData,>({
|
|
|
317
330
|
[table],
|
|
318
331
|
);
|
|
319
332
|
|
|
333
|
+
const visibilityCounts = getUserColumnVisibilityCounts(table);
|
|
334
|
+
const allUserColumnsHidden =
|
|
335
|
+
visibilityCounts.total > 0 && visibilityCounts.visible === 0;
|
|
336
|
+
|
|
320
337
|
return (
|
|
321
338
|
<FilterEditorProvider value={filterEditor}>
|
|
322
339
|
<div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
|
|
@@ -346,6 +363,18 @@ const DataTableInternal = <TData,>({
|
|
|
346
363
|
downloadAs={downloadAs}
|
|
347
364
|
sizeBytes={sizeBytes}
|
|
348
365
|
/>
|
|
366
|
+
{allUserColumnsHidden && (
|
|
367
|
+
<Banner className="mb-1 mx-2 rounded flex items-center justify-between">
|
|
368
|
+
<span>All columns are hidden.</span>
|
|
369
|
+
<Button
|
|
370
|
+
variant="link"
|
|
371
|
+
size="xs"
|
|
372
|
+
onClick={() => table.resetColumnVisibility(true)}
|
|
373
|
+
>
|
|
374
|
+
Unhide all
|
|
375
|
+
</Button>
|
|
376
|
+
</Banner>
|
|
377
|
+
)}
|
|
349
378
|
<Table
|
|
350
379
|
className={cn(
|
|
351
380
|
"relative",
|
|
@@ -377,6 +406,7 @@ const DataTableInternal = <TData,>({
|
|
|
377
406
|
getRowIds={getRowIds}
|
|
378
407
|
showPageSizeSelector={showPageSizeSelector}
|
|
379
408
|
tableLoading={reloading}
|
|
409
|
+
togglePanel={togglePanel}
|
|
380
410
|
/>
|
|
381
411
|
</div>
|
|
382
412
|
</CellSelectionProvider>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ArrowUpNarrowWideIcon,
|
|
9
9
|
ChevronsUpDown,
|
|
10
10
|
CopyIcon,
|
|
11
|
+
EyeOffIcon,
|
|
11
12
|
FilterX,
|
|
12
13
|
PinOffIcon,
|
|
13
14
|
WrapTextIcon,
|
|
@@ -139,6 +140,23 @@ export function renderColumnPinning<TData, TValue>(
|
|
|
139
140
|
);
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
export function HideColumn<TData, TValue>({
|
|
144
|
+
column,
|
|
145
|
+
}: {
|
|
146
|
+
column: Column<TData, TValue>;
|
|
147
|
+
}) {
|
|
148
|
+
if (!column.getCanHide()) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
|
154
|
+
<EyeOffIcon className="mo-dropdown-icon" />
|
|
155
|
+
Hide column
|
|
156
|
+
</DropdownMenuItem>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
142
160
|
export function renderCopyColumn<TData, TValue>(column: Column<TData, TValue>) {
|
|
143
161
|
if (!column.getCanCopy?.()) {
|
|
144
162
|
return null;
|
|
@@ -233,7 +251,6 @@ export function renderSorts<TData, TValue>(
|
|
|
233
251
|
{sortDirection === "desc" && renderSortIndex()}
|
|
234
252
|
</DropdownMenuItem>
|
|
235
253
|
{renderClearSort()}
|
|
236
|
-
<DropdownMenuSeparator />
|
|
237
254
|
</>
|
|
238
255
|
);
|
|
239
256
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
|
|
5
|
+
import type { Table, VisibilityState } from "@tanstack/react-table";
|
|
6
|
+
import { dequal as isDeepEqual } from "dequal";
|
|
7
|
+
import type React from "react";
|
|
8
|
+
|
|
9
|
+
interface UseColumnVisibilityResult {
|
|
10
|
+
columnVisibility: VisibilityState;
|
|
11
|
+
setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useColumnVisibility(
|
|
15
|
+
hiddenColumns?: string[],
|
|
16
|
+
): UseColumnVisibilityResult {
|
|
17
|
+
const [columnVisibility, setColumnVisibility] =
|
|
18
|
+
useInternalStateWithSync<VisibilityState>(
|
|
19
|
+
Object.fromEntries((hiddenColumns ?? []).map((c) => [c, false])),
|
|
20
|
+
isDeepEqual,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return { columnVisibility, setColumnVisibility };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ColumnVisibilityCounts {
|
|
27
|
+
total: number;
|
|
28
|
+
visible: number;
|
|
29
|
+
hidden: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getUserColumnVisibilityCounts<TData>(
|
|
33
|
+
table: Table<TData>,
|
|
34
|
+
): ColumnVisibilityCounts {
|
|
35
|
+
const userColumns = table.getAllLeafColumns().filter((c) => c.getCanHide());
|
|
36
|
+
const visible = userColumns.filter((c) => c.getIsVisible()).length;
|
|
37
|
+
return {
|
|
38
|
+
total: userColumns.length,
|
|
39
|
+
visible,
|
|
40
|
+
hidden: userColumns.length - visible,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -443,15 +443,28 @@ export function prettifyRowCount(rowCount: number, locale: string): string {
|
|
|
443
443
|
export const prettifyRowColumnCount = ({
|
|
444
444
|
numRows,
|
|
445
445
|
totalColumns,
|
|
446
|
+
hiddenColumns,
|
|
446
447
|
locale,
|
|
447
448
|
}: {
|
|
448
449
|
numRows: number | "too_many";
|
|
449
450
|
totalColumns: number;
|
|
451
|
+
hiddenColumns?: number;
|
|
450
452
|
locale: string;
|
|
451
|
-
}): string => {
|
|
453
|
+
}): { rowsAndColumns: string; hiddenSuffix: string | null } => {
|
|
452
454
|
const rowsLabel =
|
|
453
455
|
numRows === "too_many" ? "Unknown" : prettifyRowCount(numRows, locale);
|
|
454
|
-
const columnsLabel = `${prettyNumber(totalColumns, locale)} ${new PluralWord("column").pluralize(totalColumns)}`;
|
|
455
456
|
|
|
456
|
-
|
|
457
|
+
const hidden = hiddenColumns ?? 0;
|
|
458
|
+
const visibleColumns = totalColumns - hidden;
|
|
459
|
+
|
|
460
|
+
const columnsLabel =
|
|
461
|
+
hidden > 0
|
|
462
|
+
? `${prettyNumber(visibleColumns, locale)} visible`
|
|
463
|
+
: `${prettyNumber(totalColumns, locale)} ${new PluralWord("column").pluralize(totalColumns)}`;
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
rowsAndColumns: [rowsLabel, columnsLabel].join(", "),
|
|
467
|
+
hiddenSuffix:
|
|
468
|
+
hidden > 0 ? `(${prettyNumber(hidden, locale)} hidden)` : null,
|
|
469
|
+
};
|
|
457
470
|
};
|
|
@@ -190,6 +190,7 @@ interface Data<T> {
|
|
|
190
190
|
fieldTypes?: FieldTypesWithExternalType | null;
|
|
191
191
|
freezeColumnsLeft?: string[];
|
|
192
192
|
freezeColumnsRight?: string[];
|
|
193
|
+
hiddenColumns?: string[];
|
|
193
194
|
textJustifyColumns?: Record<string, "left" | "center" | "right">;
|
|
194
195
|
wrappedColumns?: string[];
|
|
195
196
|
headerTooltip?: Record<string, string>;
|
|
@@ -265,6 +266,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
|
|
|
265
266
|
rowHeaders: columnToFieldTypesSchema,
|
|
266
267
|
freezeColumnsLeft: z.array(z.string()).optional(),
|
|
267
268
|
freezeColumnsRight: z.array(z.string()).optional(),
|
|
269
|
+
hiddenColumns: z.array(z.string()).optional(),
|
|
268
270
|
textJustifyColumns: z
|
|
269
271
|
.record(z.string(), z.enum(["left", "center", "right"]))
|
|
270
272
|
.optional(),
|
|
@@ -814,6 +816,7 @@ const DataTableComponent = ({
|
|
|
814
816
|
reloading,
|
|
815
817
|
freezeColumnsLeft,
|
|
816
818
|
freezeColumnsRight,
|
|
819
|
+
hiddenColumns,
|
|
817
820
|
textJustifyColumns,
|
|
818
821
|
wrappedColumns,
|
|
819
822
|
headerTooltip,
|
|
@@ -1090,6 +1093,7 @@ const DataTableComponent = ({
|
|
|
1090
1093
|
onRowSelectionChange={handleRowSelectionChange}
|
|
1091
1094
|
freezeColumnsLeft={freezeColumnsLeft}
|
|
1092
1095
|
freezeColumnsRight={freezeColumnsRight}
|
|
1096
|
+
hiddenColumns={hiddenColumns}
|
|
1093
1097
|
onCellSelectionChange={handleCellSelectionChange}
|
|
1094
1098
|
getRowIds={get_row_ids}
|
|
1095
1099
|
toggleDisplayHeader={toggleDisplayHeader}
|