WIP split Lookup into Lookup & CategoryLookup

This commit is contained in:
Keith Grant
2019-11-14 15:35:08 -08:00
parent 2a722ba8d0
commit 5a207f155e
7 changed files with 516 additions and 248 deletions

View File

@@ -0,0 +1,348 @@
import React, { Fragment } 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,
ToolbarItem,
} from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import AnsibleSelect from '../AnsibleSelect';
import PaginatedDataList from '../PaginatedDataList';
import VerticalSeperator from '../VerticalSeparator';
import DataListToolbar from '../DataListToolbar';
import CheckboxListItem from '../CheckboxListItem';
import SelectedList from '../SelectedList';
import { ChipGroup, Chip, CredentialChip } 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;
`;
class CategoryLookup extends React.Component {
constructor(props) {
super(props);
this.assertCorrectValueType();
let selectedItems = [];
if (props.value) {
selectedItems = props.multiple ? [...props.value] : [props.value];
}
this.state = {
isModalOpen: false,
selectedItems,
error: null,
};
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() {
const { multiple, value, selectCategoryOptions } = this.props;
if (selectCategoryOptions) {
return;
}
if (!multiple && Array.isArray(value)) {
throw new Error(
'CategoryLookup value must not be an array unless `multiple` is set'
);
}
if (multiple && !Array.isArray(value)) {
throw new Error(
'CategoryLookup value must be an array if `multiple` is set'
);
}
}
removeItem(row) {
const { selectedItems } = this.state;
const { onToggleItem } = this.props;
if (onToggleItem) {
this.setState({ selectedItems: onToggleItem(selectedItems, row) });
return;
}
this.setState({
selectedItems: selectedItems.filter(item => item.id !== row.id),
});
}
addItem(row) {
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: clean up
handleModalToggle() {
const { isModalOpen } = this.state;
const { value, multiple, selectCategory } = 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();
if (selectCategory) {
selectCategory(null, 'Machine');
}
}
this.setState(prevState => ({
isModalOpen: !prevState.isModalOpen,
}));
}
removeItemAndSave(row) {
const { value, onChange, multiple } = this.props;
if (multiple) {
onChange(value.filter(item => item.id !== row.id));
} else if (value.id === row.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 ns = qsConfig.namespace;
const otherParts = parts.filter(param => !param.startsWith(`${ns}.`));
history.push(`${history.location.pathname}?${otherParts.join('&')}`);
}
render() {
const { isModalOpen, selectedItems, error } = this.state;
const {
id,
items,
count,
lookupHeader,
value,
columns,
multiple,
name,
onBlur,
qsConfig,
required,
selectCategory,
selectCategoryOptions,
selectedCategory,
i18n,
} = this.props;
const header = lookupHeader || i18n._(t`Items`);
const canDelete = !required || (multiple && value.length > 1);
const chips = () => {
return selectCategoryOptions && selectCategoryOptions.length > 0 ? (
<ChipGroup>
{(multiple ? value : [value]).map(chip => (
<CredentialChip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
credential={chip}
/>
))}
</ChipGroup>
) : (
<ChipGroup>
{(multiple ? value : [value]).map(chip => (
<Chip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
>
{chip.name}
</Chip>
))}
</ChipGroup>
);
};
return (
<Fragment>
<InputGroup onBlur={onBlur}>
<SearchButton
aria-label="Search"
id={id}
onClick={this.handleModalToggle}
variant={ButtonVariant.tertiary}
>
<SearchIcon />
</SearchButton>
<ChipHolder className="pf-c-form-control">
{value ? chips(value) : null}
</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>,
]}
>
{selectCategoryOptions && selectCategoryOptions.length > 0 && (
<ToolbarItem css=" display: flex; align-items: center;">
<span css="flex: 0 0 25%;">Selected Category</span>
<VerticalSeperator />
<AnsibleSelect
css="flex: 1 1 75%;"
id="multiCredentialsLookUp-select"
label="Selected Category"
data={selectCategoryOptions}
value={selectedCategory.label}
onChange={selectCategory}
/>
</ToolbarItem>
)}
{selectedItems.length > 0 && (
<SelectedList
label={i18n._(t`Selected`)}
selected={selectedItems}
showOverflowAfter={5}
onRemove={this.removeItem}
isReadOnly={!canDelete}
isCredentialList={
selectCategoryOptions && selectCategoryOptions.length > 0
}
/>
)}
<PaginatedDataList
items={items}
itemCount={count}
pluralizedItemName={lookupHeader}
qsConfig={qsConfig}
toolbarColumns={columns}
renderItem={item => (
<CheckboxListItem
key={item.id}
itemId={item.id}
name={multiple ? item.name : name}
label={item.name}
isSelected={selectedItems.some(i => i.id === item.id)}
onSelect={() => this.addItem(item)}
isRadio={
!multiple ||
(selectCategoryOptions &&
selectCategoryOptions.length &&
selectedCategory.value !== 'Vault')
}
/>
)}
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
showPageSizeOptions={false}
/>
{error ? <div>error: {error.message}</div> : ''}
</Modal>
</Fragment>
);
}
}
const Item = shape({
id: number.isRequired,
});
CategoryLookup.propTypes = {
id: string,
items: arrayOf(shape({})).isRequired,
// TODO: change to `header`
lookupHeader: string,
name: string,
onChange: func.isRequired,
value: oneOfType([Item, arrayOf(Item)]),
multiple: bool,
required: bool,
qsConfig: QSConfig.isRequired,
selectCategory: func.isRequired,
selectCategoryOptions: oneOfType(shape({})).isRequired,
selectedCategory: shape({}).isRequired,
};
CategoryLookup.defaultProps = {
id: 'lookup-search',
lookupHeader: null,
name: null,
value: null,
multiple: false,
required: false,
};
export { CategoryLookup as _CategoryLookup };
export default withI18n()(withRouter(CategoryLookup));

View File

@@ -1,84 +1,114 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import { arrayOf, string, func, object } 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, Tooltip } from '@patternfly/react-core'; import { FormGroup, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import { InstanceGroupsAPI } from '@api'; import { InstanceGroupsAPI } from '@api';
import Lookup from '@components/Lookup'; import Lookup from '@components/Lookup';
import { getQSConfig, parseQueryString } from '@util/qs';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)` const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px; margin-left: 10px;
`; `;
const getInstanceGroups = async params => InstanceGroupsAPI.read(params); const QS_CONFIG = getQSConfig('instance-groups', {
page: 1,
page_size: 5,
order_by: 'name',
});
// const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
class InstanceGroupsLookup extends React.Component { function InstanceGroupsLookup({
render() { value,
const { value, tooltip, onChange, className, i18n } = this.props; onChange,
tooltip,
className,
history,
i18n,
}) {
const [instanceGroups, setInstanceGroups] = 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 InstanceGroupsAPI.read(params);
setInstanceGroups(data.results);
setCount(data.count);
} catch (err) {
setError(err);
}
})();
}, [history.location]);
/*
Wrapping <div> added to workaround PF bug: Wrapping <div> added to workaround PF bug:
https://github.com/patternfly/patternfly-react/issues/2855 https://github.com/patternfly/patternfly-react/issues/2855
*/ */
return ( return (
<div className={className}> <div className={className}>
<FormGroup <FormGroup
label={i18n._(t`Instance Groups`)} label={i18n._(t`Instance Groups`)}
fieldId="org-instance-groups" fieldId="org-instance-groups"
> >
{tooltip && ( {tooltip && (
<Tooltip position="right" content={tooltip}> <Tooltip position="right" content={tooltip}>
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
)} )}
<Lookup <Lookup
id="org-instance-groups" id="org-instance-groups"
lookupHeader={i18n._(t`Instance Groups`)} lookupHeader={i18n._(t`Instance Groups`)}
name="instanceGroups" name="instanceGroups"
value={value} value={value}
onChange={onChange} onChange={onChange}
getItems={getInstanceGroups} items={instanceGroups}
qsNamespace="instance-group" count={count}
multiple qsConfig={QS_CONFIG}
columns={[ multiple
{ columns={[
name: i18n._(t`Name`), {
key: 'name', name: i18n._(t`Name`),
isSortable: true, key: 'name',
isSearchable: true, isSortable: true,
}, isSearchable: true,
{ },
name: i18n._(t`Modified`), {
key: 'modified', name: i18n._(t`Modified`),
isSortable: false, key: 'modified',
isNumeric: true, isSortable: false,
}, isNumeric: true,
{ },
name: i18n._(t`Created`), {
key: 'created', name: i18n._(t`Created`),
isSortable: false, key: 'created',
isNumeric: true, isSortable: false,
}, isNumeric: true,
]} },
sortedColumnKey="name" ]}
/> sortedColumnKey="name"
</FormGroup> />
</div> {error ? <div>error {error.message}</div> : ''}
); </FormGroup>
} </div>
);
} }
InstanceGroupsLookup.propTypes = { InstanceGroupsLookup.propTypes = {
value: PropTypes.arrayOf(PropTypes.object).isRequired, value: arrayOf(object).isRequired,
tooltip: PropTypes.string, tooltip: string,
onChange: PropTypes.func.isRequired, onChange: func.isRequired,
className: string,
}; };
InstanceGroupsLookup.defaultProps = { InstanceGroupsLookup.defaultProps = {
tooltip: '', tooltip: '',
className: '',
}; };
export default withI18n()(InstanceGroupsLookup); export default withI18n()(withRouter(InstanceGroupsLookup));

