diff --git a/app/client/src/assets/icons/control/download-table.svg b/app/client/src/assets/icons/control/download-table.svg new file mode 100755 index 0000000000..cea728e494 --- /dev/null +++ b/app/client/src/assets/icons/control/download-table.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index 752ee42fde..9ba6441c8c 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -3,7 +3,25 @@ import { ColumnAction } from "components/propertyControls/ColumnActionSelectorCo import Table from "./Table"; import { RenderMode, RenderModes } from "constants/WidgetConstants"; import { debounce } from "lodash"; -import { getMenuOptions, renderActions, renderCell } from "./TableUtilities"; +import { + getMenuOptions, + getAllTableColumnKeys, +} from "components/designSystems/appsmith/TableUtilities"; + +export enum ColumnTypes { + CURRENCY = "currency", + TIME = "time", + DATE = "date", + VIDEO = "video", + IMAGE = "image", + TEXT = "text", +} + +export interface TableColumnMetaProps { + isHidden: boolean; + format?: string; + type: string; +} export interface ReactTableColumnProps { Header: string; @@ -12,6 +30,7 @@ export interface ReactTableColumnProps { minWidth: number; draggable: boolean; isHidden?: boolean; + metaProperties?: TableColumnMetaProps; Cell: (props: any) => JSX.Element; } @@ -36,6 +55,7 @@ export interface ColumnMenuSubOptionProps { interface ReactTableComponentProps { widgetId: string; + widgetName: string; searchKey: string; isDisabled?: boolean; isVisible?: boolean; @@ -72,140 +92,12 @@ interface ReactTableComponentProps { handleResizeColumn: Function; handleReorderColumn: Function; searchTableData: (searchKey: any) => void; + columns: ReactTableColumnProps[]; } const ReactTableComponent = (props: ReactTableComponentProps) => { let dragged = -1; - const getAllTableColumnKeys = () => { - const tableData: object[] = props.tableData; - const columnKeys: string[] = []; - for (let i = 0, tableRowCount = tableData.length; i < tableRowCount; i++) { - const row = tableData[i]; - for (const key in row) { - if (!columnKeys.includes(key)) { - columnKeys.push(key); - } - } - } - return columnKeys; - }; - const reorderColumns = (columns: ReactTableColumnProps[]) => { - const columnOrder = props.columnOrder || []; - const reorderedColumns = []; - const reorderedFlagMap: { [key: string]: boolean } = {}; - for (let index = 0; index < columns.length; index++) { - const accessor = columnOrder[index]; - if (accessor) { - const column = columns.filter((col: ReactTableColumnProps) => { - return col.accessor === accessor; - }); - if (column.length && !reorderedFlagMap[column[0].accessor]) { - reorderedColumns.push(column[0]); - reorderedFlagMap[column[0].accessor] = true; - } else if (!reorderedFlagMap[columns[index].accessor]) { - reorderedColumns.push(columns[index]); - reorderedFlagMap[columns[index].accessor] = true; - } - } else if (!reorderedFlagMap[columns[index].accessor]) { - reorderedColumns.push(columns[index]); - reorderedFlagMap[columns[index].accessor] = true; - } - } - if (reorderedColumns.length < columns.length) { - for (let index = 0; index < columns.length; index++) { - if (!reorderedFlagMap[columns[index].accessor]) { - reorderedColumns.push(columns[index]); - reorderedFlagMap[columns[index].accessor] = true; - } - } - } - return reorderedColumns; - }; - const getTableColumns = () => { - const tableData: object[] = props.tableData; - let columns: ReactTableColumnProps[] = []; - const hiddenColumns: ReactTableColumnProps[] = []; - if (tableData.length) { - const columnKeys: string[] = getAllTableColumnKeys(); - for (let index = 0; index < columnKeys.length; index++) { - const i = columnKeys[index]; - const columnName: string = - props.columnNameMap && props.columnNameMap[i] - ? props.columnNameMap[i] - : i; - const columnType: { type: string; format?: string } = - props.columnTypeMap && props.columnTypeMap[i] - ? props.columnTypeMap[i] - : { type: "text" }; - const columnSize: number = - props.columnSizeMap && props.columnSizeMap[i] - ? props.columnSizeMap[i] - : 150; - const isHidden = - !!props.hiddenColumns && props.hiddenColumns.includes(i); - const columnData = { - Header: columnName, - accessor: i, - width: columnSize, - minWidth: 60, - draggable: true, - isHidden: false, - Cell: (props: any) => { - return renderCell( - props.cell.value, - props.cell.row.index, - columnType.type, - isHidden, - props.widgetId, - columnType.format, - ); - }, - }; - if (isHidden) { - columnData.isHidden = true; - hiddenColumns.push(columnData); - } else { - columns.push(columnData); - } - } - columns = reorderColumns(columns); - if (props.columnActions?.length) { - columns.push({ - Header: - props.columnNameMap && props.columnNameMap["actions"] - ? props.columnNameMap["actions"] - : "Actions", - accessor: "actions", - width: 150, - minWidth: 60, - draggable: true, - Cell: () => { - return renderActions({ - columnActions: props.columnActions, - onCommandClick: props.onCommandClick, - }); - }, - }); - } - if (hiddenColumns.length && props.renderMode === RenderModes.CANVAS) { - columns = columns.concat(hiddenColumns); - } - } - return columns; - }; - - const tableColumns = React.useMemo(getTableColumns, [ - JSON.stringify({ - data: props.tableData, - columnNameMap: props.columnNameMap, - columnActions: props.columnActions, - hiddenColumns: props.hiddenColumns, - columnSizeMap: props.columnSizeMap, - columnTypeMap: props.columnTypeMap, - columnOrder: props.columnOrder, - }), - ]); useEffect(() => { const headers = Array.prototype.slice.call( document.querySelectorAll(`#table${props.widgetId} .draggable-header`), @@ -267,9 +159,9 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { e.preventDefault(); let columnOrder = props.columnOrder; if (columnOrder === undefined) { - columnOrder = getAllTableColumnKeys(); + columnOrder = props.columns.map(item => item.accessor); } - const draggedColumn = tableColumns[dragged].accessor; + const draggedColumn = props.columns[dragged].accessor; columnOrder.splice(dragged, 1); columnOrder.splice(i, 0, draggedColumn); props.handleReorderColumn(columnOrder); @@ -281,7 +173,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }); const getColumnMenu = (columnIndex: number) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnId = column.accessor; const columnType = props.columnTypeMap && props.columnTypeMap[columnId] @@ -308,7 +200,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const hideColumn = (columnIndex: number, isColumnHidden: boolean) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; let hiddenColumns = props.hiddenColumns || []; if (!isColumnHidden) { hiddenColumns.push(column.accessor); @@ -326,7 +218,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const updateColumnType = (columnIndex: number, columnType: string) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnTypeMap = props.columnTypeMap || {}; columnTypeMap[column.accessor] = { type: columnType, @@ -336,7 +228,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const handleColumnNameUpdate = (columnIndex: number, columnName: string) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnNameMap = props.columnNameMap || {}; columnNameMap[column.accessor] = columnName; props.updateColumnName(columnNameMap); @@ -346,7 +238,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { columnIndex: number, currencySymbol: string, ) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnTypeMap = props.columnTypeMap || {}; columnTypeMap[column.accessor] = { type: "currency", @@ -356,7 +248,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const handleDateFormatUpdate = (columnIndex: number, dateFormat: string) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnTypeMap = props.columnTypeMap || {}; columnTypeMap[column.accessor] = { type: "date", @@ -366,7 +258,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const handleResizeColumn = (columnIndex: number, columnWidth: string) => { - const column = tableColumns[columnIndex]; + const column = props.columns[columnIndex]; const columnSizeMap = props.columnSizeMap || {}; const width = Number(columnWidth.split("px")[0]); columnSizeMap[column.accessor] = width; @@ -391,8 +283,9 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { height={props.height} pageSize={props.pageSize || 1} widgetId={props.widgetId} + widgetName={props.widgetName} searchKey={props.searchKey} - columns={tableColumns} + columns={props.columns} hiddenColumns={props.hiddenColumns} updateHiddenColumns={props.updateHiddenColumns} data={props.tableData} diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 37544cc2c4..49f5394103 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -26,6 +26,7 @@ interface TableProps { height: number; pageSize: number; widgetId: string; + widgetName: string; searchKey: string; isLoading: boolean; columns: ReactTableColumnProps[]; @@ -64,6 +65,9 @@ export const Table = (props: TableProps) => { const pageCount = Math.ceil(props.data.length / props.pageSize); const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; const data = React.useMemo(() => props.data, [JSON.stringify(props.data)]); + const columns = React.useMemo(() => props.columns, [ + JSON.stringify(props.columns), + ]); const { getTableProps, getTableBodyProps, @@ -73,7 +77,7 @@ export const Table = (props: TableProps) => { pageOptions, } = useTable( { - columns: props.columns, + columns: columns, data, defaultColumn, initialState: { @@ -103,6 +107,8 @@ export const Table = (props: TableProps) => { id={`table${props.widgetId}`} > { pageCount={pageCount} currentPageIndex={currentPageIndex} pageOptions={pageOptions} + widgetName={props.widgetName} serverSidePaginationEnabled={props.serverSidePaginationEnabled} columns={props.columns.filter((column: ReactTableColumnProps) => { return column.accessor !== "actions"; diff --git a/app/client/src/components/designSystems/appsmith/TableColumnsVisibility.tsx b/app/client/src/components/designSystems/appsmith/TableColumnsVisibility.tsx index 0f6eab3e60..268f42d1b6 100644 --- a/app/client/src/components/designSystems/appsmith/TableColumnsVisibility.tsx +++ b/app/client/src/components/designSystems/appsmith/TableColumnsVisibility.tsx @@ -5,6 +5,7 @@ import { PopoverInteractionKind, Position, Icon, + Tooltip, } from "@blueprintjs/core"; import { IconWrapper } from "constants/IconConstants"; import styled from "styled-components"; @@ -12,19 +13,7 @@ import { Colors } from "constants/Colors"; import { ReactComponent as VisibleIcon } from "assets/icons/control/columns-visibility.svg"; import { ReactTableColumnProps } from "components/designSystems/appsmith/ReactTableComponent"; import Button from "components/editorComponents/Button"; - -const TableIconWrapper = styled.div<{ selected?: boolean; disabled?: boolean }>` - background: ${props => (props.selected ? Colors.ATHENS_GRAY : "transparent")}; - box-shadow: ${props => - props.selected ? `inset 0px 4px 0px ${Colors.GREEN}` : "none"}; - width: 48px; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - opacity: ${props => (props.disabled ? 0.6 : 1)}; - cursor: ${props => !props.disabled && "pointer"}; -`; +import { TableIconWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; const DropDownWrapper = styled.div` display: flex; @@ -117,13 +106,20 @@ const TableColumnsVisibility = (props: TableColumnsVisibilityProps) => { selectMenu(true); }} > - - - + + + + {columns.map((option: ReactTableColumnProps, index: number) => ( diff --git a/app/client/src/components/designSystems/appsmith/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableDataDownload.tsx new file mode 100644 index 0000000000..7ee6af9dd1 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableDataDownload.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { IconWrapper } from "constants/IconConstants"; +import { Tooltip } from "@blueprintjs/core"; +import { Colors } from "constants/Colors"; +import { ReactComponent as DownloadIcon } from "assets/icons/control/download-table.svg"; +import { ReactTableColumnProps } from "components/designSystems/appsmith/ReactTableComponent"; +import { TableIconWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; + +interface TableDataDownloadProps { + data: object[]; + columns: ReactTableColumnProps[]; + widgetName: string; +} + +const TableDataDownload = (props: TableDataDownloadProps) => { + const [selected, toggleButtonClick] = React.useState(false); + const downloadTableData = () => { + toggleButtonClick(true); + const csvData = []; + csvData.push( + props.columns + .map((column: ReactTableColumnProps) => { + if (column.metaProperties && !column.metaProperties.isHidden) { + return column.Header; + } + }) + .filter(i => !!i), + ); + for (let row = 0; row < props.data.length; row++) { + const data: { [key: string]: any } = props.data[row]; + const csvDataRow = []; + for (let colIndex = 0; colIndex < props.columns.length; colIndex++) { + const column = props.columns[colIndex]; + const value = data[column.accessor]; + if (column.metaProperties && !column.metaProperties.isHidden) { + csvDataRow.push(value); + } + } + csvData.push(csvDataRow); + } + let csvContent = ""; + csvData.forEach(function(infoArray, index) { + const dataString = infoArray.join(","); + csvContent += index < csvData.length ? dataString + "\n" : dataString; + }); + const fileName = `${props.widgetName}.csv`; + const anchor = document.createElement("a"); + const mimeType = "application/octet-stream"; + if (navigator.msSaveBlob) { + navigator.msSaveBlob( + new Blob([csvContent], { + type: mimeType, + }), + fileName, + ); + } else if (URL && "download" in anchor) { + anchor.href = URL.createObjectURL( + new Blob([csvContent], { + type: mimeType, + }), + ); + anchor.setAttribute("download", fileName); + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + } + toggleButtonClick(false); + }; + + return ( + { + downloadTableData(); + }} + > + + + + + + + ); +}; + +export default TableDataDownload; diff --git a/app/client/src/components/designSystems/appsmith/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableHeader.tsx index 2afdb285ea..92b6ac5e0b 100644 --- a/app/client/src/components/designSystems/appsmith/TableHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/TableHeader.tsx @@ -1,9 +1,6 @@ import React from "react"; import styled from "styled-components"; import { Icon, NumericInput } from "@blueprintjs/core"; -import SearchComponent from "components/designSystems/appsmith/SearchComponent"; -import TableColumnsVisibility from "components/designSystems/appsmith/TableColumnsVisibility"; -import { ReactTableColumnProps } from "components/designSystems/appsmith/ReactTableComponent"; import { RowWrapper, PaginationWrapper, @@ -11,6 +8,10 @@ import { PaginationItemWrapper, CommonFunctionsMenuWrapper, } from "./TableStyledWrappers"; +import SearchComponent from "components/designSystems/appsmith/SearchComponent"; +import TableColumnsVisibility from "components/designSystems/appsmith/TableColumnsVisibility"; +import { ReactTableColumnProps } from "components/designSystems/appsmith/ReactTableComponent"; +import TableDataDownload from "components/designSystems/appsmith/TableDataDownload"; import { Colors } from "constants/Colors"; const PageNumberInputWrapper = styled(NumericInput)` @@ -58,12 +59,15 @@ interface TableHeaderProps { nextPageClick: () => void; prevPageClick: () => void; pageNo: number; + tableData: object[]; + tableColumns: ReactTableColumnProps[]; pageCount: number; currentPageIndex: number; pageOptions: number[]; columns: ReactTableColumnProps[]; hiddenColumns?: string[]; updateHiddenColumns: (hiddenColumns?: string[]) => void; + widgetName: string; searchKey: string; searchTableData: (searchKey: any) => void; serverSidePaginationEnabled: boolean; @@ -79,6 +83,11 @@ const TableHeader = (props: TableHeaderProps) => { onSearch={props.searchTableData} /> + {props.displayColumnActions && ( ` - width: ${props => props.width - 5}px; - height: ${props => props.height - 5}px; + width: 100%; + height: 100%; background: white; border: 1px solid ${Colors.GEYSER_LIGHT}; box-sizing: border-box; @@ -21,26 +21,14 @@ export const TableWrapper = styled.div<{ width: number; height: number }>` border-spacing: 0; color: ${Colors.BLUE_BAYOUX}; position: relative; - .thead { - overflow-y: auto; - overflow-x: hidden; - } + overflow-y: auto; + height: ${props => props.height - TABLE_SIZES.TABLE_HEADER_HEIGHT}px; + .thead, .tbody { - overflow: scroll; - /* - Here 5px is subtracted to compensate padding from widget resizers and - 113px to compensate table column header and table header heights - */ - height: ${props => - props.height - - 5 - - TABLE_SIZES.TABLE_HEADER_HEIGHT - - TABLE_SIZES.COLUMN_HEADER_HEIGHT}px; - &.no-scroll { - overflow: hidden; - } + overflow: hidden; } .tr { + overflow: hidden; :nth-child(even) { background: ${Colors.ATHENS_GRAY_DARKER}; } @@ -285,3 +273,22 @@ export const RowWrapper = styled.div` margin: 0 4px; white-space: nowrap; `; + +export const TableIconWrapper = styled.div<{ + selected?: boolean; + disabled?: boolean; +}>` + background: ${props => (props.selected ? Colors.ATHENS_GRAY : "transparent")}; + box-shadow: ${props => + props.selected ? `inset 0px 4px 0px ${Colors.GREEN}` : "none"}; + width: 48px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + opacity: ${props => (props.disabled ? 0.6 : 1)}; + cursor: ${props => !props.disabled && "pointer"}; + &:hover { + background: ${Colors.ATHENS_GRAY}; + } +`; diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx index aff1054514..e4e6efc9ea 100644 --- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -1,18 +1,22 @@ import React, { useState } from "react"; import { Icon, InputGroup } from "@blueprintjs/core"; -import moment from "moment-timezone"; import { MenuColumnWrapper, CellWrapper, ActionWrapper, } from "./TableStyledWrappers"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; -import { ColumnMenuOptionProps } from "./ReactTableComponent"; -import { isString } from "lodash"; +import { + ColumnMenuOptionProps, + ReactTableColumnProps, + ColumnTypes, +} from "components/designSystems/appsmith/ReactTableComponent"; +import { isString, isNumber } from "lodash"; import VideoComponent from "components/designSystems/appsmith/VideoComponent"; import Button from "components/editorComponents/Button"; import AutoToolTipComponent from "components/designSystems/appsmith/AutoToolTipComponent"; import TableColumnMenuPopup from "./TableColumnMenu"; +import { Colors } from "constants/Colors"; interface MenuOptionProps { columnAccessor?: string; @@ -57,86 +61,106 @@ export const getMenuOptions = (props: MenuOptionProps) => { }, { content: ( - +
Image
), closeOnClick: true, - isSelected: props.columnType === "image", + isSelected: props.columnType === ColumnTypes.IMAGE, onClick: (columnIndex: number, isSelected: boolean) => { if (isSelected) { props.updateColumnType(columnIndex, ""); } else { - props.updateColumnType(columnIndex, "image"); + props.updateColumnType(columnIndex, ColumnTypes.IMAGE); } }, }, { content: ( - +
Video
), - isSelected: props.columnType === "video", + isSelected: props.columnType === ColumnTypes.VIDEO, closeOnClick: true, onClick: (columnIndex: number, isSelected: boolean) => { if (isSelected) { props.updateColumnType(columnIndex, ""); } else { - props.updateColumnType(columnIndex, "video"); + props.updateColumnType(columnIndex, ColumnTypes.VIDEO); } }, }, { content: ( - +
Text
), closeOnClick: true, - isSelected: props.columnType === "text", + isSelected: props.columnType === ColumnTypes.TEXT, onClick: (columnIndex: number, isSelected: boolean) => { if (isSelected) { props.updateColumnType(columnIndex, ""); } else { - props.updateColumnType(columnIndex, "text"); + props.updateColumnType(columnIndex, ColumnTypes.TEXT); } }, }, { content: ( - +
Currency
), closeOnClick: false, - isSelected: props.columnType === "currency", + isSelected: props.columnType === ColumnTypes.CURRENCY, options: [ { content: "USD - $", @@ -198,23 +222,31 @@ export const getMenuOptions = (props: MenuOptionProps) => { }, { content: ( - +
Date
), closeOnClick: false, - isSelected: props.columnType === "date", + isSelected: props.columnType === ColumnTypes.DATE, options: [ { content: "MM-DD-YY", @@ -252,22 +284,26 @@ export const getMenuOptions = (props: MenuOptionProps) => { }, { content: ( - +
Time
), closeOnClick: true, - isSelected: props.columnType === "time", + isSelected: props.columnType === ColumnTypes.TIME, onClick: (columnIndex: number, isSelected: boolean) => { if (isSelected) { props.updateColumnType(columnIndex, ""); } else { - props.updateColumnType(columnIndex, "time"); + props.updateColumnType(columnIndex, ColumnTypes.TIME); } }, }, @@ -277,17 +313,11 @@ export const getMenuOptions = (props: MenuOptionProps) => { export const renderCell = ( value: any, - rowIndex: number, columnType: string, isHidden: boolean, - widgetId: string, - format?: string, ) => { - if (!value) { - return
; - } switch (columnType) { - case "image": + case ColumnTypes.IMAGE: if (!isString(value)) { return ( @@ -316,7 +346,7 @@ export const renderCell = ( })} ); - case "video": + case ColumnTypes.VIDEO: const youtubeRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/; if (isString(value) && youtubeRegex.test(value)) { return ( @@ -329,65 +359,11 @@ export const renderCell = ( Invalid Video Link ); } - case "currency": - if (!isNaN(value)) { - return ( - {`${format}${value}`} - ); - } else { - return Invalid Value; - } - case "date": - let isValidDate = true; - if (isNaN(value)) { - const dateTime = Date.parse(value); - if (isNaN(dateTime)) { - isValidDate = false; - } - } - if (isValidDate) { - return ( - - {moment(value).format(format)} - - ); - } else { - return Invalid Date; - } - case "time": - let isValidTime = true; - if (isNaN(value)) { - const time = Date.parse(value); - if (isNaN(time)) { - isValidTime = false; - } - } - if (isValidTime) { - return ( - - {moment(value).format("HH:mm")} - - ); - } else { - return Invalid Time; - } - case "text": - const text = isString(value) ? value : JSON.stringify(value); - return ( - - {text} - - ); default: - const data = isString(value) ? value : JSON.stringify(value); + const data = + isString(value) || isNumber(value) ? value : JSON.stringify(value); return ( - + {data} ); @@ -586,3 +562,51 @@ export const TableHeaderCell = (props: { ); }; + +export const getAllTableColumnKeys = (tableData: object[]) => { + const columnKeys: string[] = []; + for (let i = 0, tableRowCount = tableData.length; i < tableRowCount; i++) { + const row = tableData[i]; + for (const key in row) { + if (!columnKeys.includes(key)) { + columnKeys.push(key); + } + } + } + return columnKeys; +}; + +export const reorderColumns = ( + columns: ReactTableColumnProps[], + columnOrder: string[], +) => { + const reorderedColumns = []; + const reorderedFlagMap: { [key: string]: boolean } = {}; + for (let index = 0; index < columns.length; index++) { + const accessor = columnOrder[index]; + if (accessor) { + const column = columns.filter((col: ReactTableColumnProps) => { + return col.accessor === accessor; + }); + if (column.length && !reorderedFlagMap[column[0].accessor]) { + reorderedColumns.push(column[0]); + reorderedFlagMap[column[0].accessor] = true; + } else if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } else if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } + if (reorderedColumns.length < columns.length) { + for (let index = 0; index < columns.length; index++) { + if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } + } + return reorderedColumns; +}; diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 3eb6ef4c80..7ea372d2dd 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -2,9 +2,19 @@ import React, { Suspense } from "react"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/ActionConstants"; -import ReactTableComponent from "components/designSystems/appsmith/ReactTableComponent"; +import ReactTableComponent, { + ReactTableColumnProps, + ColumnTypes, +} from "components/designSystems/appsmith/ReactTableComponent"; +import { + getAllTableColumnKeys, + renderCell, + renderActions, + reorderColumns, +} from "components/designSystems/appsmith/TableUtilities"; import { TABLE_SIZES } from "components/designSystems/appsmith/Table"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; +import { RenderMode, RenderModes } from "constants/WidgetConstants"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, @@ -12,6 +22,7 @@ import { import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; +import moment from "moment"; class TableWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -51,6 +62,141 @@ class TableWidget extends BaseWidget { }; } + getTableColumns = (tableData: object[]) => { + let columns: ReactTableColumnProps[] = []; + const hiddenColumns: ReactTableColumnProps[] = []; + const { + columnNameMap, + columnSizeMap, + columnTypeMap, + widgetId, + columnActions, + } = this.props; + if (tableData.length) { + const columnKeys: string[] = getAllTableColumnKeys(tableData); + for (let index = 0; index < columnKeys.length; index++) { + const i = columnKeys[index]; + const columnName: string = + columnNameMap && columnNameMap[i] ? columnNameMap[i] : i; + const columnType: { type: string; format?: string } = + columnTypeMap && columnTypeMap[i] + ? columnTypeMap[i] + : { type: ColumnTypes.TEXT }; + const columnSize: number = + columnSizeMap && columnSizeMap[i] ? columnSizeMap[i] : 150; + const isHidden = + !!this.props.hiddenColumns && this.props.hiddenColumns.includes(i); + const columnData = { + Header: columnName, + accessor: i, + width: columnSize, + minWidth: 60, + draggable: true, + isHidden: false, + metaProperties: { + isHidden: isHidden, + type: columnType.type, + format: columnType.format, + }, + Cell: (props: any) => { + return renderCell(props.cell.value, columnType.type, isHidden); + }, + }; + if (isHidden) { + columnData.isHidden = true; + hiddenColumns.push(columnData); + } else { + columns.push(columnData); + } + } + columns = reorderColumns(columns, this.props.columnOrder || []); + if (columnActions?.length) { + columns.push({ + Header: + columnNameMap && columnNameMap["actions"] + ? columnNameMap["actions"] + : "Actions", + accessor: "actions", + width: 150, + minWidth: 60, + draggable: true, + Cell: () => { + return renderActions({ + columnActions: columnActions, + onCommandClick: this.onCommandClick, + }); + }, + }); + } + if ( + hiddenColumns.length && + this.props.renderMode === RenderModes.CANVAS + ) { + columns = columns.concat(hiddenColumns); + } + } + return columns; + }; + + transformData = (tableData: object[], columns: ReactTableColumnProps[]) => { + const updatedTableData = []; + for (let row = 0; row < tableData.length; row++) { + const data: { [key: string]: any } = tableData[row]; + const tableRow: { [key: string]: any } = {}; + for (let colIndex = 0; colIndex < columns.length; colIndex++) { + const column = columns[colIndex]; + const { accessor } = column; + const value = data[accessor]; + if (column.metaProperties) { + const type = column.metaProperties.type; + const format = column.metaProperties.format; + switch (type) { + case ColumnTypes.CURRENCY: + if (!isNaN(value)) { + tableRow[accessor] = `${format}${value ? value : ""}`; + } else { + tableRow[accessor] = "Invalid Value"; + } + break; + case ColumnTypes.DATE: + let isValidDate = true; + if (isNaN(value)) { + const dateTime = Date.parse(value); + if (isNaN(dateTime)) { + isValidDate = false; + } + } + if (isValidDate) { + tableRow[accessor] = moment(value).format(format); + } else { + tableRow[accessor] = "Invalid Value"; + } + break; + case ColumnTypes.TIME: + let isValidTime = true; + if (isNaN(value)) { + const time = Date.parse(value); + if (isNaN(time)) { + isValidTime = false; + } + } + if (isValidTime) { + tableRow[accessor] = moment(value).format("HH:mm"); + } else { + tableRow[accessor] = "Invalid Value"; + } + break; + default: + tableRow[accessor] = value; + break; + } + } + } + updatedTableData.push(tableRow); + } + return updatedTableData; + }; + searchTableData = (tableData: object[]) => { const searchKey = this.props.searchKey !== undefined @@ -66,8 +212,9 @@ class TableWidget extends BaseWidget { getPageView() { const { tableData, hiddenColumns } = this.props; + const tableColumns = this.getTableColumns(tableData); const filteredTableData = this.searchTableData(tableData); - + const transformedData = this.transformData(filteredTableData, tableColumns); const serverSidePaginationEnabled = (this.props .serverSidePaginationEnabled && this.props.serverSidePaginationEnabled) as boolean; @@ -94,9 +241,11 @@ class TableWidget extends BaseWidget {