Feature/table ui download data (#52)

* Created header for common functionalities in Table Widget

* Client side searching added in Table Widget. Action created for server side searching also.

* Columns visibility feature initial commit

* Column visibility list added in Table Widget

* Changed pagination designs in accordance with new layout. This enable user to jump page as well.

* Using colors values from constants

* Table widget pagination, numeric input page number clamped between 1 and total pages

* Adding tool tip to truncated values in table widget. Added AutoToolTipComponent that adds tooltip when text is truncated.

* Table data download changes. Added downlaod icon and button to table widget.

* Table data download changes

* Download table data as CSV implemented

* table data download, unused code removed

* Code review changes

* code review changes

Co-authored-by: Arpit Mohan <me@arpitmohan.com>
This commit is contained in:
vicky-primathon 2020-07-20 11:34:05 +05:30 committed by GitHub
parent eabe496fbd
commit 4f47a8ad3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 461 additions and 277 deletions

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M16.125 8.36658C15.5583 5.49158 13.0333 3.33325 10 3.33325C7.59167 3.33325 5.5 4.69992 4.45833 6.69992C1.95 6.96658 0 9.09158 0 11.6666C0 14.4249 2.24167 16.6666 5 16.6666H15.8333C18.1333 16.6666 20 14.7999 20 12.4999C20 10.2999 18.2917 8.51658 16.125 8.36658ZM14.1667 10.8333L10 14.9999L5.83333 10.8333H8.33333V7.49992H11.6667V10.8333H14.1667Z" fill="#2E3D49"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -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}

View File

@ -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}`}
>
<TableHeader
tableData={props.data}
tableColumns={props.columns}
searchTableData={props.searchTableData}
searchKey={props.searchKey}
updatePageNo={props.updatePageNo}
@ -112,6 +118,7 @@ export const Table = (props: TableProps) => {
pageCount={pageCount}
currentPageIndex={currentPageIndex}
pageOptions={pageOptions}
widgetName={props.widgetName}
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
columns={props.columns.filter((column: ReactTableColumnProps) => {
return column.accessor !== "actions";

View File

@ -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);
}}
>
<IconWrapper
width={20}
height={20}
color={selected ? Colors.OXFORD_BLUE : Colors.CADET_BLUE}
<Tooltip
autoFocus={false}
hoverOpenDelay={1000}
content="Hidden Fields"
position="top"
>
<VisibilityIcon />
</IconWrapper>
<IconWrapper
width={20}
height={20}
color={selected ? Colors.OXFORD_BLUE : Colors.CADET_BLUE}
>
<VisibilityIcon />
</IconWrapper>
</Tooltip>
</TableIconWrapper>
<DropDownWrapper>
{columns.map((option: ReactTableColumnProps, index: number) => (

View File

@ -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 (
<TableIconWrapper
onClick={() => {
downloadTableData();
}}
>
<Tooltip
autoFocus={false}
hoverOpenDelay={1000}
content="Download"
position="top"
>
<IconWrapper
width={20}
height={20}
color={selected ? Colors.OXFORD_BLUE : Colors.CADET_BLUE}
>
<DownloadIcon />
</IconWrapper>
</Tooltip>
</TableIconWrapper>
);
};
export default TableDataDownload;

View File

@ -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}
/>
<CommonFunctionsMenuWrapper>
<TableDataDownload
data={props.tableData}
columns={props.tableColumns}
widgetName={props.widgetName}
/>
{props.displayColumnActions && (
<TableColumnsVisibility
columns={props.columns}

View File

@ -3,8 +3,8 @@ import { Colors } from "constants/Colors";
import { TABLE_SIZES } from "components/designSystems/appsmith/Table";
export const TableWrapper = styled.div<{ width: number; height: number }>`
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};
}
`;

View File

@ -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: (
<MenuColumnWrapper selected={props.columnType === "image"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.IMAGE}>
<Icon
icon="media"
iconSize={12}
color={props.columnType === "image" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.IMAGE
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Image</div>
</MenuColumnWrapper>
),
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: (
<MenuColumnWrapper selected={props.columnType === "video"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.VIDEO}>
<Icon
icon="video"
iconSize={12}
color={props.columnType === "video" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.VIDEO
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Video</div>
</MenuColumnWrapper>
),
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: (
<MenuColumnWrapper selected={props.columnType === "text"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.TEXT}>
<Icon
icon="label"
iconSize={12}
color={props.columnType === "text" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.TEXT
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Text</div>
</MenuColumnWrapper>
),
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: (
<MenuColumnWrapper selected={props.columnType === "currency"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.CURRENCY}>
<Icon
icon="dollar"
iconSize={12}
color={props.columnType === "currency" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.CURRENCY
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Currency</div>
<Icon
className="sub-menu"
icon="chevron-right"
iconSize={16}
color={props.columnType === "currency" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.CURRENCY
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
</MenuColumnWrapper>
),
closeOnClick: false,
isSelected: props.columnType === "currency",
isSelected: props.columnType === ColumnTypes.CURRENCY,
options: [
{
content: "USD - $",
@ -198,23 +222,31 @@ export const getMenuOptions = (props: MenuOptionProps) => {
},
{
content: (
<MenuColumnWrapper selected={props.columnType === "date"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.DATE}>
<Icon
icon="calendar"
iconSize={12}
color={props.columnType === "date" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.DATE
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Date</div>
<Icon
className="sub-menu"
icon="chevron-right"
iconSize={16}
color={props.columnType === "date" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.DATE
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
</MenuColumnWrapper>
),
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: (
<MenuColumnWrapper selected={props.columnType === "time"}>
<MenuColumnWrapper selected={props.columnType === ColumnTypes.TIME}>
<Icon
icon="time"
iconSize={12}
color={props.columnType === "time" ? "#ffffff" : "#2E3D49"}
color={
props.columnType === ColumnTypes.TIME
? Colors.WHITE
: Colors.OXFORD_BLUE
}
/>
<div className="title">Time</div>
</MenuColumnWrapper>
),
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 <div></div>;
}
switch (columnType) {
case "image":
case ColumnTypes.IMAGE:
if (!isString(value)) {
return (
<CellWrapper isHidden={isHidden}>
@ -316,7 +346,7 @@ export const renderCell = (
})}
</CellWrapper>
);
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 = (
<CellWrapper isHidden={isHidden}>Invalid Video Link</CellWrapper>
);
}
case "currency":
if (!isNaN(value)) {
return (
<AutoToolTipComponent
title={`${format}${value}`}
isHidden={isHidden}
>{`${format}${value}`}</AutoToolTipComponent>
);
} else {
return <CellWrapper isHidden={isHidden}>Invalid Value</CellWrapper>;
}
case "date":
let isValidDate = true;
if (isNaN(value)) {
const dateTime = Date.parse(value);
if (isNaN(dateTime)) {
isValidDate = false;
}
}
if (isValidDate) {
return (
<AutoToolTipComponent
title={moment(value).format(format)}
isHidden={isHidden}
>
{moment(value).format(format)}
</AutoToolTipComponent>
);
} else {
return <CellWrapper isHidden={isHidden}>Invalid Date</CellWrapper>;
}
case "time":
let isValidTime = true;
if (isNaN(value)) {
const time = Date.parse(value);
if (isNaN(time)) {
isValidTime = false;
}
}
if (isValidTime) {
return (
<CellWrapper isHidden={isHidden}>
{moment(value).format("HH:mm")}
</CellWrapper>
);
} else {
return <CellWrapper isHidden={isHidden}>Invalid Time</CellWrapper>;
}
case "text":
const text = isString(value) ? value : JSON.stringify(value);
return (
<AutoToolTipComponent title={text} isHidden={isHidden}>
{text}
</AutoToolTipComponent>
);
default:
const data = isString(value) ? value : JSON.stringify(value);
const data =
isString(value) || isNumber(value) ? value : JSON.stringify(value);
return (
<AutoToolTipComponent title={data} isHidden={isHidden}>
<AutoToolTipComponent title={data.toString()} isHidden={isHidden}>
{data}
</AutoToolTipComponent>
);
@ -586,3 +562,51 @@ export const TableHeaderCell = (props: {
</div>
);
};
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;
};

View File

@ -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<TableWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
@ -51,6 +62,141 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
}
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<TableWidgetProps, WidgetState> {
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<TableWidgetProps, WidgetState> {
<ReactTableComponent
height={componentHeight}
width={componentWidth}
tableData={filteredTableData}
tableData={transformedData}
columns={tableColumns}
isLoading={this.props.isLoading}
widgetId={this.props.widgetId}
widgetName={this.props.widgetName}
searchKey={this.props.searchKey}
renderMode={this.props.renderMode}
hiddenColumns={hiddenColumns}