/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; // tanstack/table is not compatible with React compiler // https://github.com/TanStack/table/issues/5567 import type { Table } from "@tanstack/react-table"; import { Columns3Icon, EyeIcon, EyeOffIcon, ScanEyeIcon } from "lucide-react"; import React, { useMemo } from "react"; import { ColumnName } from "@/components/datasources/components"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { type BulkAction, useSelectList } from "@/components/ui/select-core"; import type { DataType } from "@/core/kernel/messages"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; import { smartMatchFilter } from "@/utils/smartMatch"; import { NAMELESS_COLUMN_PREFIX } from "./columns"; import { applyShowOnlyColumns, isShowingOnlyColumns, } from "./hooks/use-column-visibility"; import { ShowOnlyColumnButton } from "./show-only-column-button"; import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "./types"; function getUserColumns(table: Table) { return table .getAllLeafColumns() .filter( (column) => column.id !== SELECT_COLUMN_ID && column.id !== INDEX_COLUMN_NAME && !column.id.startsWith(NAMELESS_COLUMN_PREFIX), ); } export const ColumnVisibilityDropdown = ({ table, }: { table: Table; }) => { const userColumns = getUserColumns(table); const options = userColumns.map((column) => ({ value: column.id, label: column.id, disabled: !column.getCanHide(), data: { dataType: column.columnDef.meta?.dataType }, })); // Modeled as a select list over hidden columns: "selected" means hidden, so // the hook's pinning floats hidden columns to the top and freezes that order // while the menu is open. const hiddenIds = userColumns .filter((column) => !column.getIsVisible()) .map((column) => column.id); const hideableIds = userColumns .filter((column) => column.getCanHide()) .map((column) => column.id); const visibleHideableCount = hideableIds.filter( (id) => !hiddenIds.includes(id), ).length; const applyHidden = (next: string[] | string | null) => { const hidden = new Set(Array.isArray(next) ? next : []); table.setColumnVisibility((previous) => ({ ...previous, ...Object.fromEntries( userColumns.map((column) => [column.id, !hidden.has(column.id)]), ), })); }; const list = useSelectList({ options, value: hiddenIds, onChange: applyHidden, multiple: true, filterFn: smartMatchFilter, pinSelected: true, }); // With selection modeling hidden columns, select-matching hides the visible // matches and deselect-matching shows the hidden ones. const matchingActions = list.bulkActions.filter( ( action, ): action is Extract< BulkAction, { kind: "select-matching" | "deselect-matching" } > => action.kind === "select-matching" || action.kind === "deselect-matching", ); const showOnlyMatchIds = list.searchQuery !== "" ? list.visibleOptions .filter((option) => !option.disabled) .map((option) => option.value) : []; const showOnlyColumnState = useMemo( () => ({ visibleHideableColumnIds: hideableIds.filter( (id) => !hiddenIds.includes(id), ), hideableColumnIdSet: new Set(hideableIds), }), [hideableIds, hiddenIds], ); const hasSearchBulkActions = showOnlyMatchIds.length > 0 || matchingActions.length > 0; return ( No results. {list.searchQuery === "" ? ( <> applyHidden([])} className="cursor-pointer" > Show all applyHidden(hideableIds)} className="cursor-pointer" > Hide all ) : ( hasSearchBulkActions && ( <> { applyShowOnlyColumns(table, showOnlyMatchIds); }} className="cursor-pointer" > Show only {showOnlyMatchIds.length} matching {matchingActions.map((action) => ( {action.kind === "select-matching" ? ( ) : ( )} {action.kind === "select-matching" ? "Hide" : "Show"}{" "} {action.items.length} matching ))} ) )} {list.visibleOptions.map((option, index) => { const hidden = list.isChecked(option.value); const { dataType } = option.data as { dataType: DataType | undefined; }; const isSectionBoundary = index === list.pinnedCount && list.pinnedCount > 0 && list.pinnedCount < list.visibleOptions.length; const label = ( {option.label} ); return ( {isSectionBoundary && } list.toggle(option.value)} className="flex items-center gap-1.5 cursor-pointer" > {dataType === undefined ? ( label ) : ( )} {!option.disabled && ( )} ); })} ); };