diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css index e4a05fbffd..3b7e5decf0 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css @@ -23,3 +23,7 @@ button.selectTriggerButton { align-items: center; flex: 1; } + +.selectTriggerButton [data-select-text][data-placeholder] { + color: var(--color-fg-neutral-subtle); +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/anvilConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/anvilConfig.ts deleted file mode 100644 index dc7fe21e10..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/anvilConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { AnvilConfig } from "WidgetProvider/constants"; - -export const anvilConfig: AnvilConfig = { - isLargeWidget: false, - widgetSize: { - minWidth: { - base: "100%", - "180px": "sizing-30", - }, - }, -}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/autocompleteConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/autocompleteConfig.ts deleted file mode 100644 index e709dc5e38..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/autocompleteConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; - -export const autocompleteConfig = { - "!doc": - "A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.", - "!url": "https://docs.appsmith.com/widget-reference/radio", - isVisible: DefaultAutocompleteDefinitions.isVisible, - options: "[$__dropdownOption__$]", - selectedOptionValue: "string", - isRequired: "bool", -}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/defaultsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/defaultsConfig.ts deleted file mode 100644 index 96c64b6cf9..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/defaultsConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; -import type { WidgetDefaultProps } from "WidgetProvider/constants"; - -export const defaultsConfig = { - animateLoading: true, - label: "Label", - options: [ - { label: "Option 1", value: "1" }, - { label: "Option 2", value: "2" }, - { label: "Option 3", value: "3" }, - ], - defaultOptionValue: "", - isRequired: false, - isDisabled: false, - isVisible: true, - isInline: false, - widgetName: "ComboBox", - widgetType: "COMBOBOX", - version: 1, - responsiveBehavior: ResponsiveBehavior.Fill, -} as unknown as WidgetDefaultProps; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/index.ts deleted file mode 100644 index 995925903b..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./propertyPaneConfig"; -export { metaConfig } from "./metaConfig"; -export { anvilConfig } from "./anvilConfig"; -export { defaultsConfig } from "./defaultsConfig"; -export { settersConfig } from "./settersConfig"; -export { methodsConfig } from "./methodsConfig"; -export { autocompleteConfig } from "./autocompleteConfig"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/metaConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/metaConfig.ts deleted file mode 100644 index 2554b202cf..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/metaConfig.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { WIDGET_TAGS } from "constants/WidgetConstants"; - -export const metaConfig = { - name: "ComboBox", - tags: [WIDGET_TAGS.SELECT], - needsMeta: true, - searchTags: [ - "choice", - "option", - "choose", - "pick", - "combobox", - "select", - "dropdown", - "filter", - "autocomplete", - "input", - ], -}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/methodsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/methodsConfig.ts deleted file mode 100644 index ec09415217..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/methodsConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { - PropertyUpdates, - SnipingModeProperty, -} from "WidgetProvider/constants"; -import { ComboboxSelectIcon, ComboboxSelectThumbnail } from "appsmith-icons"; - -export const methodsConfig = { - getSnipingModeUpdates: ( - propValueMap: SnipingModeProperty, - ): PropertyUpdates[] => { - return [ - { - propertyPath: "options", - propertyValue: propValueMap.data, - isDynamicPropertyPath: true, - }, - ]; - }, - IconCmp: ComboboxSelectIcon, - ThumbnailCmp: ComboboxSelectThumbnail, -}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.test.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.test.ts deleted file mode 100644 index 2825bdf127..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { WidgetProps } from "widgets/BaseWidget"; -import { handleWidgetTypeUpdate } from "./contentConfig"; - -describe("handleWidgetTypeUpdate", () => { - it("should update the widget type and type property", () => { - const props = {} as WidgetProps; - const propertyName = "widgetType"; - const propertyValue = "COMBOBOX"; - - expect(handleWidgetTypeUpdate(props, propertyName, propertyValue)).toEqual([ - { - propertyPath: propertyName, - propertyValue: propertyValue, - }, - { - propertyPath: "type", - propertyValue: "WDS_COMBOBOX_WIDGET", - }, - ]); - }); - - it("should not update the type property for unknown widget type", () => { - const props = {} as WidgetProps; - const propertyName = "widgetType"; - const propertyValue = "UNKNOWN"; - - // @ts-expect-error unknown widget type - expect(handleWidgetTypeUpdate(props, propertyName, propertyValue)).toEqual([ - { - propertyPath: propertyName, - propertyValue: propertyValue, - }, - ]); - }); -}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.ts deleted file mode 100644 index 48eb11ad00..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/contentConfig.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { ValidationTypes } from "constants/WidgetValidation"; -import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; -import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; -import type { PropertyUpdates } from "WidgetProvider/constants"; -import type { WidgetProps } from "widgets/BaseWidget"; -import type { WDSComboBoxWidgetProps } from "../../widget/types"; -import { optionsCustomValidation } from "./validations"; - -type WidgetTypeValue = "SELECT" | "COMBOBOX"; - -export const handleWidgetTypeUpdate = ( - _props: WidgetProps, - propertyName: string, - propertyValue: WidgetTypeValue, -) => { - const updates: PropertyUpdates[] = [ - { - propertyPath: propertyName, - propertyValue: propertyValue, - }, - ]; - - // Handle widget morphing - if (propertyName === "widgetType") { - const morphingMap: Record = { - SELECT: "WDS_SELECT_WIDGET", - COMBOBOX: "WDS_COMBOBOX_WIDGET", - }; - - const targetWidgetType = morphingMap[propertyValue]; - - if (targetWidgetType) { - updates.push({ - propertyPath: "type", - propertyValue: targetWidgetType, - }); - } - } - - return updates; -}; - -export const propertyPaneContentConfig = [ - { - sectionName: "Data", - children: [ - { - propertyName: "widgetType", - label: "Data type", - controlType: "DROP_DOWN", - options: [ - { - label: "Select", - value: "SELECT", - }, - { - label: "ComboBox", - value: "COMBOBOX", - }, - ], - isBindProperty: false, - isTriggerProperty: false, - updateHook: handleWidgetTypeUpdate, - }, - { - helpText: "Displays a list of unique options", - propertyName: "options", - label: "Options", - controlType: "OPTION_INPUT", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: false, - dependencies: ["optionLabel", "optionValue"], - validation: { - type: ValidationTypes.FUNCTION, - params: { - fn: optionsCustomValidation, - expected: { - type: 'Array<{ "label": "string", "value": "string" | number}>', - example: `[{"label": "One", "value": "one"}]`, - autocompleteDataType: AutocompleteDataType.STRING, - }, - }, - }, - evaluationSubstitutionType: EvaluationSubstitutionType.SMART_SUBSTITUTE, - }, - ], - }, - { - sectionName: "Label", - children: [ - { - helpText: "Sets the label text of the options widget", - propertyName: "label", - label: "Text", - controlType: "INPUT_TEXT", - placeholderText: "Label", - isBindProperty: true, - isTriggerProperty: false, - validation: { type: ValidationTypes.TEXT }, - }, - ], - }, - { - sectionName: "Validations", - children: [ - { - propertyName: "isRequired", - label: "Required", - helpText: "Makes input to the widget mandatory", - controlType: "SWITCH", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: false, - validation: { type: ValidationTypes.BOOLEAN }, - }, - ], - }, - { - sectionName: "General", - children: [ - { - helpText: "Show help text or details about current input", - propertyName: "labelTooltip", - label: "Tooltip", - controlType: "INPUT_TEXT", - placeholderText: "", - isBindProperty: true, - isTriggerProperty: false, - validation: { type: ValidationTypes.TEXT }, - }, - { - helpText: "Sets a placeholder text for the select", - propertyName: "placeholderText", - label: "Placeholder", - controlType: "INPUT_TEXT", - placeholderText: "", - isBindProperty: true, - isTriggerProperty: false, - validation: { type: ValidationTypes.TEXT }, - hidden: (props: WDSComboBoxWidgetProps) => { - return Boolean(props.isReadOnly); - }, - }, - { - 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 }, - }, - ], - }, - { - sectionName: "Events", - children: [ - { - helpText: "when a user changes the selected option", - propertyName: "onSelectionChange", - label: "onSelectionChange", - controlType: "ACTION_SELECTOR", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: true, - }, - ], - }, -]; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/index.ts deleted file mode 100644 index 7f43d3bde5..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { propertyPaneContentConfig } from "./contentConfig"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/index.ts deleted file mode 100644 index 99196ff3ed..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { optionsCustomValidation } from "./optionsCustomValidation"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts deleted file mode 100644 index 331cf8dae8..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { ValidationResponse } from "constants/WidgetValidation"; -import type { LoDashStatic } from "lodash"; -import type { WidgetProps } from "widgets/BaseWidget"; - -interface ValidationErrorMessage { - name: string; - message: string; -} - -/** - * Validation rules: - * 1. This property will take the value in the following format: Array<{ "label": "string", "value": "string" | number}> - * 2. The `value` property should consists of unique values only. - * 3. Data types of all the value props should be the same. - */ -export function optionsCustomValidation( - options: unknown, - _props: WidgetProps, - _: LoDashStatic, -): ValidationResponse { - // UTILS - const createErrorValidationResponse = ( - value: unknown, - message: ValidationErrorMessage, - ): ValidationResponse => ({ - isValid: false, - parsed: value, - messages: [message], - }); - - const createSuccessValidationResponse = ( - value: unknown, - ): ValidationResponse => ({ - isValid: true, - parsed: value, - }); - - const hasDuplicates = (array: unknown[]): boolean => - new Set(array).size !== array.length; - - if (Array.isArray(options)) { - const isValidKeys = options.every((option) => { - return ( - _.isPlainObject(option) && - _.has(option, "label") && - _.has(option, "value") - ); - }); - - if (!isValidKeys) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }); - } - - return createSuccessValidationResponse(options); - } - - // JS expects options to be a string - if (!_.isString(options)) { - return createErrorValidationResponse(options, { - name: "TypeError", - message: "This value does not evaluate to type string", - }); - } - - const validationUtil = (options: unknown[]) => { - let _isValid = true; - let message = { name: "", message: "" }; - - if (options.length === 0) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: "Options cannot be an empty array", - }); - } - - const isValidKeys = options.every((option) => { - return ( - _.isPlainObject(option) && - _.has(option, "label") && - _.has(option, "value") - ); - }); - - if (!isValidKeys) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }); - } - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - - if (!_.isPlainObject(option)) { - _isValid = false; - message = { - name: "ValidationError", - message: "This value does not evaluate to type Object", - }; - break; - } - - if (_.keys(option).length === 0) { - _isValid = false; - message = { - name: "ValidationError", - message: - 'This value does not evaluate to type { "label": "string", "value": "string" | number }', - }; - break; - } - - if (hasDuplicates(_.keys(option))) { - _isValid = false; - message = { - name: "ValidationError", - message: "All the keys must be unique", - }; - break; - } - } - - return { - isValid: _isValid, - parsed: _isValid ? options : [], - messages: [message], - }; - }; - - const invalidResponse = { - isValid: false, - parsed: [], - messages: [ - { - name: "TypeError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }, - ], - }; - - try { - options = JSON.parse(options as string); - - if (!Array.isArray(options)) { - return invalidResponse; - } - - return validationUtil(options); - } catch (_error) { - return invalidResponse; - } -} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/settersConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/settersConfig.ts deleted file mode 100644 index e6cf8f7fc4..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/config/settersConfig.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const settersConfig = { - __setters: { - setVisibility: { - path: "isVisible", - type: "boolean", - }, - setDisabled: { - path: "isDisabled", - type: "boolean", - }, - setData: { - path: "options", - type: "array", - }, - }, -}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/helpers.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/helpers.ts deleted file mode 100644 index 175b9665e5..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Validation } from "modules/ui-builder/ui/wds/WDSInputWidget/widget/types"; -import type { WDSComboBoxWidgetProps } from "./types"; - -export function validateInput(props: WDSComboBoxWidgetProps): Validation { - if (!props.isValid) { - return { - validationStatus: "invalid", - errorMessage: "Please select an option", - }; - } - - return { - validationStatus: "valid", - errorMessage: "", - }; -} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/index.tsx index f7977faa0c..47f230d181 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/index.tsx @@ -1,187 +1,68 @@ -import { ComboBox, ListBoxItem } from "@appsmith/wds"; -import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -import type { SetterConfig, Stylesheet } from "entities/AppTheming"; -import isNumber from "lodash/isNumber"; import React from "react"; -import type { - AnvilConfig, - AutocompletionDefinitions, -} from "WidgetProvider/constants"; -import type { WidgetState } from "widgets/BaseWidget"; -import BaseWidget from "widgets/BaseWidget"; -import { - anvilConfig, - autocompleteConfig, - defaultsConfig, - metaConfig, - methodsConfig, - propertyPaneContentConfig, - settersConfig, -} from "../config"; -import { validateInput } from "./helpers"; -import type { WDSComboBoxWidgetProps } from "./types"; +import { ComboBox, ListBoxItem } from "@appsmith/wds"; +import { validateInput } from "../../WDSSelectWidget/widget/helpers"; +import { ComboboxSelectIcon, ComboboxSelectThumbnail } from "appsmith-icons"; -const isTrueObject = (item: unknown): item is Record => { - return Object.prototype.toString.call(item) === "[object Object]"; -}; +import { WDSSelectWidget } from "../../WDSSelectWidget"; +import isArray from "lodash/isArray"; -class WDSComboBoxWidget extends BaseWidget< - WDSComboBoxWidgetProps, - WidgetState -> { +class WDSComboBoxWidget extends WDSSelectWidget { static type = "WDS_COMBOBOX_WIDGET"; static getConfig() { - return metaConfig; + return { + ...super.getConfig(), + name: "ComboBox", + }; } static getDefaults() { - return defaultsConfig; + return { + ...super.getDefaults(), + widgetName: "ComboBox", + }; } static getMethods() { - return methodsConfig; - } - - static getAnvilConfig(): AnvilConfig | null { - return anvilConfig; - } - - static getAutocompleteDefinitions(): AutocompletionDefinitions { - return autocompleteConfig; - } - - static getPropertyPaneContentConfig() { - return propertyPaneContentConfig; - } - - static getPropertyPaneStyleConfig() { - return []; - } - - static getDerivedPropertiesMap() { return { - selectedOption: - "{{_.find(this.options, { value: this.selectedOptionValue })}}", - isValid: `{{ this.isRequired ? !!this.selectedOptionValue : true }}`, - value: `{{this.selectedOptionValue}}`, + ...super.getMethods(), + IconCmp: ComboboxSelectIcon, + ThumbnailCmp: ComboboxSelectThumbnail, }; } - static getDefaultPropertiesMap(): Record { - return { - selectedOptionValue: "defaultOptionValue", - }; - } - - static getMetaPropertiesMap() { - return { - selectedOptionValue: undefined, - isDirty: false, - }; - } - - static getStylesheetConfig(): Stylesheet { - return {}; - } - - componentDidUpdate(prevProps: WDSComboBoxWidgetProps): void { - if ( - this.props.defaultOptionValue !== prevProps.defaultOptionValue && - this.props.isDirty - ) { - this.props.updateWidgetMetaProperty("isDirty", false); - } - } - - static getSetterConfig(): SetterConfig { - return settersConfig; - } - - static getDependencyMap(): Record { - return { - optionLabel: ["options"], - optionValue: ["options"], - defaultOptionValue: ["options"], - }; - } - - handleSelectionChange = (updatedValue: string | number | null) => { - let newVal; - - if (updatedValue === null) { - newVal = ""; - } else { - if (isNumber(updatedValue)) { - newVal = updatedValue; - } else if ( - isTrueObject(this.props.options[0]) && - isNumber(this.props.options[0].value) - ) { - newVal = parseFloat(updatedValue); - } else { - newVal = updatedValue; - } - } - - const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; - - // Set isDirty to true when the selection changes - if (!this.props.isDirty) { - pushBatchMetaUpdates("isDirty", true); - } - - pushBatchMetaUpdates("selectedOptionValue", newVal, { - triggerPropertyName: "onSelectionChange", - dynamicString: this.props.onSelectionChange, - event: { - type: EventType.ON_OPTION_CHANGE, - }, - }); - - commitBatchMetaUpdates(); - }; - - optionsToItems = (options: WDSComboBoxWidgetProps["options"]) => { - if (Array.isArray(options)) { - const items = options.map((option) => ({ - label: option["label"] as string, - id: option["value"] as string, - })); - - const isValidItems = items.every( - (item) => item.label !== undefined && item.id !== undefined, - ); - - return isValidItems ? items : []; - } - - return []; - }; - getWidgetView() { - const { - labelTooltip, - options, - placeholderText, - selectedOptionValue, - ...rest - } = this.props; - + const { labelTooltip, placeholderText, selectedOptionValue, ...rest } = + this.props; const validation = validateInput(this.props); + const options = (isArray(this.props.options) ? this.props.options : []) as { + value: string; + label: string; + }[]; + // This is key is used to force re-render of the widget when the options change. + // Why force re-render on options change? + // Sometimes when the user is changing options, the select throws an error ( related to react-aria code ) saying "cannot change id of item". + const key = options.map((option) => option.value).join(","); return ( - {this.optionsToItems(options).map((option) => ( - + {options.map((option) => ( + {option.label} ))} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/types.ts b/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/types.ts deleted file mode 100644 index b707f47930..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSComboBoxWidget/widget/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { WidgetProps } from "widgets/BaseWidget"; - -export interface WDSComboBoxWidgetProps extends WidgetProps { - options: Record[] | string; - selectedOptionValue: string; - onSelectionChange: string; - defaultOptionValue: string; - isRequired?: boolean; - isDisabled?: boolean; - label: string; - labelTooltip?: string; - isDirty: boolean; -} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/autocompleteConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/autocompleteConfig.ts index 18c4a96be3..610e53170c 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/autocompleteConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/autocompleteConfig.ts @@ -2,10 +2,21 @@ import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; export const autocompleteConfig = { "!doc": - "Select widget lets the user choose one option from a dropdown list. It is similar to a SingleSelect Dropdown in its functionality", - "!url": "https://docs.appsmith.com/widget-reference/radio", + "Select is used to capture user input/s from a specified list of permitted inputs. A Select can capture a single choice", + "!url": "https://docs.appsmith.com/widget-reference/dropdown", isVisible: DefaultAutocompleteDefinitions.isVisible, + selectedOptionValue: { + "!type": "string", + "!doc": "The value selected in a single select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/dropdown", + }, + selectedOptionLabel: { + "!type": "string", + "!doc": "The selected option label in a single select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/dropdown", + }, + isDisabled: "bool", + isValid: "bool", + isDirty: "bool", options: "[$__dropdownOption__$]", - selectedOptionValue: "string", - isRequired: "bool", }; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/defaultsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/defaultsConfig.ts index 2f0b40d1e1..34caf371f9 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/defaultsConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/defaultsConfig.ts @@ -1,14 +1,13 @@ import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; import type { WidgetDefaultProps } from "WidgetProvider/constants"; +import { SAMPLE_DATA } from "../widget/constants"; export const defaultsConfig = { animateLoading: true, label: "Label", - options: [ - { label: "Option 1", value: "1" }, - { label: "Option 2", value: "2" }, - { label: "Option 3", value: "3" }, - ], + sourceData: JSON.stringify(SAMPLE_DATA, null, 2), + optionLabel: "name", + optionValue: "code", defaultOptionValue: "", isRequired: false, isDisabled: false, @@ -18,4 +17,6 @@ export const defaultsConfig = { widgetType: "SELECT", version: 1, responsiveBehavior: ResponsiveBehavior.Fill, + dynamicPropertyPathList: [{ key: "sourceData" }], + placeholderText: "Select an item", } as unknown as WidgetDefaultProps; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/methodsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/methodsConfig.ts index d0c86d2c7d..612d557eff 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/methodsConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/methodsConfig.ts @@ -2,7 +2,13 @@ import type { PropertyUpdates, SnipingModeProperty, } from "WidgetProvider/constants"; +import type { + WidgetQueryConfig, + WidgetQueryGenerationFormConfig, +} from "WidgetQueryGenerators/types"; import { RadioGroupIcon, SelectThumbnail } from "appsmith-icons"; +import type { DynamicPath } from "utils/DynamicBindingUtils"; +import type { WidgetProps } from "widgets/BaseWidget"; export const methodsConfig = { getSnipingModeUpdates: ( @@ -10,12 +16,50 @@ export const methodsConfig = { ): PropertyUpdates[] => { return [ { - propertyPath: "options", + propertyPath: "sourceData", propertyValue: propValueMap.data, isDynamicPropertyPath: true, }, ]; }, + getQueryGenerationConfig(widget: WidgetProps) { + return { + select: { + where: `${widget.widgetName}.filterText`, + }, + }; + }, + getPropertyUpdatesForQueryBinding( + queryConfig: WidgetQueryConfig, + widget: WidgetProps, + formConfig: WidgetQueryGenerationFormConfig, + ) { + let modify; + + const dynamicPropertyPathList: DynamicPath[] = [ + ...(widget.dynamicPropertyPathList || []), + ]; + + if (queryConfig.select) { + modify = { + sourceData: queryConfig.select.data, + optionLabel: formConfig.aliases.find((d) => d.name === "label")?.alias, + optionValue: formConfig.aliases.find((d) => d.name === "value")?.alias, + defaultOptionValue: "", + serverSideFiltering: false, + onFilterUpdate: queryConfig.select.run, + }; + + dynamicPropertyPathList.push({ key: "sourceData" }); + } + + return { + modify, + dynamicUpdates: { + dynamicPropertyPathList, + }, + }; + }, IconCmp: RadioGroupIcon, ThumbnailCmp: SelectThumbnail, }; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.tsx similarity index 57% rename from app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.ts rename to app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.tsx index a31fbc12e0..b0f2c003de 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/contentConfig.tsx @@ -1,13 +1,23 @@ +import React from "react"; import { ValidationTypes } from "constants/WidgetValidation"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import type { WDSSelectWidgetProps } from "../../widget/types"; -import { - defaultOptionValidation, - optionsCustomValidation, -} from "./validations"; +import { defaultOptionValueValidation } from "./validations"; import type { WidgetProps } from "widgets/BaseWidget"; import type { PropertyUpdates } from "WidgetProvider/constants"; +import { valueKeyValidation } from "./validations/valueKeyValidation"; +import { + defaultValueExpressionPrefix, + getDefaultValueExpressionSuffix, + getLabelValueAdditionalAutocompleteData, + getLabelValueKeyOptions, + getOptionLabelValueExpressionPrefix, + optionLabelValueExpressionSuffix, +} from "../../widget/helpers"; +import { labelKeyValidation } from "./validations/labelKeyValidation"; +import { Flex } from "@appsmith/ads"; +import { SAMPLE_DATA } from "../../widget/constants"; type WidgetTypeValue = "SELECT" | "COMBOBOX"; @@ -64,52 +74,138 @@ export const propertyPaneContentConfig = [ }, }, { - helpText: "Displays a list of unique options", - propertyName: "options", - label: "Options", - controlType: "OPTION_INPUT", + helpText: + "Takes in an array of objects to display options. Bind data from an API using {{}}", + propertyName: "sourceData", + label: "Source Data", + controlType: "ONE_CLICK_BINDING_CONTROL", + controlConfig: { + aliases: [ + { + name: "label", + isSearcheable: true, + isRequired: true, + }, + { + name: "value", + isRequired: true, + }, + ], + sampleData: JSON.stringify(SAMPLE_DATA, null, 2), + }, isJSConvertible: true, + placeholderText: '[{ "label": "label1", "value": "value1" }]', isBindProperty: true, isTriggerProperty: false, - dependencies: ["optionLabel", "optionValue"], validation: { - type: ValidationTypes.FUNCTION, + type: ValidationTypes.ARRAY, params: { - fn: optionsCustomValidation, - expected: { - type: 'Array<{ "label": "string", "value": "string" | number}>', - example: `[{"label": "One", "value": "one"}]`, - autocompleteDataType: AutocompleteDataType.STRING, + children: { + type: ValidationTypes.OBJECT, + params: { + required: true, + }, }, }, }, evaluationSubstitutionType: EvaluationSubstitutionType.SMART_SUBSTITUTE, }, { - helpText: "Sets a default selected option", - propertyName: "defaultOptionValue", - label: "Default selected value", + helpText: "Choose or set a field from source data as the display label", + propertyName: "optionLabel", + label: "Label key", + controlType: "DROP_DOWN", + customJSControl: "WRAPPED_CODE_EDITOR", + controlConfig: { + wrapperCode: { + prefix: getOptionLabelValueExpressionPrefix, + suffix: optionLabelValueExpressionSuffix, + }, + }, placeholderText: "", - controlType: "INPUT_TEXT", isBindProperty: true, isTriggerProperty: false, - dependencies: ["options"], - /** - * Changing the validation to FUNCTION. - * If the user enters Integer inside {{}} e.g. {{1}} then value should evalute to integer. - * If user enters 1 e.g. then it should evaluate as string. - */ + isJSConvertible: true, + evaluatedDependencies: ["sourceData"], + options: getLabelValueKeyOptions, + alwaysShowSelected: true, validation: { type: ValidationTypes.FUNCTION, params: { - fn: defaultOptionValidation, + fn: labelKeyValidation, expected: { - type: `string |\nnumber (only works in mustache syntax)`, - example: `abc | {{1}}`, + type: "String or Array", + example: `color | ["blue", "green"]`, autocompleteDataType: AutocompleteDataType.STRING, }, }, }, + additionalAutoComplete: getLabelValueAdditionalAutocompleteData, + }, + { + helpText: "Choose or set a field from source data as the value", + propertyName: "optionValue", + label: "Value key", + controlType: "DROP_DOWN", + customJSControl: "WRAPPED_CODE_EDITOR", + controlConfig: { + wrapperCode: { + prefix: getOptionLabelValueExpressionPrefix, + suffix: optionLabelValueExpressionSuffix, + }, + }, + placeholderText: "", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + evaluatedDependencies: ["sourceData"], + options: getLabelValueKeyOptions, + alwaysShowSelected: true, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: valueKeyValidation, + expected: { + type: "String or Array", + example: `color | [1, "orange"]`, + autocompleteDataType: AutocompleteDataType.STRING, + }, + }, + }, + additionalAutoComplete: getLabelValueAdditionalAutocompleteData, + }, + { + helpText: "Selects the option with value by default", + propertyName: "defaultOptionValue", + label: "Default selected value", + controlType: "WRAPPED_CODE_EDITOR", + controlConfig: { + wrapperCode: { + prefix: defaultValueExpressionPrefix, + suffix: getDefaultValueExpressionSuffix, + }, + }, + placeholderText: '{ "label": "label1", "value": "value1" }', + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: defaultOptionValueValidation, + expected: { + type: 'value1 or { "label": "label1", "value": "value1" }', + example: `value1 | { "label": "label1", "value": "value1" }`, + autocompleteDataType: AutocompleteDataType.STRING, + }, + }, + }, + dependencies: ["options"], + helperText: ( + + Make sure the default value is present in the source data to have it + selected by default in the UI. + + ), }, ], }, diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValidation.ts deleted file mode 100644 index f5bf114adf..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValidation.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ValidationResponse } from "constants/WidgetValidation"; -import type { LoDashStatic } from "lodash"; -import type { WidgetProps } from "widgets/BaseWidget"; - -interface ValidationErrorMessage { - name: string; - message: string; -} - -interface ValidationErrorMessage { - name: string; - message: string; -} - -export function defaultOptionValidation( - value: unknown, - widgetProps: WidgetProps, - _: LoDashStatic, -): ValidationResponse { - // UTILS - const isTrueObject = (item: unknown): item is Record => { - return Object.prototype.toString.call(item) === "[object Object]"; - }; - - const createErrorValidationResponse = ( - value: unknown, - message: ValidationErrorMessage, - ): ValidationResponse => ({ - isValid: false, - parsed: value, - messages: [message], - }); - - const createSuccessValidationResponse = ( - value: unknown, - ): ValidationResponse => ({ - isValid: true, - parsed: value, - }); - - const { options } = widgetProps; - - if (value === "") { - return createSuccessValidationResponse(value); - } - - // Is Form mode, otherwise it is JS mode - if (Array.isArray(options)) { - const values = _.map(widgetProps.options, (option) => { - if (isTrueObject(option)) { - return option["value"]; - } - }); - - if (!values.includes(value)) { - return createErrorValidationResponse(value, { - name: "ValidationError", - message: - "Default value is missing in options. Please update the value.", - }); - } - - return createSuccessValidationResponse(value); - } - - return createSuccessValidationResponse(value); -} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValueValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValueValidation.ts new file mode 100644 index 0000000000..0d37c5e3b8 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/defaultOptionValueValidation.ts @@ -0,0 +1,57 @@ +import type { LoDashStatic } from "lodash"; +import type { ValidationResponse } from "constants/WidgetValidation"; + +import type { WDSSelectWidgetProps } from "../../../widget/types"; + +/** + * Validation rules: + * 1. Can be a string, number, or an object with "label" and "value" properties. + * 2. If it's a string, it should be a valid JSON string. + */ +export function defaultOptionValueValidation( + value: unknown, + props: WDSSelectWidgetProps, + _: LoDashStatic, +): ValidationResponse { + function isValidSelectOption(value: unknown): boolean { + if (!_.isPlainObject(value)) return false; + + const obj = value as Record; + + return ( + obj.hasOwnProperty("label") && + obj.hasOwnProperty("value") && + _.isString(obj.label) && + (_.isString(obj.value) || _.isFinite(obj.value)) + ); + } + + function tryParseJSON(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return value; + } + } + + const processedValue = + typeof value === "string" ? tryParseJSON(value) : value; + + const isValid = + _.isString(processedValue) || + _.isFinite(processedValue) || + isValidSelectOption(processedValue); + + return { + isValid, + parsed: isValid ? processedValue : undefined, + messages: [ + { + name: isValid ? "" : "TypeError", + message: isValid + ? "" + : 'Value must be a string, number, or an object with "label" and "value" properties', + }, + ], + }; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/index.ts index a7a09713fb..b7dcdeafc3 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/index.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/index.ts @@ -1,2 +1 @@ -export { defaultOptionValidation } from "./defaultOptionValidation"; -export { optionsCustomValidation } from "./optionsCustomValidation"; +export { defaultOptionValueValidation } from "./defaultOptionValueValidation"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/labelKeyValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/labelKeyValidation.ts new file mode 100644 index 0000000000..63e1fd935a --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/labelKeyValidation.ts @@ -0,0 +1,68 @@ +import type { LoDashStatic } from "lodash"; +import type { ValidationResponse } from "constants/WidgetValidation"; + +import type { WDSSelectWidgetProps } from "../../../widget/types"; + +/** + * Validation rules: + * 1. Can be a string + * 2. Can be an Array of strings + */ +export function labelKeyValidation( + value: unknown, + props: WDSSelectWidgetProps, + _: LoDashStatic, +): ValidationResponse { + if (value === "" || _.isNil(value)) { + return { + parsed: "", + isValid: false, + messages: [ + { + name: "ValidationError", + message: "Value cannot be empty or null", + }, + ], + }; + } + + // Handle string values + if (_.isString(value)) { + return { + parsed: value, + isValid: true, + messages: [{ name: "", message: "" }], + }; + } + + // Handle array values + if (_.isArray(value)) { + const errorIndex = value.findIndex((item) => !_.isString(item)); + const isValid = errorIndex === -1; + + return { + parsed: isValid ? value : [], + isValid, + messages: [ + { + name: isValid ? "" : "ValidationError", + message: isValid + ? "" + : `Invalid entry at index: ${errorIndex}. Value must be a string`, + }, + ], + }; + } + + // Handle invalid types + return { + parsed: "", + isValid: false, + messages: [ + { + name: "ValidationError", + message: "Value must be a string or an array of strings", + }, + ], + }; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts deleted file mode 100644 index 331cf8dae8..0000000000 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/optionsCustomValidation.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { ValidationResponse } from "constants/WidgetValidation"; -import type { LoDashStatic } from "lodash"; -import type { WidgetProps } from "widgets/BaseWidget"; - -interface ValidationErrorMessage { - name: string; - message: string; -} - -/** - * Validation rules: - * 1. This property will take the value in the following format: Array<{ "label": "string", "value": "string" | number}> - * 2. The `value` property should consists of unique values only. - * 3. Data types of all the value props should be the same. - */ -export function optionsCustomValidation( - options: unknown, - _props: WidgetProps, - _: LoDashStatic, -): ValidationResponse { - // UTILS - const createErrorValidationResponse = ( - value: unknown, - message: ValidationErrorMessage, - ): ValidationResponse => ({ - isValid: false, - parsed: value, - messages: [message], - }); - - const createSuccessValidationResponse = ( - value: unknown, - ): ValidationResponse => ({ - isValid: true, - parsed: value, - }); - - const hasDuplicates = (array: unknown[]): boolean => - new Set(array).size !== array.length; - - if (Array.isArray(options)) { - const isValidKeys = options.every((option) => { - return ( - _.isPlainObject(option) && - _.has(option, "label") && - _.has(option, "value") - ); - }); - - if (!isValidKeys) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }); - } - - return createSuccessValidationResponse(options); - } - - // JS expects options to be a string - if (!_.isString(options)) { - return createErrorValidationResponse(options, { - name: "TypeError", - message: "This value does not evaluate to type string", - }); - } - - const validationUtil = (options: unknown[]) => { - let _isValid = true; - let message = { name: "", message: "" }; - - if (options.length === 0) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: "Options cannot be an empty array", - }); - } - - const isValidKeys = options.every((option) => { - return ( - _.isPlainObject(option) && - _.has(option, "label") && - _.has(option, "value") - ); - }); - - if (!isValidKeys) { - return createErrorValidationResponse(options, { - name: "ValidationError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }); - } - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - - if (!_.isPlainObject(option)) { - _isValid = false; - message = { - name: "ValidationError", - message: "This value does not evaluate to type Object", - }; - break; - } - - if (_.keys(option).length === 0) { - _isValid = false; - message = { - name: "ValidationError", - message: - 'This value does not evaluate to type { "label": "string", "value": "string" | number }', - }; - break; - } - - if (hasDuplicates(_.keys(option))) { - _isValid = false; - message = { - name: "ValidationError", - message: "All the keys must be unique", - }; - break; - } - } - - return { - isValid: _isValid, - parsed: _isValid ? options : [], - messages: [message], - }; - }; - - const invalidResponse = { - isValid: false, - parsed: [], - messages: [ - { - name: "TypeError", - message: - 'This value does not evaluate to type Array<{ "label": "string", "value": "string" | number }>', - }, - ], - }; - - try { - options = JSON.parse(options as string); - - if (!Array.isArray(options)) { - return invalidResponse; - } - - return validationUtil(options); - } catch (_error) { - return invalidResponse; - } -} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/valueKeyValidation.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/valueKeyValidation.ts new file mode 100644 index 0000000000..650c30019f --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/propertyPaneConfig/validations/valueKeyValidation.ts @@ -0,0 +1,122 @@ +import type { LoDashStatic } from "lodash"; +import type { WDSSelectWidgetProps } from "../../../widget/types"; + +/** + * Validation rules: + * 1. Can be a string (representing a key in sourceData) + * 2. Can be an Array of string, number, boolean (for direct option values) + * 3. Values must be unique + */ +export function valueKeyValidation( + value: unknown, + props: WDSSelectWidgetProps, + _: LoDashStatic, +) { + if (value === "" || _.isNil(value)) { + return { + parsed: "", + isValid: false, + messages: [ + { + name: "ValidationError", + message: `value does not evaluate to type: string | Array`, + }, + ], + }; + } + + let options: unknown[] = []; + + if (_.isString(value)) { + const sourceData = _.isArray(props.sourceData) ? props.sourceData : []; + + const keys = sourceData.reduce((keys, curr) => { + Object.keys(curr).forEach((d) => keys.add(d)); + + return keys; + }, new Set()); + + if (!keys.has(value)) { + return { + parsed: value, + isValid: false, + messages: [ + { + name: "ValidationError", + message: `value key should be present in the source data`, + }, + ], + }; + } + + options = sourceData.map((d: Record) => d[value]); + } else if (_.isArray(value)) { + // Here assumption is that if evaluated array is all equal, then it is a key, + // and we can return the parsed value(from source data) as the options. + const areAllValuesEqual = value.every((item, _, arr) => item === arr[0]); + + if ( + areAllValuesEqual && + props.sourceData[0].hasOwnProperty(String(value[0])) + ) { + const parsedValue = props.sourceData.map( + (d: Record) => d[String(value[0])], + ); + + return { + parsed: parsedValue, + isValid: true, + messages: [], + }; + } + + const errorIndex = value.findIndex( + (d) => + !(_.isString(d) || (_.isNumber(d) && !_.isNaN(d)) || _.isBoolean(d)), + ); + + if (errorIndex !== -1) { + return { + parsed: [], + isValid: false, + messages: [ + { + name: "ValidationError", + message: `Invalid entry at index: ${errorIndex}. This value does not evaluate to type: string | number | boolean`, + }, + ], + }; + } else { + options = value; + } + } else { + return { + parsed: "", + isValid: false, + messages: [ + { + name: "ValidationError", + message: + "value does not evaluate to type: string | Array", + }, + ], + }; + } + + const isValid = options.every( + (d: unknown, i: number, arr: unknown[]) => arr.indexOf(d) === i, + ); + + return { + parsed: value, + isValid: isValid, + messages: isValid + ? [] + : [ + { + name: "ValidationError", + message: "Duplicate values found, value must be unique", + }, + ], + }; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/settersConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/settersConfig.ts index e6cf8f7fc4..986aca1def 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/settersConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/config/settersConfig.ts @@ -8,9 +8,18 @@ export const settersConfig = { path: "isDisabled", type: "boolean", }, - setData: { + setRequired: { + path: "isRequired", + type: "boolean", + }, + setOptions: { path: "options", type: "array", }, + setSelectedOption: { + path: "defaultOptionValue", + type: "string", + accessor: "selectedOptionValue", + }, }, }; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/constants.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/constants.ts new file mode 100644 index 0000000000..ecea66e71d --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/constants.ts @@ -0,0 +1,5 @@ +export const SAMPLE_DATA = [ + { name: "Blue", code: "BLUE" }, + { name: "Green", code: "GREEN" }, + { name: "Red", code: "RED" }, +]; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/derived.js b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/derived.js new file mode 100644 index 0000000000..9973efaaec --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/derived.js @@ -0,0 +1,123 @@ +/* eslint-disable @typescript-eslint/no-unused-vars*/ +export default { + getOptions: (props, moment, _) => { + let labels = [], + values = [], + sourceData = props.sourceData || []; + + const processOptionArray = (optionArray, sourceData) => { + if (!sourceData.length) return []; + + const allEqual = optionArray.every((item, _, arr) => item === arr[0]); + const keyExistsInSource = optionArray[0] in sourceData[0]; + + return allEqual && keyExistsInSource + ? sourceData.map((d) => d[optionArray[0]]) + : optionArray; + }; + + /** + * SourceData: + * [{ + * "name": "Blue", + * "code": "name" + * },{ + * "name": "Green", + * "code": "name" + * },{ + * "name": "Red", + * "code": "name" + * }] + * The `Label key` in UI can take following values: + * 1. Normal string, without any quotes. e.g `name` + * This can be assumed as a key in each item of sourceData. We search it in each item of sourceData. + * 2. Except this everything comes in `{{}}`. It can have 2 types of values: + * a. Expressions that evaluate to a normal string. e.g `{{(() => `name`)()}}` + * In this case evaluated value will be ['name', 'name', 'name']. + * i. This can be assumed as a key in each item of sourceData. Handled by `allLabelsEqual` check. + * b. Dynamic property accessed via `item` object. e.g `{{item.name}}` + * In this case evaluated value will be actual values form sourceData ['Red', 'Green', 'Blue']. + * Hence we can assume that this array is the labels array. + * */ + if (typeof props.optionLabel === "string") { + labels = sourceData.map((d) => d[props.optionLabel]); + } else if (_.isArray(props.optionLabel)) { + labels = processOptionArray(props.optionLabel, sourceData); + } + + if (typeof props.optionValue === "string") { + values = sourceData.map((d) => d[props.optionValue]); + } else if (_.isArray(props.optionValue)) { + values = processOptionArray(props.optionValue, sourceData); + } + + return sourceData.map((d, i) => ({ + label: labels[i], + value: values[i], + })); + }, + getIsValid: (props, moment, _) => { + return props.isRequired + ? !_.isNil(props.selectedOptionValue) && props.selectedOptionValue !== "" + : true; + }, + getSelectedOptionValue: (props, moment, _) => { + const isServerSideFiltered = props.serverSideFiltering; + const options = props.options ?? []; + let value = props.value?.value ?? props.value; + + const valueIndex = _.findIndex(options, (option) => option.value === value); + + if (valueIndex === -1) { + if (!isServerSideFiltered) { + value = ""; + } + + if ( + isServerSideFiltered && + !_.isPlainObject(props.value) && + !props.isDirty + ) { + value = ""; + } + } + + return value; + }, + // + getSelectedOptionLabel: (props, moment, _) => { + const isServerSideFiltered = props.serverSideFiltering; + const options = props.options ?? []; + let label = props.label?.label ?? props.label; + const labelIndex = _.findIndex( + options, + (option) => + option.label === label && option.value === props.selectedOptionValue, + ); + + if (labelIndex === -1) { + if ( + !_.isNil(props.selectedOptionValue) && + props.selectedOptionValue !== "" + ) { + const selectedOption = _.find( + options, + (option) => option.value === props.selectedOptionValue, + ); + + if (selectedOption) { + label = selectedOption.label; + } + } else { + if ( + !isServerSideFiltered || + (isServerSideFiltered && props.selectedOptionValue === "") + ) { + label = ""; + } + } + } + + return label; + }, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/helpers.ts b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/helpers.ts index c98beb7f15..2c41b23a03 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/helpers.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/helpers.ts @@ -1,6 +1,15 @@ +import get from "lodash/get"; +import uniq from "lodash/uniq"; +import isArray from "lodash/isArray"; +import isString from "lodash/isString"; +import isPlainObject from "lodash/isPlainObject"; +import type { WidgetProps } from "widgets/BaseWidget"; + import type { Validation } from "modules/ui-builder/ui/wds/WDSInputWidget/widget/types"; import type { WDSSelectWidgetProps } from "./types"; +import { EVAL_VALUE_PATH } from "utils/DynamicBindingUtils"; + export function validateInput(props: WDSSelectWidgetProps): Validation { if (!props.isValid) { return { @@ -14,3 +23,56 @@ export function validateInput(props: WDSSelectWidgetProps): Validation { errorMessage: "", }; } + +export function getLabelValueKeyOptions(widget: WidgetProps) { + const sourceData = get(widget, `${EVAL_VALUE_PATH}.sourceData`); + + let parsedValue: Record | undefined = sourceData; + + if (isString(sourceData)) { + try { + parsedValue = JSON.parse(sourceData); + } catch (e) {} + } + + if (isArray(parsedValue)) { + return uniq( + parsedValue.reduce((keys, obj) => { + if (isPlainObject(obj)) { + Object.keys(obj).forEach((d) => keys.push(d)); + } + + return keys; + }, []), + ).map((d: unknown) => ({ + label: d, + value: d, + })); + } else { + return []; + } +} + +export function getLabelValueAdditionalAutocompleteData(props: WidgetProps) { + const keys = getLabelValueKeyOptions(props); + + return { + item: keys + .map((d) => d.label) + .reduce((prev: Record, curr: unknown) => { + prev[curr as string] = ""; + + return prev; + }, {}), + }; +} + +export const defaultValueExpressionPrefix = `{{ ((options, serverSideFiltering) => ( `; + +export const getDefaultValueExpressionSuffix = (widget: WidgetProps) => + `))(${widget.widgetName}.options, ${widget.widgetName}.serverSideFiltering) }}`; + +export const getOptionLabelValueExpressionPrefix = (widget: WidgetProps) => + `{{${widget.widgetName}.sourceData.map((item) => (`; + +export const optionLabelValueExpressionSuffix = `))}}`; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/index.tsx index 11398d323b..65c7063081 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSSelectWidget/widget/index.tsx @@ -2,7 +2,7 @@ import { Select, ListBoxItem } from "@appsmith/wds"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import isNumber from "lodash/isNumber"; -import React from "react"; +import React, { type Key } from "react"; import type { AnvilConfig, AutocompletionDefinitions, @@ -20,6 +20,9 @@ import { } from "../config"; import { validateInput } from "./helpers"; import type { WDSSelectWidgetProps } from "./types"; +import derivedPropertyFns from "./derived"; +import { parseDerivedProperties } from "widgets/WidgetUtils"; +import isArray from "lodash/isArray"; const isTrueObject = (item: unknown): item is Record => { return Object.prototype.toString.call(item) === "[object Object]"; @@ -46,9 +49,8 @@ class WDSSelectWidget extends BaseWidget { static getDependencyMap(): Record { return { - optionLabel: ["options"], - optionValue: ["options"], - defaultOptionValue: ["options"], + optionLabel: ["sourceData"], + optionValue: ["sourceData"], }; } @@ -65,11 +67,13 @@ class WDSSelectWidget extends BaseWidget { } static getDerivedPropertiesMap() { + const parsedDerivedProperties = parseDerivedProperties(derivedPropertyFns); + return { - selectedOption: - "{{_.find(this.options, { value: this.selectedOptionValue })}}", - isValid: `{{ this.isRequired ? !!this.selectedOptionValue : true }}`, - value: `{{this.selectedOptionValue}}`, + options: `{{(()=>{${parsedDerivedProperties.getOptions}})()}}`, + isValid: `{{(()=>{${parsedDerivedProperties.getIsValid}})()}}`, + selectedOptionValue: `{{(()=>{${parsedDerivedProperties.getSelectedOptionValue}})()}}`, + selectedOptionLabel: `{{(()=>{${parsedDerivedProperties.getSelectedOptionLabel}})()}}`, }; } @@ -90,6 +94,7 @@ class WDSSelectWidget extends BaseWidget { return {}; } + // in case default value changes, we need to reset isDirty to false componentDidUpdate(prevProps: WDSSelectWidgetProps): void { if ( this.props.defaultOptionValue !== prevProps.defaultOptionValue && @@ -103,9 +108,11 @@ class WDSSelectWidget extends BaseWidget { return settersConfig; } - handleChange = (updatedValue: string | number) => { + handleChange = (updatedValue: Key | null) => { let newVal; + if (updatedValue === null) return; + if (isNumber(updatedValue)) { newVal = updatedValue; } else if ( @@ -134,46 +141,39 @@ class WDSSelectWidget extends BaseWidget { commitBatchMetaUpdates(); }; - optionsToSelectItems = (options: WDSSelectWidgetProps["options"]) => { - if (Array.isArray(options)) { - const items = options.map((option) => ({ - label: option[this.props.optionLabel || "label"] as string, - id: option[this.props.optionValue || "value"] as string, - })); - - const isValidItems = items.every( - (item) => item.label !== undefined && item.id !== undefined, - ); - - return isValidItems ? items : []; - } - - return []; - }; - getWidgetView() { - const { - labelTooltip, - options, - placeholderText, - selectedOptionValue, - ...rest - } = this.props; - + const { labelTooltip, placeholderText, selectedOptionValue, ...rest } = + this.props; const validation = validateInput(this.props); + const options = (isArray(this.props.options) ? this.props.options : []) as { + value: string; + label: string; + }[]; + // This is key is used to force re-render of the widget when the options change. + // Why force re-render on options change? + // When the user is changing options from propety pane, the select throws an error ( related to react-aria code ) saying "cannot change id of item" due + // change in options's id. + const key = options.map((option) => option.value).join(","); return (