568 lines
18 KiB
TypeScript
568 lines
18 KiB
TypeScript
import React, { ReactNode } from "react";
|
|
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
|
|
import { TextSize, WidgetType } from "constants/WidgetConstants";
|
|
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
|
import { isArray, findIndex } from "lodash";
|
|
import {
|
|
ValidationResponse,
|
|
ValidationTypes,
|
|
} from "constants/WidgetValidation";
|
|
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
|
|
import { DefaultValueType } from "rc-select/lib/interface/generator";
|
|
import { Layers } from "constants/Layers";
|
|
import { isString } from "../../../utils/helpers";
|
|
import { AutocompleteDataType } from "utils/autocomplete/TernServer";
|
|
import { GRID_DENSITY_MIGRATION_V1, MinimumPopupRows } from "widgets/constants";
|
|
import SingleSelectTreeComponent from "../component";
|
|
import { LabelPosition } from "components/constants";
|
|
import { Alignment } from "@blueprintjs/core";
|
|
|
|
function defaultOptionValueValidation(value: unknown): ValidationResponse {
|
|
if (typeof value === "string") return { isValid: true, parsed: value.trim() };
|
|
if (value === undefined || value === null)
|
|
return {
|
|
isValid: false,
|
|
parsed: "",
|
|
messages: ["This value does not evaluate to type: string"],
|
|
};
|
|
return { isValid: true, parsed: value };
|
|
}
|
|
class SingleSelectTreeWidget extends BaseWidget<
|
|
SingleSelectTreeWidgetProps,
|
|
WidgetState
|
|
> {
|
|
static getPropertyPaneConfig() {
|
|
return [
|
|
{
|
|
sectionName: "General",
|
|
children: [
|
|
{
|
|
helpText:
|
|
"Allows users to select multiple options. Values must be unique",
|
|
propertyName: "options",
|
|
label: "Options",
|
|
controlType: "INPUT_TEXT",
|
|
placeholderText: "Enter option value",
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
isJSConvertible: false,
|
|
validation: {
|
|
type: ValidationTypes.NESTED_OBJECT_ARRAY,
|
|
params: {
|
|
unique: ["value"],
|
|
default: [],
|
|
children: {
|
|
type: ValidationTypes.OBJECT,
|
|
params: {
|
|
allowedKeys: [
|
|
{
|
|
name: "label",
|
|
type: ValidationTypes.TEXT,
|
|
params: {
|
|
default: "",
|
|
required: true,
|
|
},
|
|
},
|
|
{
|
|
name: "value",
|
|
type: ValidationTypes.TEXT,
|
|
params: {
|
|
default: "",
|
|
},
|
|
},
|
|
{
|
|
name: "children",
|
|
type: ValidationTypes.ARRAY,
|
|
required: false,
|
|
params: {
|
|
children: {
|
|
type: ValidationTypes.OBJECT,
|
|
params: {
|
|
allowedKeys: [
|
|
{
|
|
name: "label",
|
|
type: ValidationTypes.TEXT,
|
|
params: {
|
|
default: "",
|
|
required: true,
|
|
},
|
|
},
|
|
{
|
|
name: "value",
|
|
type: ValidationTypes.TEXT,
|
|
params: {
|
|
default: "",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
evaluationSubstitutionType:
|
|
EvaluationSubstitutionType.SMART_SUBSTITUTE,
|
|
},
|
|
{
|
|
helpText: "Selects the option with value by default",
|
|
propertyName: "defaultOptionValue",
|
|
label: "Default Value",
|
|
controlType: "INPUT_TEXT",
|
|
placeholderText: "Enter option value",
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: {
|
|
type: ValidationTypes.FUNCTION,
|
|
params: {
|
|
fn: defaultOptionValueValidation,
|
|
expected: {
|
|
type: "value",
|
|
example: `value1`,
|
|
autocompleteDataType: AutocompleteDataType.STRING,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
helpText: "Sets a Placeholder Text",
|
|
propertyName: "placeholderText",
|
|
label: "Placeholder",
|
|
controlType: "INPUT_TEXT",
|
|
placeholderText: "Enter placeholder text",
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
propertyName: "isRequired",
|
|
label: "Required",
|
|
helpText: "Makes input to the widget mandatory",
|
|
controlType: "SWITCH",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
{
|
|
helpText: "Controls the visibility of the widget",
|
|
propertyName: "isVisible",
|
|
label: "Visible",
|
|
controlType: "SWITCH",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
{
|
|
propertyName: "isDisabled",
|
|
label: "Disabled",
|
|
helpText: "Disables input to this widget",
|
|
controlType: "SWITCH",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
{
|
|
propertyName: "animateLoading",
|
|
label: "Animate Loading",
|
|
controlType: "SWITCH",
|
|
helpText: "Controls the loading of the widget",
|
|
defaultValue: true,
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
{
|
|
propertyName: "allowClear",
|
|
label: "Allow Clearing Value",
|
|
helpText: "Enables Icon to clear all Selections",
|
|
controlType: "SWITCH",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
{
|
|
propertyName: "expandAll",
|
|
label: "Expand all by default",
|
|
helpText: "Expand All nested options",
|
|
controlType: "SWITCH",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.BOOLEAN },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sectionName: "Events",
|
|
children: [
|
|
{
|
|
helpText: "Triggers an action when a user selects an option",
|
|
propertyName: "onOptionChange",
|
|
label: "onOptionChange",
|
|
controlType: "ACTION_SELECTOR",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sectionName: "Label",
|
|
children: [
|
|
{
|
|
helpText: "Sets the label text of the widget",
|
|
propertyName: "labelText",
|
|
label: "Text",
|
|
controlType: "INPUT_TEXT",
|
|
placeholderText: "Enter label text",
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
helpText: "Sets the label position of the widget",
|
|
propertyName: "labelPosition",
|
|
label: "Position",
|
|
controlType: "DROP_DOWN",
|
|
options: [
|
|
{ label: "Left", value: LabelPosition.Left },
|
|
{ label: "Top", value: LabelPosition.Top },
|
|
{ label: "Auto", value: LabelPosition.Auto },
|
|
],
|
|
isBindProperty: false,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
helpText: "Sets the label alignment of the widget",
|
|
propertyName: "labelAlignment",
|
|
label: "Alignment",
|
|
controlType: "LABEL_ALIGNMENT_OPTIONS",
|
|
options: [
|
|
{
|
|
icon: "LEFT_ALIGN",
|
|
value: Alignment.LEFT,
|
|
},
|
|
{
|
|
icon: "RIGHT_ALIGN",
|
|
value: Alignment.RIGHT,
|
|
},
|
|
],
|
|
isBindProperty: false,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
hidden: (props: SingleSelectTreeWidgetProps) =>
|
|
props.labelPosition !== LabelPosition.Left,
|
|
dependencies: ["labelPosition"],
|
|
},
|
|
{
|
|
helpText:
|
|
"Sets the label width of the widget as the number of columns",
|
|
propertyName: "labelWidth",
|
|
label: "Width (in columns)",
|
|
controlType: "NUMERIC_INPUT",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
min: 0,
|
|
validation: {
|
|
type: ValidationTypes.NUMBER,
|
|
params: {
|
|
natural: true,
|
|
},
|
|
},
|
|
hidden: (props: SingleSelectTreeWidgetProps) =>
|
|
props.labelPosition !== LabelPosition.Left,
|
|
dependencies: ["labelPosition"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sectionName: "Styles",
|
|
children: [
|
|
{
|
|
propertyName: "accentColor",
|
|
label: "Accent Color",
|
|
controlType: "COLOR_PICKER",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
invisible: true,
|
|
},
|
|
{
|
|
propertyName: "borderRadius",
|
|
label: "Border Radius",
|
|
helpText:
|
|
"Rounds the corners of the icon button's outer border edge",
|
|
controlType: "BORDER_RADIUS_OPTIONS",
|
|
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
propertyName: "boxShadow",
|
|
label: "Box Shadow",
|
|
helpText:
|
|
"Enables you to cast a drop shadow from the frame of the widget",
|
|
controlType: "BOX_SHADOW_OPTIONS",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
propertyName: "labelStyle",
|
|
label: "Label Font Style",
|
|
controlType: "BUTTON_TABS",
|
|
options: [
|
|
{
|
|
icon: "BOLD_FONT",
|
|
value: "BOLD",
|
|
},
|
|
{
|
|
icon: "ITALICS_FONT",
|
|
value: "ITALIC",
|
|
},
|
|
],
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
propertyName: "labelTextColor",
|
|
label: "Label Text Color",
|
|
controlType: "COLOR_PICKER",
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
{
|
|
propertyName: "labelTextSize",
|
|
label: "Label Text Size",
|
|
controlType: "DROP_DOWN",
|
|
defaultValue: "0.875rem",
|
|
options: [
|
|
{
|
|
label: "S",
|
|
value: "0.875rem",
|
|
subText: "0.875rem",
|
|
},
|
|
{
|
|
label: "M",
|
|
value: "1rem",
|
|
subText: "1rem",
|
|
},
|
|
{
|
|
label: "L",
|
|
value: "1.25rem",
|
|
subText: "1.25rem",
|
|
},
|
|
{
|
|
label: "XL",
|
|
value: "1.875rem",
|
|
subText: "1.875rem",
|
|
},
|
|
{
|
|
label: "XXL",
|
|
value: "3rem",
|
|
subText: "3rem",
|
|
},
|
|
{
|
|
label: "3XL",
|
|
value: "3.75rem",
|
|
subText: "3.75rem",
|
|
},
|
|
],
|
|
isJSConvertible: true,
|
|
isBindProperty: true,
|
|
isTriggerProperty: false,
|
|
validation: { type: ValidationTypes.TEXT },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
static getDerivedPropertiesMap() {
|
|
return {
|
|
selectedOptionLabel: `{{ this.selectedLabel[0] }}`,
|
|
selectedOptionValue:
|
|
'{{ JSON.stringify(this.options).match(new RegExp(`"value":${Number.isFinite(this.selectedOption) ? this.selectedOption : `"${this.selectedOption}"` }`), "g") ? this.selectedOption : undefined }}',
|
|
isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`,
|
|
value: `{{this.selectedOptionValue}}`,
|
|
};
|
|
}
|
|
|
|
static getDefaultPropertiesMap(): Record<string, string> {
|
|
return {
|
|
selectedOption: "defaultOptionValue",
|
|
selectedLabel: "defaultOptionValue",
|
|
};
|
|
}
|
|
|
|
static getMetaPropertiesMap(): Record<string, any> {
|
|
return {
|
|
selectedOption: undefined,
|
|
selectedOptionValueArr: undefined,
|
|
selectedLabel: [],
|
|
isDirty: false,
|
|
};
|
|
}
|
|
|
|
componentDidUpdate(prevProps: SingleSelectTreeWidgetProps): void {
|
|
if (
|
|
this.props.defaultOptionValue !== prevProps.defaultOptionValue &&
|
|
this.props.isDirty
|
|
) {
|
|
this.props.updateWidgetMetaProperty("isDirty", false);
|
|
}
|
|
}
|
|
|
|
getPageView() {
|
|
const options =
|
|
isArray(this.props.options) &&
|
|
!this.props.__evaluation__?.errors.options.length
|
|
? this.props.options
|
|
: [];
|
|
const value: string | number | undefined =
|
|
isString(this.props.selectedOption) ||
|
|
Number.isFinite(this.props.selectedOption)
|
|
? this.props.selectedOption
|
|
: undefined;
|
|
const isInvalid =
|
|
"isValid" in this.props && !this.props.isValid && !!this.props.isDirty;
|
|
const filteredValue = this.filterValue(value);
|
|
const dropDownWidth = MinimumPopupRows * this.props.parentColumnSpace;
|
|
const { componentWidth } = this.getComponentDimensions();
|
|
return (
|
|
<SingleSelectTreeComponent
|
|
accentColor={this.props.accentColor}
|
|
allowClear={this.props.allowClear}
|
|
borderRadius={this.props.borderRadius}
|
|
boxShadow={this.props.boxShadow}
|
|
compactMode={
|
|
!(
|
|
(this.props.bottomRow - this.props.topRow) /
|
|
GRID_DENSITY_MIGRATION_V1 >
|
|
1
|
|
)
|
|
}
|
|
disabled={this.props.isDisabled ?? false}
|
|
dropDownWidth={dropDownWidth}
|
|
dropdownStyle={{
|
|
zIndex: Layers.dropdownModalWidget,
|
|
}}
|
|
expandAll={this.props.expandAll}
|
|
isFilterable
|
|
isValid={!isInvalid}
|
|
labelAlignment={this.props.labelAlignment}
|
|
labelPosition={this.props.labelPosition}
|
|
labelStyle={this.props.labelStyle}
|
|
labelText={this.props.labelText}
|
|
labelTextColor={this.props.labelTextColor}
|
|
labelTextSize={this.props.labelTextSize}
|
|
labelWidth={this.getLabelWidth()}
|
|
loading={this.props.isLoading}
|
|
onChange={this.onOptionChange}
|
|
options={options}
|
|
placeholder={this.props.placeholderText as string}
|
|
renderMode={this.props.renderMode}
|
|
value={filteredValue}
|
|
widgetId={this.props.widgetId}
|
|
width={componentWidth}
|
|
/>
|
|
);
|
|
}
|
|
|
|
onOptionChange = (value?: DefaultValueType, labelList?: ReactNode[]) => {
|
|
if (!this.props.isDirty) {
|
|
this.props.updateWidgetMetaProperty("isDirty", true);
|
|
}
|
|
|
|
this.props.updateWidgetMetaProperty("selectedOption", value);
|
|
this.props.updateWidgetMetaProperty("selectedLabel", labelList, {
|
|
triggerPropertyName: "onOptionChange",
|
|
dynamicString: this.props.onOptionChange,
|
|
event: {
|
|
type: EventType.ON_OPTION_CHANGE,
|
|
},
|
|
});
|
|
if (!this.props.isDirty) {
|
|
this.props.updateWidgetMetaProperty("isDirty", true);
|
|
}
|
|
};
|
|
|
|
flat(array: DropdownOption[]) {
|
|
let result: { value: string | number }[] = [];
|
|
array.forEach((a) => {
|
|
result.push({ value: a.value });
|
|
if (Array.isArray(a.children)) {
|
|
result = result.concat(this.flat(a.children));
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
filterValue(value: string | number | undefined) {
|
|
const options = this.props.options ? this.flat(this.props.options) : [];
|
|
|
|
if (isString(value) || Number.isFinite(value)) {
|
|
const index = findIndex(options, { value: value as string });
|
|
return index > -1 ? value : undefined;
|
|
}
|
|
}
|
|
|
|
static getWidgetType(): WidgetType {
|
|
return "SINGLE_SELECT_TREE_WIDGET";
|
|
}
|
|
}
|
|
|
|
export interface DropdownOption {
|
|
label: string;
|
|
value: string | number;
|
|
disabled?: boolean;
|
|
children?: DropdownOption[];
|
|
}
|
|
|
|
export interface SingleSelectTreeWidgetProps extends WidgetProps {
|
|
placeholderText?: string;
|
|
selectedIndex?: number;
|
|
options?: DropdownOption[];
|
|
onOptionChange: string;
|
|
defaultOptionValue: string;
|
|
isRequired: boolean;
|
|
isLoading: boolean;
|
|
allowClear: boolean;
|
|
selectedLabel: string[];
|
|
selectedOption: string | number;
|
|
selectedOptionValue: string;
|
|
selectedOptionLabel: string;
|
|
expandAll: boolean;
|
|
labelText: string;
|
|
labelPosition?: LabelPosition;
|
|
labelAlignment?: Alignment;
|
|
labelWidth?: number;
|
|
labelTextColor?: string;
|
|
labelTextSize?: TextSize;
|
|
labelStyle?: string;
|
|
borderRadius: string;
|
|
boxShadow?: string;
|
|
accentColor: string;
|
|
isDirty?: boolean;
|
|
}
|
|
|
|
export default SingleSelectTreeWidget;
|