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']",
"paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.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 {
color: ${(props) => props.theme.colors.dropdown.header.text} !important;
}
font-weight: ${(props) => props.theme.fontWeights[1]};
}
`;
const StyledMenu = styled(Menu)<MenuComponentProps>`
@ -58,14 +59,16 @@ const StyledMenuItem = styled(MenuItem)`
}
`;
class DropdownComponent extends Component<DropdownComponentProps> {
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<DropdownComponentProps> {
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<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);
};
// 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 (
<StyledDropdown
@ -196,7 +202,8 @@ class DropdownComponent extends Component<DropdownComponentProps> {
noResults={<MenuItem disabled text="No results." />}
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 || (
<StyledButtonWrapper height={height} width={width}>
@ -212,23 +219,36 @@ class DropdownComponent extends Component<DropdownComponentProps> {
}
}
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;

View File

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

View File

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

View File

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

View File

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