diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/TableStories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/TableStories.tsx index 129c08f505753..b8ea6d9cb5c85 100644 --- a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/TableStories.tsx +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/TableStories.tsx @@ -66,6 +66,7 @@ function loadData( alignPn = false, showCellBars = true, includeSearch = true, + allowRearrangeColumns = false, }, ): TableChartProps { if (!props.queriesData || !props.queriesData[0]) return props; @@ -86,6 +87,7 @@ function loadData( page_length: pageLength, show_cell_bars: showCellBars, include_search: includeSearch, + allow_rearrange_columns: allowRearrangeColumns, }, height: window.innerHeight - 130, }; @@ -117,8 +119,12 @@ export const BigTable = ({ width, height }) => { const cols = number('Columns', 8, { range: true, min: 1, max: 20 }); const pageLength = number('Page size', 50, { range: true, min: 0, max: 100 }); const includeSearch = boolean('Include search', true); - const alignPn = boolean('Algin PosNeg', false); + const alignPn = boolean('Align PosNeg', false); const showCellBars = boolean('Show Cell Bars', true); + const allowRearrangeColumns = boolean( + 'Allow end user to drag-and-drop column headers to rearrange them.', + false, + ); const chartProps = loadData(birthNames, { pageLength, rows, @@ -126,6 +132,7 @@ export const BigTable = ({ width, height }) => { alignPn, showCellBars, includeSearch, + allowRearrangeColumns, }); return ( extends TableOptions { sticky?: boolean; rowCount: number; wrapperRef?: MutableRefObject; + onColumnOrderChange: () => void; } export interface RenderHTMLCellProps extends HTMLProps { @@ -95,12 +97,14 @@ export default typedMemo(function DataTable({ hooks, serverPagination, wrapperRef: userWrapperRef, + onColumnOrderChange, ...moreUseTableOptions }: DataTableProps): JSX.Element { const tableHooks: PluginHook[] = [ useGlobalFilter, useSortBy, usePagination, + useColumnOrder, doSticky ? useSticky : [], hooks || [], ].flat(); @@ -172,6 +176,8 @@ export default typedMemo(function DataTable({ setGlobalFilter, setPageSize: setPageSize_, wrapStickyTable, + setColumnOrder, + allColumns, state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} }, } = useTable( { @@ -211,6 +217,33 @@ export default typedMemo(function DataTable({ const shouldRenderFooter = columns.some(x => !!x.Footer); + let columnBeingDragged = -1; + + const onDragStart = (e: React.DragEvent) => { + const el = e.target as HTMLTableCellElement; + columnBeingDragged = allColumns.findIndex( + col => col.id === el.dataset.columnName, + ); + e.dataTransfer.setData('text/plain', `${columnBeingDragged}`); + }; + + const onDrop = (e: React.DragEvent) => { + const el = e.target as HTMLTableCellElement; + const newPosition = allColumns.findIndex( + col => col.id === el.dataset.columnName, + ); + + if (newPosition !== -1) { + const currentCols = allColumns.map(c => c.id); + const colToBeMoved = currentCols.splice(columnBeingDragged, 1); + currentCols.splice(newPosition, 0, colToBeMoved[0]); + setColumnOrder(currentCols); + // toggle value in TableChart to trigger column width recalc + onColumnOrderChange(); + } + e.preventDefault(); + }; + const renderTable = () => ( @@ -223,6 +256,8 @@ export default typedMemo(function DataTable({ column.render('Header', { key: column.id, ...column.getSortByToggleProps(), + onDragStart, + onDrop, }), )} diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx index 9a98fee431817..6fd4d839ce661 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx @@ -350,6 +350,7 @@ function useInstance(instance: TableInstance) { data, page, rows, + allColumns, getTableSize = () => undefined, } = instance; @@ -370,7 +371,7 @@ function useInstance(instance: TableInstance) { useMountedMemo(getTableSize, [getTableSize]) || sticky; // only change of data should trigger re-render // eslint-disable-next-line react-hooks/exhaustive-deps - const table = useMemo(renderer, [page, rows]); + const table = useMemo(renderer, [page, rows, allColumns]); useLayoutEffect(() => { if (!width || !height) { diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts b/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts index 52a18d54e1b0f..c1f49ea396f25 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts @@ -36,6 +36,8 @@ import { UseSortByState, UseTableHooks, UseSortByHooks, + UseColumnOrderState, + UseColumnOrderInstanceProps, Renderer, HeaderProps, TableFooterProps, @@ -64,6 +66,7 @@ declare module 'react-table' { UseRowSelectInstanceProps, UseRowStateInstanceProps, UseSortByInstanceProps, + UseColumnOrderInstanceProps, UseStickyInstanceProps {} export interface TableState @@ -73,6 +76,7 @@ declare module 'react-table' { UsePaginationState, UseRowSelectState, UseSortByState, + UseColumnOrderState, UseStickyState {} // Typing from @types/react-table is incomplete @@ -82,12 +86,19 @@ declare module 'react-table' { onClick?: React.MouseEventHandler; } + interface TableRearrangeColumnsProps { + onDragStart: (e: React.DragEvent) => void; + onDrop: (e: React.DragEvent) => void; + } + export interface ColumnInterface extends UseGlobalFiltersColumnOptions, UseSortByColumnOptions { // must define as a new property because it's not possible to override // the existing `Header` renderer option - Header?: Renderer>; + Header?: Renderer< + TableSortByToggleProps & HeaderProps & TableRearrangeColumnsProps + >; Footer?: Renderer>; } diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 296a7125949a0..f0b125940bb83 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties, useCallback, useMemo } from 'react'; +import React, { CSSProperties, useCallback, useMemo, useState } from 'react'; import { ColumnInstance, ColumnWithLooseAccessor, @@ -192,12 +192,16 @@ export default function TableChart( filters, sticky = true, // whether to use sticky header columnColorFormatters, + allowRearrangeColumns = false, } = props; const timestampFormatter = useCallback( value => getTimeFormatterForGranularity(timeGrain)(value), [timeGrain], ); + // keep track of whether column order changed, so that column widths can too + const [columnOrderToggle, setColumnOrderToggle] = useState(false); + const handleChange = useCallback( (filters: { [x: string]: DataRecordValue[] }) => { if (!emitFilter) { @@ -413,7 +417,7 @@ export default function TableChart( // render `Cell`. This saves some time for large tables. return {text}; }, - Header: ({ column: col, onClick, style }) => ( + Header: ({ column: col, onClick, style, onDragStart, onDrop }) => ( @@ -469,6 +482,7 @@ export default function TableChart( toggleFilter, totals, columnColorFormatters, + columnOrderToggle, ], ); @@ -498,6 +512,7 @@ export default function TableChart( height={height} serverPagination={serverPagination} onServerPaginationChange={handleServerPaginationChange} + onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)} // 9 page items in > 340px works well even for 100+ pages maxPageItemCount={width > 340 ? 9 : 7} noResults={getNoResultsMessage} diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index c121547518e46..bb855bd7ccc56 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -455,6 +455,20 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'allow_rearrange_columns', + config: { + type: 'CheckboxControl', + label: t('Allow columns to be rearranged'), + renderTrigger: true, + default: false, + description: t( + "Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.", + ), + }, + }, + ], [ { name: 'column_config', diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index 8b701422bb950..5cf4fd1e83c43 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -220,6 +220,7 @@ const transformProps = ( query_mode: queryMode, show_totals: showTotals, conditional_formatting: conditionalFormatting, + allow_rearrange_columns: allowRearrangeColumns, } = formData; const timeGrain = extractTimegrain(formData); @@ -272,6 +273,7 @@ const transformProps = ( onChangeFilter, columnColorFormatters, timeGrain, + allowRearrangeColumns, }; }; diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index 7c50f99cb4ae6..f5b83fa8bfd7e 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -71,6 +71,7 @@ export type TableChartFormData = QueryFormData & { emit_filter?: boolean; time_grain_sqla?: TimeGranularity; column_config?: Record; + allow_rearrange_columns?: boolean; }; export interface TableChartProps extends ChartProps { @@ -109,6 +110,7 @@ export interface TableChartTransformedProps { emitFilter?: boolean; onChangeFilter?: ChartProps['hooks']['onAddFilter']; columnColorFormatters?: ColorFormatters; + allowRearrangeColumns?: boolean; } export default {};
( ...style, }} onClick={onClick} + data-column-name={col.id} + {...(allowRearrangeColumns && { + draggable: 'true', + onDragStart, + onDragOver: e => e.preventDefault(), + onDragEnter: e => e.preventDefault(), + onDrop, + })} > {/* can't use `columnWidth &&` because it may also be zero */} {config.columnWidth ? ( @@ -434,12 +446,13 @@ export default function TableChart( /> ) : null}
- {label} + {label}