/** * 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 { PureComponent, ReactNode } from 'react'; import { nanoid } from 'nanoid'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import { t, styled, css } from '@superset-ui/core'; import { Icons } from 'src/components/Icons'; import { FilterValue } from 'react-table'; import { Button } from '../Button'; import Fieldset from './Fieldset'; import { recurseReactClone } from './utils'; import { type CRUDCollectionProps, type CRUDCollectionState, type Sort, } from './types'; import Table, { type ColumnsType, type SortOrder, type SorterResult, type TablePaginationConfig, TableSize, } from '../Table'; const CrudButtonWrapper = styled.div` text-align: right; ${({ theme }) => `margin-bottom: ${theme.sizeUnit * 2}px`} `; const StyledButtonWrapper = styled.span` ${({ theme }) => ` margin-top: ${theme.sizeUnit * 3}px; margin-left: ${theme.sizeUnit * 3}px; button>span>:first-of-type { margin-right: 0; } `} `; type CollectionItem = { id: string | number; [key: string]: any }; function createCollectionArray(collection: Record) { return Object.keys(collection).map(k => collection[k] as CollectionItem); } function createKeyedCollection(arr: Array) { const collectionArray = arr.map( (o: any) => ({ ...o, id: o.id || nanoid(), }) as CollectionItem, ); const collection: Record = {}; collectionArray.forEach((o: CollectionItem) => { collection[o.id] = o; }); return { collection, collectionArray, }; } export default class CRUDCollection extends PureComponent< CRUDCollectionProps, CRUDCollectionState > { constructor(props: CRUDCollectionProps) { super(props); const { collection, collectionArray } = createKeyedCollection( props.collection, ); this.state = { expandedColumns: {}, collection, collectionArray, sortColumn: '', sort: 0, }; this.onAddItem = this.onAddItem.bind(this); this.renderExpandableSection = this.renderExpandableSection.bind(this); this.getLabel = this.getLabel.bind(this); this.onFieldsetChange = this.onFieldsetChange.bind(this); this.changeCollection = this.changeCollection.bind(this); this.handleTableChange = this.handleTableChange.bind(this); this.buildTableColumns = this.buildTableColumns.bind(this); this.toggleExpand = this.toggleExpand.bind(this); } UNSAFE_componentWillReceiveProps(nextProps: CRUDCollectionProps) { if (nextProps.collection !== this.props.collection) { const { collection, collectionArray } = createKeyedCollection( nextProps.collection, ); this.setState(prevState => ({ collection, collectionArray, expandedColumns: prevState.expandedColumns, })); } } onCellChange(id: number, col: string, val: boolean) { this.setState(prevState => { const updatedCollection = { ...prevState.collection, [id]: { ...prevState.collection[id], [col]: val, }, }; const updatedCollectionArray = prevState.collectionArray.map(item => item.id === id ? updatedCollection[id] : item, ); if (this.props.onChange) { this.props.onChange(updatedCollectionArray); } return { collection: updatedCollection, collectionArray: updatedCollectionArray, }; }); } onAddItem() { if (this.props.itemGenerator) { let newItem = this.props.itemGenerator(); const shouldStartExpanded = newItem.expanded === true; if (!newItem.id) { newItem = { ...newItem, id: nanoid() }; } delete newItem.expanded; this.setState( prevState => { const newCollection = { ...prevState.collection, [newItem.id]: newItem, }; const newExpandedColumns = shouldStartExpanded ? { ...prevState.expandedColumns, [newItem.id]: true } : prevState.expandedColumns; const newCollectionArray = [newItem, ...prevState.collectionArray]; return { collection: newCollection, collectionArray: newCollectionArray, expandedColumns: newExpandedColumns, }; }, () => { if (this.props.onChange) { this.props.onChange(this.state.collectionArray); } }, ); } } onFieldsetChange(item: any) { this.changeCollection({ ...this.state.collection, [item.id]: item, }); } getLabel(col: any): string { const { columnLabels } = this.props; let label = columnLabels?.[col] ? columnLabels[col] : col; if (label.startsWith('__')) { label = ''; } return label; } getTooltip(col: string): string | undefined { const { columnLabelTooltips } = this.props; return columnLabelTooltips?.[col]; } changeCollection(collection: any) { console.log({ collection }); const newCollectionArray = createCollectionArray(collection); console.log({ newCollectionArray }); this.setState({ collection, collectionArray: newCollectionArray }); if (this.props.onChange) { this.props.onChange(newCollectionArray); } } deleteItem(id: string | number) { const newColl = { ...this.state.collection }; delete newColl[id]; this.changeCollection(newColl); } toggleExpand(id: any) { this.setState(prevState => ({ expandedColumns: { ...prevState.expandedColumns, [id]: !prevState.expandedColumns[id], }, })); } handleTableChange( _pagination: TablePaginationConfig, _filters: Record, sorter: SorterResult | SorterResult[], ) { const columnSorter = Array.isArray(sorter) ? sorter[0] : sorter; let newSortColumn = ''; let newSortOrder = 0; if (columnSorter?.columnKey && columnSorter?.order) { newSortColumn = columnSorter.columnKey as string; newSortOrder = columnSorter.order === 'ascend' ? 1 : 2; } const { sortColumns } = this.props; const col = newSortColumn; if (sortColumns?.includes(col) || newSortOrder === 0) { let sortedArray = [...this.props.collection]; if (newSortOrder !== 0) { const compareSort = (m: Sort, n: Sort) => { if (typeof m === 'string' && typeof n === 'string') { return (m || '').localeCompare(n || ''); } if (typeof m === 'number' && typeof n === 'number') { return m - n; } if (typeof m === 'boolean' && typeof n === 'boolean') { return m === n ? 0 : m ? 1 : -1; } const mStr = String(m ?? ''); const nStr = String(n ?? ''); return mStr.localeCompare(nStr); }; sortedArray.sort((a: any, b: any) => compareSort(a[col], b[col])); if (newSortOrder === 2) { sortedArray.reverse(); } } else { const { collectionArray } = createKeyedCollection( this.props.collection, ); sortedArray = collectionArray; } this.setState({ collectionArray: sortedArray, sortColumn: newSortColumn, sort: newSortOrder, }); } } renderExpandableSection(item: any): ReactNode { const propsGenerator = () => ({ item, onChange: this.onFieldsetChange }); return recurseReactClone( this.props.expandFieldset, Fieldset, propsGenerator, ); } renderCell(record: any, col: any): ReactNode { const renderer = this.props.itemRenderers?.[col]; const val = record[col]; const onChange = this.onCellChange.bind(this, record.id, col); return renderer ? renderer(val, onChange, this.getLabel(col), record) : val; } buildTableColumns() { const { tableColumns, allowDeletes, sortColumns = [] } = this.props; const antdColumns: ColumnsType = tableColumns.map(col => { const label = this.getLabel(col); const tooltip = this.getTooltip(col); const isSortable = sortColumns.includes(col); const currentSortOrder: SortOrder | null | undefined = this.state.sortColumn === col ? this.state.sort === 1 ? 'ascend' : this.state.sort === 2 ? 'descend' : null : null; return { key: col, dataIndex: col, minWidth: 100, title: ( <> {label} {tooltip && ( <> {' '} )} ), render: (text: any, record: CollectionItem) => this.renderCell(record, col), onCell: (record: CollectionItem) => { const cellPropsFn = this.props.itemCellProps?.[col]; const val = record[col]; return cellPropsFn ? cellPropsFn(val, label, record) : {}; }, sorter: isSortable, sortOrder: currentSortOrder, }; }); if (allowDeletes) { antdColumns.push({ key: '__actions', dataIndex: '__actions', sorter: false, title: <>, onCell: () => ({}), sortOrder: null, minWidth: 50, render: (_, record: CollectionItem) => ( this.deleteItem(record.id)} iconSize="l" /> ), }); } return antdColumns as ColumnsType; } render() { const { stickyHeader, emptyMessage = t('No items'), expandFieldset, } = this.props; const tableColumns = this.buildTableColumns(); const expandedRowKeys = Object.keys(this.state.expandedColumns).filter( id => this.state.expandedColumns[id], ); const expandableConfig = expandFieldset ? { expandedRowRender: (record: CollectionItem) => this.renderExpandableSection(record), rowExpandable: () => true, expandedRowKeys, onExpand: (expanded: boolean, record: CollectionItem) => { this.toggleExpand(record.id); }, } : undefined; return ( <> {this.props.allowAddItem && ( )} data-test="crud-table" columns={tableColumns} data={this.state.collectionArray as CollectionItem[]} rowKey={(record: CollectionItem) => String(record.id)} sticky={stickyHeader} pagination={false} onChange={this.handleTableChange} locale={{ emptyText: emptyMessage }} css={ stickyHeader && css` height: 350px; overflow: auto; ` } expandable={expandableConfig} size={TableSize.Middle} tableLayout="auto" /> ); } }