diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index ccae503504..e5968c1eee 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -56,5 +56,5 @@ "paramsTab": "//li//span[text()='Params']", "paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0", "paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0", - "multipartTypeDropdown":"button:contains('Text')" + "multipartTypeDropdown":"button:contains('Type')" } diff --git a/app/client/src/components/editorComponents/DropdownComponent.tsx b/app/client/src/components/editorComponents/DropdownComponent.tsx index 7d8cf7d436..ec8a133977 100644 --- a/app/client/src/components/editorComponents/DropdownComponent.tsx +++ b/app/client/src/components/editorComponents/DropdownComponent.tsx @@ -39,6 +39,7 @@ const StyledButtonWrapper = styled.div` & > span { color: ${(props) => props.theme.colors.dropdown.header.text} !important; } + font-weight: ${(props) => props.theme.fontWeights[1]}; } `; const StyledMenu = styled(Menu)` @@ -58,14 +59,16 @@ const StyledMenuItem = styled(MenuItem)` } `; -class DropdownComponent extends Component { - componentDidMount() { - const { input, options } = this.props; - // set selected option to first option by default - if (input && !input.value) { - input.onChange(options[0].value); - } - } +// function checks if dropdown is connected to a redux form (of interface 'FormDropdownComponentProps') +const isFormDropdown = ( + props: DropdownComponentProps | FormDropdownComponentProps, +): props is FormDropdownComponentProps => { + return "input" in props && props.input !== undefined; +}; + +class DropdownComponent extends Component< + DropdownComponentProps | FormDropdownComponentProps +> { private newItemTextInput: HTMLInputElement | null = null; private setNewItemTextInput = (element: HTMLInputElement | null) => { this.newItemTextInput = element; @@ -127,10 +130,13 @@ class DropdownComponent extends Component { option.label.toLowerCase().indexOf(query.toLowerCase()) > -1) ); }; + // function is called after user selects an option onItemSelect = (item: DropdownOption): void => { - const { input, selectHandler } = this.props; - input && input.onChange(item.value); - selectHandler && selectHandler(item.value); + if (isFormDropdown(this.props)) { + this.props.input.onChange(item.value); + } else { + this.props.selectHandler(item.value); + } }; renderItem: ItemRenderer = ( @@ -152,37 +158,37 @@ class DropdownComponent extends Component { ); }; - getDropdownOption = (value: string): DropdownOption | undefined => { + // helper function that returns a dropdown option given its value + // returns undefined if option isn't found + getDropdownOption = ( + value: string | undefined, + ): DropdownOption | undefined => { return this.props.options.find((option) => option.value === value); }; + // this function returns the selected item's label + // returns the "placeholder" in the event that no option is selected. getSelectedDisplayText = () => { - const { input, selected } = this.props; + const value = isFormDropdown(this.props) + ? this.props.input.value + : this.props.selected?.value; - if (input) { - const item = this.getDropdownOption(input.value); - return item && item.label; - } - if (selected) { - const item = this.getDropdownOption(selected.value); - return item && item.label; - } - return ""; + const item = this.getDropdownOption(value); + return item ? item.label : this.props.placeholder; }; - getActiveOption = (): DropdownOption => { - const { input, options, selected } = this.props; - const defaultActiveOption = options[0]; - - if (input) { - return this.getDropdownOption(input.value) || defaultActiveOption; + // this function returns the active option + // returns undefined if no option is selected + getActiveOption = (): DropdownOption | undefined => { + if (isFormDropdown(this.props)) { + return this.getDropdownOption(this.props.input.value); } else { - return selected || defaultActiveOption; + return this.props.selected; } }; render() { - const { autocomplete, height, input, options, width } = this.props; + const { autocomplete, height, options, width } = this.props; return ( { noResults={} onItemSelect={this.onItemSelect} popoverProps={{ minimal: true }} - {...input} + // Destructure the "input" prop if dropdown is form-connected + {...(isFormDropdown(this.props) ? this.props.input : {})} > {this.props.toggle || ( @@ -212,23 +219,36 @@ class DropdownComponent extends Component { } } -export interface DropdownComponentProps { - hasLabel?: boolean; - options: DropdownOption[]; - selectHandler?: (selectedValue: string) => void; - selected?: DropdownOption; - multiselectDisplayType?: "TAGS" | "CHECKBOXES"; - checked?: boolean; - multi?: boolean; - autocomplete?: boolean; +// Dropdown can either be connected to a redux-form +// or be a stand-alone component + +// Props common to both classes of dropdowns +export interface BaseDropdownComponentProps { addItem?: { displayText: string; addItemHandler: (name: string) => void; }; - toggle?: ReactNode; - input?: WrappedFieldInputProps; + autocomplete?: boolean; + checked?: boolean; + hasLabel?: boolean; height?: string; + multi?: boolean; + multiselectDisplayType?: "TAGS" | "CHECKBOXES"; + options: DropdownOption[]; + placeholder: string; + toggle?: ReactNode; width?: string; } +// stand-alone dropdown interface +export interface DropdownComponentProps extends BaseDropdownComponentProps { + selectHandler: (selectedValue: string) => void; + selected: DropdownOption | undefined; +} + +// Form-connected dropdown interface +export interface FormDropdownComponentProps extends BaseDropdownComponentProps { + input: WrappedFieldInputProps; +} + export default DropdownComponent; diff --git a/app/client/src/components/editorComponents/form/fields/DynamicDropdownField.tsx b/app/client/src/components/editorComponents/form/fields/DynamicDropdownField.tsx index 17ba684e79..53c45e9510 100644 --- a/app/client/src/components/editorComponents/form/fields/DynamicDropdownField.tsx +++ b/app/client/src/components/editorComponents/form/fields/DynamicDropdownField.tsx @@ -7,6 +7,7 @@ interface DynamicDropdownFieldOptions { options: DropdownOption[]; height?: string; width?: string; + placeholder: string; } type DynamicDropdownFieldProps = BaseFieldProps & DynamicDropdownFieldOptions; diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index 93c75383c2..781c876788 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -16,6 +16,7 @@ import { Classes } from "components/ads/common"; import { AutocompleteDataType } from "utils/autocomplete/TernServer"; import DynamicDropdownField from "./DynamicDropdownField"; import { + DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER, DEFAULT_MULTI_PART_DROPDOWN_WIDTH, MULTI_PART_DROPDOWN_OPTIONS, } from "constants/ApiEditorConstants"; @@ -187,6 +188,7 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) { height="36px" name={`${field}.type`} options={MULTI_PART_DROPDOWN_OPTIONS} + placeholder={DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER} width={DEFAULT_MULTI_PART_DROPDOWN_WIDTH} /> diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index e780f943e6..51873719b6 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -96,4 +96,5 @@ export const MULTI_PART_DROPDOWN_OPTIONS: MULTI_PART_DROPDOWN_OPTION[] = [ }, ]; -export const DEFAULT_MULTI_PART_DROPDOWN_WIDTH = "75px"; +export const DEFAULT_MULTI_PART_DROPDOWN_WIDTH = "77px"; +export const DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER = "Type"; diff --git a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx index ac6eacd2c7..ec5915bf4f 100644 --- a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx +++ b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx @@ -93,7 +93,7 @@ function PostBodyData(props: Props) { key={key} label="" name="actionConfiguration.bodyFormData" - pushFields + // pushFields theme={theme} /> ), @@ -105,7 +105,7 @@ function PostBodyData(props: Props) { key={key} label="" name="actionConfiguration.bodyFormData" - pushFields + // pushFields theme={theme} /> ), diff --git a/app/client/src/transformers/RestActionTransformer.ts b/app/client/src/transformers/RestActionTransformer.ts index 8ecdcd3b50..e3bad1af51 100644 --- a/app/client/src/transformers/RestActionTransformer.ts +++ b/app/client/src/transformers/RestActionTransformer.ts @@ -56,9 +56,12 @@ export const transformRestAction = (data: ApiAction): ApiAction => { return action; }; +// Filters empty key-value pairs or key-value-type(Multipart) from form data, headers and query params function removeEmptyPairs(keyValueArray: any) { if (!keyValueArray || !keyValueArray.length) return keyValueArray; return keyValueArray.filter( - (data: any) => data && (!isEmpty(data.key) || !isEmpty(data.value)), + (data: any) => + data && + (!isEmpty(data.key) || !isEmpty(data.value) || !isEmpty(data.type)), ); } diff --git a/app/client/src/transformers/RestActionTransformers.test.ts b/app/client/src/transformers/RestActionTransformers.test.ts index 96964d9a30..981ee936ef 100644 --- a/app/client/src/transformers/RestActionTransformers.test.ts +++ b/app/client/src/transformers/RestActionTransformers.test.ts @@ -1,6 +1,9 @@ import { transformRestAction } from "transformers/RestActionTransformer"; import { PluginType, ApiAction } from "entities/Action"; -import { POST_BODY_FORMAT_OPTIONS } from "constants/ApiEditorConstants"; +import { + MultiPartOptionTypes, + POST_BODY_FORMAT_OPTIONS, +} from "constants/ApiEditorConstants"; // jest.mock("POST_"); @@ -243,4 +246,62 @@ describe("Api action transformer", () => { const result = transformRestAction(input); expect(result).toEqual(output); }); + + it("filters empty pairs from form data", () => { + const input: ApiAction = { + ...BASE_ACTION, + actionConfiguration: { + ...BASE_ACTION.actionConfiguration, + httpMethod: "POST", + headers: [ + { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[2].value }, + ], + body: "", + bodyFormData: [ + { + key: "hey", + value: "ho", + type: MultiPartOptionTypes.TEXT, + editable: true, + mandatory: false, + description: "I been tryin to do it right", + }, + { + key: "", + value: "", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], + }, + }; + + // output object should not include the second bodyFormData object + // as its key, value and type are empty + const output: ApiAction = { + ...BASE_ACTION, + actionConfiguration: { + ...BASE_ACTION.actionConfiguration, + httpMethod: "POST", + headers: [ + { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[2].value }, + ], + body: "", + bodyFormData: [ + { + key: "hey", + value: "ho", + type: MultiPartOptionTypes.TEXT, + editable: true, + mandatory: false, + description: "I been tryin to do it right", + }, + ], + }, + }; + const result = transformRestAction(input); + expect(result).toEqual(output); + }); });