PromucFlow_constructor/app/client/src/components/propertyControls/PrimaryColumnsControlV2.tsx
Nilansh Bansal 6763fa5abc
fix: Table and JSON Widgets columns max height adjusted (#31882)
## Description
> This PR adjusts the maximum number of columns to be shown in Table and
JSON Widgets to 4.

<img width="842" alt="image"
src="https://github.com/appsmithorg/appsmith/assets/25542733/55f85f98-2fc9-4fa1-960b-fc685d74b0d8">


Fixes #31833  

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!CAUTION]  
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8355362590>
> Commit: `576e01175a9427bbe8e2a463ad31881fc2875123`
> Cypress dashboard: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8355362590&attempt=1&selectiontype=test&testsstatus=failed&specsstatus=fail"
target="_blank"> Click here!</a>
> The following are new failures, please fix them before merging the PR:
<ol>
>
<li>cypress/e2e/Regression/ClientSide/EmbedSettings/EmbedSettings_spec.js
>
<li>cypress/e2e/Regression/ClientSide/Widgets/Others/IconButton_2_spec.ts
> <li>cypress/e2e/Regression/ClientSide/Widgets/Select/RTL_support.ts
</ol>
> To know the list of identified flaky tests - <a
href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master"
target="_blank">Refer here</a>

<!-- end of auto-generated comment: Cypress test results  -->








<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new property to enhance the layout consistency of
draggable list controls.
- **Refactor**
- Updated the layout configuration for improved user interface
consistency in list controls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-20 08:50:46 +00:00

563 lines
17 KiB
TypeScript

