chore: fix wds select widget bugs + refactor (#38304)

Fixes #38197 

/ok-to-test tags="@tag.Anvil"

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Enhanced styling for select input components, improving visual
consistency.
- Introduced new validation functions for widget properties, ensuring
better data integrity.
- Added properties for improved configuration of select widgets,
including dynamic data binding.
- New utility functions for handling options in select widgets,
enhancing functionality.
- Introduced a constant for sample data to facilitate testing and
development.

- **Bug Fixes**
- Resolved issues with widget rendering and responsiveness to property
changes.

- **Refactor**
- Streamlined widget implementation by leveraging inherited
functionalities and simplifying methods.
- Updated methods to improve handling of derived properties and options.
- Removed obsolete configuration files and validation functions to clean
up the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/12480977859>
> Commit: d669e05ed5b7ce07a54259b7e301eb65341c1b02
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12480977859&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Anvil`
> Spec:
> <hr>Tue, 24 Dec 2024 11:43:59 UTC
<!-- end of auto-generated comment: Cypress test results  -->
This commit is contained in:
Pawan Kumar 2024-12-24 17:28:57 +05:30 committed by GitHub
parent d9a3253e92
commit ecf9934859
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 717 additions and 986 deletions

View File

@ -23,3 +23,7 @@ button.selectTriggerButton {
align-items: center;
flex: 1;
}
.selectTriggerButton [data-select-text][data-placeholder] {
color: var(--color-fg-neutral-subtle);
}

View File

@ -1,11 +0,0 @@
import type { AnvilConfig } from "WidgetProvider/constants";
export const anvilConfig: AnvilConfig = {
isLargeWidget: false,
widgetSize: {
minWidth: {
base: "100%",
"180px": "sizing-30",
},
},
};

View File

@ -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",
};

View File

@ -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;

View File

@ -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";

View File

@ -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",
],
};

View File

@ -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,
};

View File

@ -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,
},
]);
});
});

View File

@ -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<WidgetTypeValue, string> = {
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,
},
],
},
];

View File

@ -1 +0,0 @@
export { propertyPaneContentConfig } from "./contentConfig";

View File

@ -1 +0,0 @@
export { optionsCustomValidation } from "./optionsCustomValidation";

View File

@ -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;
}
}

View File

@ -1,16 +0,0 @@
export const settersConfig = {
__setters: {
setVisibility: {
path: "isVisible",
type: "boolean",
},
setDisabled: {
path: "isDisabled",
type: "boolean",
},
setData: {
path: "options",
type: "array",
},
},
};

View File

@ -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: "",
};
}

View File

@ -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<string, unknown> => {
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<string, string> {
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<string, string[]> {
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 (
<ComboBox
{...rest}
contextualHelp={labelTooltip}
errorMessage={validation.errorMessage}
isInvalid={validation.validationStatus === "invalid"}
onSelectionChange={this.handleSelectionChange}
isInvalid={
validation.validationStatus === "invalid" && this.props.isDirty
}
key={key}
onSelectionChange={this.handleChange}
placeholder={placeholderText}
selectedKey={selectedOptionValue}
>
{this.optionsToItems(options).map((option) => (
<ListBoxItem key={option.id} textValue={option.label}>
{options.map((option) => (
<ListBoxItem
id={option.value}
key={option.value}
textValue={option.label}
>
{option.label}
</ListBoxItem>
))}

View File

@ -1,13 +0,0 @@
import type { WidgetProps } from "widgets/BaseWidget";
export interface WDSComboBoxWidgetProps extends WidgetProps {
options: Record<string, unknown>[] | string;
selectedOptionValue: string;
onSelectionChange: string;
defaultOptionValue: string;
isRequired?: boolean;
isDisabled?: boolean;
label: string;
labelTooltip?: string;
isDirty: boolean;
}

View File

@ -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",
};

View File

@ -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;

View File

@ -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,
};

View File

@ -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<string>",
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<string | number | boolean>",
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: (
<Flex marginTop="spaces-2">
Make sure the default value is present in the source data to have it
selected by default in the UI.
</Flex>
),
},
],
},

View File

@ -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<string, unknown> => {
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);
}

View File

@ -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<string, unknown>;
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',
},
],
};
}

View File

@ -1,2 +1 @@
export { defaultOptionValidation } from "./defaultOptionValidation";
export { optionsCustomValidation } from "./optionsCustomValidation";
export { defaultOptionValueValidation } from "./defaultOptionValueValidation";

View File

@ -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",
},
],
};
}

View File

@ -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;
}
}

View File

@ -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<string| number | boolean>`,
},
],
};
}
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<string, unknown>) => 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<string, unknown>) => 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<string | number | boolean>",
},
],
};
}
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",
},
],
};
}

View File

@ -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",
},
},
};

View File

@ -0,0 +1,5 @@
export const SAMPLE_DATA = [
{ name: "Blue", code: "BLUE" },
{ name: "Green", code: "GREEN" },
{ name: "Red", code: "RED" },
];

View File

@ -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;
},
};

View File

@ -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<string, unknown> | 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<string, string>, 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 = `))}}`;

View File

@ -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<string, unknown> => {
return Object.prototype.toString.call(item) === "[object Object]";
@ -46,9 +49,8 @@ class WDSSelectWidget extends BaseWidget<WDSSelectWidgetProps, WidgetState> {
static getDependencyMap(): Record<string, string[]> {
return {
optionLabel: ["options"],
optionValue: ["options"],
defaultOptionValue: ["options"],
optionLabel: ["sourceData"],
optionValue: ["sourceData"],
};
}
@ -65,11 +67,13 @@ class WDSSelectWidget extends BaseWidget<WDSSelectWidgetProps, WidgetState> {
}
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<WDSSelectWidgetProps, WidgetState> {
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<WDSSelectWidgetProps, WidgetState> {
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<WDSSelectWidgetProps, WidgetState> {
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 (
<Select
{...rest}
contextualHelp={labelTooltip}
errorMessage={validation.errorMessage}
isInvalid={validation.validationStatus === "invalid"}
isInvalid={
validation.validationStatus === "invalid" && this.props.isDirty
}
key={key}
onSelectionChange={this.handleChange}
placeholder={placeholderText}
selectedKey={selectedOptionValue}
>
{this.optionsToSelectItems(options).map((option) => (
<ListBoxItem key={option.id} textValue={option.label}>
{options.map((option) => (
<ListBoxItem
id={option.value}
key={option.value}
textValue={option.label}
>
{option.label}
</ListBoxItem>
))}

View File

@ -17,9 +17,5 @@ export const settersConfig = {
type: "boolean",
accessor: "isSwitchedOn",
},
setColor: {
path: "accentColor",
type: "string",
},
},
};