diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index fd6e1559c7..d934ac7141 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -43,6 +43,7 @@ "next": "?page=2&pageSize=10", "prev": "?page=1&pageSize=10", "apiFormDataBodyType": "x-www-form-urlencoded", + "apiMultipartBodyType": "multi-part", "defaultMoustacheData": "{{Input1.text", "defaultInputWidget": "{{Table1.selectedRow.id", "deafultDropDownWidget": [ diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Multipart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Multipart_spec.js new file mode 100644 index 0000000000..717ea5a717 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Multipart_spec.js @@ -0,0 +1,20 @@ +const testdata = require("../../../../fixtures/testdata.json"); +const apiEditor = require("../../../../locators/ApiEditor.json"); +const apiwidget = require("../../../../locators/apiWidgetslocator.json"); + +describe("API Panel request body", function() { + it("Check whether input and type dropdown selector exist when multi-part is selected", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("FirstAPI"); + + cy.SelectAction(testdata.postAction); + + cy.contains(apiEditor.bodyTab).click(); + cy.contains(testdata.apiFormDataBodyType).click(); + cy.contains(testdata.apiMultipartBodyType).click(); + + cy.get(apiwidget.formEncoded).should("be.visible"); + cy.get(apiwidget.multipartTypeDropdown).should("be.visible"); + cy.DeleteAPI(); + }); +}); diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index ffc8c6bebe..ccae503504 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -55,5 +55,6 @@ "renameEntity": ".single-select >div:contains('Edit Name')", "paramsTab": "//li//span[text()='Params']", "paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0", - "paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0" + "paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0", + "multipartTypeDropdown":"button:contains('Text')" } diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index a367e200fd..1748f419f9 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -21,7 +21,25 @@ import { Toaster } from "components/ads/Toast"; import ReCAPTCHA from "react-google-recaptcha"; const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => { - if (props.filled) return props.theme.colors.textOnDarkBG; + if (props.filled) { + return props.accent === "grey" + ? props.theme.colors.textOnGreyBG + : props.theme.colors.textOnDarkBG; + } + if (props.accent) { + if (props.accent === "secondary") { + return props.theme.colors[AccentColorMap["primary"]]; + } + return props.theme.colors[AccentColorMap[props.accent]]; + } +}; + +const getButtonFillStyles = (props: { theme: Theme } & ButtonStyleProps) => { + if (props.filled) { + return props.accent === "grey" + ? props.theme.colors.dropdownIconDarkBg + : props.theme.colors.textOnDarkBG; + } if (props.accent) { if (props.accent === "secondary") { return props.theme.colors[AccentColorMap["primary"]]; @@ -33,7 +51,7 @@ const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => { const ButtonColorStyles = css` color: ${getButtonColorStyles}; svg { - fill: ${getButtonColorStyles}; + fill: ${getButtonFillStyles}; } `; @@ -48,6 +66,7 @@ const AccentColorMap: Record = { primary: "primaryOld", secondary: "secondaryOld", error: "error", + grey: "dropdownGreyBg", }; const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => ( @@ -67,9 +86,14 @@ const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => ( props.accent ? props.theme.colors[AccentColorMap[props.accent]] : props.theme.colors.primary}; + color: ${(props) => + props.accent === "grey" + ? props.theme.colors.textOnGreyBG + : props.theme.colors.textOnDarkBG}; border-radius: 0; font-weight: ${(props) => props.theme.fontWeights[2]}; outline: none; + &.bp3-button { padding: 0px 10px; } @@ -80,10 +104,10 @@ const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => ( display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; - max-height: 100%; overflow: hidden; } + &&:hover, &&:focus { ${ButtonColorStyles}; @@ -122,7 +146,7 @@ const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => ( } `; -export type ButtonStyleName = "primary" | "secondary" | "error"; +export type ButtonStyleName = "primary" | "secondary" | "error" | "grey"; type ButtonStyleProps = { accent?: ButtonStyleName; diff --git a/app/client/src/components/editorComponents/DropdownComponent.tsx b/app/client/src/components/editorComponents/DropdownComponent.tsx index a9b8177159..d17447c355 100644 --- a/app/client/src/components/editorComponents/DropdownComponent.tsx +++ b/app/client/src/components/editorComponents/DropdownComponent.tsx @@ -1,7 +1,16 @@ import React, { Component, ReactNode } from "react"; import styled from "styled-components"; -import { MenuItem, Menu, ControlGroup, InputGroup } from "@blueprintjs/core"; -import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; +import { + MenuItem, + Menu, + ControlGroup, + InputGroup, + IMenuProps, +} from "@blueprintjs/core"; +import { + BaseButton, + ButtonStyleName, +} from "components/designSystems/blueprint/ButtonComponent"; import { ItemRenderer, Select, @@ -9,11 +18,40 @@ import { IItemListRendererProps, } from "@blueprintjs/select"; import { DropdownOption } from "widgets/DropdownWidget"; +import { WrappedFieldInputProps } from "redux-form"; + +interface ButtonWrapperProps { + width?: string; +} +interface MenuProps { + width?: string; +} + +type MenuComponentProps = IMenuProps & MenuProps; const Dropdown = Select.ofType(); const StyledDropdown = styled(Dropdown)``; +const StyledButtonWrapper = styled.div` + width: ${(props) => props.width || "100%"}; +`; +const StyledMenu = styled(Menu)` + min-width: ${(props) => props.width || "100%"}; + border-radius: 0; +`; +const StyledMenuItem = styled(MenuItem)` + border-radius: 0; + + &&&.bp3-active { + background: ${(props) => props.theme.colors.propertyPane.activeButtonText}; + } +`; + class DropdownComponent extends Component { + componentDidMount() { + const { input, selected } = this.props; + input && input.onChange(selected?.value); + } private newItemTextInput: HTMLInputElement | null = null; private setNewItemTextInput = (element: HTMLInputElement | null) => { this.newItemTextInput = element; @@ -41,36 +79,35 @@ class DropdownComponent extends Component { renderItemList: ItemListRenderer = ( props: IItemListRendererProps, ) => { - if (this.props.addItem) { - const renderItems = props.items.map(props.renderItem).filter(Boolean); - const displayMode = ( - - ); - const editMode = ( - - - - - ); - return ( - - {renderItems} - {!this.state.isEditing ? displayMode : editMode} - - ); - } + const { items, renderItem } = props; + const { addItem, width } = this.props; + const renderItems = items.map(renderItem).filter(Boolean); - return null; + const displayMode = ( + + ); + const editMode = ( + + + + + ); + return ( + + {renderItems} + {addItem && (!this.state.isEditing ? displayMode : editMode)} + + ); }; searchItem = (query: string, option: DropdownOption): boolean => { @@ -82,6 +119,7 @@ class DropdownComponent extends Component { ); }; onItemSelect = (item: DropdownOption): void => { + this.props.input?.onChange(item.value); this.props.selectHandler(item.value); }; @@ -93,13 +131,13 @@ class DropdownComponent extends Component { return null; } return ( - ); }; @@ -110,31 +148,45 @@ class DropdownComponent extends Component { (option) => option.value === selectedValue, ); - return item && (item.label || item.label); + return item && item.label; } return ""; }; render() { + const { + accent, + autocomplete, + filled, + input, + options, + selected, + width, + } = this.props; + return ( } onItemSelect={this.onItemSelect} popoverProps={{ minimal: true }} + {...input} > {this.props.toggle || ( - + + + )} ); @@ -142,6 +194,7 @@ class DropdownComponent extends Component { } export interface DropdownComponentProps { + hasLabel?: boolean; options: DropdownOption[]; selectHandler: (selectedValue: string) => void; selected?: DropdownOption; @@ -154,6 +207,10 @@ export interface DropdownComponentProps { addItemHandler: (name: string) => void; }; toggle?: ReactNode; + accent?: ButtonStyleName; + filled?: boolean; + input?: WrappedFieldInputProps; + width?: string; } 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 new file mode 100644 index 0000000000..223a8097c5 --- /dev/null +++ b/app/client/src/components/editorComponents/form/fields/DynamicDropdownField.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Field, BaseFieldProps } from "redux-form"; +import DropdownComponent from "components/editorComponents/DropdownComponent"; +import { ButtonStyleName } from "components/designSystems/blueprint/ButtonComponent"; +import { DropdownOption } from "widgets/DropdownWidget"; + +interface DynamicDropdownFieldOptions { + options: DropdownOption[]; + accent?: ButtonStyleName; + filled?: boolean; + width?: string; +} + +type DynamicDropdownFieldProps = BaseFieldProps & DynamicDropdownFieldOptions; + +class DynamicDropdownField extends React.Component< + DynamicDropdownFieldProps, + { + selectedOption: DropdownOption; + } +> { + constructor(props: DynamicDropdownFieldProps) { + super(props); + this.state = { + selectedOption: this.props.options[0], + }; + } + + handleOptionSelection = (selectedValue: string): void => { + const selectedOption = this.props.options.find( + (option) => option.value === selectedValue, + ) as DropdownOption; + this.setState({ + selectedOption, + }); + }; + + render() { + const dropdownProps = { + selectHandler: this.handleOptionSelection, + selected: this.state.selectedOption, + }; + + return ( + + ); + } +} + +export default DynamicDropdownField; diff --git a/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx b/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx index 9247f2bbc0..922d65d084 100644 --- a/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx +++ b/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx @@ -30,6 +30,7 @@ class DynamicTextField extends React.Component< theme: this.props.theme || EditorTheme.LIGHT, size: this.props.size || EditorSize.COMPACT, }; + return ; } } diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index fafd3f1df4..ea5a768271 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -14,6 +14,12 @@ import { import Text, { Case, TextType } from "components/ads/Text"; import { Classes } from "components/ads/common"; import { AutocompleteDataType } from "utils/autocomplete/TernServer"; +import DynamicDropdownField from "./DynamicDropdownField"; +import { Colors } from "constants/Colors"; +import { + DEFAULT_MULTI_PART_DROPDOWN_WIDTH, + MULTI_PART_DROPDOWN_OPTIONS, +} from "constants/ApiEditorConstants"; type CustomStack = { removeTopPadding?: boolean; @@ -83,6 +89,23 @@ const FlexContainer = styled.div` } `; +const DynamicTextFieldWithDropdownWrapper = styled.div` + display: flex; + position: relative; + border-bottom: solid 1px ${Colors.MERCURY}; + margin-bottom: 10px; + top: -2px; + & .CodeEditorTarget * { + border-bottom: none !important; + } +`; + +const DynamicDropdownFieldWrapper = styled.div` + position: relative; + top: 1px; + margin-left: 5px; +`; + const expected = { type: FIELD_VALUES.API_ACTION.params, example: "", @@ -136,16 +159,41 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) { return ( - + {props.hasType ? ( + + + + + + + + ) : ( + + )} {!props.actionConfig && ( @@ -239,6 +287,7 @@ type Props = { dataTreePath?: string; hideHeader?: boolean; theme?: EditorTheme; + hasType?: boolean; }; function KeyValueFieldArray(props: Props) { diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index 61d22b5f6c..e780f943e6 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -48,7 +48,7 @@ export const CONTENT_TYPE_HEADER_KEY = "content-type"; export enum ApiContentTypes { JSON = "json", FORM_URLENCODED = "x-www-form-urlencoded", - MULTIPART_FORM_DATA = "form-data", + MULTIPART_FORM_DATA = "multi-part", RAW = "raw", } @@ -74,3 +74,26 @@ export const POST_BODY_FORMAT_TITLES = POST_BODY_FORMAT_OPTIONS.map( return { title: option.label, key: option.value }; }, ); + +export enum MultiPartOptionTypes { + TEXT = "Text", + FILE = "File", +} + +export interface MULTI_PART_DROPDOWN_OPTION { + label: MultiPartOptionTypes; + value: string; +} + +export const MULTI_PART_DROPDOWN_OPTIONS: MULTI_PART_DROPDOWN_OPTION[] = [ + { + label: MultiPartOptionTypes.TEXT, + value: "TEXT", + }, + { + label: MultiPartOptionTypes.FILE, + value: "FILE", + }, +]; + +export const DEFAULT_MULTI_PART_DROPDOWN_WIDTH = "75px"; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 007d1b2fc2..8a01fc44c8 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -2706,6 +2706,7 @@ export const theme: Theme = { inputInactiveBG: Colors.AQUA_HAZE, textDefault: Colors.BLACK_PEARL, textOnDarkBG: Colors.WHITE, + textOnGreyBG: Colors.CHARCOAL, textAnchor: Colors.PURPLE, border: Colors.GEYSER, paneCard: Colors.SHARK, @@ -2748,6 +2749,8 @@ export const theme: Theme = { dropdownIconBg: Colors.ALTO2, welcomeTourStickySidebarColor: Colors.WHITE, welcomeTourStickySidebarBackground: "#F86A2B", + dropdownIconDarkBg: Colors.DARK_GRAY, + dropdownGreyBg: Colors.Gallery, }, lineHeights: [0, 14, 16, 18, 22, 24, 28, 36, 48, 64, 80], diff --git a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx index bf8f537c2e..c8addbd683 100644 --- a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx +++ b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx @@ -63,6 +63,65 @@ function PostBodyData(props: Props) { updateBodyContentType, } = props; + const tabComponentsMap = ( + key: string, + contentType: ApiContentTypes, + ): JSX.Element => { + return { + [ApiContentTypes.JSON]: ( + + + + ), + [ApiContentTypes.FORM_URLENCODED]: ( + + ), + + [ApiContentTypes.MULTIPART_FORM_DATA]: ( + + ), + + [ApiContentTypes.RAW]: ( + + + + ), + }[contentType]; + }; + return ( { - let component = ( - - - - ); - if ( - el.title === ApiContentTypes.FORM_URLENCODED || - el.title === ApiContentTypes.MULTIPART_FORM_DATA - ) { - component = ( - - ); - } else if (el.title === ApiContentTypes.RAW) { - component = ( - - - - ); - } - return { key: el.key, title: el.title, panelComponent: component }; + return { + key: el.key, + title: el.title, + panelComponent: tabComponentsMap(el.key, el.title), + }; })} />