From a96fb81647b7c788fc70349cb5f159bc4e2b99b8 Mon Sep 17 00:00:00 2001 From: ztocode Date: Tue, 2 Jun 2026 15:19:12 -0400 Subject: [PATCH] make re-add columns follow metadata order/update drag/hide ui --- src/assets/styles/routes/datasets.scss | 89 +++++--- src/components/partials/DatasetTable.jsx | 215 +++++++++--------- .../partials/DatasetTableContextMenu.jsx | 13 +- src/utils/datasetTablePreview.js | 75 ++++-- 4 files changed, 229 insertions(+), 163 deletions(-) diff --git a/src/assets/styles/routes/datasets.scss b/src/assets/styles/routes/datasets.scss index 091c2fa..453a0c8 100644 --- a/src/assets/styles/routes/datasets.scss +++ b/src/assets/styles/routes/datasets.scss @@ -1185,7 +1185,7 @@ .dataset-table-context-menu { position: fixed; z-index: 9999; - min-width: 10rem; + min-width: 14rem; padding: 0.25rem 0; background: #fff; border: 1px solid rgba(0, 0, 0, 0.12); @@ -1193,7 +1193,9 @@ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); &__item { - display: block; + display: flex; + align-items: center; + gap: 0.5rem; width: 100%; padding: 0.5rem 0.85rem; border: none; @@ -1202,10 +1204,30 @@ font-size: 0.875rem; cursor: pointer; - &:hover { + &:hover, + &:focus-visible { + cursor: pointer; background: $color_bg-light; } } + + &__icon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 1rem; + color: $color_font-medium; + cursor: pointer; + pointer-events: none; + } + + &__label { + flex: 1; + min-width: 0; + cursor: pointer; + pointer-events: none; + } } .table-wrapper { @@ -1473,57 +1495,50 @@ gap: 0.2rem; } - .sort-icon-wrap { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.35rem; - height: 1.35rem; - margin-left: 0; - } - - .sort-icon { - font-size: 0.8125rem; - line-height: 1; - color: $color_font-medium; - vertical-align: middle; - - svg { - display: block; - } - - &--inactive { - opacity: 0.45; - } - - &--active { - opacity: 0.9; - } - } - - .dataset-table__hide-btn { + .dataset-table__header-menu-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; border: none; + border-radius: 4px; background: transparent; color: $color_font-medium; - font-size: 1rem; - line-height: 1; width: 1.35rem; height: 1.35rem; padding: 0; cursor: pointer; - opacity: 0; - transition: opacity 0.15s ease; + opacity: 0.55; + transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease; + + &:hover, + &:focus-visible { + cursor: pointer; + } &:hover { + opacity: 1; + background: rgba($color_brand-secondary, 0.1); + color: $color_brand-secondary--dark; + } + + &:focus-visible { + outline: 2px solid $color_brand-secondary; + outline-offset: 1px; + opacity: 1; + } + + svg { + pointer-events: none; + } + + &--sorted { + opacity: 0.9; color: $color_brand-secondary--dark; } } - th:hover .dataset-table__hide-btn { + th:hover .dataset-table__header-menu-btn { opacity: 0.9; } diff --git a/src/components/partials/DatasetTable.jsx b/src/components/partials/DatasetTable.jsx index 8747606..1f296a2 100644 --- a/src/components/partials/DatasetTable.jsx +++ b/src/components/partials/DatasetTable.jsx @@ -1,12 +1,19 @@ import React from "react"; import PropTypes from "prop-types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowDown, faArrowUp, faArrowsUpDown, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowDown, + faArrowUp, + faChevronDown, + faEyeSlash, +} from "@fortawesome/free-solid-svg-icons"; import DataRow from "./DataRow"; import DatasetTableContextMenu from "./DatasetTableContextMenu"; import { applyPreviewRowOrder, getDatasetRowKey, + isVisibleColumnOrderCustom, + mergeVisibleColumnReorder, orderColumnKeys, reorderList, } from "../../utils/datasetTablePreview"; @@ -26,7 +33,7 @@ class DatasetTable extends React.Component { this.onPageNumberUpdate = this.onPageNumberUpdate.bind(this); this.onPageNumberBlur = this.onPageNumberBlur.bind(this); this.closeContextMenu = this.closeContextMenu.bind(this); - this.openColumnContextMenu = this.openColumnContextMenu.bind(this); + this.openColumnHeaderMenu = this.openColumnHeaderMenu.bind(this); } componentDidUpdate(prevProps) { @@ -39,25 +46,39 @@ class DatasetTable extends React.Component { this.setState({ contextMenu: null }); } - openColumnContextMenu(e, column) { + openColumnHeaderMenu(e, column) { e.preventDefault(); e.stopPropagation(); - const { updateSelectedColumns } = this.props; - if (!updateSelectedColumns) return; + const { sortColumn, sortDirection } = this.state; + const { updateSelectedColumns, selectedColumns } = this.props; + const rect = e.currentTarget.getBoundingClientRect(); + const isSorted = sortColumn === column.name; + const sortAscendingNext = !isSorted || sortDirection === "desc"; + + const items = [ + { + label: sortAscendingNext + ? "Sort this column in ascending order" + : "Sort this column in descending order", + icon: sortAscendingNext ? faArrowUp : faArrowDown, + onSelect: () => this.handleSort(column.name), + }, + ]; + + if (updateSelectedColumns && selectedColumns.includes(column.name)) { + items.push({ + label: "Hide this column", + icon: faEyeSlash, + onSelect: () => updateSelectedColumns(column.name), + }); + } + this.setState({ contextMenu: { - x: e.clientX, - y: e.clientY, - items: [ - { - label: "Hide column", - onSelect: () => { - if (this.props.selectedColumns.includes(column.name)) { - updateSelectedColumns(column.name); - } - }, - }, - ], + x: rect.left, + y: rect.bottom + 4, + columnName: column.name, + items, }, }); } @@ -79,28 +100,12 @@ class DatasetTable extends React.Component { }); } - getSortIcon(columnName) { + getHeaderMenuIcon(columnName) { const { sortColumn, sortDirection } = this.state; if (sortColumn !== columnName) { - return ; + return faChevronDown; } - return sortDirection === "asc" ? ( - - ) : ( - - ); - } - - getSortTooltip(columnName, columnAlias) { - const { sortColumn, sortDirection } = this.state; - const label = columnAlias || columnName; - if (sortColumn !== columnName) { - return `Sort ascending by ${label}`; - } - if (sortDirection === "asc") { - return `Sorted ascending by ${label}. Click to sort descending.`; - } - return `Sorted descending by ${label}. Click to sort ascending.`; + return sortDirection === "asc" ? faArrowUp : faArrowDown; } getAriaSort(columnName) { @@ -109,12 +114,53 @@ class DatasetTable extends React.Component { return sortDirection === "asc" ? "ascending" : "descending"; } - handleHideColumnClick(e, column) { - e.preventDefault(); - e.stopPropagation(); - const { updateSelectedColumns, selectedColumns } = this.props; - if (!updateSelectedColumns || !selectedColumns.includes(column.name)) return; - updateSelectedColumns(column.name); + renderVisibleHeader(header, visibleIndex, columnNames) { + const { sortColumn } = this.state; + const isSorted = sortColumn === header.name; + + return ( + this.handleColumnDrop(e, visibleIndex, columnNames)} + > +
+ this.handleColumnDragStart(e, visibleIndex)} + onDragEnd={() => this.setState({ dragColumnIndex: null })} + onMouseDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + title="Drag to reorder column" + aria-label="Drag to reorder column" + /> + {header.alias} +
+ +
+
+ + ); + } + + setTableHeaders(orderedColumnKeys) { + const columnNames = orderedColumnKeys.map((col) => col.name); + return orderedColumnKeys.map((header, index) => + this.renderVisibleHeader(header, index, columnNames), + ); } handleColumnDragStart(e, index) { @@ -134,14 +180,18 @@ class DatasetTable extends React.Component { const { previewColumnOrder, onPreviewColumnOrderChange } = this.props; if (dragColumnIndex == null || !onPreviewColumnOrderChange || !columnNames?.length) return; - const order = previewColumnOrder?.length + const visibleOrder = previewColumnOrder?.length ? previewColumnOrder.filter((name) => columnNames.includes(name)) : [...columnNames]; columnNames.forEach((name) => { - if (!order.includes(name)) order.push(name); + if (!visibleOrder.includes(name)) visibleOrder.push(name); }); - onPreviewColumnOrderChange(reorderList(order, dragColumnIndex, toIndex)); + const reorderedVisible = reorderList(visibleOrder, dragColumnIndex, toIndex); + const fullOrder = previewColumnOrder?.length ? previewColumnOrder : [...columnNames]; + const nextOrder = mergeVisibleColumnReorder(fullOrder, columnNames, reorderedVisible); + + onPreviewColumnOrderChange(nextOrder); this.setState({ dragColumnIndex: null }); } @@ -171,50 +221,6 @@ class DatasetTable extends React.Component { this.setState({ dragRowIndex: null }); } - setTableHeaders(orderedColumnKeys) { - const columnNames = orderedColumnKeys.map((col) => col.name); - return orderedColumnKeys.map((header, index) => ( - this.handleSort(header.name)} - onContextMenu={(e) => this.openColumnContextMenu(e, header)} - onDragOver={this.handleColumnDragOver} - onDrop={(e) => this.handleColumnDrop(e, index, columnNames)} - style={{ cursor: "pointer" }} - > -
- this.handleColumnDragStart(e, index)} - onDragEnd={() => this.setState({ dragColumnIndex: null })} - onMouseDown={(e) => e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - title="Drag to reorder column" - aria-label="Drag to reorder column" - /> - {header.alias} -
- - {this.getSortIcon(header.name)} - - -
-
- - )); - } sortData(data, columnName, direction) { if (!columnName) return data; @@ -302,29 +308,15 @@ class DatasetTable extends React.Component { const { sortColumn, sortDirection, inputPageNum, contextMenu, dragRowIndex } = this.state; const orderedColumnKeys = orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder); + const hasVisibleColumns = orderedColumnKeys.length > 0; // Avoid a broken table (row drag gutter only) when all columns are deselected. - if (columnKeys.length > 0 && orderedColumnKeys.length === 0) { - const showRowPreviewControls = Boolean(updateSelectedColumns || onPreviewRowOrderChange); - const canCustomizeLayout = Boolean( - showRowPreviewControls || onPreviewColumnOrderChange, - ); - const defaultColumnNames = orderedColumnKeys.map((col) => col.name); - const columnOrderCustom = - previewColumnOrder.length > 0 && previewColumnOrder.join("|") !== defaultColumnNames.join("|"); - const hasPreviewCustomization = columnOrderCustom || previewRowOrder.length > 0; + if (columnKeys.length > 0 && !hasVisibleColumns) { const isEmbedView = new URLSearchParams(location.search).get("embed") === "1"; return (
- {canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? ( -
- -
- ) : null}
@@ -352,6 +344,7 @@ class DatasetTable extends React.Component { ); } + const visibleColumnNames = orderedColumnKeys.map((col) => col.name); const renderedHeaders = this.setTableHeaders(orderedColumnKeys); const selectedYearsSet = new Set(selectedYears); @@ -391,7 +384,7 @@ class DatasetTable extends React.Component { key.name)} + headers={visibleColumnNames} linkRowsToDatasetView={linkRowsToDatasetView} showRowDragControls={showRowDragControls} isDragging={dragRowIndex === i} @@ -409,15 +402,17 @@ class DatasetTable extends React.Component { const backButtonClasses = currentPage === 1 ? "button-wrapper lift disabled" : "button-wrapper lift"; const forwardButtonClasses = currentPage === numOfPages ? "button-wrapper list disabled" : "button-wrapper lift"; const isEmbedView = new URLSearchParams(location.search).get("embed") === "1"; - const defaultColumnNames = orderedColumnKeys.map((col) => col.name); - const columnOrderCustom = - previewColumnOrder.length > 0 && previewColumnOrder.join("|") !== defaultColumnNames.join("|"); + const columnOrderCustom = isVisibleColumnOrderCustom( + previewColumnOrder, + visibleColumnNames, + columnKeys, + ); const hasPreviewCustomization = columnOrderCustom || previewRowOrder.length > 0; return (
- {canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? ( + {hasVisibleColumns && canCustomizeLayout && hasPreviewCustomization && onResetPreviewLayout ? (
))}
@@ -48,6 +54,7 @@ DatasetTableContextMenu.propTypes = { items: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string.isRequired, + icon: PropTypes.object, onSelect: PropTypes.func.isRequired, }), ).isRequired, diff --git a/src/utils/datasetTablePreview.js b/src/utils/datasetTablePreview.js index 46b3f87..1b8dda2 100644 --- a/src/utils/datasetTablePreview.js +++ b/src/utils/datasetTablePreview.js @@ -35,21 +35,70 @@ export function getDatasetRowKey(row, fallbackIndex = 0) { } } -/** Keep custom column order in sync when selection changes. */ -export function syncPreviewColumnOrder(prevOrder, selectedColumns, columnKeys) { - const selectedSet = new Set(selectedColumns || []); - const kept = (prevOrder || []).filter((name) => selectedSet.has(name)); - const keptSet = new Set(kept); - const added = (columnKeys || []) +/** + * user selected column(includes hidden/deselected columns). + * Deselected columns stay in place so re-adding restores their prior position. + */ +export function syncPreviewColumnOrder(prevOrder, _selectedColumns, columnKeys) { + const allColumnNames = columnKeys.map((col) => col.name); + const columnKeySet = new Set(allColumnNames); + // Filter out columns that are not in the columnKeys + const order = prevOrder.filter((name) => columnKeySet.has(name)); + + allColumnNames.forEach((name) => { + if (!order.includes(name)) { + order.push(name); + } + }); + + return order; +} + +/** + * Apply a reorder of visible columns while keeping hidden columns at their slots. + */ +export function mergeVisibleColumnReorder(fullOrder, visibleColumnNames, reorderedVisible) { + const visibleSet = new Set(visibleColumnNames); + const queue = [...reorderedVisible]; + const merged = []; + + fullOrder.forEach((name) => { + if (visibleSet.has(name)) { + if (queue.length) merged.push(queue.shift()); + } else { + merged.push(name); + } + }); + + queue.forEach((name) => merged.push(name)); + return merged; +} + +/** + * True when visible columns are ordered differently than metadata default. + * Compares layout (previewColumnOrder) to columnKeys order — not to the already-rendered order. + */ +export function isVisibleColumnOrderCustom(previewColumnOrder, visibleColumnNames, columnKeys) { + if (!visibleColumnNames?.length) return false; + + const visibleSet = new Set(visibleColumnNames); + const defaultVisibleOrder = (columnKeys || []) .map((col) => col.name) - .filter((name) => selectedSet.has(name) && !keptSet.has(name)); - return [...kept, ...added]; + .filter((name) => visibleSet.has(name)); + + const layoutVisibleOrder = (previewColumnOrder || []).filter((name) => visibleSet.has(name)); + const currentVisibleOrder = + layoutVisibleOrder.length === defaultVisibleOrder.length + ? layoutVisibleOrder + : defaultVisibleOrder; + + return currentVisibleOrder.join("|") !== defaultVisibleOrder.join("|"); } export function orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder) { const selectedSet = new Set(selectedColumns || []); - const visible = (columnKeys || []).filter((col) => selectedSet.has(col.name)); - if (!previewColumnOrder?.length) return visible; + const visible = columnKeys.filter((col) => selectedSet.has(col.name)); + if (!previewColumnOrder.length) return visible; const byName = new Map(visible.map((col) => [col.name, col])); const ordered = []; @@ -65,9 +114,9 @@ export function orderColumnKeys(columnKeys, selectedColumns, previewColumnOrder) /** Merge custom row order with filtered rows; append rows not yet in order. */ export function applyPreviewRowOrder(rows, previewRowOrder) { - const visible = (rows || []).map((row, i) => ({ row, key: getDatasetRowKey(row, i) })); + const visible = (rows).map((row, i) => ({ row, key: getDatasetRowKey(row, i) })); - if (!previewRowOrder?.length) return visible.map(({ row }) => row); + if (!previewRowOrder.length) return visible.map(({ row }) => row); const buckets = new Map(); visible.forEach(({ row, key }) => { @@ -89,7 +138,7 @@ export function applyPreviewRowOrder(rows, previewRowOrder) { } export function reorderList(list, fromIndex, toIndex) { - if (!list?.length || fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return list; + if (!list.length || fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return list; const next = [...list]; if (fromIndex >= next.length || toIndex >= next.length) return list; const [item] = next.splice(fromIndex, 1);