mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 22:16:00 -03:30
start Lookup reducer
This commit is contained in:
@@ -16,6 +16,7 @@ const CheckboxListItem = ({
|
|||||||
label,
|
label,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onDeselect,
|
||||||
isRadio,
|
isRadio,
|
||||||
}) => {
|
}) => {
|
||||||
const CheckboxRadio = isRadio ? DataListRadio : DataListCheck;
|
const CheckboxRadio = isRadio ? DataListRadio : DataListCheck;
|
||||||
@@ -25,7 +26,7 @@ const CheckboxListItem = ({
|
|||||||
<CheckboxRadio
|
<CheckboxRadio
|
||||||
id={`selected-${itemId}`}
|
id={`selected-${itemId}`}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={onSelect}
|
onChange={isSelected ? onDeselect : onSelect}
|
||||||
aria-labelledby={`check-action-item-${itemId}`}
|
aria-labelledby={`check-action-item-${itemId}`}
|
||||||
name={name}
|
name={name}
|
||||||
value={itemId}
|
value={itemId}
|
||||||
@@ -60,6 +61,7 @@ CheckboxListItem.propTypes = {
|
|||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
onDeselect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CheckboxListItem;
|
export default CheckboxListItem;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ 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, CredentialChip } from '../Chip';
|
||||||
import { QSConfig } from '@types';
|
import { QSConfig } from '@types';
|
||||||
|
|
||||||
const SearchButton = styled(Button)`
|
const SearchButton = styled(Button)`
|
||||||
@@ -57,7 +57,7 @@ class CategoryLookup extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.assertCorrectValueType();
|
// this.assertCorrectValueType();
|
||||||
let selectedItems = [];
|
let selectedItems = [];
|
||||||
if (props.value) {
|
if (props.value) {
|
||||||
selectedItems = props.multiple ? [...props.value] : [props.value];
|
selectedItems = props.multiple ? [...props.value] : [props.value];
|
||||||
@@ -74,22 +74,22 @@ class CategoryLookup extends React.Component {
|
|||||||
this.clearQSParams = this.clearQSParams.bind(this);
|
this.clearQSParams = this.clearQSParams.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertCorrectValueType() {
|
// assertCorrectValueType() {
|
||||||
const { multiple, value, selectCategoryOptions } = this.props;
|
// const { multiple, value, selectCategoryOptions } = this.props;
|
||||||
if (selectCategoryOptions) {
|
// if (selectCategoryOptions) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (!multiple && Array.isArray(value)) {
|
// if (!multiple && Array.isArray(value)) {
|
||||||
throw new Error(
|
// throw new Error(
|
||||||
'CategoryLookup value must not be an array unless `multiple` is set'
|
// 'CategoryLookup value must not be an array unless `multiple` is set'
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
if (multiple && !Array.isArray(value)) {
|
// if (multiple && !Array.isArray(value)) {
|
||||||
throw new Error(
|
// throw new Error(
|
||||||
'CategoryLookup value must be an array if `multiple` is set'
|
// 'CategoryLookup value must be an array if `multiple` is set'
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
removeItem(row) {
|
removeItem(row) {
|
||||||
const { selectedItems } = this.state;
|
const { selectedItems } = this.state;
|
||||||
@@ -193,32 +193,6 @@ class CategoryLookup extends React.Component {
|
|||||||
} = 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>
|
|
||||||
{(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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup onBlur={onBlur}>
|
<InputGroup onBlur={onBlur}>
|
||||||
@@ -231,7 +205,16 @@ class CategoryLookup extends React.Component {
|
|||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</SearchButton>
|
</SearchButton>
|
||||||
<ChipHolder className="pf-c-form-control">
|
<ChipHolder className="pf-c-form-control">
|
||||||
{value ? chips(value) : null}
|
<ChipGroup>
|
||||||
|
{(multiple ? value : [value]).map(chip => (
|
||||||
|
<CredentialChip
|
||||||
|
key={chip.id}
|
||||||
|
onClick={() => this.removeItemAndSave(chip)}
|
||||||
|
isReadOnly={!canDelete}
|
||||||
|
credential={chip}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
</ChipHolder>
|
</ChipHolder>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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 { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import Lookup from './NewLookup';
|
||||||
|
|
||||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
|||||||
@@ -20,10 +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 PaginatedDataList from '../PaginatedDataList';
|
import SelectList from './shared/SelectList';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
|
||||||
import CheckboxListItem from '../CheckboxListItem';
|
|
||||||
import SelectedList from '../SelectedList';
|
|
||||||
import { ChipGroup, Chip } from '../Chip';
|
import { ChipGroup, Chip } from '../Chip';
|
||||||
import { QSConfig } from '@types';
|
import { QSConfig } from '@types';
|
||||||
|
|
||||||
@@ -227,34 +224,17 @@ class Lookup extends React.Component {
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{selectedItems.length > 0 && (
|
<SelectList
|
||||||
<SelectedList
|
value={selectedItems}
|
||||||
label={i18n._(t`Selected`)}
|
onChange={newVal => this.setState({ selectedItems: newVal })}
|
||||||
selected={selectedItems}
|
options={items}
|
||||||
showOverflowAfter={5}
|
optionCount={count}
|
||||||
onRemove={this.removeItem}
|
columns={columns}
|
||||||
isReadOnly={!canDelete}
|
multiple={multiple}
|
||||||
/>
|
header={lookupHeader}
|
||||||
)}
|
name={name}
|
||||||
<PaginatedDataList
|
|
||||||
items={items}
|
|
||||||
itemCount={count}
|
|
||||||
pluralizedItemName={lookupHeader}
|
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
toolbarColumns={columns}
|
readOnly={!canDelete}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
|
||||||
showPageSizeOptions={false}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
192
awx/ui_next/src/components/Lookup/NewLookup.jsx
Normal file
192
awx/ui_next/src/components/Lookup/NewLookup.jsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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 SelectList from './shared/SelectList';
|
||||||
|
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,
|
||||||
|
items,
|
||||||
|
count,
|
||||||
|
header,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
columns,
|
||||||
|
value,
|
||||||
|
multiple,
|
||||||
|
required,
|
||||||
|
qsConfig,
|
||||||
|
i18n,
|
||||||
|
} = props;
|
||||||
|
const [state, dispatch] = useReducer(reducer, props, initReducer);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'SET_MULTIPLE', value: multiple });
|
||||||
|
}, [multiple]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'SET_VALUE', value });
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
const { selectedItems } = state;
|
||||||
|
const val = multiple ? selectedItems : selectedItems[0] || null;
|
||||||
|
onChange(val);
|
||||||
|
dispatch({ type: 'CLOSE_MODAL' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = item => {
|
||||||
|
if (multiple) {
|
||||||
|
onChange(value.filter(i => i.id !== item.id));
|
||||||
|
} else {
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 => (
|
||||||
|
<Chip
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => removeItem(item)}
|
||||||
|
isReadOnly={!canDelete}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
</ChipHolder>
|
||||||
|
</InputGroup>
|
||||||
|
<Modal
|
||||||
|
className="awx-c-modal"
|
||||||
|
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||||
|
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={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SelectList
|
||||||
|
value={selectedItems}
|
||||||
|
options={items}
|
||||||
|
optionCount={count}
|
||||||
|
columns={columns}
|
||||||
|
multiple={multiple}
|
||||||
|
header={header}
|
||||||
|
name={name}
|
||||||
|
qsConfig={qsConfig}
|
||||||
|
readOnly={!canDelete}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item = shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lookup.propTypes = {
|
||||||
|
id: string,
|
||||||
|
items: arrayOf(shape({})).isRequired,
|
||||||
|
count: number.isRequired,
|
||||||
|
// TODO: change to `header`
|
||||||
|
header: string,
|
||||||
|
name: string,
|
||||||
|
onChange: func.isRequired,
|
||||||
|
value: oneOfType([Item, arrayOf(Item)]),
|
||||||
|
multiple: bool,
|
||||||
|
required: bool,
|
||||||
|
onBlur: func,
|
||||||
|
qsConfig: QSConfig.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
Lookup.defaultProps = {
|
||||||
|
id: 'lookup-search',
|
||||||
|
header: null,
|
||||||
|
name: null,
|
||||||
|
value: null,
|
||||||
|
multiple: false,
|
||||||
|
required: false,
|
||||||
|
onBlur: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Lookup as _Lookup };
|
||||||
|
export default withI18n()(withRouter(Lookup));
|
||||||
5
awx/ui_next/src/components/Lookup/README.md
Normal file
5
awx/ui_next/src/components/Lookup/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Lookup
|
||||||
|
|
||||||
|
required single select lookups should not include a close X on the tag... you would have to select something else to change it
|
||||||
|
|
||||||
|
optional single select lookups should include a close X to remove it on the spot
|
||||||
84
awx/ui_next/src/components/Lookup/shared/SelectList.jsx
Normal file
84
awx/ui_next/src/components/Lookup/shared/SelectList.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
arrayOf,
|
||||||
|
shape,
|
||||||
|
bool,
|
||||||
|
func,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
oneOfType,
|
||||||
|
} from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import SelectedList from '../../SelectedList';
|
||||||
|
import PaginatedDataList from '../../PaginatedDataList';
|
||||||
|
import CheckboxListItem from '../../CheckboxListItem';
|
||||||
|
import DataListToolbar from '../../DataListToolbar';
|
||||||
|
import { QSConfig } from '@types';
|
||||||
|
|
||||||
|
function SelectList({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
optionCount,
|
||||||
|
columns,
|
||||||
|
multiple,
|
||||||
|
header,
|
||||||
|
name,
|
||||||
|
qsConfig,
|
||||||
|
readOnly,
|
||||||
|
dispatch,
|
||||||
|
i18n,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<SelectedList
|
||||||
|
label={i18n._(t`Selected`)}
|
||||||
|
selected={value}
|
||||||
|
showOverflowAfter={5}
|
||||||
|
onRemove={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||||
|
isReadOnly={readOnly}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PaginatedDataList
|
||||||
|
items={options}
|
||||||
|
itemCount={optionCount}
|
||||||
|
pluralizedItemName={header}
|
||||||
|
qsConfig={qsConfig}
|
||||||
|
toolbarColumns={columns}
|
||||||
|
renderItem={item => (
|
||||||
|
<CheckboxListItem
|
||||||
|
key={item.id}
|
||||||
|
itemId={item.id}
|
||||||
|
name={multiple ? item.name : name}
|
||||||
|
label={item.name}
|
||||||
|
isSelected={value.some(i => i.id === item.id)}
|
||||||
|
onSelect={() => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
|
onDeselect={() => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||||
|
isRadio={!multiple}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||||
|
showPageSizeOptions={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item = shape({
|
||||||
|
id: oneOfType([number, string]).isRequired,
|
||||||
|
});
|
||||||
|
SelectList.propTypes = {
|
||||||
|
value: arrayOf(Item).isRequired,
|
||||||
|
options: arrayOf(Item).isRequired,
|
||||||
|
optionCount: number.isRequired,
|
||||||
|
columns: arrayOf(shape({})).isRequired,
|
||||||
|
multiple: bool,
|
||||||
|
qsConfig: QSConfig.isRequired,
|
||||||
|
dispatch: func.isRequired,
|
||||||
|
};
|
||||||
|
SelectList.defaultProps = {
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(SelectList);
|
||||||
110
awx/ui_next/src/components/Lookup/shared/reducer.js
Normal file
110
awx/ui_next/src/components/Lookup/shared/reducer.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
export default function reducer(state, action) {
|
||||||
|
// console.log(action, state);
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SELECT_ITEM':
|
||||||
|
return selectItem(state, action.item);
|
||||||
|
case 'DESELECT_ITEM':
|
||||||
|
return deselectItem(state, action.item);
|
||||||
|
case 'TOGGLE_MODAL':
|
||||||
|
return toggleModal(state);
|
||||||
|
case 'CLOSE_MODAL':
|
||||||
|
return closeModal(state);
|
||||||
|
case 'SET_MULTIPLE':
|
||||||
|
return { ...state, multiple: action.value };
|
||||||
|
case 'SET_VALUE':
|
||||||
|
return { ...state, value: action.value };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unrecognized action type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(state, item) {
|
||||||
|
const { selectedItems, multiple } = state;
|
||||||
|
if (!multiple) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedItems: [item],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const index = selectedItems.findIndex(i => i.id === item.id);
|
||||||
|
if (index > -1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedItems: [...selectedItems, item],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectItem(state, item) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedItems: state.selectedItems.filter(i => i.id !== item.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleModal(state) {
|
||||||
|
const { isModalOpen, value, multiple } = state;
|
||||||
|
if (isModalOpen) {
|
||||||
|
return closeModal(state);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isModalOpen: !isModalOpen,
|
||||||
|
selectedItems: multiple ? [...value] : [value],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(state) {
|
||||||
|
// TODO clear QSParams & push history state?
|
||||||
|
// state.clearQSParams();
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isModalOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 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('&')}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function initReducer({
|
||||||
|
id,
|
||||||
|
items,
|
||||||
|
count,
|
||||||
|
header,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
multiple = false,
|
||||||
|
required = false,
|
||||||
|
qsConfig,
|
||||||
|
}) {
|
||||||
|
assertCorrectValueType(value, multiple);
|
||||||
|
let selectedItems = [];
|
||||||
|
if (value) {
|
||||||
|
selectedItems = multiple ? [...value] : [value];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
selectedItems,
|
||||||
|
value,
|
||||||
|
multiple,
|
||||||
|
isModalOpen: false,
|
||||||
|
required,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertCorrectValueType(value, multiple) {
|
||||||
|
if (!multiple && Array.isArray(value)) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
262
awx/ui_next/src/components/Lookup/shared/reducer.test.js
Normal file
262
awx/ui_next/src/components/Lookup/shared/reducer.test.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import reducer, { initReducer } from './reducer';
|
||||||
|
|
||||||
|
describe('Lookup reducer', () => {
|
||||||
|
describe('SELECT_ITEM', () => {
|
||||||
|
it('should add item to selected items (multiple select)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SELECT_ITEM',
|
||||||
|
item: { id: 2 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 1 }, { id: 2 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate item if already selected (multiple select)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SELECT_ITEM',
|
||||||
|
item: { id: 1 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace selected item (single select)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SELECT_ITEM',
|
||||||
|
item: { id: 2 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 2 }],
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate item if already selected (single select)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SELECT_ITEM',
|
||||||
|
item: { id: 1 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DESELECT_ITEM', () => {
|
||||||
|
it('should de-select item (multiple)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }, { id: 2 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'DESELECT_ITEM',
|
||||||
|
item: { id: 1 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 2 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change list if item not selected (multiple)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }, { id: 2 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'DESELECT_ITEM',
|
||||||
|
item: { id: 3 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [{ id: 1 }, { id: 2 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should de-select item (single select)', () => {
|
||||||
|
const state = {
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'DESELECT_ITEM',
|
||||||
|
item: { id: 1 },
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
selectedItems: [],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TOGGLE_MODAL', () => {
|
||||||
|
it('should open the modal (single)', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [],
|
||||||
|
value: { id: 1 },
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'TOGGLE_MODAL',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: true,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: { id: 1 },
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the modal (multiple)', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'TOGGLE_MODAL',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: true,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: true,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'TOGGLE_MODAL',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CLOSE_MODAL', () => {
|
||||||
|
it('should close the modal', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: true,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'CLOSE_MODAL',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SET_MULTIPLE', () => {
|
||||||
|
it('should set multiple to true', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SET_MULTIPLE',
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set multiple to false', () => {
|
||||||
|
const state = {
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SET_MULTIPLE',
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
isModalOpen: false,
|
||||||
|
selectedItems: [{ id: 1 }],
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SET_VALUE', () => {
|
||||||
|
it('should set the value', () => {
|
||||||
|
const state = {
|
||||||
|
value: [{ id: 1 }],
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
const result = reducer(state, {
|
||||||
|
type: 'SET_VALUE',
|
||||||
|
value: [{ id: 3 }],
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
value: [{ id: 3 }],
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initReducer', () => {
|
||||||
|
it('should init', () => {
|
||||||
|
const state = initReducer({
|
||||||
|
value: [],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
expect(state).toEqual({
|
||||||
|
selectedItems: [],
|
||||||
|
value: [],
|
||||||
|
multiple: true,
|
||||||
|
isModalOpen: false,
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user