import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { ValidationResponse } from "constants/WidgetValidation"; import { ValidationTypes } from "constants/WidgetValidation"; import { isArray } from "lodash"; import React from "react"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; import { Alignment } from "@blueprintjs/core"; import type { AutocompletionDefinitions, WidgetCallout, } from "WidgetProvider/constants"; import { MinimumPopupWidthInPercentage } from "WidgetProvider/constants"; import { LabelPosition } from "components/constants"; import { Layers } from "constants/Layers"; import { WIDGET_TAGS, layoutConfigurations } from "constants/WidgetConstants"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; import { buildDeprecationWidgetMessage } from "pages/Editor/utils"; import type { DraftValueType } from "rc-select/lib/Select"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import { DefaultAutocompleteDefinitions, isCompactMode, } from "widgets/WidgetUtils"; import MultiSelectComponent from "../component"; import IconSVG from "../icon.svg"; function defaultOptionValueValidation(value: unknown): ValidationResponse { let values: string[] = []; if (typeof value === "string") { try { values = JSON.parse(value); if (!Array.isArray(values)) { throw new Error(); } } catch { values = value.length ? value.split(",") : []; if (values.length > 0) { values = values.map((_v: string) => _v.trim()); } } } if (Array.isArray(value)) { values = Array.from(new Set(value)); } return { isValid: true, parsed: values, }; } class MultiSelectWidget extends BaseWidget< MultiSelectWidgetProps, WidgetState > { static type = "MULTI_SELECT_WIDGET"; static getConfig() { return { name: "MultiSelect", iconSVG: IconSVG, needsMeta: true, hideCard: true, isDeprecated: true, replacement: "MULTI_SELECT_WIDGET_V2", tags: [WIDGET_TAGS.SELECT], }; } static getDefaults() { return { rows: 7, columns: 20, animateLoading: true, labelText: "Label", labelPosition: LabelPosition.Left, labelAlignment: Alignment.LEFT, labelWidth: 5, options: [ { label: "Blue", value: "BLUE" }, { label: "Green", value: "GREEN" }, { label: "Red", value: "RED" }, ], widgetName: "MultiSelect", serverSideFiltering: false, defaultOptionValue: ["GREEN"], version: 1, isRequired: false, isDisabled: false, placeholderText: "Select option(s)", responsiveBehavior: ResponsiveBehavior.Fill, minWidth: FILL_WIDGET_MIN_WIDTH, }; } static getMethods() { return { getEditorCallouts(): WidgetCallout[] { return [ { message: buildDeprecationWidgetMessage( MultiSelectWidget.getConfig().name, ), }, ]; }, }; } static getAutocompleteDefinitions(): AutocompletionDefinitions { return { "!doc": "MultiSelect is used to capture user input/s from a specified list of permitted inputs. A MultiSelect captures multiple choices from a list of options", "!url": "https://docs.appsmith.com/widget-reference/dropdown", isVisible: DefaultAutocompleteDefinitions.isVisible, filterText: { "!type": "string", "!doc": "The filter text for Server side filtering", }, selectedOptionValues: { "!type": "[string]", "!doc": "The array of values selected in a multi select dropdown", "!url": "https://docs.appsmith.com/widget-reference/dropdown", }, selectedOptionLabels: { "!type": "[string]", "!doc": "The array of selected option labels in a multi select dropdown", "!url": "https://docs.appsmith.com/widget-reference/dropdown", }, isDisabled: "bool", options: "[$__dropdownOption__$]", }; } 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: '[{ "label": "Option1", "value": "Option2" }]', isBindProperty: true, isTriggerProperty: false, isJSConvertible: false, validation: { type: ValidationTypes.ARRAY, params: { unique: ["value"], children: { type: ValidationTypes.OBJECT, params: { required: true, 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: "[GREEN]", isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.FUNCTION, params: { fn: defaultOptionValueValidation, expected: { type: "Array of values", example: `['option1', 'option2']`, autocompleteDataType: AutocompleteDataType.ARRAY, }, }, }, }, { helpText: "Sets a Placeholder Text", propertyName: "placeholderText", label: "Placeholder", controlType: "INPUT_TEXT", placeholderText: "Search", 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 }, }, { helpText: "Enables server side filtering of the data", propertyName: "serverSideFiltering", label: "Server side filtering", controlType: "SWITCH", isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, { helpText: "Controls the visibility of select all option in dropdown.", propertyName: "allowSelectAll", label: "Allow select all", controlType: "SWITCH", isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, ], }, { 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 }, ], defaultValue: LabelPosition.Top, isBindProperty: false, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, }, { helpText: "Sets the label alignment of the widget", propertyName: "labelAlignment", label: "Alignment", controlType: "LABEL_ALIGNMENT_OPTIONS", fullWidth: false, options: [ { startIcon: "align-left", value: Alignment.LEFT, }, { startIcon: "align-right", value: Alignment.RIGHT, }, ], isBindProperty: false, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, hidden: (props: MultiSelectWidgetProps) => 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: MultiSelectWidgetProps) => 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: "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 }, }, { propertyName: "labelStyle", label: "Label Font Style", controlType: "BUTTON_GROUP", options: [ { startIcon: "text-bold", value: "BOLD", }, { startIcon: "text-italic", value: "ITALIC", }, ], isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, }, ], }, { sectionName: "Actions", children: [ { helpText: "when a user selects an option", propertyName: "onOptionChange", label: "onOptionChange", controlType: "ACTION_SELECTOR", isJSConvertible: true, isBindProperty: true, isTriggerProperty: true, }, { helpText: "Trigger an action on change of filterText", hidden: (props: MultiSelectWidgetProps) => !props.serverSideFiltering, dependencies: ["serverSideFiltering"], propertyName: "onFilterUpdate", label: "onFilterUpdate", controlType: "ACTION_SELECTOR", isJSConvertible: true, isBindProperty: true, isTriggerProperty: true, }, ], }, { sectionName: "Styles", children: [ { propertyName: "backgroundColor", helpText: "Sets the background color of the widget", label: "Background color", controlType: "COLOR_PICKER", isBindProperty: false, isTriggerProperty: false, }, ], }, ]; } static getDerivedPropertiesMap() { return { selectedIndexArr: `{{ this.selectedOptionValues.map(o => _.findIndex(this.options, { value: o })) }}`, selectedOptionLabels: `{{ this.selectedOptionValueArr.map((o) => { const index = _.findIndex(this.options, { value: o }); return this.options[index]?.label ?? this.options[index]?.value; }) }}`, selectedOptionValues: `{{ this.selectedOptionValueArr.filter((o) => { const index = _.findIndex(this.options, { value: o }); return index > -1; }) }}`, isValid: `{{this.isRequired ? !!this.selectedIndexArr && this.selectedIndexArr.length > 0 : true}}`, }; } static getDefaultPropertiesMap(): Record { return { selectedOptionValueArr: "defaultOptionValue", filterText: "", }; } // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any static getMetaPropertiesMap(): Record { return { selectedOptionValueArr: undefined, filterText: "", }; } static getStylesheetConfig(): Stylesheet { return { accentColor: "{{appsmith.theme.colors.primaryColor}}", borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "none", }; } static getSetterConfig(): SetterConfig { return { __setters: { setVisibility: { path: "isVisible", type: "boolean", }, setDisabled: { path: "isDisabled", type: "boolean", }, setRequired: { path: "isRequired", type: "boolean", }, setSelectedOptions: { path: "defaultOptionValue", type: "array", accessor: "selectedOptionValues", }, }, }; } getWidgetView() { const options = isArray(this.props.options) ? this.props.options : []; const values: string[] = isArray(this.props.selectedOptionValues) ? this.props.selectedOptionValues : []; const dropDownWidth = (MinimumPopupWidthInPercentage / 100) * (this.props.mainCanvasWidth ?? layoutConfigurations.MOBILE.maxWidth); const { componentHeight, componentWidth } = this.props; return ( ); } onOptionChange = (value: DraftValueType) => { this.props.updateWidgetMetaProperty("selectedOptionValueArr", value, { triggerPropertyName: "onOptionChange", dynamicString: this.props.onOptionChange, event: { type: EventType.ON_OPTION_CHANGE, }, }); // Empty filter after Selection this.onFilterChange(""); }; onFilterChange = (value: string) => { this.props.updateWidgetMetaProperty("filterText", value); if (this.props.onFilterUpdate) { super.executeAction({ triggerPropertyName: "onFilterUpdate", dynamicString: this.props.onFilterUpdate, event: { type: EventType.ON_FILTER_UPDATE, }, }); } }; } export interface DropdownOption { label: string; value: string; disabled?: boolean; } export interface MultiSelectWidgetProps extends WidgetProps { placeholderText?: string; selectedIndex?: number; selectedIndexArr?: number[]; selectedOption: DropdownOption; options?: DropdownOption[]; onOptionChange: string; onFilterChange: string; defaultOptionValue: string | string[]; isRequired: boolean; isLoading: boolean; selectedOptionValueArr: string[]; filterText: string; selectedOptionValues: string[]; selectedOptionLabels: string[]; serverSideFiltering: boolean; onFilterUpdate: string; backgroundColor: string; borderRadius: string; boxShadow?: string; accentColor: string; allowSelectAll?: boolean; labelText: string; labelPosition?: LabelPosition; labelAlignment?: Alignment; labelWidth?: number; labelComponentWidth?: number; } export default MultiSelectWidget;