fix: improve dropdown component (#8183)

Improved multipart form's dropdown component
This commit is contained in:
Favour Ohanekwu 2021-10-06 20:20:35 +01:00 committed by GitHub
parent 82dc82633f
commit 803e5e7cc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 48 deletions

View File

@ -56,5 +56,5 @@
"paramsTab": "//li//span[text()='Params']", "paramsTab": "//li//span[text()='Params']",
"paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0", "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')" "multipartTypeDropdown":"button:contains('Type')"
} }

View File

@ -39,6 +39,7 @@ const StyledButtonWrapper = styled.div<ButtonWrapperProps>`
& > span { & > span {
color: ${(props) => props.theme.colors.dropdown.header.text} !important; color: ${(props) => props.theme.colors.dropdown.header.text} !important;
} }
font-weight: ${(props) => props.theme.fontWeights[1]};
} }
`; `;
const StyledMenu = styled(Menu)<MenuComponentProps>` const StyledMenu = styled(Menu)<MenuComponentProps>`
@ -58,14 +59,16 @@ const StyledMenuItem = styled(MenuItem)`
} }
`; `;
class DropdownComponent extends Component<DropdownComponentProps> { // function checks if dropdown is connected to a redux form (of interface 'FormDropdownComponentProps')
componentDidMount() { const isFormDropdown = (
const { input, options } = this.props; props: DropdownComponentProps | FormDropdownComponentProps,
// set selected option to first option by default ): props is FormDropdownComponentProps => {
if (input && !input.value) { return "input" in props && props.input !== undefined;
input.onChange(options[0].value); };
}
} class DropdownComponent extends Component<
DropdownComponentProps | FormDropdownComponentProps
> {
private newItemTextInput: HTMLInputElement | null = null; private newItemTextInput: HTMLInputElement | null = null;
private setNewItemTextInput = (element: HTMLInputElement | null) => { private setNewItemTextInput = (element: HTMLInputElement | null) => {
this.newItemTextInput = element; this.newItemTextInput = element;
@ -127,10 +130,13 @@ class DropdownComponent extends Component<DropdownComponentProps> {
option.label.toLowerCase().indexOf(query.toLowerCase()) > -1) option.label.toLowerCase().indexOf(query.toLowerCase()) > -1)
); );
}; };
// function is called after user selects an option
onItemSelect = (item: DropdownOption): void => { onItemSelect = (item: DropdownOption): void => {
const { input, selectHandler } = this.props; if (isFormDropdown(this.props)) {
input && input.onChange(item.value); this.props.input.onChange(item.value);
selectHandler && selectHandler(item.value); } else {
this.props.selectHandler(item.value);
}
}; };
renderItem: ItemRenderer<DropdownOption> = ( renderItem: ItemRenderer<DropdownOption> = (
@ -152,37 +158,37 @@ class DropdownComponent extends Component<DropdownComponentProps> {
); );
}; };
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); 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 = () => { 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(value);
const item = this.getDropdownOption(input.value); return item ? item.label : this.props.placeholder;
return item && item.label;
}
if (selected) {
const item = this.getDropdownOption(selected.value);
return item && item.label;
}
return "";
}; };
getActiveOption = (): DropdownOption => { // this function returns the active option
const { input, options, selected } = this.props; // returns undefined if no option is selected
const defaultActiveOption = options[0]; getActiveOption = (): DropdownOption | undefined => {
if (isFormDropdown(this.props)) {
if (input) { return this.getDropdownOption(this.props.input.value);
return this.getDropdownOption(input.value) || defaultActiveOption;
} else { } else {
return selected || defaultActiveOption; return this.props.selected;
} }
}; };
render() { render() {
const { autocomplete, height, input, options, width } = this.props; const { autocomplete, height, options, width } = this.props;
return ( return (
<StyledDropdown <StyledDropdown
@ -196,7 +202,8 @@ class DropdownComponent extends Component<DropdownComponentProps> {
noResults={<MenuItem disabled text="No results." />} noResults={<MenuItem disabled text="No results." />}
onItemSelect={this.onItemSelect} onItemSelect={this.onItemSelect}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
{...input} // Destructure the "input" prop if dropdown is form-connected
{...(isFormDropdown(this.props) ? this.props.input : {})}
> >
{this.props.toggle || ( {this.props.toggle || (
<StyledButtonWrapper height={height} width={width}> <StyledButtonWrapper height={height} width={width}>
@ -212,23 +219,36 @@ class DropdownComponent extends Component<DropdownComponentProps> {
} }
} }
export interface DropdownComponentProps { // Dropdown can either be connected to a redux-form
hasLabel?: boolean; // or be a stand-alone component
options: DropdownOption[];
selectHandler?: (selectedValue: string) => void; // Props common to both classes of dropdowns
selected?: DropdownOption; export interface BaseDropdownComponentProps {
multiselectDisplayType?: "TAGS" | "CHECKBOXES";
checked?: boolean;
multi?: boolean;
autocomplete?: boolean;
addItem?: { addItem?: {
displayText: string; displayText: string;
addItemHandler: (name: string) => void; addItemHandler: (name: string) => void;
}; };
toggle?: ReactNode; autocomplete?: boolean;
input?: WrappedFieldInputProps; checked?: boolean;
hasLabel?: boolean;
height?: string; height?: string;
multi?: boolean;
multiselectDisplayType?: "TAGS" | "CHECKBOXES";
options: DropdownOption[];
placeholder: string;
toggle?: ReactNode;
width?: string; 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; export default DropdownComponent;

View File

@ -7,6 +7,7 @@ interface DynamicDropdownFieldOptions {
options: DropdownOption[]; options: DropdownOption[];
height?: string; height?: string;
width?: string; width?: string;
placeholder: string;
} }
type DynamicDropdownFieldProps = BaseFieldProps & DynamicDropdownFieldOptions; type DynamicDropdownFieldProps = BaseFieldProps & DynamicDropdownFieldOptions;

View File

@ -16,6 +16,7 @@ import { Classes } from "components/ads/common";
import { AutocompleteDataType } from "utils/autocomplete/TernServer"; import { AutocompleteDataType } from "utils/autocomplete/TernServer";
import DynamicDropdownField from "./DynamicDropdownField"; import DynamicDropdownField from "./DynamicDropdownField";
import { import {
DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER,
DEFAULT_MULTI_PART_DROPDOWN_WIDTH, DEFAULT_MULTI_PART_DROPDOWN_WIDTH,
MULTI_PART_DROPDOWN_OPTIONS, MULTI_PART_DROPDOWN_OPTIONS,
} from "constants/ApiEditorConstants"; } from "constants/ApiEditorConstants";
@ -187,6 +188,7 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
height="36px" height="36px"
name={`${field}.type`} name={`${field}.type`}
options={MULTI_PART_DROPDOWN_OPTIONS} options={MULTI_PART_DROPDOWN_OPTIONS}
placeholder={DEFAULT_MULTI_PART_DROPDOWN_PLACEHOLDER}
width={DEFAULT_MULTI_PART_DROPDOWN_WIDTH} width={DEFAULT_MULTI_PART_DROPDOWN_WIDTH}
/> />
</DynamicDropdownFieldWrapper> </DynamicDropdownFieldWrapper>

View File

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

View File

@ -93,7 +93,7 @@ function PostBodyData(props: Props) {
key={key} key={key}
label="" label=""
name="actionConfiguration.bodyFormData" name="actionConfiguration.bodyFormData"
pushFields // pushFields
theme={theme} theme={theme}
/> />
), ),
@ -105,7 +105,7 @@ function PostBodyData(props: Props) {
key={key} key={key}
label="" label=""
name="actionConfiguration.bodyFormData" name="actionConfiguration.bodyFormData"
pushFields // pushFields
theme={theme} theme={theme}
/> />
), ),

View File

@ -56,9 +56,12 @@ export const transformRestAction = (data: ApiAction): ApiAction => {
return action; return action;
}; };
// Filters empty key-value pairs or key-value-type(Multipart) from form data, headers and query params
function removeEmptyPairs(keyValueArray: any) { function removeEmptyPairs(keyValueArray: any) {
if (!keyValueArray || !keyValueArray.length) return keyValueArray; if (!keyValueArray || !keyValueArray.length) return keyValueArray;
return keyValueArray.filter( return keyValueArray.filter(
(data: any) => data && (!isEmpty(data.key) || !isEmpty(data.value)), (data: any) =>
data &&
(!isEmpty(data.key) || !isEmpty(data.value) || !isEmpty(data.type)),
); );
} }

View File

@ -1,6 +1,9 @@
import { transformRestAction } from "transformers/RestActionTransformer"; import { transformRestAction } from "transformers/RestActionTransformer";
import { PluginType, ApiAction } from "entities/Action"; 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_"); // jest.mock("POST_");
@ -243,4 +246,62 @@ describe("Api action transformer", () => {
const result = transformRestAction(input); const result = transformRestAction(input);
expect(result).toEqual(output); 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);
});
}); });