import React, { Component } from "react";
import type { AppState } from "@appsmith/reducers";
import { connect } from "react-redux";
import type { Placement } from "popper.js";
import * as Sentry from "@sentry/react";
import _, { toString } from "lodash";
import type { ControlProps } from "./BaseControl";
import BaseControl from "./BaseControl";
import styled from "styled-components";
import type { Indices } from "constants/Layers";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import type { CodeEditorExpected } from "components/editorComponents/CodeEditor";
import type { ColumnProperties } from "widgets/TableWidgetV2/component/Constants";
import { StickyType } from "widgets/TableWidgetV2/component/Constants";
import {
itemHeight,
noOfItemsToDisplay,
extraSpace,
} from "widgets/TableWidgetV2/component/Constants";
import {
createColumn,
isColumnTypeEditable,
reorderColumns,
} from "widgets/TableWidgetV2/widget/utilities";
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import {
getDataTreeForAutocomplete,
getPathEvalErrors,
} from "selectors/dataTreeSelectors";
import type { EvaluationError } from "utils/DynamicBindingUtils";
import { isDynamicValue } from "utils/DynamicBindingUtils";
import { DraggableListCard } from "components/propertyControls/DraggableListCard";
import { Checkbox } from "design-system";
import { ColumnTypes } from "widgets/TableWidgetV2/constants";
import { DraggableListControl } from "pages/Editor/PropertyPane/DraggableListControl";
import { Button } from "design-system";
const EdtiableCheckboxWrapper = styled.div<{ rightPadding: boolean | null }>`
position: relative;
${(props) => props.rightPadding && `right: 6px;`}
align-items: center;
.ads-v2-checkbox {
width: 16px;
height: 16px;
padding: 0;
}
`;
const EmptyStateLabel = styled.div`
margin: 20px 0px;
text-align: center;
color: var(--ads-v2-color-fg);
`;
interface ReduxStateProps {
dynamicData: DataTree;
datasources: any;
errors: EvaluationError[];
}
interface EvaluatedValueProps {
isFocused: boolean;
theme: EditorTheme;
popperPlacement?: Placement;
popperZIndex?: Indices;
dataTreePath?: string;
evaluatedValue?: any;
expected?: CodeEditorExpected;
hideEvaluatedValue?: boolean;
useValidationMessage?: boolean;
children: JSX.Element;
}
type EvaluatedValuePopupWrapperProps = ReduxStateProps & EvaluatedValueProps;
type ColumnsType = Record<string, ColumnProperties>;
const getOriginalColumn = (
columns: ColumnsType,
index: number,
columnOrder?: string[],
): ColumnProperties | undefined => {
const reorderedColumns = reorderColumns(columns, columnOrder || []);
const column: ColumnProperties | undefined = Object.values(
reorderedColumns,
).find((column: ColumnProperties) => column.index === index);
return column;
};
const fixedHeight = itemHeight * noOfItemsToDisplay + extraSpace;
interface State {
focusedIndex: number | null;
duplicateColumnIds: string[];
hasScrollableList: boolean;
}
const LIST_CLASSNAME = "tablewidgetv2-primarycolumn-list";
class PrimaryColumnsControlV2 extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
const columns: ColumnsType = props.propertyValue || {};
const columnOrder = Object.keys(columns);
const reorderedColumns = reorderColumns(columns, columnOrder);
const tableColumnLabels = _.map(reorderedColumns, "label");
const duplicateColumnIds = [];
for (let index = 0; index < tableColumnLabels.length; index++) {
const currLabel = tableColumnLabels[index] as string;
const duplicateValueIndex = tableColumnLabels.indexOf(currLabel);
if (duplicateValueIndex !== index) {
// get column id from columnOrder index
duplicateColumnIds.push(reorderedColumns[columnOrder[index]].id);
}
}
this.state = {
focusedIndex: null,
duplicateColumnIds,
hasScrollableList: false,
};
}
componentDidMount(): void {
this.setHasScrollableList();
}
componentDidUpdate(prevProps: ControlProps): void {
/**
* On adding a new column the last column should get focused.
* If frozen columns are present then the focus should be on the newly added column
*/
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
const columns = Object.keys(this.props.propertyValue);
const frozenColumnIndex = Object.keys(prevProps.propertyValue)
.map((column) => prevProps.propertyValue[column])
.filter((column) => column.sticky !== StickyType.RIGHT).length;
this.updateFocus(
frozenColumnIndex === 0 ? columns.length - 1 : frozenColumnIndex,
true,
);
}
this.setHasScrollableList();
}
render() {
// Get columns from widget properties
const columns: ColumnsType = this.props.propertyValue || {};
// If there are no columns, show empty state
if (Object.keys(columns).length === 0) {
return <EmptyStateLabel>Table columns will appear here</EmptyStateLabel>;
}
// Get an empty array of length of columns
let columnOrder: string[] = new Array(Object.keys(columns).length);
if (this.props.widgetProperties.columnOrder) {
columnOrder = this.props.widgetProperties.columnOrder;
} else {
columnOrder = Object.keys(columns);
}
const reorderedColumns = reorderColumns(columns, columnOrder);
const draggableComponentColumns = Object.values(reorderedColumns).map(
(column: ColumnProperties) => {
return {
label: column.label || "",
id: column.id,
isVisible: column.isVisible,
isDerived:
column.isDerived && column.columnType !== ColumnTypes.EDIT_ACTIONS,
index: column.index,
isDuplicateLabel: _.includes(
this.state.duplicateColumnIds,
column.id,
),
isChecked:
isColumnTypeEditable(column.columnType) && column.isEditable,
isCheckboxDisabled:
!isColumnTypeEditable(column.columnType) || column.isDerived,
isDragDisabled:
column.sticky === StickyType.LEFT ||
column.sticky === StickyType.RIGHT,
};
},
);
const column: ColumnProperties | undefined = Object.values(
reorderedColumns,
).find(
(column: ColumnProperties) => column.index === this.state.focusedIndex,
);
// show popup on duplicate column label input focused
const isFocused =
!_.isNull(this.state.focusedIndex) &&
_.includes(this.state.duplicateColumnIds, column?.id);
return (
<>
<div className="flex pt-2 pb-2 justify-between">
<div>{Object.values(reorderedColumns).length} columns</div>
{this.isEditableColumnPresent() && (
<EdtiableCheckboxWrapper
className="flex t--uber-editable-checkbox"
rightPadding={this.state.hasScrollableList}
>
<span className="mr-2">Editable</span>
<Checkbox
isSelected={this.isAllColumnsEditable()}
onChange={this.toggleAllColumnsEditability}
/>
</EdtiableCheckboxWrapper>
)}
</div>
<div className="flex flex-col w-full gap-1">
<EvaluatedValuePopupWrapper {...this.props} isFocused={isFocused}>
<DraggableListControl
className={LIST_CLASSNAME}
deleteOption={this.deleteOption}
fixedHeight={fixedHeight}
focusedIndex={this.state.focusedIndex}
itemHeight={itemHeight}
items={draggableComponentColumns}
keyAccessor="id"
onEdit={this.onEdit}
propertyPath={this.props.dataTreePath}
renderComponent={(props: any) =>
DraggableListCard({
...props,
showCheckbox: true,
placeholder: "Column title",
})
}
toggleCheckbox={this.toggleCheckbox}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
updateOption={this.updateOption}
/>
</EvaluatedValuePopupWrapper>
<Button
className="self-end t--add-column-btn"
kind="tertiary"
onClick={this.addNewColumn}
size="sm"
startIcon="plus"
>
Add new column
</Button>
</div>
</>
);
}
addNewColumn = () => {
const column = createColumn(this.props.widgetProperties, "customColumn");
this.updateProperty(`${this.props.propertyName}.${column.id}`, column);
};
setHasScrollableList = () => {
const listElement = document.querySelector(`.${LIST_CLASSNAME}`);
requestAnimationFrame(() => {
const hasScrollableList =
listElement && listElement?.scrollHeight > listElement?.clientHeight;
if (hasScrollableList !== this.state.hasScrollableList) {
this.setState({
hasScrollableList: !!hasScrollableList,
});
}
});
};
onEdit = (index: number) => {
const columns: ColumnsType = this.props.propertyValue || {};
const originalColumn = getOriginalColumn(
columns,
index,
this.props.widgetProperties.columnOrder,
);
this.props.openNextPanel({
...originalColumn,
propPaneId: this.props.widgetProperties.widgetId,
});
};
//Used to reorder columns
updateItems = (items: Array<Record<string, unknown>>) => {
this.updateProperty(
"columnOrder",
items.map(({ id }) => id),
);
};
toggleVisibility = (index: number) => {
const columns: ColumnsType = this.props.propertyValue || {};
const originalColumn = getOriginalColumn(
columns,
index,
this.props.widgetProperties.columnOrder,
);
if (originalColumn) {
this.updateProperty(
`${this.props.propertyName}.${originalColumn.id}.isVisible`,
!originalColumn.isVisible,
);
}
};
getToggleColumnEditablityUpdates = (
column: ColumnProperties,
propertyName: string,
checked: boolean,
): {
propertyName: string;
propertyValue: string;
}[] => {
const updates: {
propertyName: string;
propertyValue: any;
}[] = [];
updates.push({
propertyName: `${propertyName}.${column.id}.isEditable`,
propertyValue: checked,
});
/*
* Check whether isCellEditable property of the column has dynamic value
* if not, toggle isCellEditable value as well. We're doing this to smooth
* the user experience.
*/
if (!isDynamicValue(toString(column.isCellEditable))) {
updates.push({
propertyName: `${propertyName}.${column.id}.isCellEditable`,
propertyValue: checked,
});
}
return updates;
};
toggleCheckbox = (index: number, checked: boolean) => {
const columns: ColumnsType = this.props.propertyValue || {};
const originalColumn = getOriginalColumn(
columns,
index,
this.props.widgetProperties.columnOrder,
);
if (originalColumn) {
this.batchUpdatePropertiesWithAssociatedUpdates(
this.getToggleColumnEditablityUpdates(
originalColumn,
this.props.propertyName,
checked,
),
);
}
};
deleteOption = (index: number) => {
const columns: ColumnsType = this.props.propertyValue || {};
const columnOrder = this.props.widgetProperties.columnOrder || [];
const originalColumn = getOriginalColumn(columns, index, columnOrder);
if (originalColumn) {
const propertiesToDelete = [
`${this.props.propertyName}.${originalColumn.id}`,
];
const columnOrderIndex = columnOrder.findIndex(
(column: string) => column === originalColumn.id,
);
if (columnOrderIndex > -1)
propertiesToDelete.push(`columnOrder[${columnOrderIndex}]`);
this.deleteProperties(propertiesToDelete);
// if column deleted, clean up duplicateIndexes
let duplicateColumnIds = [...this.state.duplicateColumnIds];
duplicateColumnIds = duplicateColumnIds.filter(
(id) => id !== originalColumn.id,
);
this.setState({ duplicateColumnIds });
}
};
updateOption = (index: number, updatedLabel: string) => {
const columns: ColumnsType = this.props.propertyValue || {};
const originalColumn = getOriginalColumn(
columns,
index,
this.props.widgetProperties.columnOrder,
);
if (originalColumn) {
this.updateProperty(
`${this.props.propertyName}.${originalColumn.id}.label`,
updatedLabel,
);
// check entered label is unique or duplicate
const tableColumnLabels = _.map(columns, "label");
let duplicateColumnIds = [...this.state.duplicateColumnIds];
// if duplicate, add into array
if (_.includes(tableColumnLabels, updatedLabel)) {
duplicateColumnIds.push(originalColumn.id);
this.setState({ duplicateColumnIds });
} else {
duplicateColumnIds = duplicateColumnIds.filter(
(id) => id !== originalColumn.id,
);
this.setState({ duplicateColumnIds });
}
}
};
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
isAllColumnsEditable = () => {
const columns: ColumnsType = this.props.propertyValue || {};
return !Object.values(columns).find(
(column) =>
!column.isEditable &&
isColumnTypeEditable(column.columnType) &&
!column.isDerived,
);
};
toggleAllColumnsEditability = () => {
const isEditable = this.isAllColumnsEditable();
const columns: ColumnsType = this.props.propertyValue || {};
//consolidate all column editability updates
const allUpdates = Object.values(columns)
.filter(
(column) =>
isColumnTypeEditable(column.columnType) && !column.isDerived,
)
.flatMap((column) =>
this.getToggleColumnEditablityUpdates(
column,
this.props.propertyName,
!isEditable,
),
);
this.batchUpdatePropertiesWithAssociatedUpdates(allUpdates);
if (isEditable) {
const columnOrder = this.props.widgetProperties.columnOrder || [];
const editActionColumn = Object.values(columns).find(
(column) => column.columnType === ColumnTypes.EDIT_ACTIONS,
);
if (editActionColumn) {
this.deleteOption(columnOrder.indexOf(editActionColumn.id));
}
}
};
isEditableColumnPresent = () => {
return Object.values(this.props.propertyValue).some((column) =>
isColumnTypeEditable((column as ColumnProperties).columnType),
);
};
static getControlType() {
return "PRIMARY_COLUMNS_V2";
}
}
export default PrimaryColumnsControlV2;
/**
* wrapper component on dragable primary columns
* render popup if primary column labels are not unique
* show unique name error in PRIMARY_COLUMNS
*/
class EvaluatedValuePopupWrapperClass extends Component<EvaluatedValuePopupWrapperProps> {
getPropertyValidation = (
dataTree: DataTree,
dataTreePath?: string,
): {
isInvalid: boolean;
errors: EvaluationError[];
pathEvaluatedValue: unknown;
} => {
const { errors } = this.props;
if (!dataTreePath) {
return {
isInvalid: false,
errors: [],
pathEvaluatedValue: undefined,
};
}
const pathEvaluatedValue = _.get(dataTree, dataTreePath);
return {
isInvalid: errors.length > 0,
errors: errors,
pathEvaluatedValue,
};
};
render = () => {
const {
dataTreePath,
dynamicData,
evaluatedValue,
expected,
hideEvaluatedValue,
useValidationMessage,
} = this.props;
const { errors, isInvalid, pathEvaluatedValue } =
this.getPropertyValidation(dynamicData, dataTreePath);
let evaluated = evaluatedValue;
if (dataTreePath) {
evaluated = pathEvaluatedValue;
}
return (
<EvaluatedValuePopup
errors={errors}
evaluatedValue={evaluated}
expected={expected}
hasError={isInvalid}
hideEvaluatedValue={hideEvaluatedValue}
isOpen={this.props.isFocused && isInvalid}
popperPlacement={this.props.popperPlacement}
popperZIndex={this.props.popperZIndex}
theme={this.props.theme || EditorTheme.LIGHT}
useValidationMessage={useValidationMessage}
>
{this.props.children}
</EvaluatedValuePopup>
);
};
}
const mapStateToProps = (
state: AppState,
{ dataTreePath }: EvaluatedValueProps,
): ReduxStateProps => {
return {
dynamicData: getDataTreeForAutocomplete(state),
datasources: state.entities.datasources,
errors: dataTreePath ? getPathEvalErrors(state, dataTreePath) : [],
};
};
const EvaluatedValuePopupWrapper = Sentry.withProfiler(
connect(mapStateToProps)(EvaluatedValuePopupWrapperClass),
);