convert all lookups to use new Lookup component

This commit is contained in:
Keith Grant
2019-11-26 13:38:44 -08:00
parent 639b297027
commit cb07e9c757
11 changed files with 406 additions and 536 deletions

View File

@@ -38,7 +38,7 @@ class AnsibleSelect extends React.Component {
> >
{data.map(option => ( {data.map(option => (
<FormSelectOption <FormSelectOption
key={option.id} key={option.key}
value={option.value} value={option.value}
label={option.label} label={option.label}
isDisabled={option.isDisabled} isDisabled={option.isDisabled}
@@ -50,7 +50,7 @@ class AnsibleSelect extends React.Component {
} }
const Option = shape({ const Option = shape({
id: oneOfType([string, number]).isRequired, key: oneOfType([string, number]).isRequired,
value: oneOfType([string, number]).isRequired, value: oneOfType([string, number]).isRequired,
label: string.isRequired, label: string.isRequired,
isDisabled: bool, isDisabled: bool,

View File

@@ -1,11 +1,20 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { withI18n } from '@lingui/react';
import { bool, func, number, string, oneOfType } from 'prop-types'; import { bool, func, number, string, oneOfType } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { CredentialsAPI } from '@api'; import { CredentialsAPI } from '@api';
import { Credential } from '@types'; import { Credential } from '@types';
import { mergeParams } from '@util/qs'; import { getQSConfig, parseQueryString, mergeParams } from '@util/qs';
import { FormGroup } from '@patternfly/react-core'; import { FormGroup } from '@patternfly/react-core';
import Lookup from '@components/Lookup'; import Lookup from '@components/Lookup';
import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
const QS_CONFIG = getQSConfig('credentials', {
page: 1,
page_size: 5,
order_by: 'name',
});
function CredentialLookup({ function CredentialLookup({
helperTextInvalid, helperTextInvalid,
@@ -16,11 +25,28 @@ function CredentialLookup({
required, required,
credentialTypeId, credentialTypeId,
value, value,
history,
}) { }) {
const getCredentials = async params => const [credentials, setCredentials] = useState([]);
CredentialsAPI.read( const [count, setCount] = useState(0);
mergeParams(params, { credential_type: credentialTypeId }) const [error, setError] = useState(null);
);
useEffect(() => {
(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
try {
const { data } = await CredentialsAPI.read(
mergeParams(params, { credential_type: credentialTypeId })
);
setCredentials(data.results);
setCount(data.count);
} catch (err) {
if (setError) {
setError(err);
}
}
})();
});
return ( return (
<FormGroup <FormGroup
@@ -32,15 +58,25 @@ function CredentialLookup({
> >
<Lookup <Lookup
id="credential" id="credential"
lookupHeader={label} header={label}
name="credential"
value={value} value={value}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
getItems={getCredentials}
required={required} required={required}
sortedColumnKey="name" renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={credentials}
optionCount={count}
header={label}
qsConfig={QS_CONFIG}
readOnly={canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/> />
<LookupErrorMessage error={error} />
</FormGroup> </FormGroup>
); );
} }
@@ -65,4 +101,4 @@ CredentialLookup.defaultProps = {
}; };
export { CredentialLookup as _CredentialLookup }; export { CredentialLookup as _CredentialLookup };
export default withI18n()(CredentialLookup); export default withI18n()(withRouter(CredentialLookup));

View File

@@ -7,8 +7,9 @@ import { FormGroup } from '@patternfly/react-core';
import { InstanceGroupsAPI } from '@api'; import { InstanceGroupsAPI } from '@api';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
import { FieldTooltip } from '@components/FormField'; import { FieldTooltip } from '@components/FormField';
import Lookup from './NewLookup'; import Lookup from './Lookup';
import SelectList from './shared/SelectList'; import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
const QS_CONFIG = getQSConfig('instance_groups', { const QS_CONFIG = getQSConfig('instance_groups', {
page: 1, page: 1,
@@ -45,17 +46,12 @@ function InstanceGroupsLookup(props) {
<Lookup <Lookup
id="org-instance-groups" id="org-instance-groups"
header={i18n._(t`Instance Groups`)} header={i18n._(t`Instance Groups`)}
// name="instanceGroups"
value={value} value={value}
onChange={onChange} onChange={onChange}
// items={instanceGroups}
// count={count}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
multiple multiple
// columns={} renderOptionsList={({ state, dispatch, canDelete }) => (
sortedColumnKey="name" <OptionsList
renderSelectList={({ state, dispatch, canDelete }) => (
<SelectList
value={state.selectedItems} value={state.selectedItems}
options={instanceGroups} options={instanceGroups}
optionCount={count} optionCount={count}
@@ -89,7 +85,7 @@ function InstanceGroupsLookup(props) {
/> />
)} )}
/> />
{error ? <div>error {error.message}</div> : ''} <LookupErrorMessage error={error} />
</FormGroup> </FormGroup>
); );
} }

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { string, func, bool } from 'prop-types'; import { string, func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core'; import { FormGroup } from '@patternfly/react-core';
@@ -7,61 +8,93 @@ import { InventoriesAPI } from '@api';
import { Inventory } from '@types'; import { Inventory } from '@types';
import Lookup from '@components/Lookup'; import Lookup from '@components/Lookup';
import { FieldTooltip } from '@components/FormField'; import { FieldTooltip } from '@components/FormField';
import { getQSConfig, parseQueryString } from '@util/qs';
import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
const getInventories = async params => InventoriesAPI.read(params); const QS_CONFIG = getQSConfig('inventory', {
page: 1,
page_size: 5,
order_by: 'name',
});
class InventoryLookup extends React.Component { function InventoryLookup({
render() { value,
const { tooltip,
value, onChange,
tooltip, onBlur,
onChange, required,
onBlur, isValid,
required, helperTextInvalid,
isValid, i18n,
helperTextInvalid, history,
i18n, }) {
} = this.props; const [inventories, setInventories] = useState([]);
const [count, setCount] = useState(0);
const [error, setError] = useState(null);
return ( useEffect(() => {
<FormGroup (async () => {
label={i18n._(t`Inventory`)} const params = parseQueryString(QS_CONFIG, history.location.search);
isRequired={required} try {
fieldId="inventory-lookup" const { data } = await InventoriesAPI.read(params);
isValid={isValid} setInventories(data.results);
helperTextInvalid={helperTextInvalid} setCount(data.count);
> } catch (err) {
{tooltip && <FieldTooltip content={tooltip} />} setError(err);
<Lookup }
id="inventory-lookup" })();
lookupHeader={i18n._(t`Inventory`)} }, [history.location]);
name="inventory"
value={value} return (
onChange={onChange} <FormGroup
onBlur={onBlur} label={i18n._(t`Inventory`)}
getItems={getInventories} isRequired={required}
required={required} fieldId="inventory-lookup"
qsNamespace="inventory" isValid={isValid}
columns={[ helperTextInvalid={helperTextInvalid}
{ name: i18n._(t`Name`), key: 'name', isSortable: true }, >
{ {tooltip && <FieldTooltip content={tooltip} />}
name: i18n._(t`Modified`), <Lookup
key: 'modified', id="inventory-lookup"
isSortable: false, header={i18n._(t`Inventory`)}
isNumeric: true, value={value}
}, onChange={onChange}
{ onBlur={onBlur}
name: i18n._(t`Created`), qsConfig={QS_CONFIG}
key: 'created', renderOptionsList={({ state, dispatch, canDelete }) => (
isSortable: false, <OptionsList
isNumeric: true, value={state.selectedItems}
}, options={inventories}
]} optionCount={count}
sortedColumnKey="name" columns={[
/> { name: i18n._(t`Name`), key: 'name', isSortable: true },
</FormGroup> {
); name: i18n._(t`Modified`),
} key: 'modified',
isSortable: false,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: false,
isNumeric: true,
},
]}
multiple={state.multiple}
header={i18n._(t`Inventory`)}
name="inventory"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
<LookupErrorMessage error={error} />
</FormGroup>
);
} }
InventoryLookup.propTypes = { InventoryLookup.propTypes = {
@@ -77,4 +110,4 @@ InventoryLookup.defaultProps = {
required: false, required: false,
}; };
export default withI18n()(InventoryLookup); export default withI18n()(withRouter(InventoryLookup));

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react'; import React, { Fragment, useReducer, useEffect } from 'react';
import { import {
string, string,
bool, bool,
@@ -20,7 +20,7 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import SelectList from './shared/SelectList'; import reducer, { initReducer } from './shared/reducer';
import { ChipGroup, Chip } from '../Chip'; import { ChipGroup, Chip } from '../Chip';
import { QSConfig } from '@types'; import { QSConfig } from '@types';
@@ -48,198 +48,118 @@ const ChipHolder = styled.div`
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
`; `;
class Lookup extends React.Component { function Lookup(props) {
constructor(props) { const {
super(props); id,
header,
onChange,
onBlur,
value,
multiple,
required,
qsConfig,
renderItemChip,
renderOptionsList,
history,
i18n,
} = props;
this.assertCorrectValueType(); const [state, dispatch] = useReducer(
let selectedItems = []; reducer,
if (props.value) { { value, multiple, required },
selectedItems = props.multiple ? [...props.value] : [props.value]; initReducer
} );
this.state = {
isModalOpen: false,
selectedItems,
};
this.handleModalToggle = this.handleModalToggle.bind(this);
this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.saveModal = this.saveModal.bind(this);
this.clearQSParams = this.clearQSParams.bind(this);
}
assertCorrectValueType() { useEffect(() => {
const { multiple, value } = this.props; dispatch({ type: 'SET_MULTIPLE', value: multiple });
if (!multiple && Array.isArray(value)) { }, [multiple]);
throw new Error(
'Lookup value must not be an array unless `multiple` is set'
);
}
if (multiple && !Array.isArray(value)) {
throw new Error('Lookup value must be an array if `multiple` is set');
}
}
removeItem(row) { useEffect(() => {
const { selectedItems } = this.state; dispatch({ type: 'SET_VALUE', value });
const { onToggleItem } = this.props; }, [value]);
if (onToggleItem) {
this.setState({ selectedItems: onToggleItem(selectedItems, row) });
return;
}
this.setState({
selectedItems: selectedItems.filter(item => item.id !== row.id),
});
}
addItem(row) { const clearQSParams = () => {
const { selectedItems } = this.state;
const { multiple, onToggleItem } = this.props;
if (onToggleItem) {
this.setState({ selectedItems: onToggleItem(selectedItems, row) });
return;
}
const index = selectedItems.findIndex(item => item.id === row.id);
if (!multiple) {
this.setState({ selectedItems: [row] });
return;
}
if (index > -1) {
return;
}
this.setState({ selectedItems: [...selectedItems, row] });
}
// TODO: cleanup
handleModalToggle() {
const { isModalOpen } = this.state;
const { value, multiple } = this.props;
// Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and
// opens it again
if (!isModalOpen) {
let selectedItems = [];
if (value) {
selectedItems = multiple ? [...value] : [value];
}
this.setState({ selectedItems });
} else {
this.clearQSParams();
}
this.setState(prevState => ({
isModalOpen: !prevState.isModalOpen,
}));
}
removeItemAndSave(item) {
const { value, onChange, multiple } = this.props;
if (multiple) {
onChange(value.filter(i => i.id !== item.id));
} else if (value.id === item.id) {
onChange(null);
}
}
saveModal() {
const { onChange, multiple } = this.props;
const { selectedItems } = this.state;
const value = multiple ? selectedItems : selectedItems[0] || null;
this.handleModalToggle();
onChange(value);
}
clearQSParams() {
const { qsConfig, history } = this.props;
const parts = history.location.search.replace(/^\?/, '').split('&'); const parts = history.location.search.replace(/^\?/, '').split('&');
const ns = qsConfig.namespace; const ns = qsConfig.namespace;
const otherParts = parts.filter(param => !param.startsWith(`${ns}.`)); const otherParts = parts.filter(param => !param.startsWith(`${ns}.`));
history.push(`${history.location.pathname}?${otherParts.join('&')}`); history.push(`${history.location.pathname}?${otherParts.join('&')}`);
} };
render() { const save = () => {
const { isModalOpen, selectedItems } = this.state; const { selectedItems } = state;
const { const val = multiple ? selectedItems : selectedItems[0] || null;
id, onChange(val);
lookupHeader, clearQSParams();
value, dispatch({ type: 'CLOSE_MODAL' });
items, };
count,
columns, const removeItem = item => {
multiple, if (multiple) {
name, onChange(value.filter(i => i.id !== item.id));
onBlur, } else {
required, onChange(null);
qsConfig, }
i18n, };
} = this.props;
const header = lookupHeader || i18n._(t`Items`); const closeModal = () => {
const canDelete = !required || (multiple && value.length > 1); clearQSParams();
return ( dispatch({ type: 'CLOSE_MODAL' });
<Fragment> };
<InputGroup onBlur={onBlur}>
<SearchButton const { isModalOpen, selectedItems } = state;
aria-label="Search" const canDelete = !required || (multiple && value.length > 1);
id={id} return (
onClick={this.handleModalToggle} <Fragment>
variant={ButtonVariant.tertiary} <InputGroup onBlur={onBlur}>
> <SearchButton
<SearchIcon /> aria-label="Search"
</SearchButton> id={id}
<ChipHolder className="pf-c-form-control"> onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
<ChipGroup defaultIsOpen numChips={5}> variant={ButtonVariant.tertiary}
{(multiple ? value : [value]).map(chip => (
<Chip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
>
{chip.name}
</Chip>
))}
</ChipGroup>
</ChipHolder>
</InputGroup>
<Modal
className="awx-c-modal"
title={i18n._(t`Select ${header}`)}
isOpen={isModalOpen}
onClose={this.handleModalToggle}
actions={[
<Button
key="select"
variant="primary"
onClick={this.saveModal}
style={selectedItems.length === 0 ? { display: 'none' } : {}}
>
{i18n._(t`Select`)}
</Button>,
<Button
key="cancel"
variant="secondary"
onClick={this.handleModalToggle}
>
{i18n._(t`Cancel`)}
</Button>,
]}
> >
<SelectList <SearchIcon />
value={selectedItems} </SearchButton>
onChange={newVal => this.setState({ selectedItems: newVal })} <ChipHolder className="pf-c-form-control">
options={items} <ChipGroup>
optionCount={count} {(multiple ? value : [value]).map(item =>
columns={columns} renderItemChip({
multiple={multiple} item,
header={lookupHeader} removeItem,
name={name} canDelete,
qsConfig={qsConfig} })
readOnly={!canDelete} )}
/> </ChipGroup>
</Modal> </ChipHolder>
</Fragment> </InputGroup>
); <Modal
} className="awx-c-modal"
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
isOpen={isModalOpen}
onClose={closeModal}
actions={[
<Button
key="select"
variant="primary"
onClick={save}
style={
required && selectedItems.length === 0 ? { display: 'none' } : {}
}
>
{i18n._(t`Select`)}
</Button>,
<Button key="cancel" variant="secondary" onClick={closeModal}>
{i18n._(t`Cancel`)}
</Button>,
]}
>
{renderOptionsList({
state,
dispatch,
canDelete,
})}
</Modal>
</Fragment>
);
} }
const Item = shape({ const Item = shape({
@@ -248,25 +168,33 @@ const Item = shape({
Lookup.propTypes = { Lookup.propTypes = {
id: string, id: string,
items: arrayOf(shape({})).isRequired, header: string,
count: number.isRequired,
// TODO: change to `header`
lookupHeader: string,
name: string,
onChange: func.isRequired, onChange: func.isRequired,
value: oneOfType([Item, arrayOf(Item)]), value: oneOfType([Item, arrayOf(Item)]),
multiple: bool, multiple: bool,
required: bool, required: bool,
onBlur: func,
qsConfig: QSConfig.isRequired, qsConfig: QSConfig.isRequired,
renderItemChip: func,
renderOptionsList: func.isRequired,
}; };
Lookup.defaultProps = { Lookup.defaultProps = {
id: 'lookup-search', id: 'lookup-search',
lookupHeader: null, header: null,
name: null,
value: null, value: null,
multiple: false, multiple: false,
required: false, required: false,
onBlur: () => {},
renderItemChip: ({ item, removeItem, canDelete }) => (
<Chip
key={item.id}
onClick={() => removeItem(item)}
isReadOnly={!canDelete}
>
{item.name}
</Chip>
),
}; };
export { Lookup as _Lookup }; export { Lookup as _Lookup };

View File

@@ -10,8 +10,8 @@ import { FieldTooltip } from '@components/FormField';
import { CredentialChip } from '@components/Chip'; import { CredentialChip } from '@components/Chip';
import VerticalSeperator from '@components/VerticalSeparator'; import VerticalSeperator from '@components/VerticalSeparator';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
import Lookup from './NewLookup'; import Lookup from './Lookup';
import SelectList from './shared/SelectList'; import OptionsList from './shared/OptionsList';
const QS_CONFIG = getQSConfig('credentials', { const QS_CONFIG = getQSConfig('credentials', {
page: 1, page: 1,
@@ -37,7 +37,6 @@ function MultiCredentialsLookup(props) {
const [selectedType, setSelectedType] = useState(null); const [selectedType, setSelectedType] = useState(null);
const [credentials, setCredentials] = useState([]); const [credentials, setCredentials] = useState([]);
const [credentialsCount, setCredentialsCount] = useState(0); const [credentialsCount, setCredentialsCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -60,12 +59,10 @@ function MultiCredentialsLookup(props) {
} }
try { try {
const params = parseQueryString(QS_CONFIG, history.location.search); const params = parseQueryString(QS_CONFIG, history.location.search);
setIsLoading(true);
const { results, count } = await loadCredentials( const { results, count } = await loadCredentials(
params, params,
selectedType.id selectedType.id
); );
setIsLoading(false);
setCredentials(results); setCredentials(results);
setCredentialsCount(count); setCredentialsCount(count);
} catch (err) { } catch (err) {
@@ -74,7 +71,7 @@ function MultiCredentialsLookup(props) {
})(); })();
}, [selectedType]); }, [selectedType]);
const isMultiple = selectedType && selectedType.value === 'Vault'; const isMultiple = selectedType && selectedType.name === 'Vault';
const renderChip = ({ item, removeItem, canDelete }) => ( const renderChip = ({ item, removeItem, canDelete }) => (
<CredentialChip <CredentialChip
key={item.id} key={item.id}
@@ -95,7 +92,7 @@ function MultiCredentialsLookup(props) {
onChange={onChange} onChange={onChange}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
renderItemChip={renderChip} renderItemChip={renderChip}
renderSelectList={({ state, dispatch, canDelete }) => { renderOptionsList={({ state, dispatch, canDelete }) => {
return ( return (
<Fragment> <Fragment>
{credentialTypes && credentialTypes.length > 0 && ( {credentialTypes && credentialTypes.length > 0 && (
@@ -107,7 +104,7 @@ function MultiCredentialsLookup(props) {
id="multiCredentialsLookUp-select" id="multiCredentialsLookUp-select"
label={i18n._(t`Selected Category`)} label={i18n._(t`Selected Category`)}
data={credentialTypes.map(type => ({ data={credentialTypes.map(type => ({
id: type.id, key: type.id,
value: type.id, value: type.id,
label: type.name, label: type.name,
isDisabled: false, isDisabled: false,
@@ -121,7 +118,7 @@ function MultiCredentialsLookup(props) {
/> />
</ToolbarItem> </ToolbarItem>
)} )}
<SelectList <OptionsList
value={state.selectedItems} value={state.selectedItems}
options={credentials} options={credentials}
optionCount={credentialsCount} optionCount={credentialsCount}

View File

@@ -1,200 +0,0 @@
import React, { Fragment, useReducer, useEffect } from 'react';
import {
string,
bool,
arrayOf,
func,
number,
oneOfType,
shape,
} from 'prop-types';
import { withRouter } from 'react-router-dom';
import { SearchIcon } from '@patternfly/react-icons';
import {
Button,
ButtonVariant,
InputGroup as PFInputGroup,
Modal,
} from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import reducer, { initReducer } from './shared/reducer';
import { ChipGroup, Chip } from '../Chip';
import { QSConfig } from '@types';
const SearchButton = styled(Button)`
::after {
border: var(--pf-c-button--BorderWidth) solid
var(--pf-global--BorderColor--200);
}
`;
const InputGroup = styled(PFInputGroup)`
${props =>
props.multiple &&
`
--pf-c-form-control--Height: 90px;
overflow-y: auto;
`}
`;
const ChipHolder = styled.div`
--pf-c-form-control--BorderTopColor: var(--pf-global--BorderColor--200);
--pf-c-form-control--BorderRightColor: var(--pf-global--BorderColor--200);
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
`;
function Lookup(props) {
const {
id,
header,
onChange,
onBlur,
value,
multiple,
required,
qsConfig,
renderItemChip,
renderSelectList,
history,
i18n,
} = props;
const [state, dispatch] = useReducer(
reducer,
{ value, multiple, required },
initReducer
);
useEffect(() => {
dispatch({ type: 'SET_MULTIPLE', value: multiple });
}, [multiple]);
useEffect(() => {
dispatch({ type: 'SET_VALUE', value });
}, [value]);
const clearQSParams = () => {
const parts = history.location.search.replace(/^\?/, '').split('&');
const ns = qsConfig.namespace;
const otherParts = parts.filter(param => !param.startsWith(`${ns}.`));
history.push(`${history.location.pathname}?${otherParts.join('&')}`);
};
const save = () => {
const { selectedItems } = state;
const val = multiple ? selectedItems : selectedItems[0] || null;
onChange(val);
clearQSParams();
dispatch({ type: 'CLOSE_MODAL' });
};
const removeItem = item => {
if (multiple) {
onChange(value.filter(i => i.id !== item.id));
} else {
onChange(null);
}
};
const closeModal = () => {
clearQSParams();
dispatch({ type: 'CLOSE_MODAL' });
};
const { isModalOpen, selectedItems } = state;
const canDelete = !required || (multiple && value.length > 1);
return (
<Fragment>
<InputGroup onBlur={onBlur}>
<SearchButton
aria-label="Search"
id={id}
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
variant={ButtonVariant.tertiary}
>
<SearchIcon />
</SearchButton>
<ChipHolder className="pf-c-form-control">
<ChipGroup>
{(multiple ? value : [value]).map(item =>
renderItemChip({
item,
removeItem,
canDelete,
})
)}
</ChipGroup>
</ChipHolder>
</InputGroup>
<Modal
className="awx-c-modal"
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
isOpen={isModalOpen}
onClose={closeModal}
actions={[
<Button
key="select"
variant="primary"
onClick={save}
style={
required && selectedItems.length === 0 ? { display: 'none' } : {}
}
>
{i18n._(t`Select`)}
</Button>,
<Button key="cancel" variant="secondary" onClick={closeModal}>
{i18n._(t`Cancel`)}
</Button>,
]}
>
{renderSelectList({
state,
dispatch,
canDelete,
})}
</Modal>
</Fragment>
);
}
const Item = shape({
id: number.isRequired,
});
Lookup.propTypes = {
id: string,
header: string,
onChange: func.isRequired,
value: oneOfType([Item, arrayOf(Item)]),
multiple: bool,
required: bool,
onBlur: func,
qsConfig: QSConfig.isRequired,
renderItemChip: func,
renderSelectList: func.isRequired,
};
Lookup.defaultProps = {
id: 'lookup-search',
header: null,
value: null,
multiple: false,
required: false,
onBlur: () => {},
renderItemChip: ({ item, removeItem, canDelete }) => (
<Chip
key={item.id}
onClick={() => removeItem(item)}
isReadOnly={!canDelete}
>
{item.name}
</Chip>
),
};
export { Lookup as _Lookup };
export default withI18n()(withRouter(Lookup));

View File

@@ -1,13 +1,17 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { string, func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { string, func, bool } from 'prop-types';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import { Organization } from '@types'; import { Organization } from '@types';
import { FormGroup } from '@patternfly/react-core'; import { FormGroup } from '@patternfly/react-core';
import Lookup from '@components/Lookup'; import { getQSConfig, parseQueryString } from '@util/qs';
import Lookup from './Lookup';
import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
const getOrganizations = async params => OrganizationsAPI.read(params); const QS_CONFIG = getQSConfig('organizations', {});
function OrganizationLookup({ function OrganizationLookup({
helperTextInvalid, helperTextInvalid,
@@ -17,7 +21,25 @@ function OrganizationLookup({
onChange, onChange,
required, required,
value, value,
history,
}) { }) {
const [organizations, setOrganizations] = useState([]);
const [count, setCount] = useState(0);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
try {
const { data } = await OrganizationsAPI.read(params);
setOrganizations(data.results);
setCount(data.count);
} catch (err) {
setError(err);
}
})();
}, [history.location]);
return ( return (
<FormGroup <FormGroup
fieldId="organization" fieldId="organization"
@@ -28,15 +50,29 @@ function OrganizationLookup({
> >
<Lookup <Lookup
id="organization" id="organization"
lookupHeader={i18n._(t`Organization`)} header={i18n._(t`Organization`)}
name="organization"
value={value} value={value}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
getItems={getOrganizations} qsConfig={QS_CONFIG}
required={required} required={required}
sortedColumnKey="name" sortedColumnKey="name"
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={organizations}
optionCount={count}
multiple={state.multiple}
header={i18n._(t`Organization`)}
name="organization"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/> />
<LookupErrorMessage error={error} />
</FormGroup> </FormGroup>
); );
} }
@@ -58,5 +94,5 @@ OrganizationLookup.defaultProps = {
value: null, value: null,
}; };
export default withI18n()(OrganizationLookup);
export { OrganizationLookup as _OrganizationLookup }; export { OrganizationLookup as _OrganizationLookup };
export default withI18n()(withRouter(OrganizationLookup));

View File

@@ -1,59 +1,87 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { string, func, bool } from 'prop-types'; import { string, func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core'; import { FormGroup } from '@patternfly/react-core';
import { ProjectsAPI } from '@api'; import { ProjectsAPI } from '@api';
import { Project } from '@types'; import { Project } from '@types';
import Lookup from '@components/Lookup';
import { FieldTooltip } from '@components/FormField'; import { FieldTooltip } from '@components/FormField';
import { getQSConfig, parseQueryString } from '@util/qs';
import Lookup from './Lookup';
import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
class ProjectLookup extends React.Component { const QS_CONFIG = getQSConfig('project', {
render() { page: 1,
const { page_size: 5,
helperTextInvalid, order_by: 'name',
i18n, });
isValid,
onChange,
required,
tooltip,
value,
onBlur,
} = this.props;
const loadProjects = async params => { function ProjectLookup({
const response = await ProjectsAPI.read(params); helperTextInvalid,
const { results, count } = response.data; i18n,
if (count === 1) { isValid,
onChange(results[0], 'project'); onChange,
required,
tooltip,
value,
onBlur,
history,
}) {
const [projects, setProjects] = useState([]);
const [count, setCount] = useState(0);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
try {
const { data } = await ProjectsAPI.read(params);
setProjects(data.results);
setCount(data.count);
} catch (err) {
setError(err);
} }
return response; })();
}; }, [history.location]);
return ( return (
<FormGroup <FormGroup
fieldId="project" fieldId="project"
helperTextInvalid={helperTextInvalid} helperTextInvalid={helperTextInvalid}
isRequired={required} isRequired={required}
isValid={isValid} isValid={isValid}
label={i18n._(t`Project`)} label={i18n._(t`Project`)}
> >
{tooltip && <FieldTooltip content={tooltip} />} {tooltip && <FieldTooltip content={tooltip} />}
<Lookup <Lookup
id="project" id="project"
lookupHeader={i18n._(t`Project`)} lookupHeader={i18n._(t`Project`)}
name="project" name="project"
value={value} value={value}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
getItems={loadProjects} required={required}
required={required} qsConfig={QS_CONFIG}
sortedColumnKey="name" renderOptionsList={({ state, dispatch, canDelete }) => (
qsNamespace="project" <OptionsList
/> value={state.selectedItems}
</FormGroup> options={projects}
); optionCount={count}
} multiple={state.multiple}
header={i18n._(t`Project`)}
name="project"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
<LookupErrorMessage error={error} />
</FormGroup>
);
} }
ProjectLookup.propTypes = { ProjectLookup.propTypes = {
@@ -75,4 +103,5 @@ ProjectLookup.defaultProps = {
onBlur: () => {}, onBlur: () => {},
}; };
export default withI18n()(ProjectLookup); export { ProjectLookup as _ProjectLookup };
export default withI18n()(withRouter(ProjectLookup));

View File

@@ -0,0 +1,15 @@
import React from 'react';
function LookupErrorMessage({ error }) {
if (!error) {
return null;
}
return (
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
{error.message || 'An error occured'}
</div>
);
}
export default LookupErrorMessage;

View File

@@ -16,7 +16,7 @@ import CheckboxListItem from '../../CheckboxListItem';
import DataListToolbar from '../../DataListToolbar'; import DataListToolbar from '../../DataListToolbar';
import { QSConfig } from '@types'; import { QSConfig } from '@types';
function SelectList({ function OptionsList({
value, value,
options, options,
optionCount, optionCount,
@@ -71,7 +71,7 @@ function SelectList({
const Item = shape({ const Item = shape({
id: oneOfType([number, string]).isRequired, id: oneOfType([number, string]).isRequired,
}); });
SelectList.propTypes = { OptionsList.propTypes = {
value: arrayOf(Item).isRequired, value: arrayOf(Item).isRequired,
options: arrayOf(Item).isRequired, options: arrayOf(Item).isRequired,
optionCount: number.isRequired, optionCount: number.isRequired,
@@ -82,9 +82,9 @@ SelectList.propTypes = {
deselectItem: func.isRequired, deselectItem: func.isRequired,
renderItemChip: func, renderItemChip: func,
}; };
SelectList.defaultProps = { OptionsList.defaultProps = {
multiple: false, multiple: false,
renderItemChip: null, renderItemChip: null,
}; };
export default withI18n()(SelectList); export default withI18n()(OptionsList);