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 { return { selectedOption: "defaultOptionValue", selectedLabel: "defaultOptionValue", }; } static getMetaPropertiesMap(): Record { 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 ( 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;