import React, { useState } from "react"; import { MenuItem, Classes, Button as BButton, Alignment, } from "@blueprintjs/core"; import { CellWrapper, CellCheckboxWrapper, CellCheckbox, ActionWrapper, DraggableHeaderWrapper, IconButtonWrapper, } from "./TableStyledWrappers"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { ColumnTypes, CellAlignmentTypes, VerticalAlignmentTypes, ColumnProperties, CellLayoutProperties, TableStyles, MenuItems, } from "./Constants"; import { isString, isEmpty, findIndex, isNil, isNaN, get, set } from "lodash"; import PopoverVideo from "widgets/VideoWidget/component/PopoverVideo"; import AutoToolTipComponent from "widgets/TableWidget/component/AutoToolTipComponent"; import { ControlIcons } from "icons/ControlIcons"; import { AnyStyledComponent } from "styled-components"; import styled from "constants/DefaultTheme"; import { Colors } from "constants/Colors"; import { DropdownOption } from "widgets/DropdownWidget/constants"; import { IconName, IconNames } from "@blueprintjs/icons"; import { Select, IItemRendererProps } from "@blueprintjs/select"; import { FontStyleTypes } from "constants/WidgetConstants"; import { noop } from "utils/AppsmithUtils"; import { ReactComponent as CheckBoxLineIcon } from "assets/icons/widget/table/checkbox-line.svg"; import { ReactComponent as CheckBoxCheckIcon } from "assets/icons/widget/table/checkbox-check.svg"; import { ButtonVariant } from "components/constants"; //TODO(abstraction leak) import { StyledButton } from "widgets/IconButtonWidget/component"; import MenuButtonTableComponent from "./components/menuButtonTableComponent"; import { stopClickEventPropagation } from "utils/helpers"; import tinycolor from "tinycolor2"; import { generateTableColumnId } from "./TableHelpers"; export const renderCell = ( value: any, columnType: string, isHidden: boolean, cellProperties: CellLayoutProperties, tableWidth: number, isCellVisible: boolean, onClick: () => void = noop, isSelected?: boolean, ) => { switch (columnType) { case ColumnTypes.IMAGE: if (!value) { return ( ); } else if (!isString(value)) { return (
Invalid Image
); } // better regex: /(? {value .toString() // imageSplitRegex matched "," and char before it, so add space before "," .replace(imageSplitRegex, (match) => match.length > 1 ? `${match.charAt(0)} ,` : " ,", ) .split(imageSplitRegex) .map((item: string, index: number) => { if (imageUrlRegex.test(item) || base64ImageRegex.test(item)) { return (
{ if (isSelected) { e.stopPropagation(); } onClick(); }} >
); } else { return
Invalid Image
; } })} ); case ColumnTypes.VIDEO: const youtubeRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|\?v=)([^#&?]*).*/; if (!value) { return ( ); } else if (isString(value) && youtubeRegex.test(value)) { return ( ); } else { return ( Invalid Video Link ); } default: return ( {value && columnType === ColumnTypes.URL && cellProperties.displayText ? cellProperties.displayText : !isNil(value) && !isNaN(value) ? value.toString() : ""} ); } }; interface RenderIconButtonProps { isSelected: boolean; columnActions?: ColumnAction[]; iconName?: IconName; buttonVariant: ButtonVariant; buttonColor: string; borderRadius: string; boxShadow: string; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; isCellVisible: boolean; disabled: boolean; } export const renderIconButton = ( props: RenderIconButtonProps, isHidden: boolean, cellProperties: CellLayoutProperties, ) => { if (!props.columnActions) return ( ); return ( {props.columnActions.map((action: ColumnAction, index: number) => { return ( ); })} ); }; function IconButton(props: { iconName?: IconName; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; isSelected: boolean; action: ColumnAction; buttonColor: string; buttonVariant: ButtonVariant; borderRadius: string; boxShadow: string; disabled: boolean; }): JSX.Element { const [loading, setLoading] = useState(false); const onComplete = () => { setLoading(false); }; const handlePropagation = ( e: React.MouseEvent, ) => { if (props.isSelected) { e.stopPropagation(); } }; const handleClick = () => { if (props.action.dynamicTrigger) { setLoading(true); props.onCommandClick(props.action.dynamicTrigger, onComplete); } }; return ( ); } interface RenderActionProps { isSelected: boolean; columnActions?: ColumnAction[]; backgroundColor: string; borderRadius: string; boxShadow?: string; buttonLabelColor: string; buttonVariant: ButtonVariant; isDisabled: boolean; isCellVisible: boolean; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; } export interface RenderMenuButtonProps { isSelected: boolean; // columnActions?: ColumnAction[]; label: string; isDisabled: boolean; isCellVisible: boolean; onCommandClick: (dynamicTrigger: string, onComplete?: () => void) => void; isCompact?: boolean; menuItems: MenuItems; menuVariant?: ButtonVariant; menuColor?: string; borderRadius?: string; boxShadow?: string; iconName?: IconName; iconAlign?: Alignment; } export const renderActions = ( props: RenderActionProps, isHidden: boolean, cellProperties: CellLayoutProperties, ) => { if (!props.columnActions) return ( ); return ( {props.columnActions.map((action: ColumnAction, index: number) => { return ( ); })} ); }; export const renderMenuButton = ( props: RenderMenuButtonProps, isHidden: boolean, cellProperties: CellLayoutProperties, ) => { return ( ); }; interface MenuButtonProps extends Omit { action?: ColumnAction; } function MenuButton({ borderRadius, boxShadow, iconAlign, iconName, isCompact, isDisabled, isSelected, label, menuColor, menuItems, menuVariant, onCommandClick, }: MenuButtonProps): JSX.Element { const handlePropagation = ( e: React.MouseEvent, ) => { if (isSelected) { e.stopPropagation(); } }; const onItemClicked = (onClick?: string) => { if (onClick) { onCommandClick(onClick); } }; return (
); } function TableAction(props: { isSelected: boolean; action: ColumnAction; backgroundColor: string; boxShadow?: string; buttonLabelColor: string; buttonVariant: ButtonVariant; isDisabled: boolean; isCellVisible: boolean; borderRadius: string; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; }) { const [loading, setLoading] = useState(false); const onComplete = () => { setLoading(false); }; const handleClick = () => { if (props.action.dynamicTrigger) { setLoading(true); props.onCommandClick(props.action.dynamicTrigger, onComplete); } }; return ( { if (props.isSelected) { e.stopPropagation(); } }} > {props.isCellVisible ? ( ) : null} ); } export const renderCheckBoxCell = ( isChecked: boolean, accentColor: string, borderRadius: string, ) => ( {isChecked && } ); export const renderCheckBoxHeaderCell = ( onClick: (e: React.MouseEvent) => void, checkState: number | null, accentColor: string, borderRadius: string, ) => ( {checkState === 1 && } {checkState === 2 && ( )} ); export const renderEmptyRows = ( rowCount: number, columns: any, tableWidth: number, page: any, prepareRow: any, multiRowSelection = false, accentColor: string, borderRadius: string, ) => { const rows: string[] = new Array(rowCount).fill(""); if (page.length) { const row = page[0]; return rows.map((item: string, index: number) => { prepareRow(row); const rowProps = { ...row.getRowProps(), style: { display: "flex" }, }; return (
{multiRowSelection && renderCheckBoxCell(false, accentColor, borderRadius)} {row.cells.map((cell: any, cellIndex: number) => { const cellProps = cell.getCellProps(); set( cellProps, "style.backgroundColor", get(cell, "column.columnProperties.cellBackground"), ); return (
); })}
); }); } else { const tableColumns = columns.length ? columns : new Array(3).fill({ width: tableWidth / 3, isHidden: false }); return ( <> {rows.map((row: string, index: number) => { return (
{multiRowSelection && renderCheckBoxCell(false, accentColor, borderRadius)} {tableColumns.map((column: any, colIndex: number) => { return (
); })}
); })} ); } }; const AscendingIcon = styled(ControlIcons.SORT_CONTROL as AnyStyledComponent)` padding: 0; position: relative; top: 3px; cursor: pointer; transform: rotate(180deg); && svg { path { fill: ${Colors.LIGHT_GREYISH_BLUE}; } } `; const DescendingIcon = styled(ControlIcons.SORT_CONTROL as AnyStyledComponent)` padding: 0; position: relative; top: 3px; cursor: pointer; && svg { path { fill: ${Colors.LIGHT_GREYISH_BLUE}; } } `; export function TableHeaderCell(props: { columnName: string; columnIndex: number; isHidden: boolean; isAscOrder?: boolean; sortTableColumn: (columnIndex: number, asc: boolean) => void; isResizingColumn: boolean; column: any; editMode?: boolean; isSortable?: boolean; width: number; }) { const { column, editMode, isSortable, width } = props; const handleSortColumn = () => { if (props.isResizingColumn) return; let columnIndex = props.columnIndex; if (props.isAscOrder === true) { columnIndex = -1; } const sortOrder = props.isAscOrder === undefined ? false : !props.isAscOrder; props.sortTableColumn(columnIndex, sortOrder); }; const disableSort = editMode === false && isSortable === false; return (
{props.columnName} {props.isAscOrder !== undefined ? (
{props.isAscOrder ? ( ) : ( )}
) : null}
) => { e.preventDefault(); e.stopPropagation(); }} />
); } export function getDefaultColumnProperties( accessor: string, index: number, widgetProperties: any, isDerived?: boolean, ): ColumnProperties { const id = generateTableColumnId(accessor); const columnProps = { index: index, width: 150, id, horizontalAlignment: CellAlignmentTypes.LEFT, verticalAlignment: VerticalAlignmentTypes.CENTER, columnType: ColumnTypes.TEXT, textColor: Colors.THUNDER, textSize: "0.875rem", fontStyle: FontStyleTypes.REGULAR, enableFilter: true, enableSort: true, isVisible: true, isDisabled: false, isCellVisible: true, isDerived: !!isDerived, label: accessor, computedValue: isDerived ? "" : `{{${widgetProperties}.sanitizedTableData.map((currentRow) => ( currentRow.${id}))}}`, }; return columnProps; } export function getTableStyles(props: TableStyles) { return { textColor: props.textColor, textSize: props.textSize, fontStyle: props.fontStyle, cellBackground: props.cellBackground, verticalAlignment: props.verticalAlignment, horizontalAlignment: props.horizontalAlignment, }; } const SingleDropDown = Select.ofType(); const StyledSingleDropDown = styled(SingleDropDown)` div { padding: 0 10px; display: flex; justify-content: center; align-items: center; height: 100%; } span { width: 100%; height: 100%; position: relative; } .${Classes.BUTTON} { display: flex; width: 100%; align-items: center; justify-content: space-between; box-shadow: none; background: transparent; min-height: 32px; } .${Classes.BUTTON_TEXT} { text-overflow: ellipsis; text-align: left; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } && { .${Classes.ICON} { width: fit-content; color: ${Colors.SLATE_GRAY}; } } `; export const renderDropdown = (props: { options: DropdownOption[]; isCellVisible: boolean; onItemSelect: (onOptionChange: string, item: DropdownOption) => void; onOptionChange: string; selectedIndex?: number; }) => { const isOptionSelected = (selectedOption: DropdownOption) => { const optionIndex = findIndex(props.options, (option) => { return option.value === selectedOption.value; }); return optionIndex === props.selectedIndex; }; const renderSingleSelectItem = ( option: DropdownOption, itemProps: IItemRendererProps, ) => { if (!itemProps.modifiers.matchesPredicate) { return null; } if (!props.isCellVisible) { return null; } const isSelected: boolean = isOptionSelected(option); return ( ); }; return (
{ props.onItemSelect(props.onOptionChange, item); }} popoverProps={{ minimal: true, usePortal: true, popoverClassName: "select-popover-wrapper", }} > -1 ? props.options[props.selectedIndex].label : "-- Select --" } />
); }; /** * returns selected row bg color * * if the color is dark, use 80% lighter color for selected row * if color is light, use 10% darker color for selected row * * @param accentColor */ export const getSelectedRowBgColor = (accentColor: string) => { const tinyAccentColor = tinycolor(accentColor); const brightness = tinycolor(accentColor) .greyscale() .getBrightness(); const percentageBrightness = (brightness / 255) * 100; let nextBrightness = 0; switch (true) { case percentageBrightness > 70: nextBrightness = 10; break; case percentageBrightness > 50: nextBrightness = 35; break; case percentageBrightness > 50: nextBrightness = 55; break; default: nextBrightness = 60; } if (brightness > 180) { return tinyAccentColor.darken(10).toString(); } else { return tinyAccentColor.lighten(nextBrightness).toString(); } };