PromucFlow_constructor/app/client/src/components/propertyControls/PrimaryColumnsControl.tsx

428 lines
13 KiB
TypeScript
Raw Normal View History

import React, { Component } from "react";
import { AppState } from "reducers";
import { connect } from "react-redux";
import { Placement } from "popper.js";
import * as Sentry from "@sentry/react";
import _ from "lodash";
import BaseControl, { ControlProps } from "./BaseControl";
import { StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { Indices } from "constants/Layers";
import { DroppableComponent } from "components/ads/DraggableListComponent";
import { Size, Category } from "components/ads/Button";
import EmptyDataState from "components/utils/EmptyDataState";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { CodeEditorExpected } from "components/editorComponents/CodeEditor";
import { ColumnProperties } from "widgets/TableWidget/component/Constants";
import {
getDefaultColumnProperties,
getTableStyles,
} from "widgets/TableWidget/component/TableUtilities";
import { reorderColumns } from "widgets/TableWidget/component/TableHelpers";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import {
EvaluationError,
getEvalErrorPath,
getEvalValuePath,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { getNextEntityName } from "utils/AppsmithUtils";
import { DraggableListCard } from "components/ads/DraggableListCard";
const TabsWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
const AddColumnButton = styled(StyledPropertyPaneButton)`
width: 100%;
display: flex;
justify-content: center;
&&&& {
margin-top: 12px;
margin-bottom: 8px;
}
`;
interface ReduxStateProps {
dynamicData: DataTree;
datasources: any;
}
type EvaluatedValuePopupWrapperProps = ReduxStateProps & {
isFocused: boolean;
theme: EditorTheme;
popperPlacement?: Placement;
popperZIndex?: Indices;
dataTreePath?: string;
evaluatedValue?: any;
expected?: CodeEditorExpected;
hideEvaluatedValue?: boolean;
useValidationMessage?: boolean;
children: JSX.Element;
};
const getOriginalColumn = (
columns: Record<string, ColumnProperties>,
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;
};
type State = {
focusedIndex: number | null;
duplicateColumnIds: string[];
};
class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
const columns: Record<string, ColumnProperties> = 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,
};
}
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
render() {
// Get columns from widget properties
const columns: Record<string, ColumnProperties> =
this.props.propertyValue || {};
// If there are no columns, show empty state
if (Object.keys(columns).length === 0) {
return <EmptyDataState />;
}
// 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 {
feat: JSON Form widget (#8472) * initial layout * updated parser to support nested array * array field rendering * changes * ts fix * minor revert FormWidget * modified schema structure * select and switch fields * added checkbox field * added RadioGroupField * partial DateField and defaults, typing refactoring * added label and field type change * minor ts changes * changes * modified widget/utils for nested panelConfig, modified schema to object approach * array/object label support * hide field configuration when children not present * added tooltip * field visibility option * disabled state * upgraded tslib, form initial values * custom field configuration - add/hide/edit * field configuration - label change * return input when field configuration reaches max depth * minor changes * form - scroll, fixedfooter, enitity defn and other minior changes * form title * unregister on unmount * fixes * zero state * fix field padding * patched updating form values, removed linting warnings * configured action buttons * minor fix * minor change * property pane - sort fields in field configuration * refactor include all properties * checkbox properties * date properties * refactor typings and radio group properties * switch, multselect, select, array, object properties * minor changes * default value * ts fixes * checkbox field properties implementation * date field prop implementation * switch field * select field and fix deep nested meta properties * multiselect implementation * minor change * input field implementation * fix position jump on field type change * initial accordian * field state property and auto-complete of JSONFormComputeControl * merge fixes * renamed FormBuilder to JSONForm * source data validation minor change * custom field default value fix * Editable keys for custom field * minor fixes * replaced useFieldArray with custom logic, added widget icon * array and object accordian with border/background styling * minor change * disabled states for array and objects * default value minor fix * form level styles * modified logic for isDisabled for array and object, added disabledWhenInvalid, exposed isValid to fieldState for text input, removed useDisableChildren * added isValid for all field types * fixed reset to default values * debounce form values update * minor change * minor change * fix crash - source data change multi-select to array, fix crash - change of options * fix positioning * detect date type in source data * fix crash - when object is passed to regex input field * fixed default sourceData path for fields * accodion keep children mounted on collapse * jest test for schemaParser * widget/helper and useRegisterFieldInvalid test * tests for property config helper and generatePanelPropertyConfig * fix input field validation not appearing * fix date field type detection * rename data -> formData * handle null/undefined field value change in sourceData * added null/undefined as valid values for defaultValue text field * auto detect email field * set formData default value on initial load * switch field inline positioning * field margin fix for row direction * select full width * fiex date field default value - out of range * fix any field type to array * array default value logic change * base cypress test changes * initial json form render cy test * key sanitization * fix fieldState update logic * required design, object/array background color, accordion changes, fix - add new custom field * minor change * cypress tests * fix date formatted value, field state cypress test * cypress - field properties test and fixes * rename test file * fix accessort change to blank value, cypress tests * fix array field default value for modified accessor * minor fix * added animate loading * fix empty state, add new custom field * test data fix * fix warnings * fix timePrecision visibility * button styling * ported input v2 * fix jest tests * fix cypress tests * perf changes * perf improvement * added comments * multiselect changes * input field perf refactor * array field, object field refactor performance * checkbox field refactor * refectored date, radio, select and switch * fixes * test fixes * fixes * minor fix * rename field renderer * remove tracked fieldRenderer field * cypress test fixes * cypress changes * array default value fixes * arrayfield passedDefaultValue * auto enabled JS mode for few properties, reverted swith and date property controls * cypress changes * added widget sniping mode and fixed object passedDefaultValue * multiselect v2 * select v2 * fix jest tests * test fixes * field limit * rename field type dropdown texts * field type changes fixes * jest fixes * loading state submit button * default source data for new widget * modify limit message * multiseelct default value changes and cypress fix * select default value * keep default value intact on field type change * TextTable cypress text fix * review changes * fixed footer changes * collapse styles section by default * fixed footer changes * form modes * custom field key rentention * fixed footer fix in view mode * non ascii characters * fix meta merge in dataTreeWidget * minor fixes * rename useRegisterFieldInvalid.ts -> useRegisterFieldValidity.ts * modified dependency injection into evaluated values * refactored fixedfooter logic * minor change * accessor update * minor change * fixes * QA fixes date field, scroll content * fix phone number field, removed visiblity option from array item * fix sourceData autocomplete * reset logic * fix multiselect reset * form values hydration on widget drag * code review changes * reverted order of merge dataTreeWidget * fixes * added button titles, fixed hydration issue * default value fixes * upgraded react hook form, modified array-level/field-level default value logic * fixed select validation * added icon entity explorer, modified icon align control * modify accessor validation for mongo db _id * update email field regex * review changes * explicitly handle empty source data validation
2022-03-24 07:13:25 +00:00
label: column.label || "",
id: column.id,
isVisible: column.isVisible,
isDerived: column.isDerived,
index: column.index,
isDuplicateLabel: _.includes(
this.state.duplicateColumnIds,
column.id,
),
};
},
);
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 (
<TabsWrapper>
<EvaluatedValuePopupWrapper {...this.props} isFocused={isFocused}>
<DroppableComponent
deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45}
items={draggableComponentColumns}
onEdit={this.onEdit}
renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: false,
placeholder: "Column Title",
})
}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
updateOption={this.updateOption}
/>
</EvaluatedValuePopupWrapper>
<AddColumnButton
category={Category.tertiary}
className="t--add-column-btn"
icon="plus"
onClick={this.addNewColumn}
size={Size.medium}
tag="button"
text="Add a new column"
type="button"
/>
</TabsWrapper>
);
}
addNewColumn = () => {
const columns: Record<string, ColumnProperties> =
this.props.propertyValue || {};
const columnIds = Object.keys(columns);
const newColumnName = getNextEntityName("customColumn", columnIds);
const nextIndex = columnIds.length;
const columnProps: ColumnProperties = getDefaultColumnProperties(
newColumnName,
nextIndex,
this.props.widgetProperties.widgetName,
true,
);
const tableStyles = getTableStyles(this.props.widgetProperties);
const column = {
...columnProps,
buttonStyle: "rgb(3, 179, 101)",
isDisabled: false,
...tableStyles,
};
this.updateProperty(`${this.props.propertyName}.${column.id}`, column);
};
onEdit = (index: number) => {
const columns: Record<string, ColumnProperties> =
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: Record<string, ColumnProperties> =
this.props.propertyValue || {};
const originalColumn = getOriginalColumn(
columns,
index,
this.props.widgetProperties.columnOrder,
);
if (originalColumn) {
this.updateProperty(
`${this.props.propertyName}.${originalColumn.id}.isVisible`,
!originalColumn.isVisible,
);
}
};
deleteOption = (index: number) => {
const columns: Record<string, ColumnProperties> =
this.props.propertyValue || {};
const derivedColumns = this.props.widgetProperties.derivedColumns || {};
const columnOrder = this.props.widgetProperties.columnOrder || [];
const originalColumn = getOriginalColumn(columns, index, columnOrder);
if (originalColumn) {
const propertiesToDelete = [
`${this.props.propertyName}.${originalColumn.id}`,
];
if (derivedColumns[originalColumn.id])
propertiesToDelete.push(`derivedColumns.${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: Record<string, ColumnProperties> =
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 });
};
// updateCurrentFocusedInput = (index: number | null) => {};
static getControlType() {
return "PRIMARY_COLUMNS";
}
}
export default PrimaryColumnsControl;
/**
* 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;
} => {
if (!dataTreePath) {
return {
isInvalid: false,
errors: [],
pathEvaluatedValue: undefined,
};
}
const errors = _.get(
dataTree,
getEvalErrorPath(dataTreePath),
[],
) as EvaluationError[];
const filteredLintErrors = errors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const pathEvaluatedValue = _.get(dataTree, getEvalValuePath(dataTreePath));
return {
isInvalid: filteredLintErrors.length > 0,
errors: filteredLintErrors,
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): ReduxStateProps => ({
dynamicData: getDataTreeForAutocomplete(state),
datasources: state.entities.datasources,
});
const EvaluatedValuePopupWrapper = Sentry.withProfiler(
connect(mapStateToProps)(EvaluatedValuePopupWrapperClass),
);