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:
parent
d9a3253e92
commit
ecf9934859
|
|
@ -23,3 +23,7 @@ button.selectTriggerButton {
|
|||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selectTriggerButton [data-select-text][data-placeholder] {
|
||||
color: var(--color-fg-neutral-subtle);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import type { AnvilConfig } from "WidgetProvider/constants";
|
||||
|
||||
export const anvilConfig: AnvilConfig = {
|
||||
isLargeWidget: false,
|
||||
widgetSize: {
|
||||
minWidth: {
|
||||
base: "100%",
|
||||
"180px": "sizing-30",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
@ -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",
|
||||
],
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { propertyPaneContentConfig } from "./contentConfig";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { optionsCustomValidation } from "./optionsCustomValidation";
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
export const settersConfig = {
|
||||
__setters: {
|
||||
setVisibility: {
|
||||
path: "isVisible",
|
||||
type: "boolean",
|
||||
},
|
||||
setDisabled: {
|
||||
path: "isDisabled",
|
||||
type: "boolean",
|
||||
},
|
||||
setData: {
|
||||
path: "options",
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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: "",
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
export { defaultOptionValidation } from "./defaultOptionValidation";
|
||||
export { optionsCustomValidation } from "./optionsCustomValidation";
|
||||
export { defaultOptionValueValidation } from "./defaultOptionValueValidation";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export const SAMPLE_DATA = [
|
||||
{ name: "Blue", code: "BLUE" },
|
||||
{ name: "Green", code: "GREEN" },
|
||||
{ name: "Red", code: "RED" },
|
||||
];
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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 = `))}}`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,5 @@ export const settersConfig = {
|
|||
type: "boolean",
|
||||
accessor: "isSwitchedOn",
|
||||
},
|
||||
setColor: {
|
||||
path: "accentColor",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user