View File

@@ -15,20 +15,17 @@ import {
ButtonVariant, ButtonVariant,
InputGroup as PFInputGroup, InputGroup as PFInputGroup,
Modal, Modal,
ToolbarItem,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { withI18n } from '@lingui/react'; 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 AnsibleSelect from '../AnsibleSelect';
import PaginatedDataList from '../PaginatedDataList'; import PaginatedDataList from '../PaginatedDataList';
import VerticalSeperator from '../VerticalSeparator';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
import CheckboxListItem from '../CheckboxListItem'; import CheckboxListItem from '../CheckboxListItem';
import SelectedList from '../SelectedList'; import SelectedList from '../SelectedList';
import { ChipGroup, Chip, CredentialChip } from '../Chip'; import { ChipGroup, Chip } from '../Chip';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { QSConfig } from '@types';
const SearchButton = styled(Button)` const SearchButton = styled(Button)`
::after { ::after {
@@ -66,42 +63,16 @@ class Lookup extends React.Component {
this.state = { this.state = {
isModalOpen: false, isModalOpen: false,
selectedItems, selectedItems,
results: [],
count: 0,
error: null,
}; };
this.qsConfig = getQSConfig(props.qsNamespace, {
page: 1,
page_size: 5,
order_by: props.sortedColumnKey,
});
this.handleModalToggle = this.handleModalToggle.bind(this); this.handleModalToggle = this.handleModalToggle.bind(this);
this.addItem = this.addItem.bind(this); this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this); this.removeItem = this.removeItem.bind(this);
this.saveModal = this.saveModal.bind(this); this.saveModal = this.saveModal.bind(this);
this.getData = this.getData.bind(this);
this.clearQSParams = this.clearQSParams.bind(this); this.clearQSParams = this.clearQSParams.bind(this);
} }
componentDidMount() {
this.getData();
}
componentDidUpdate(prevProps) {
const { location, selectedCategory } = this.props;
if (
location !== prevProps.location ||
prevProps.selectedCategory !== selectedCategory
) {
this.getData();
}
}
assertCorrectValueType() { assertCorrectValueType() {
const { multiple, value, selectCategoryOptions } = this.props; const { multiple, value } = this.props;
if (selectCategoryOptions) {
return;
}
if (!multiple && Array.isArray(value)) { if (!multiple && Array.isArray(value)) {
throw new Error( throw new Error(
'Lookup value must not be an array unless `multiple` is set' 'Lookup value must not be an array unless `multiple` is set'
@@ -112,27 +83,6 @@ class Lookup extends React.Component {
} }
} }
async getData() {
const {
getItems,
location: { search },
} = this.props;
const queryParams = parseQueryString(this.qsConfig, search);
this.setState({ error: false });
try {
const { data } = await getItems(queryParams);
const { results, count } = data;
this.setState({
results,
count,
});
} catch (err) {
this.setState({ error: true });
}
}
removeItem(row) { removeItem(row) {
const { selectedItems } = this.state; const { selectedItems } = this.state;
const { onToggleItem } = this.props; const { onToggleItem } = this.props;
@@ -163,55 +113,11 @@ class Lookup extends React.Component {
} }
this.setState({ selectedItems: [...selectedItems, row] }); this.setState({ selectedItems: [...selectedItems, row] });
} }
// toggleSelected(row) {
// const {
// name,
// onChange,
// multiple,
// onToggleItem,
// selectCategoryOptions,
// onChange,
// value
// } = this.props;
// const {
// selectedItems: updatedSelectedItems,
// isModalOpen,
// } = this.state;
// const selectedIndex = updatedSelectedItems.findIndex(
// selectedRow => selectedRow.id === row.id
// );
//
// if (multiple) {
//
// if (selectCategoryOptions) {
//
// onToggleItem(row, isModalOpen);
// }
// if (selectedIndex > -1) {
//
// const valueToUpdate = value.filter(itemValue => itemValue.id !==row.id );
// onChange(valueToUpdate)
// } else {
//
// onChange([...value, row])
// }
// } else {
//
// onChange(row)
// }
// Updates the selected items from parent state
// This handles the case where the user removes chips from the lookup input
// while the modal is closed
// if (!isModalOpen) {
// onChange(updatedSelectedItems, name);
// }
// }
// TODO: cleanup
handleModalToggle() { handleModalToggle() {
const { isModalOpen } = this.state; const { isModalOpen } = this.state;
const { value, multiple, selectCategory } = this.props; const { value, multiple } = this.props;
// Resets the selected items from parent state whenever modal is opened // Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and // This handles the case where the user closes/cancels the modal and
// opens it again // opens it again
@@ -223,20 +129,17 @@ class Lookup extends React.Component {
this.setState({ selectedItems }); this.setState({ selectedItems });
} else { } else {
this.clearQSParams(); this.clearQSParams();
if (selectCategory) {
selectCategory(null, 'Machine');
}
} }
this.setState(prevState => ({ this.setState(prevState => ({
isModalOpen: !prevState.isModalOpen, isModalOpen: !prevState.isModalOpen,
})); }));
} }
removeItemAndSave(row) { removeItemAndSave(item) {
const { value, onChange, multiple } = this.props; const { value, onChange, multiple } = this.props;
if (multiple) { if (multiple) {
onChange(value.filter(item => item.id !== row.id)); onChange(value.filter(i => i.id !== item.id));
} else if (value.id === row.id) { } else if (value.id === item.id) {
onChange(null); onChange(null);
} }
} }
@@ -251,58 +154,31 @@ class Lookup extends React.Component {
} }
clearQSParams() { clearQSParams() {
const { history } = this.props; const { qsConfig, history } = this.props;
const parts = history.location.search.replace(/^\?/, '').split('&'); const parts = history.location.search.replace(/^\?/, '').split('&');
const ns = this.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() { render() {
const { isModalOpen, selectedItems, error, results, count } = this.state; const { isModalOpen, selectedItems } = this.state;
const { const {
form,
id, id,
lookupHeader, lookupHeader,
value, value,
items,
count,
columns, columns,
multiple, multiple,
name, name,
onBlur, onBlur,
selectCategory,
required, required,
qsConfig,
i18n, i18n,
selectCategoryOptions,
selectedCategory,
} = this.props; } = this.props;
const header = lookupHeader || i18n._(t`Items`); const header = lookupHeader || i18n._(t`Items`);
const canDelete = !required || (multiple && value.length > 1); const canDelete = !required || (multiple && value.length > 1);
const chips = () => {
return selectCategoryOptions && selectCategoryOptions.length > 0 ? (
<ChipGroup defaultIsOpen numChips={5}>
{(multiple ? value : [value]).map(chip => (
<CredentialChip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
credential={chip}
/>
))}
</ChipGroup>
) : (
<ChipGroup defaultIsOpen numChips={5}>
{(multiple ? value : [value]).map(chip => (
<Chip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
>
{chip.name}
</Chip>
))}
</ChipGroup>
);
};
return ( return (
<Fragment> <Fragment>
<InputGroup onBlur={onBlur}> <InputGroup onBlur={onBlur}>
@@ -315,7 +191,17 @@ class Lookup extends React.Component {
<SearchIcon /> <SearchIcon />
</SearchButton> </SearchButton>
<ChipHolder className="pf-c-form-control"> <ChipHolder className="pf-c-form-control">
{value ? chips(value) : null} <ChipGroup defaultIsOpen numChips={5}>
{(multiple ? value : [value]).map(chip => (
<Chip
key={chip.id}
onClick={() => this.removeItemAndSave(chip)}
isReadOnly={!canDelete}
>
{chip.name}
</Chip>
))}
</ChipGroup>
</ChipHolder> </ChipHolder>
</InputGroup> </InputGroup>
<Modal <Modal
@@ -341,21 +227,6 @@ class Lookup extends React.Component {
</Button>, </Button>,
]} ]}
> >
{selectCategoryOptions && selectCategoryOptions.length > 0 && (
<ToolbarItem css=" display: flex; align-items: center;">
<span css="flex: 0 0 25%;">Selected Category</span>
<VerticalSeperator />
<AnsibleSelect
css="flex: 1 1 75%;"
id="multiCredentialsLookUp-select"
label="Selected Category"
data={selectCategoryOptions}
value={selectedCategory.label}
onChange={selectCategory}
form={form}
/>
</ToolbarItem>
)}
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<SelectedList <SelectedList
label={i18n._(t`Selected`)} label={i18n._(t`Selected`)}
@@ -363,16 +234,13 @@ class Lookup extends React.Component {
showOverflowAfter={5} showOverflowAfter={5}
onRemove={this.removeItem} onRemove={this.removeItem}
isReadOnly={!canDelete} isReadOnly={!canDelete}
isCredentialList={
selectCategoryOptions && selectCategoryOptions.length > 0
}
/> />
)} )}
<PaginatedDataList <PaginatedDataList
items={results} items={items}
itemCount={count} itemCount={count}
pluralizedItemName={lookupHeader} pluralizedItemName={lookupHeader}
qsConfig={this.qsConfig} qsConfig={qsConfig}
toolbarColumns={columns} toolbarColumns={columns}
renderItem={item => ( renderItem={item => (
<CheckboxListItem <CheckboxListItem
@@ -382,18 +250,12 @@ class Lookup extends React.Component {
label={item.name} label={item.name}
isSelected={selectedItems.some(i => i.id === item.id)} isSelected={selectedItems.some(i => i.id === item.id)}
onSelect={() => this.addItem(item)} onSelect={() => this.addItem(item)}
isRadio={ isRadio={!multiple}
!multiple ||
(selectCategoryOptions &&
selectCategoryOptions.length &&
selectedCategory.value !== 'Vault')
}
/> />
)} )}
renderToolbar={props => <DataListToolbar {...props} fillWidth />} renderToolbar={props => <DataListToolbar {...props} fillWidth />}
showPageSizeOptions={false} showPageSizeOptions={false}
/> />
{error ? <div>error</div> : ''}
</Modal> </Modal>
</Fragment> </Fragment>
); );
@@ -406,15 +268,16 @@ const Item = shape({
Lookup.propTypes = { Lookup.propTypes = {
id: string, id: string,
getItems: func.isRequired, items: arrayOf(shape({})).isRequired,
count: number.isRequired,
// TODO: change to `header`
lookupHeader: string, lookupHeader: string,
name: string, name: string,
onChange: func.isRequired, onChange: func.isRequired,
value: oneOfType([Item, arrayOf(Item)]), value: oneOfType([Item, arrayOf(Item)]),
sortedColumnKey: string.isRequired,
multiple: bool, multiple: bool,
required: bool, required: bool,
qsNamespace: string, qsConfig: QSConfig.isRequired,
}; };
Lookup.defaultProps = { Lookup.defaultProps = {
@@ -424,7 +287,6 @@ Lookup.defaultProps = {
value: null, value: null,
multiple: false, multiple: false,
required: false, required: false,
qsNamespace: 'lookup',
}; };
export { Lookup as _Lookup }; export { Lookup as _Lookup };

View File

@@ -1,22 +1,29 @@
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { FormGroup, Tooltip } from '@patternfly/react-core'; import { FormGroup, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import { CredentialsAPI, CredentialTypesAPI } from '@api'; import { CredentialsAPI, CredentialTypesAPI } from '@api';
import Lookup from '@components/Lookup'; import CategoryLookup from '@components/Lookup/CategoryLookup';
import { getQSConfig, parseQueryString } from '@util/qs';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)` const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px; margin-left: 10px;
`; `;
const QS_CONFIG = getQSConfig('credentials', {
page: 1,
page_size: 5,
order_by: 'name',
});
function toggleCredentialSelection(credentialsToUpdate, newCredential) { function toggleCredentialSelection(credentialsToUpdate, newCredential) {
let newCredentialsList; let newCredentialsList;
const isSelectedCredentialInState = const isSelectedCredentialInState =
credentialsToUpdate.filter(cred => cred.id === newCredential.id).length > credentialsToUpdate.filter(cred => cred.id === newCredential.id).length > 0;
0;
if (isSelectedCredentialInState) { if (isSelectedCredentialInState) {
newCredentialsList = credentialsToUpdate.filter( newCredentialsList = credentialsToUpdate.filter(
@@ -31,6 +38,7 @@ function toggleCredentialSelection(credentialsToUpdate, newCredential) {
} }
return newCredentialsList; return newCredentialsList;
} }
class MultiCredentialsLookup extends React.Component { class MultiCredentialsLookup extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -48,6 +56,7 @@ class MultiCredentialsLookup extends React.Component {
componentDidMount() { componentDidMount() {
this.loadCredentialTypes(); this.loadCredentialTypes();
this.loadCredentials();
} }
async loadCredentialTypes() { async loadCredentialTypes() {
@@ -80,23 +89,38 @@ class MultiCredentialsLookup extends React.Component {
} }
} }
async loadCredentials(params) { async loadCredentials() {
const { history, onError } = this.props;
const { selectedCredentialType } = this.state; const { selectedCredentialType } = this.state;
const params = parseQueryString(QS_CONFIG, history.location.search);
params.credential_type = selectedCredentialType.id || 1; params.credential_type = selectedCredentialType.id || 1;
return CredentialsAPI.read(params); try {
const { data } = await CredentialsAPI.read(params);
this.setState({
credentials: data.results,
count: data.count,
});
} catch (err) {
onError(err);
}
} }
handleCredentialTypeSelect(value, type) { handleCredentialTypeSelect(value, type) {
const { credentialTypes } = this.state; const { credentialTypes } = this.state;
const selectedType = credentialTypes.filter(item => item.label === type); const selectedType = credentialTypes.filter(item => item.label === type);
this.setState({ selectedCredentialType: selectedType[0] }); this.setState({ selectedCredentialType: selectedType[0] }, () => {
this.loadCredentials();
});
} }
render() { render() {
const { selectedCredentialType, credentialTypes } = this.state; const {
const { tooltip, i18n, credentials, onChange } = this.props; selectedCredentialType,
credentialTypes,
credentials,
count,
} = this.state;
const { tooltip, i18n, value, onChange } = this.props;
return ( return (
<FormGroup label={i18n._(t`Credentials`)} fieldId="multiCredential"> <FormGroup label={i18n._(t`Credentials`)} fieldId="multiCredential">
{tooltip && ( {tooltip && (
@@ -105,7 +129,7 @@ class MultiCredentialsLookup extends React.Component {
</Tooltip> </Tooltip>
)} )}
{credentialTypes && ( {credentialTypes && (
<Lookup <CategoryLookup
selectCategoryOptions={credentialTypes} selectCategoryOptions={credentialTypes}
selectCategory={this.handleCredentialTypeSelect} selectCategory={this.handleCredentialTypeSelect}
selectedCategory={selectedCredentialType} selectedCategory={selectedCredentialType}
@@ -114,11 +138,12 @@ class MultiCredentialsLookup extends React.Component {
id="multiCredential" id="multiCredential"
lookupHeader={i18n._(t`Credentials`)} lookupHeader={i18n._(t`Credentials`)}
name="credentials" name="credentials"
value={credentials} value={value}
multiple multiple
onChange={onChange} onChange={onChange}
getItems={this.loadCredentials} items={credentials}
qsNamespace="credentials" count={count}
qsConfig={QS_CONFIG}
columns={[ columns={[
{ {
name: i18n._(t`Name`), name: i18n._(t`Name`),
@@ -127,7 +152,6 @@ class MultiCredentialsLookup extends React.Component {
isSearchable: true, isSearchable: true,
}, },
]} ]}
sortedColumnKey="name"
/> />
)} )}
</FormGroup> </FormGroup>
@@ -137,7 +161,7 @@ class MultiCredentialsLookup extends React.Component {
MultiCredentialsLookup.propTypes = { MultiCredentialsLookup.propTypes = {
tooltip: PropTypes.string, tooltip: PropTypes.string,
credentials: PropTypes.arrayOf( value: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
id: PropTypes.number, id: PropTypes.number,
name: PropTypes.string, name: PropTypes.string,
@@ -152,8 +176,8 @@ MultiCredentialsLookup.propTypes = {
MultiCredentialsLookup.defaultProps = { MultiCredentialsLookup.defaultProps = {
tooltip: '', tooltip: '',
credentials: [], value: [],
}; };
export { MultiCredentialsLookup as _MultiCredentialsLookup }; export { MultiCredentialsLookup as _MultiCredentialsLookup };
export default withI18n()(MultiCredentialsLookup); export default withI18n()(withRouter(MultiCredentialsLookup));

View File

@@ -1,4 +1,5 @@
export { default } from './Lookup'; export { default } from './Lookup';
export { default as CategoryLookup } from './CategoryLookup';
export { default as InstanceGroupsLookup } from './InstanceGroupsLookup'; export { default as InstanceGroupsLookup } from './InstanceGroupsLookup';
export { default as InventoryLookup } from './InventoryLookup'; export { default as InventoryLookup } from './InventoryLookup';
export { default as ProjectLookup } from './ProjectLookup'; export { default as ProjectLookup } from './ProjectLookup';

View File

@@ -28,6 +28,7 @@ class SelectedList extends Component {
isReadOnly, isReadOnly,
isCredentialList, isCredentialList,
} = this.props; } = this.props;
// TODO: replace isCredentialList with renderChip ?
const chips = isCredentialList const chips = isCredentialList
? selected.map(item => ( ? selected.map(item => (
<CredentialChip <CredentialChip
@@ -66,6 +67,7 @@ SelectedList.propTypes = {
onRemove: PropTypes.func, onRemove: PropTypes.func,
selected: PropTypes.arrayOf(PropTypes.object).isRequired, selected: PropTypes.arrayOf(PropTypes.object).isRequired,
isReadOnly: PropTypes.bool, isReadOnly: PropTypes.bool,
isCredentialList: PropTypes.bool,
}; };
SelectedList.defaultProps = { SelectedList.defaultProps = {
@@ -73,6 +75,7 @@ SelectedList.defaultProps = {
label: 'Selected', label: 'Selected',
onRemove: () => null, onRemove: () => null,
isReadOnly: false, isReadOnly: false,
isCredentialList: false,
}; };
export default SelectedList; export default SelectedList;

View File

@@ -317,7 +317,7 @@ class JobTemplateForm extends Component {
fieldId="template-credentials" fieldId="template-credentials"
render={({ field }) => ( render={({ field }) => (
<MultiCredentialsLookup <MultiCredentialsLookup
credentials={field.value} value={field.value}
onChange={newCredentials => onChange={newCredentials =>
setFieldValue('credentials', newCredentials) setFieldValue('credentials', newCredentials)
} }