/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import _ from "lodash"; import * as React from "react"; import { useCallback } from "react"; import * as ReactTable from "react-table"; import { BeeTableHeaderVisibility, BoxedExpression, InsertRowColumnsDirection } from "../../api"; import { BeeTableTh } from "./BeeTableTh"; import { BeeTableThResizable } from "./BeeTableThResizable"; import { ResizerStopBehavior } from "../../resizing/ResizingWidthsContext"; import { getCanvasFont, getTextWidth } from "../../resizing/WidthsToFitData"; import { BeeTableThController } from "./BeeTableThController"; import { assertUnreachable } from "../../expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector"; import { InlineEditableTextInput } from "./InlineEditableTextInput"; import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../../expressionVariable/ExpressionVariableMenu"; export interface BeeTableColumnUpdate { typeRef: string | undefined; name: string; column: ReactTable.ColumnInstance; columnIndex: number; } export interface BeeTableCellUpdate { value: string; column: ReactTable.ColumnInstance; columnIndex: number; row: R; rowIndex: number; } export interface BeeTableHeaderProps { /** Table instance */ reactTableInstance: ReactTable.TableInstance; /** Optional label, that may depend on column, to be used for the popover that appears when clicking on column header */ editColumnLabel?: string | { [groupType: string]: string }; /** The way in which the header will be rendered */ headerVisibility?: BeeTableHeaderVisibility; /** True, for skipping the creation in the DOM of the last defined header group */ skipLastHeaderGroup: boolean; /** Custom function for getting column key prop, and avoid using the column index */ getColumnKey: (column: ReactTable.ColumnInstance) => string; /** Columns instance */ tableColumns: ReactTable.Column[]; /** Function to be executed when columns are modified */ onColumnUpdates?: (columnUpdates: BeeTableColumnUpdate[]) => void; /** Function to be executed when a column's header is clicked */ onHeaderClick?: (columnKey: string) => void; /** Function to be executed when a key up event occurs in a column's header */ onHeaderKeyUp?: (columnKey: string) => void; /** Option to enable or disable header edits */ isEditableHeader: boolean; /** */ onColumnAdded?: (args: { beforeIndex: number; groupType: string | undefined; columnsCount: number; insertDirection: InsertRowColumnsDirection; }) => void; shouldRenderRowIndexColumn: boolean; shouldShowRowsInlineControls: boolean; resizerStopBehavior: ResizerStopBehavior; lastColumnMinWidth?: number; setActiveCellEditing: (isEditing: boolean) => void; isReadOnly: boolean; } export function BeeTableHeader({ reactTableInstance, editColumnLabel, headerVisibility = BeeTableHeaderVisibility.AllLevels, skipLastHeaderGroup, getColumnKey, onColumnUpdates, isEditableHeader, onColumnAdded, onHeaderClick, onHeaderKeyUp, shouldRenderRowIndexColumn, shouldShowRowsInlineControls, resizerStopBehavior, lastColumnMinWidth, setActiveCellEditing, isReadOnly, }: BeeTableHeaderProps) { const getColumnLabel: (groupType: string) => string | undefined = useCallback( (groupType) => { if (_.isObject(editColumnLabel) && _.has(editColumnLabel, groupType)) { return editColumnLabel[groupType]; } if (typeof editColumnLabel === "string") { return editColumnLabel; } }, [editColumnLabel] ); const onExpressionHeaderUpdated = useCallback< ( column: ReactTable.ColumnInstance, columnIndex: number ) => (args: Pick) => void >( (column, columnIndex) => { return ({ "@_label": name = DEFAULT_EXPRESSION_VARIABLE_NAME, "@_typeRef": typeRef = undefined }) => { onColumnUpdates?.([ { // Subtract one because of the rowIndex column. columnIndex: columnIndex - 1, typeRef, name, column, }, ]); }; }, [onColumnUpdates] ); const renderRowIndexColumn = useCallback< (column: ReactTable.ColumnInstance, rowIndex: number, rowSpan: number) => JSX.Element >( (column, rowIndex, rowSpan) => { const columnKey = getColumnKey(column); const classNames = `${columnKey} fixed-column no-clickable-cell counter-header-cell`; return (
{column.label}
); }, [getColumnKey, isReadOnly, shouldShowRowsInlineControls] ); const renderColumn = useCallback< ( rowIndex: number, column: ReactTable.ColumnInstance, columnIndex: number, visitedColumns: Set>, rowSpan: number ) => JSX.Element >( (rowIndex, column, columnIndex, visitedColumns, rowSpan) => { const thRef = React.createRef(); const ret = column.isRowIndexColumn ? ( {shouldRenderRowIndexColumn && renderRowIndexColumn(column, rowIndex, rowSpan)} ) : ( {!visitedColumns.has(column) && ( onExpressionHeaderUpdated(column, columnIndex)({ "@_label": name, "@_typeRef": typeRef }) } lastColumnMinWidth={ columnIndex === reactTableInstance.allColumns.length - 1 ? lastColumnMinWidth : undefined } onGetWidthToFitData={() => { if (column.isInlineEditable) { const inlineEditablePreview = thRef.current!.querySelector(".inline-editable-preview")!; return Math.ceil( getTextWidth(inlineEditablePreview.textContent ?? "", getCanvasFont(inlineEditablePreview)) ); } else { const name = thRef.current!.querySelector(".expression-info-name")!; const typeRef = thRef.current!.querySelector(".expression-info-data-type")!; return Math.ceil( Math.max( getTextWidth(name.textContent ?? "", getCanvasFont(name)), getTextWidth(typeRef.textContent ?? "", getCanvasFont(typeRef)) ) ); } }} headerCellInfo={
{column.headerCellElement ? ( column.headerCellElement ) : column.isInlineEditable && !isReadOnly ? ( { onExpressionHeaderUpdated( column, columnIndex )({ "@_label": value, "@_typeRef": column.dataType }); }} isReadOnly={isReadOnly} /> ) : (

{column.label}

)} {column.dataType ? (

({column.dataType})

) : null} {column.headerCellElementExtension !== undefined && (
{column.headerCellElementExtension}
)}
} /> )}
); visitedColumns.add(column); return ret; }, [ shouldRenderRowIndexColumn, renderRowIndexColumn, getColumnKey, reactTableInstance, onHeaderClick, onHeaderKeyUp, resizerStopBehavior, isEditableHeader, shouldShowRowsInlineControls, getColumnLabel, onColumnAdded, lastColumnMinWidth, setActiveCellEditing, onExpressionHeaderUpdated, isReadOnly, ] ); const shouldRenderHeaderGroup = useCallback( (rowIndex: number) => { if (rowIndex === -1 && skipLastHeaderGroup) { return false; } switch (headerVisibility) { case BeeTableHeaderVisibility.None: return false; case BeeTableHeaderVisibility.AllLevels: return true; case BeeTableHeaderVisibility.SecondToLastLevel: return rowIndex === -2; case BeeTableHeaderVisibility.LastLevel: return rowIndex === -1; default: assertUnreachable(headerVisibility); } }, [headerVisibility, skipLastHeaderGroup] ); const renderHeaderGroups = useCallback(() => { const visitedColumns = new Set>(); return reactTableInstance.headerGroups.map((headerGroup, index) => { // rowIndex === -1 --> Last headerGroup // rowIndex === -2 --> Second to last headerGroup // ... and so on const rowIndex = -(reactTableInstance.headerGroups.length - index); let lastParentalHeaderCellIndex = 0; const { key, ...props } = { ...headerGroup.getHeaderGroupProps(), style: {} }; if (shouldRenderHeaderGroup(rowIndex)) { return ( {headerGroup.headers.map((column) => { const { placeholder, depth } = getDeepestPlaceholder(column); const columnIndex = getColumnIndexOfHeader(reactTableInstance, placeholder) >= 0 ? getColumnIndexOfHeader(reactTableInstance, placeholder) : lastParentalHeaderCellIndex++; if (placeholder.isRowIndexColumn) { if (headerVisibility === BeeTableHeaderVisibility.AllLevels) { if (rowIndex === -reactTableInstance.headerGroups.length) { return renderColumn( rowIndex + depth - 1, placeholder, columnIndex, visitedColumns, reactTableInstance.headerGroups.length ); } } else { return renderColumn(rowIndex + depth - 1, placeholder, columnIndex, visitedColumns, depth); } } else { return renderColumn(rowIndex + depth - 1, placeholder, columnIndex, visitedColumns, depth); } })} ); } else { return ( {headerGroup.headers.map((column) => { const { placeholder } = getDeepestPlaceholder(column); const columnIndex = getColumnIndexOfHeader(reactTableInstance, placeholder) >= 0 ? getColumnIndexOfHeader(reactTableInstance, placeholder) : lastParentalHeaderCellIndex++; return ( ); })} ); } }); }, [ getColumnKey, headerVisibility, reactTableInstance, renderColumn, shouldRenderHeaderGroup, shouldRenderRowIndexColumn, ]); return <>{{renderHeaderGroups()}}; } function getDeepestPlaceholder(column: ReactTable.ColumnInstance) { let currentDepth = 1; while (column.placeholderOf) { column = column.placeholderOf; currentDepth++; } return { placeholder: column, depth: currentDepth, }; } function getColumnIndexOfHeader( reactTableInstance: ReactTable.TableInstance, column: ReactTable.ColumnInstance ) { return reactTableInstance.allColumns.indexOf(column); }