PromucFlow_constructor/app/client/src/widgets/TableWidget/component/TableUtilities.tsx
Arsalan Yaldram 119f0be18b
chore: update Styled components to latest version and related cleanup (#19284)
## Description
We need to upgrade `styled-components`, so that it will become easy to
upgrade to version 6.0 when it is out. This is because, v6.0 has an
important functionality which isn't available in today's version.

### Tasks completed
- Update Styled components to latest version.
- Prepare codebase by cleaning up the styled components functions that
will be deprecated in version 6
- We are still using the `withTheme` HOC, we should instead use the
`useTheme` hook (best practices)
- Remove the `AnyStyledComponent` type it is un-necessary and will be
deprecated

Fixes #19463


## Type of change
- Non breaking change. The application should work as before and should
not effect any visual elements or UI.

## How Has This Been Tested?
- Manual @appsmithorg/qa please refer to the test plan for areas of
interest.
- Cypress: All existing test cases must pass.

### Test Plan
- We need to do a sanity check on the Product Updates Modal, Release
section.
- We also need to do a sanity check on the Login, Signup, ResetPassword
pages.
- I think we can merge this Pull Request and continue with our weekly
regression, because there are no style changes in this Pull Request,
everything should work as expected.


## Checklist:
### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test
2023-01-13 16:35:59 +05:30

870 lines
23 KiB
TypeScript

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 styled from "styled-components";
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 (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
/>
);
} else if (!isString(value)) {
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
>
<div>Invalid Image </div>
</CellWrapper>
);
}
// better regex: /(?<!base64),/g ; can't use due to safari incompatibility
const imageSplitRegex = /[^(base64)],/g;
const imageUrlRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpeg|jpg|gif|png)??(?:&?[^=&]*=[^=&]*)*/;
const base64ImageRegex = /^data:image\/.*;base64/;
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
>
{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 (
<div
className="image-cell-wrapper"
key={index}
onClick={(e) => {
if (isSelected) {
e.stopPropagation();
}
onClick();
}}
>
<div
className="image-cell"
style={{ backgroundImage: `url("${item}")` }}
/>
</div>
);
} else {
return <div key={index}>Invalid Image</div>;
}
})}
</CellWrapper>
);
case ColumnTypes.VIDEO:
const youtubeRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|\?v=)([^#&?]*).*/;
if (!value) {
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
/>
);
} else if (isString(value) && youtubeRegex.test(value)) {
return (
<CellWrapper
cellProperties={cellProperties}
className="video-cell"
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
>
<PopoverVideo url={value} />
</CellWrapper>
);
} else {
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={isCellVisible}
isHidden={isHidden}
isPadding
>
Invalid Video Link
</CellWrapper>
);
}
default:
return (
<AutoToolTipComponent
cellProperties={cellProperties}
columnType={columnType}
isCellVisible={isCellVisible}
isHidden={isHidden}
tableWidth={tableWidth}
title={!!value ? value.toString() : ""}
>
{value && columnType === ColumnTypes.URL && cellProperties.displayText
? cellProperties.displayText
: !isNil(value) && !isNaN(value)
? value.toString()
: ""}
</AutoToolTipComponent>
);
}
};
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 (
<CellWrapper
cellProperties={cellProperties}
isHidden={isHidden}
isPadding
/>
);
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={props.isCellVisible}
isHidden={isHidden}
isPadding
>
{props.columnActions.map((action: ColumnAction, index: number) => {
return (
<IconButton
action={action}
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
buttonColor={props.buttonColor}
buttonVariant={props.buttonVariant}
disabled={props.disabled}
iconName={props.iconName}
isSelected={props.isSelected}
key={index}
onCommandClick={props.onCommandClick}
/>
);
})}
</CellWrapper>
);
};
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<HTMLDivElement, MouseEvent>,
) => {
if (props.isSelected) {
e.stopPropagation();
}
};
const handleClick = () => {
if (props.action.dynamicTrigger) {
setLoading(true);
props.onCommandClick(props.action.dynamicTrigger, onComplete);
}
};
return (
<IconButtonWrapper disabled={props.disabled} onClick={handlePropagation}>
<StyledButton
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
buttonColor={props.buttonColor}
buttonVariant={props.buttonVariant}
disabled={props.disabled}
icon={props.iconName}
loading={loading}
onClick={handleClick}
/>
</IconButtonWrapper>
);
}
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 (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={props.isCellVisible}
isHidden={isHidden}
isPadding
/>
);
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={props.isCellVisible}
isHidden={isHidden}
isPadding
>
{props.columnActions.map((action: ColumnAction, index: number) => {
return (
<TableAction
action={action}
backgroundColor={props.backgroundColor}
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
buttonLabelColor={props.buttonLabelColor}
buttonVariant={props.buttonVariant}
isCellVisible={props.isCellVisible}
isDisabled={props.isDisabled}
isSelected={props.isSelected}
key={index}
onCommandClick={props.onCommandClick}
/>
);
})}
</CellWrapper>
);
};
export const renderMenuButton = (
props: RenderMenuButtonProps,
isHidden: boolean,
cellProperties: CellLayoutProperties,
) => {
return (
<CellWrapper
cellProperties={cellProperties}
isCellVisible={props.isCellVisible}
isHidden={isHidden}
isPadding
>
<MenuButton {...props} />
</CellWrapper>
);
};
interface MenuButtonProps extends Omit<RenderMenuButtonProps, "columnActions"> {
action?: ColumnAction;
}
function MenuButton({
borderRadius,
boxShadow,
iconAlign,
iconName,
isCompact,
isDisabled,
isSelected,
label,
menuColor,
menuItems,
menuVariant,
onCommandClick,
}: MenuButtonProps): JSX.Element {
const handlePropagation = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
if (isSelected) {
e.stopPropagation();
}
};
const onItemClicked = (onClick?: string) => {
if (onClick) {
onCommandClick(onClick);
}
};
return (
<div onClick={handlePropagation}>
<MenuButtonTableComponent
borderRadius={borderRadius}
boxShadow={boxShadow}
iconAlign={iconAlign}
iconName={iconName}
isCompact={isCompact}
isDisabled={isDisabled}
label={label}
menuColor={menuColor}
menuItems={{ ...menuItems }}
menuVariant={menuVariant}
onItemClicked={onItemClicked}
/>
</div>
);
}
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 (
<ActionWrapper
disabled={props.isDisabled}
onClick={(e) => {
if (props.isSelected) {
e.stopPropagation();
}
}}
>
{props.isCellVisible ? (
<StyledButton
borderRadius={props.borderRadius}
boxShadow={props.boxShadow}
buttonColor={props.backgroundColor}
buttonVariant={props.buttonVariant}
disabled={props.isDisabled}
filled
loading={loading}
onClick={handleClick}
small
text={props.action.label}
/>
) : null}
</ActionWrapper>
);
}
export const renderCheckBoxCell = (
isChecked: boolean,
accentColor: string,
borderRadius: string,
) => (
<CellCheckboxWrapper
accentColor={accentColor}
borderRadius={borderRadius}
className="td t--table-multiselect"
isCellVisible
isChecked={isChecked}
>
<CellCheckbox>
{isChecked && <CheckBoxCheckIcon className="th-svg" />}
</CellCheckbox>
</CellCheckboxWrapper>
);
export const renderCheckBoxHeaderCell = (
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void,
checkState: number | null,
accentColor: string,
borderRadius: string,
) => (
<CellCheckboxWrapper
accentColor={accentColor}
borderRadius={borderRadius}
className="th header-reorder t--table-multiselect-header"
isChecked={!!checkState}
onClick={onClick}
role="columnheader"
>
<CellCheckbox>
{checkState === 1 && <CheckBoxCheckIcon className="th-svg" />}
{checkState === 2 && (
<CheckBoxLineIcon className="th-svg t--table-multiselect-header-half-check-svg" />
)}
</CellCheckbox>
</CellCheckboxWrapper>
);
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 (
<div {...rowProps} className="tr" key={index}>
{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 (
<div
{...cellProps}
className="td"
data-cy={`empty-row-${index}-cell-${cellIndex}`}
key={cellIndex}
/>
);
})}
</div>
);
});
} else {
const tableColumns = columns.length
? columns
: new Array(3).fill({ width: tableWidth / 3, isHidden: false });
return (
<>
{rows.map((row: string, index: number) => {
return (
<div
className="tr"
key={index}
style={{
display: "flex",
flex: "1 0 auto",
}}
>
{multiRowSelection &&
renderCheckBoxCell(false, accentColor, borderRadius)}
{tableColumns.map((column: any, colIndex: number) => {
return (
<div
className="td"
key={colIndex}
style={{
width: column.width + "px",
boxSizing: "border-box",
flex: `${column.width} 0 auto`,
backgroundColor: get(
column,
"columnProperties.cellBackground",
),
}}
/>
);
})}
</div>
);
})}
</>
);
}
};
const AscendingIcon = styled(ControlIcons.SORT_CONTROL)`
padding: 0;
position: relative;
top: 3px;
cursor: pointer;
transform: rotate(180deg);
&& svg {
path {
fill: ${Colors.LIGHT_GREYISH_BLUE};
}
}
`;
const DescendingIcon = styled(ControlIcons.SORT_CONTROL)`
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 (
<div
{...column.getHeaderProps()}
className="th header-reorder"
onClick={!disableSort && props ? handleSortColumn : undefined}
>
<DraggableHeaderWrapper
className={!props.isHidden ? `draggable-header` : "hidden-header"}
horizontalAlignment={column.columnProperties.horizontalAlignment}
>
<AutoToolTipComponent
noPadding
tableWidth={width}
title={props.columnName}
>
{props.columnName}
</AutoToolTipComponent>
</DraggableHeaderWrapper>
{props.isAscOrder !== undefined ? (
<div>
{props.isAscOrder ? (
<AscendingIcon height={16} width={16} />
) : (
<DescendingIcon height={16} width={16} />
)}
</div>
) : null}
<div
{...column.getResizerProps()}
className={`resizer ${column.isResizing ? "isResizing" : ""}`}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
}}
/>
</div>
);
}
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<DropdownOption>();
const StyledSingleDropDown = styled(SingleDropDown)<{
children?: React.ReactNode;
}>`
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 (
<MenuItem
active={isSelected}
className="single-select"
key={option.value}
onClick={itemProps.handleClick}
text={option.label}
/>
);
};
return (
<div onClick={stopClickEventPropagation} style={{ height: "100%" }}>
<StyledSingleDropDown
filterable={false}
itemRenderer={renderSingleSelectItem}
items={props.options}
onItemSelect={(item: DropdownOption) => {
props.onItemSelect(props.onOptionChange, item);
}}
popoverProps={{
minimal: true,
usePortal: true,
popoverClassName: "select-popover-wrapper",
}}
>
<BButton
rightIcon={IconNames.CHEVRON_DOWN}
text={
!isEmpty(props.options) &&
props.selectedIndex !== undefined &&
props.selectedIndex > -1
? props.options[props.selectedIndex].label
: "-- Select --"
}
/>
</StyledSingleDropDown>
</div>
);
};
/**
* 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();
}
};