diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index 6e074fa91f..33ec49e692 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -21,7 +21,7 @@ import Tooltip from "components/ads/Tooltip"; import { isEllipsisActive } from "utils/helpers"; import SegmentHeader from "components/ads/ListSegmentHeader"; import { useTheme } from "styled-components"; -import { findIndex } from "lodash"; +import { findIndex, isArray } from "lodash"; export type DropdownOnSelect = (value?: string, dropdownOption?: any) => void; @@ -252,7 +252,7 @@ const Selected = styled.div<{ export const DropdownContainer = styled.div<{ width: string; height?: string }>` width: ${(props) => props.width}; - height: ${(props) => props.height || `38px`}; + height: ${(props) => props.height || `auto`}; position: relative; span.bp3-popover-target { display: inline-block; @@ -272,6 +272,7 @@ const DropdownSelect = styled.div``; export const DropdownWrapper = styled.div<{ width: string; + isOpen: boolean; }>` width: ${(props) => props.width}; height: fit-content; @@ -279,6 +280,8 @@ export const DropdownWrapper = styled.div<{ background-color: ${(props) => props.theme.colors.dropdown.menu.bg}; border: 1px solid ${(props) => props.theme.colors.dropdown.menu.border}; padding: ${(props) => props.theme.spaces[3]}px 0; + overflow: hidden; + display: ${(props) => (props.isOpen ? "inline-block" : "none")}; .dropdown-search { margin: 4px 12px 8px; width: calc(100% - 24px); @@ -633,11 +636,13 @@ function DefaultDropDownValueNode({ interface DropdownOptionsProps extends DropdownProps, DropdownSearchProps { optionClickHandler: (option: DropdownOption) => void; + selectedOptionClickHandler: (option: DropdownOption) => void; renderOption?: RenderOption; headerLabel?: string; selected: DropdownOption | DropdownOption[]; optionWidth: string; isMultiSelect?: boolean; + isOpen: boolean; // dropdown popover options flashes when closed, this prop helps to make sure it never happens again. } export function RenderDropdownOptions(props: DropdownOptionsProps) { @@ -663,6 +668,7 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { return ( {props.enableSearch && ( @@ -706,7 +712,12 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { aria-selected={isSelected} className="t--dropdown-option" key={index} - onClick={() => props.optionClickHandler(option)} + onClick={ + // users should be able to unselect a selected option by clicking the option again. + isSelected + ? () => props.selectedOptionClickHandler(option) + : () => props.optionClickHandler(option) + } role="option" selected={isSelected} > @@ -803,7 +814,7 @@ export default function Dropdown(props: DropdownProps) { (option: DropdownOption) => { if (props.isMultiSelect) { // Multi select -> typeof selected is array of objects - if (!selected) { + if (isArray(selected) && selected.length < 1) { setSelected([option]); } else { const newOptions: DropdownOption[] = [ @@ -942,7 +953,7 @@ export default function Dropdown(props: DropdownProps) { className={props.className} disabled={props.disabled} hasError={errorFlag} - height={props.height || getMinHeight(props.isMultiSelect)} + height={props.height || "38px"} isMultiSelect={props.isMultiSelect} isOpen={isOpen} onClick={() => setIsOpen(!isOpen)} @@ -986,7 +997,7 @@ export default function Dropdown(props: DropdownProps) { ); } - -function getMinHeight(isMultiSelect?: boolean): string { - if (isMultiSelect) return "44px"; - return "38px"; -} diff --git a/app/client/src/components/formControls/ProjectionSelectorControl.tsx b/app/client/src/components/formControls/ProjectionSelectorControl.tsx new file mode 100644 index 0000000000..aed0662ea6 --- /dev/null +++ b/app/client/src/components/formControls/ProjectionSelectorControl.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import BaseControl, { ControlProps } from "./BaseControl"; +import styled from "styled-components"; +import Dropdown, { DropdownOption } from "components/ads/Dropdown"; +import { ControlType } from "constants/PropertyControlConstants"; +import _ from "lodash"; +import { FieldArray, getFormValues } from "redux-form"; +import { connect } from "react-redux"; +import { AppState } from "reducers"; +import { QUERY_EDITOR_FORM_NAME } from "constants/forms"; +import { QueryAction } from "entities/Action"; + +const DropdownSelect = styled.div` + font-size: 14px; + width: 50vh; +`; + +class ProjectionSelectorControl extends BaseControl< + ProjectionSelectorControlProps +> { + render() { + let width = "50vh"; + if (this.props.customStyles && this.props?.customStyles?.width) { + width = this.props?.customStyles?.width; + } + + return ( + + + + ); + } + + getControlType(): ControlType { + return "PROJECTION"; + } +} + +function renderDropdown(props: any): JSX.Element { + // values that have been selected and stored in redux form state. + let selectedValues = props?.fields.getAll(); + if (_.isUndefined(selectedValues)) { + selectedValues = props?.initialValue || []; + } + + // options that correspond to the selected values kept in redux form state. + // the dropdown component requires the entire option object and not just the value. + const selectedOptions = props.dropDownOptions.filter( + (option: DropdownOption) => selectedValues.includes(option.value), + ); + + // select option + const onSelectOptions = (option: string | undefined) => { + if (option) { + props.fields.push(option); + } + }; + + // remove options + const removeOption = (option: string | undefined) => { + const options = selectedValues.map((field: DropdownOption) => field); + const optionIndex = options.indexOf(option); + + props.fields.remove(optionIndex); + }; + + return ( + + ); +} + +export interface ProjectionSelectorControlProps extends ControlProps { + options: DropdownOption[]; + placeholderText: string; + propertyValue: string; + subtitle?: string; + isMultiSelect?: boolean; + isSearchable?: boolean; + fetchOptionsCondtionally?: boolean; +} +const mapStateToProps = ( + state: AppState, + ownProps: ProjectionSelectorControlProps, +) => { + let dropDownOptions: DropdownOption[] = []; + + // if the component has an option enabled to fetch the options dynamically, + if (ownProps.fetchOptionsCondtionally) { + // TODO: this is just a test, will be updated once the fetchDynamicFormData is implemented + dropDownOptions = [ + { label: "Test1", value: "SINGLE" }, + { label: "Test2", value: "ALL" }, + ]; + const dynamicFormDataString = _.get( + getFormValues(QUERY_EDITOR_FORM_NAME)(state) as QueryAction, + "actionConfiguration.formData.updateMany.query", + ); + + // ownProps.configProperty will be used to filter from the array of data + // const dynamicFormDataString = getFormEvaluationState(state); + + try { + dropDownOptions = JSON.parse(dynamicFormDataString); + } catch (e) { + dropDownOptions = []; + } + } else { + dropDownOptions = ownProps.options; + } + + return { + dropDownOptions, + }; +}; + +export default connect(mapStateToProps)(ProjectionSelectorControl); diff --git a/app/client/src/utils/FormControlRegistry.tsx b/app/client/src/utils/FormControlRegistry.tsx index 6f7dc17ff6..90103f2a45 100644 --- a/app/client/src/utils/FormControlRegistry.tsx +++ b/app/client/src/utils/FormControlRegistry.tsx @@ -42,6 +42,9 @@ import PaginationControl, { import SortingControl, { SortingControlProps, } from "components/formControls/SortingControl"; +import ProjectionSelectorControl, { + ProjectionSelectorControlProps, +} from "components/formControls/ProjectionSelectorControl"; class FormControlRegistry { static registerFormControlBuilders() { @@ -132,6 +135,13 @@ class FormControlRegistry { return ; }, }); + FormControlFactory.registerControlBuilder("PROJECTION", { + buildPropertyControl( + controlProps: ProjectionSelectorControlProps, + ): JSX.Element { + return ; + }, + }); } }