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 } 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();
if (columns[0]?.columnProperties?.cellBackground) {
cellProps.style.background =
columns[0].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();
}
};