Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Custom Dropdown component with search support #148

Merged
merged 8 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@import './components/DualPaneMapper/DualPaneMapper.scss';
@import './components/SourceClusterSelect/SourceClusterSelect.scss';
@import './components/WarningModal/WarningModal.scss';

.dual-pane-mapper-form {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { length } from 'redux-form-validators';
import { noop, bindMethods } from 'patternfly-react';
import SourceClusterSelect from '../SourceClusterSelect/SourceClusterSelect';
import DatastoresStepForm from './components/DatastoresStepForm/DatastoresStepForm';
import { BootstrapSelect } from '../../../../../common/forms/BootstrapSelect';
import { getClusterOptions } from '../helpers';

class MappingWizardDatastoresStep extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -109,16 +110,27 @@ class MappingWizardDatastoresStep extends React.Component {

const { selectedCluster, selectedClusterMapping } = this.state;

const clusterOptions = getClusterOptions(clusterMappings);

// first we render the dropdown selection for each source cluster in clusterMappings,
// then we call `selectSourceCluster` and go get that cluster's datastores on selection
return (
<div>
<SourceClusterSelect
clusterMappings={clusterMappings}
selectSourceCluster={this.selectSourceCluster}
selectedCluster={selectedCluster}
selectedClusterMapping={selectedClusterMapping}
form={form}
<Field
name="cluster_select"
label={__('Map source datastores to target datastores for cluster')}
data_live_search="true"
component={BootstrapSelect}
options={clusterOptions}
option_key="id"
option_value="name"
onSelect={this.selectSourceCluster}
pre_selected_value={
clusterOptions.length === 1 ? clusterOptions[0].id : ''
}
choose_text={`<${__('Select a source cluster')}>`}
render_within_form="true"
form_name={form}
Copy link
Contributor

@michaelkro michaelkro Mar 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, since we're now hooking the selects into redux-form, this enables us to make a couple improvements

  1. Lift the selectedCluster component state to the redux store
  2. We can now use redux-form validation to handle the next button disabling. Before I had to use a workaround where I was rendering DualPaneMapper, but hiding it (find hack here). We can instead use length from redux-form-validators and go back to conditionally rendering this guy

If we want to make these changes, I imagine it would be best to do it as a separate PR

Copy link
Member

@priley86 priley86 Mar 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - i think lifting selectedCluster to redux store is fine (so good w/ # 1).

For # 2 (handle next button disabling)... i think that's probably OK but we have to vet this change w/ @mturley 's wizard abstraction. I believe shouldDisableNextStep(activeStepIndex) callback is called for a step now, so we'd just have to verify we can still map this to redux-form-validators. This should all jive in a future PR... but I'm fine w/ what's been added here...

/>
<Field
name="datastoresMappings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { noop, bindMethods } from 'patternfly-react';
import { Field, reduxForm } from 'redux-form';
import { length } from 'redux-form-validators';
import SourceClusterSelect from '../SourceClusterSelect/SourceClusterSelect';
import NetworksStepForm from './components/NetworksStepForm/NetworksStepForm';
import { BootstrapSelect } from '../../../../../common/forms/BootstrapSelect';
import { getClusterOptions } from '../helpers';

class MappingWizardNetworksStep extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -107,14 +108,25 @@ class MappingWizardNetworksStep extends React.Component {

const { selectedCluster, selectedClusterMapping } = this.state;

const clusterOptions = getClusterOptions(clusterMappings);

return (
<div>
<SourceClusterSelect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete the SourceClusterSelect component now @michaelkro @AparnaKarve ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fo sho! This is a huge upgrade, nice job @AparnaKarve!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 alright sounds good. If we remove SourceClusterSelect I think this PR is good to merge 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback guys -- will be deleting SourceClusterSelect now...

clusterMappings={clusterMappings}
selectSourceCluster={this.selectSourceCluster}
selectedCluster={selectedCluster}
selectedClusterMapping={selectedClusterMapping}
form={form}
<Field
name="cluster_select"
label={__('Map source networks to target networks for cluster')}
data_live_search="true"
component={BootstrapSelect}
options={clusterOptions}
option_key="id"
option_value="name"
onSelect={this.selectSourceCluster}
pre_selected_value={
clusterOptions.length === 1 ? clusterOptions[0].id : ''
}
choose_text={`<${__('Select a source cluster')}>`}
render_within_form="true"
form_name={form}
/>
<Field
name="networksMappings"
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const getClusterOptions = clusterMappings => {
const sourceClustersWithAssociatedTargetClusters = clusterMappings.reduce(
(mappings, targetClusterWithSourceClusters) => {
const {
nodes: sourceClusters,
...targetCluster
} = targetClusterWithSourceClusters;
const sourceToTargetMappings = sourceClusters.map(sourceCluster => ({
sourceCluster,
targetCluster,
sourceClusterMappedToTargetCluster: {
name: `${sourceCluster.name} (${targetCluster.name})`,
id: sourceCluster.id
}
}));
return mappings.concat(sourceToTargetMappings);
},
[]
);

return sourceClustersWithAssociatedTargetClusters.map(
({ sourceClusterMappedToTargetCluster }) =>
sourceClusterMappedToTargetCluster
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import { required } from 'redux-form-validators';
import { Form } from 'patternfly-react';
import PropTypes from 'prop-types';
import { FormField } from '../../../../../common/forms/FormField';
import { BootstrapSelect } from '../../../../../common/forms/BootstrapSelect';

const PlanWizardGeneralStep = ({ transformationMappings }) => (
<Form className="form-horizontal">
<Field
name="infrastructure_mapping"
label={__('Infrastructure Mapping')}
required
component={FormField}
data_live_search="true"
component={BootstrapSelect}
validate={[required({ msg: __('Required') })]}
options={transformationMappings}
optionKey="id"
optionValue="name"
type="select"
option_key="id"
option_value="name"
form_name="planWizardGeneralStep"
/>
<Field
name="name"
Expand Down
115 changes: 115 additions & 0 deletions app/javascript/react/screens/App/common/forms/BootstrapSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Form,
FormGroup,
Grid,
ControlLabel,
bindMethods
} from 'patternfly-react';
import { focus } from 'redux-form';

const $ = require('jquery');
require('bootstrap-select');

export class BootstrapSelect extends React.Component {
constructor(props) {
super(props);
bindMethods(this, ['renderFormGroup']);
}
componentDidMount() {
const {
input,
form_name,
meta: { visited, dispatch },
onSelect,
pre_selected_value
} = this.props;

$(`#${input.name}`).selectpicker('val', input.value || pre_selected_value);

$(`.${input.name}_select`).on('click', '.dropdown-toggle', e => {
if (!visited) dispatch(focus(form_name, input.name));
});
if (onSelect)
$(`#${input.name}`).on('changed.bs.select', e => {
onSelect(e.target.value);
});
}

renderFormGroup = (labelWidth, controlWidth) => {
const {
input,
label,
required,
data_live_search,
options,
option_key,
option_value,
choose_text,
meta: { visited, error, active }
} = this.props;
const formGroupProps = { key: { label }, ...this.props };

if (visited && !active && error) formGroupProps.validationState = 'error';

return (
<FormGroup {...formGroupProps}>
<Grid.Col componentClass={ControlLabel} sm={labelWidth}>
{label}
{required && ' *'}
</Grid.Col>
<Grid.Col sm={controlWidth}>
<select
id={input.name}
data-live-search={data_live_search}
className={`form-control ${input.name}_select`}
{...input}
>
<option disabled value="">
{choose_text || `<${__('Choose')}>`}
</option>
{options.map(val => (
<option value={val[option_key]} key={val[option_value]}>
{val[option_value]}
</option>
))}
</select>
{visited &&
!active &&
error && <Form.HelpBlock>{error}</Form.HelpBlock>}
</Grid.Col>
</FormGroup>
);
};

render = () => {
const { render_within_form } = this.props;

if (render_within_form) {
return (
<div>
<Form horizontal>{this.renderFormGroup(6, 4)}</Form>
</div>
);
}
return this.renderFormGroup(2, 9);
};
}

BootstrapSelect.propTypes = {
label: PropTypes.string,
input: PropTypes.object,
required: PropTypes.bool,
data_live_search: PropTypes.string,
type: PropTypes.string,
options: PropTypes.array,
option_key: PropTypes.string,
option_value: PropTypes.string,
meta: PropTypes.object,
form_name: PropTypes.string,
onSelect: PropTypes.func,
pre_selected_value: PropTypes.string,
choose_text: PropTypes.string,
render_within_form: PropTypes.string
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
},
"dependencies": {
"axios": "^0.17.1",
"bootstrap-select": "^1.12.4",
"classnames": "^2.2.5",
"csv": "^2.0.0",
"cx": "^18.1.11",
"intl": "~1.2.5",
"jquery": "^3.3.1",
"moment": "^2.20.1",
"numeral": "^2.0.6",
"patternfly": "^3.35.1",
Expand Down