mirror of
https://github.com/ansible/awx.git
synced 2026-02-14 01:34:45 -03:30
Add namespacing for query params (#205)
* use qs utils to namespace query params * refactor Lookup and SelectResource Steps to use PaginatedDataList * preserve query params when adding new ones * require namespace for QS Configs
This commit is contained in:
@@ -186,24 +186,22 @@ class AddResourceRole extends React.Component {
|
||||
<SelectResourceStep
|
||||
columns={userColumns}
|
||||
displayKey="username"
|
||||
emptyListBody={i18n._(t`Please add users to populate this list`)}
|
||||
emptyListTitle={i18n._(t`No Users Found`)}
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={this.readUsers}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
sortedColumnKey="username"
|
||||
itemName="user"
|
||||
/>
|
||||
)}
|
||||
{selectedResource === 'teams' && (
|
||||
<SelectResourceStep
|
||||
columns={teamColumns}
|
||||
emptyListBody={i18n._(t`Please add teams to populate this list`)}
|
||||
emptyListTitle={i18n._(t`No Teams Found`)}
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={this.readTeams}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
itemName="team"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { i18nMark } from '@lingui/react';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
Title,
|
||||
DataList,
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import CheckboxListItem from '../ListItem';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import Pagination from '../Pagination';
|
||||
import SelectedList from '../SelectedList';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||
|
||||
const paginationStyling = {
|
||||
paddingLeft: '0',
|
||||
@@ -27,163 +19,113 @@ class SelectResourceStep extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { sortedColumnKey } = this.props;
|
||||
|
||||
this.state = {
|
||||
isInitialized: false,
|
||||
count: null,
|
||||
error: false,
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
resources: [],
|
||||
sortOrder: 'ascending',
|
||||
sortedColumnKey
|
||||
};
|
||||
|
||||
this.handleSetPage = this.handleSetPage.bind(this);
|
||||
this.handleSort = this.handleSort.bind(this);
|
||||
this.readResourceList = this.readResourceList.bind(this);
|
||||
this.qsConfig = getQSConfig('resource', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: props.sortedColumnKey,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { page_size, page, sortedColumnKey } = this.state;
|
||||
|
||||
this.readResourceList({ page_size, page, order_by: sortedColumnKey });
|
||||
this.readResourceList();
|
||||
}
|
||||
|
||||
handleSetPage (pageNumber) {
|
||||
const { page_size, sortedColumnKey, sortOrder } = this.state;
|
||||
const page = parseInt(pageNumber, 10);
|
||||
|
||||
let order_by = sortedColumnKey;
|
||||
|
||||
if (sortOrder === 'descending') {
|
||||
order_by = `-${order_by}`;
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.readResourceList();
|
||||
}
|
||||
|
||||
this.readResourceList({ page_size, page, order_by });
|
||||
}
|
||||
|
||||
handleSort (sortedColumnKey, sortOrder) {
|
||||
const { page_size } = this.state;
|
||||
|
||||
let order_by = sortedColumnKey;
|
||||
|
||||
if (sortOrder === 'descending') {
|
||||
order_by = `-${order_by}`;
|
||||
}
|
||||
|
||||
this.readResourceList({ page: 1, page_size, order_by });
|
||||
}
|
||||
|
||||
async readResourceList (queryParams) {
|
||||
const { onSearch } = this.props;
|
||||
const { page, order_by } = queryParams;
|
||||
|
||||
let sortOrder = 'ascending';
|
||||
let sortedColumnKey = order_by;
|
||||
|
||||
if (order_by.startsWith('-')) {
|
||||
sortOrder = 'descending';
|
||||
sortedColumnKey = order_by.substring(1);
|
||||
}
|
||||
|
||||
this.setState({ error: false });
|
||||
async readResourceList () {
|
||||
const { onSearch, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
error: false,
|
||||
});
|
||||
try {
|
||||
const { data } = await onSearch(queryParams);
|
||||
const { count, results } = data;
|
||||
|
||||
const stateToUpdate = {
|
||||
count,
|
||||
page,
|
||||
this.setState({
|
||||
resources: results,
|
||||
sortOrder,
|
||||
sortedColumnKey
|
||||
};
|
||||
|
||||
this.setState(stateToUpdate);
|
||||
count,
|
||||
isInitialized: true,
|
||||
isLoading: false,
|
||||
error: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ error: true });
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isInitialized,
|
||||
isLoading,
|
||||
count,
|
||||
error,
|
||||
page,
|
||||
page_size,
|
||||
resources,
|
||||
sortOrder,
|
||||
sortedColumnKey
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
columns,
|
||||
displayKey,
|
||||
emptyListBody,
|
||||
emptyListTitle,
|
||||
onRowClick,
|
||||
selectedLabel,
|
||||
selectedResourceRows
|
||||
selectedResourceRows,
|
||||
itemName,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
{(resources.length === 0) ? (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
{emptyListTitle}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{emptyListBody}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<Fragment>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={displayKey}
|
||||
label={selectedLabel}
|
||||
onRemove={onRowClick}
|
||||
selected={selectedResourceRows}
|
||||
showOverflowAfter={5}
|
||||
{isLoading && (<div>Loading...</div>)}
|
||||
{isInitialized && (
|
||||
<Fragment>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={displayKey}
|
||||
label={selectedLabel}
|
||||
onRemove={onRowClick}
|
||||
selected={selectedResourceRows}
|
||||
showOverflowAfter={5}
|
||||
/>
|
||||
)}
|
||||
<PaginatedDataList
|
||||
items={resources}
|
||||
itemCount={count}
|
||||
itemName={itemName}
|
||||
qsConfig={this.qsConfig}
|
||||
toolbarColumns={
|
||||
columns
|
||||
}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={selectedResourceRows.some(i => i.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item[displayKey]}
|
||||
onSelect={() => onRowClick(item)}
|
||||
/>
|
||||
)}
|
||||
<DataListToolbar
|
||||
columns={columns}
|
||||
noLeftMargin
|
||||
onSearch={this.onSearch}
|
||||
handleSort={this.handleSort}
|
||||
sortOrder={sortOrder}
|
||||
sortedColumnKey={sortedColumnKey}
|
||||
/>
|
||||
<DataList aria-label={i18nMark('Roles List')}>
|
||||
{resources.map(i => (
|
||||
<CheckboxListItem
|
||||
isSelected={selectedResourceRows.some(item => item.id === i.id)}
|
||||
itemId={i.id}
|
||||
key={i.id}
|
||||
name={i[displayKey]}
|
||||
onSelect={() => onRowClick(i)}
|
||||
/>
|
||||
))}
|
||||
</DataList>
|
||||
<Pagination
|
||||
count={count}
|
||||
onSetPage={this.handleSetPage}
|
||||
page={page}
|
||||
pageCount={Math.ceil(count / page_size)}
|
||||
pageSizeOptions={null}
|
||||
page_size={page_size}
|
||||
showPageSizeOptions={false}
|
||||
style={paginationStyling}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
alignToolbarLeft
|
||||
showPageSizeOptions={false}
|
||||
paginationStyling={paginationStyling}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
{ error ? <div>error</div> : '' }
|
||||
</Fragment>
|
||||
);
|
||||
@@ -193,23 +135,22 @@ class SelectResourceStep extends React.Component {
|
||||
SelectResourceStep.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
displayKey: PropTypes.string,
|
||||
emptyListBody: PropTypes.string,
|
||||
emptyListTitle: PropTypes.string,
|
||||
onRowClick: PropTypes.func,
|
||||
onSearch: PropTypes.func.isRequired,
|
||||
selectedLabel: PropTypes.string,
|
||||
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
||||
sortedColumnKey: PropTypes.string
|
||||
sortedColumnKey: PropTypes.string,
|
||||
itemName: PropTypes.string,
|
||||
};
|
||||
|
||||
SelectResourceStep.defaultProps = {
|
||||
displayKey: 'name',
|
||||
emptyListBody: i18nMark('Please add items to populate this list'),
|
||||
emptyListTitle: i18nMark('No Items Found'),
|
||||
onRowClick: () => {},
|
||||
selectedLabel: i18nMark('Selected Items'),
|
||||
selectedResourceRows: [],
|
||||
sortedColumnKey: 'name'
|
||||
sortedColumnKey: 'name',
|
||||
itemName: 'item',
|
||||
};
|
||||
|
||||
export default SelectResourceStep;
|
||||
export { SelectResourceStep as _SelectResourceStep };
|
||||
export default withRouter(SelectResourceStep);
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { SearchIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Chip,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
InputGroup,
|
||||
Modal,
|
||||
Title
|
||||
} from '@patternfly/react-core';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import CheckboxListItem from '../ListItem';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import SelectedList from '../SelectedList';
|
||||
import Pagination from '../Pagination';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||
|
||||
const paginationStyling = {
|
||||
paddingLeft: '0',
|
||||
@@ -39,71 +35,48 @@ class Lookup extends React.Component {
|
||||
lookupSelectedItems: [...props.value] || [],
|
||||
results: [],
|
||||
count: 0,
|
||||
error: null,
|
||||
};
|
||||
this.qsConfig = getQSConfig('lookup', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
error: null,
|
||||
sortOrder: 'ascending',
|
||||
sortedColumnKey: props.sortedColumnKey
|
||||
};
|
||||
this.onSetPage = this.onSetPage.bind(this);
|
||||
order_by: props.sortedColumnKey,
|
||||
});
|
||||
this.handleModalToggle = this.handleModalToggle.bind(this);
|
||||
this.toggleSelected = this.toggleSelected.bind(this);
|
||||
this.saveModal = this.saveModal.bind(this);
|
||||
this.getData = this.getData.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
this.onSort = this.onSort.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { page_size, page } = this.state;
|
||||
this.getData({ page_size, page });
|
||||
this.getData();
|
||||
}
|
||||
|
||||
onSearch () {
|
||||
const { sortedColumnKey, sortOrder } = this.state;
|
||||
this.onSort(sortedColumnKey, sortOrder);
|
||||
}
|
||||
|
||||
onSort (sortedColumnKey, sortOrder) {
|
||||
this.setState({ page: 1, sortedColumnKey, sortOrder }, this.getData);
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.getData();
|
||||
}
|
||||
}
|
||||
|
||||
async getData () {
|
||||
const { getItems, handleHttpError } = this.props;
|
||||
const { page, page_size, sortedColumnKey, sortOrder } = this.state;
|
||||
const { getItems, handleHttpError, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
||||
|
||||
this.setState({ error: false });
|
||||
|
||||
const queryParams = {
|
||||
page,
|
||||
page_size
|
||||
};
|
||||
|
||||
if (sortedColumnKey) {
|
||||
queryParams.order_by = sortOrder === 'descending' ? `-${sortedColumnKey}` : sortedColumnKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await getItems(queryParams);
|
||||
const { results, count } = data;
|
||||
|
||||
const stateToUpdate = {
|
||||
this.setState({
|
||||
results,
|
||||
count
|
||||
};
|
||||
|
||||
this.setState(stateToUpdate);
|
||||
});
|
||||
} catch (err) {
|
||||
handleHttpError(err) || this.setState({ error: true });
|
||||
}
|
||||
}
|
||||
|
||||
onSetPage = async (pageNumber, pageSize) => {
|
||||
const page = parseInt(pageNumber, 10);
|
||||
const page_size = parseInt(pageSize, 10);
|
||||
this.setState({ page, page_size }, this.getData);
|
||||
};
|
||||
|
||||
toggleSelected (row) {
|
||||
const { name, onLookupSave } = this.props;
|
||||
const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
|
||||
@@ -156,10 +129,6 @@ class Lookup extends React.Component {
|
||||
error,
|
||||
results,
|
||||
count,
|
||||
page,
|
||||
page_size,
|
||||
sortedColumnKey,
|
||||
sortOrder
|
||||
} = this.state;
|
||||
const { id, lookupHeader = 'items', value, columns } = this.props;
|
||||
|
||||
@@ -200,49 +169,25 @@ class Lookup extends React.Component {
|
||||
<Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>{(results.length === 0) ? i18n._(t`Close`) : i18n._(t`Cancel`)}</Button>
|
||||
]}
|
||||
>
|
||||
{(results.length === 0) ? (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
<Trans>{`No ${lookupHeader} Found`}</Trans>
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
<Trans>{`Please add ${lookupHeader.toLowerCase()} to populate this list`}</Trans>
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<Fragment>
|
||||
<DataListToolbar
|
||||
sortedColumnKey={sortedColumnKey}
|
||||
sortOrder={sortOrder}
|
||||
columns={columns}
|
||||
onSearch={this.onSearch}
|
||||
onSort={this.onSort}
|
||||
noLeftMargin
|
||||
<PaginatedDataList
|
||||
items={results}
|
||||
itemCount={count}
|
||||
itemName={lookupHeader}
|
||||
qsConfig={this.qsConfig}
|
||||
toolbarColumns={columns}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
name={item.name}
|
||||
isSelected={lookupSelectedItems.some(i => i.id === item.id)}
|
||||
onSelect={() => this.toggleSelected(item)}
|
||||
/>
|
||||
<ul className="pf-c-data-list awx-c-list">
|
||||
{results.map(i => (
|
||||
<CheckboxListItem
|
||||
key={i.id}
|
||||
itemId={i.id}
|
||||
name={i.name}
|
||||
isSelected={lookupSelectedItems.some(item => item.id === i.id)}
|
||||
onSelect={() => this.toggleSelected(i)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<Pagination
|
||||
count={count}
|
||||
page={page}
|
||||
pageCount={Math.ceil(count / page_size)}
|
||||
page_size={page_size}
|
||||
onSetPage={this.onSetPage}
|
||||
pageSizeOptions={null}
|
||||
showPageSizeOptions={false}
|
||||
style={paginationStyling}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
)}
|
||||
alignToolbarLeft
|
||||
showPageSizeOptions={false}
|
||||
paginationStyling={paginationStyling}
|
||||
/>
|
||||
{lookupSelectedItems.length > 0 && (
|
||||
<SelectedList
|
||||
label={i18n._(t`Selected`)}
|
||||
@@ -264,9 +209,10 @@ Lookup.propTypes = {
|
||||
id: PropTypes.string,
|
||||
getItems: PropTypes.func.isRequired,
|
||||
lookupHeader: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
name: PropTypes.string, // TODO: delete, unused ?
|
||||
onLookupSave: PropTypes.func.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortedColumnKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Lookup.defaultProps = {
|
||||
@@ -276,4 +222,4 @@ Lookup.defaultProps = {
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
export default withNetwork(Lookup);
|
||||
export default withNetwork(withRouter(Lookup));
|
||||
|
||||
@@ -20,8 +20,12 @@ import { withRouter, Link } from 'react-router-dom';
|
||||
|
||||
import Pagination from '../Pagination';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import { encodeQueryString, parseQueryString } from '../../util/qs';
|
||||
import {
|
||||
parseNamespacedQueryString,
|
||||
updateNamespacedQueryString,
|
||||
} from '../../util/qs';
|
||||
import { pluralize, getArticle, ucFirst } from '../../util/strings';
|
||||
import { QSConfig } from '../../types';
|
||||
|
||||
const detailWrapperStyle = {
|
||||
display: 'grid',
|
||||
@@ -47,12 +51,14 @@ class PaginatedDataList extends React.Component {
|
||||
}
|
||||
|
||||
getPageCount () {
|
||||
const { itemCount, queryParams: { page_size } } = this.props;
|
||||
return Math.ceil(itemCount / page_size);
|
||||
const { itemCount, qsConfig, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
return Math.ceil(itemCount / queryParams.page_size);
|
||||
}
|
||||
|
||||
getSortOrder () {
|
||||
const { queryParams } = this.props;
|
||||
const { qsConfig, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
||||
return [queryParams.order_by.substr(1), 'descending'];
|
||||
}
|
||||
@@ -74,13 +80,9 @@ class PaginatedDataList extends React.Component {
|
||||
}
|
||||
|
||||
pushHistoryState (newParams) {
|
||||
const { history } = this.props;
|
||||
const { history, qsConfig } = this.props;
|
||||
const { pathname, search } = history.location;
|
||||
const currentParams = parseQueryString(search);
|
||||
const qs = encodeQueryString({
|
||||
...currentParams,
|
||||
...newParams
|
||||
});
|
||||
const qs = updateNamespacedQueryString(qsConfig, search, newParams);
|
||||
history.push(`${pathname}?${qs}`);
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ class PaginatedDataList extends React.Component {
|
||||
const {
|
||||
items,
|
||||
itemCount,
|
||||
queryParams,
|
||||
qsConfig,
|
||||
renderItem,
|
||||
toolbarColumns,
|
||||
additionalControls,
|
||||
@@ -102,9 +104,14 @@ class PaginatedDataList extends React.Component {
|
||||
showSelectAll,
|
||||
isAllSelected,
|
||||
onSelectAll,
|
||||
alignToolbarLeft,
|
||||
showPageSizeOptions,
|
||||
paginationStyling,
|
||||
location,
|
||||
} = this.props;
|
||||
const { error } = this.state;
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -153,6 +160,7 @@ class PaginatedDataList extends React.Component {
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={onSelectAll}
|
||||
additionalControls={additionalControls}
|
||||
noLeftMargin={alignToolbarLeft}
|
||||
/>
|
||||
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
|
||||
{items.map(item => (renderItem ? renderItem(item) : (
|
||||
@@ -182,10 +190,12 @@ class PaginatedDataList extends React.Component {
|
||||
</DataList>
|
||||
<Pagination
|
||||
count={itemCount}
|
||||
page={queryParams.page}
|
||||
page={queryParams.page || 1}
|
||||
pageCount={this.getPageCount()}
|
||||
page_size={queryParams.page_size}
|
||||
onSetPage={this.handleSetPage}
|
||||
showPageSizeOptions={showPageSizeOptions}
|
||||
style={paginationStyling}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
@@ -202,19 +212,12 @@ const Item = PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
});
|
||||
|
||||
const QueryParams = PropTypes.shape({
|
||||
page: PropTypes.number,
|
||||
page_size: PropTypes.number,
|
||||
order_by: PropTypes.string,
|
||||
});
|
||||
|
||||
PaginatedDataList.propTypes = {
|
||||
items: PropTypes.arrayOf(Item).isRequired,
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
itemName: PropTypes.string,
|
||||
itemNamePlural: PropTypes.string,
|
||||
// TODO: determine this internally but pass in defaults?
|
||||
queryParams: QueryParams.isRequired,
|
||||
qsConfig: QSConfig.isRequired,
|
||||
renderItem: PropTypes.func,
|
||||
toolbarColumns: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
@@ -225,6 +228,9 @@ PaginatedDataList.propTypes = {
|
||||
showSelectAll: PropTypes.bool,
|
||||
isAllSelected: PropTypes.bool,
|
||||
onSelectAll: PropTypes.func,
|
||||
alignToolbarLeft: PropTypes.bool,
|
||||
showPageSizeOptions: PropTypes.bool,
|
||||
paginationStyling: PropTypes.shape(),
|
||||
};
|
||||
|
||||
PaginatedDataList.defaultProps = {
|
||||
@@ -238,6 +244,9 @@ PaginatedDataList.defaultProps = {
|
||||
showSelectAll: false,
|
||||
isAllSelected: false,
|
||||
onSelectAll: null,
|
||||
alignToolbarLeft: false,
|
||||
showPageSizeOptions: true,
|
||||
paginationStyling: null,
|
||||
};
|
||||
|
||||
export { PaginatedDataList as _PaginatedDataList };
|
||||
|
||||
@@ -6,14 +6,14 @@ import OrganizationAccessItem from '../../components/OrganizationAccessItem';
|
||||
import DeleteRoleConfirmationModal from '../../components/DeleteRoleConfirmationModal';
|
||||
import AddResourceRole from '../../../../components/AddRole/AddResourceRole';
|
||||
import { withNetwork } from '../../../../contexts/Network';
|
||||
import { parseQueryString } from '../../../../util/qs';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
||||
import { Organization } from '../../../../types';
|
||||
|
||||
const DEFAULT_QUERY_PARAMS = {
|
||||
const QS_CONFIG = getQSConfig('access', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'first_name',
|
||||
};
|
||||
});
|
||||
|
||||
class OrganizationAccess extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -54,12 +54,12 @@ class OrganizationAccess extends React.Component {
|
||||
}
|
||||
|
||||
async readOrgAccessList () {
|
||||
const { organization, api, handleHttpError } = this.props;
|
||||
const { organization, api, handleHttpError, location } = this.props;
|
||||
this.setState({ isLoading: true });
|
||||
try {
|
||||
const { data } = await api.getOrganizationAccessList(
|
||||
organization.id,
|
||||
this.getQueryParams()
|
||||
parseNamespacedQueryString(QS_CONFIG, location.search)
|
||||
);
|
||||
this.setState({
|
||||
itemCount: data.count || 0,
|
||||
@@ -75,16 +75,6 @@ class OrganizationAccess extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getQueryParams () {
|
||||
const { location } = this.props;
|
||||
const searchParams = parseQueryString(location.search.substring(1));
|
||||
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...searchParams,
|
||||
};
|
||||
}
|
||||
|
||||
confirmRemoveRole (role, accessRecord) {
|
||||
this.setState({
|
||||
roleToDelete: role,
|
||||
@@ -175,7 +165,7 @@ class OrganizationAccess extends React.Component {
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
itemName="role"
|
||||
queryParams={this.getQueryParams()}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18nMark('Name'), key: 'first_name', isSortable: true },
|
||||
{ name: i18nMark('Username'), key: 'username', isSortable: true },
|
||||
|
||||
@@ -4,13 +4,13 @@ import { withRouter } from 'react-router-dom';
|
||||
import { withNetwork } from '../../../../contexts/Network';
|
||||
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
||||
import NotificationListItem from '../../../../components/NotificationsList/NotificationListItem';
|
||||
import { parseQueryString } from '../../../../util/qs';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
||||
|
||||
const DEFAULT_QUERY_PARAMS = {
|
||||
const QS_CONFIG = getQSConfig('notification', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
};
|
||||
});
|
||||
|
||||
const COLUMNS = [
|
||||
{ key: 'name', name: 'Name', isSortable: true },
|
||||
@@ -48,19 +48,9 @@ class OrganizationNotifications extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getQueryParams () {
|
||||
const { location } = this.props;
|
||||
const searchParams = parseQueryString(location.search.substring(1));
|
||||
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...searchParams,
|
||||
};
|
||||
}
|
||||
|
||||
async readNotifications () {
|
||||
const { api, handleHttpError, id } = this.props;
|
||||
const params = this.getQueryParams();
|
||||
const { id, api, handleHttpError, location } = this.props;
|
||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||
this.setState({ isLoading: true });
|
||||
try {
|
||||
const { data } = await api.getOrganizationNotifications(id, params);
|
||||
@@ -191,7 +181,7 @@ class OrganizationNotifications extends Component {
|
||||
items={notifications}
|
||||
itemCount={itemCount}
|
||||
itemName="notification"
|
||||
queryParams={this.getQueryParams()}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={COLUMNS}
|
||||
renderItem={(notification) => (
|
||||
<NotificationListItem
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
||||
import { parseQueryString } from '../../../../util/qs';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
||||
import { withNetwork } from '../../../../contexts/Network';
|
||||
|
||||
const DEFAULT_QUERY_PARAMS = {
|
||||
const QS_CONFIG = getQSConfig('team', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
};
|
||||
});
|
||||
|
||||
class OrganizationTeams extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -37,19 +37,9 @@ class OrganizationTeams extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getQueryParams () {
|
||||
const { location } = this.props;
|
||||
const searchParams = parseQueryString(location.search.substring(1));
|
||||
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...searchParams,
|
||||
};
|
||||
}
|
||||
|
||||
async readOrganizationTeamsList () {
|
||||
const { api, handleHttpError, id } = this.props;
|
||||
const params = this.getQueryParams();
|
||||
const { id, api, handleHttpError, location } = this.props;
|
||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||
this.setState({ isLoading: true, error: null });
|
||||
try {
|
||||
const {
|
||||
@@ -86,7 +76,7 @@ class OrganizationTeams extends React.Component {
|
||||
items={teams}
|
||||
itemCount={itemCount}
|
||||
itemName="team"
|
||||
queryParams={this.getQueryParams()}
|
||||
qsConfig={QS_CONFIG}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -13,7 +13,7 @@ import PaginatedDataList, {
|
||||
ToolbarAddButton
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import OrganizationListItem from '../components/OrganizationListItem';
|
||||
import { encodeQueryString, parseQueryString } from '../../../util/qs';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../../util/qs';
|
||||
|
||||
const COLUMNS = [
|
||||
{ name: i18nMark('Name'), key: 'name', isSortable: true },
|
||||
@@ -21,11 +21,11 @@ const COLUMNS = [
|
||||
{ name: i18nMark('Created'), key: 'created', isSortable: true, isNumeric: true },
|
||||
];
|
||||
|
||||
const DEFAULT_QUERY_PARAMS = {
|
||||
const QS_CONFIG = getQSConfig('organization', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
};
|
||||
});
|
||||
|
||||
class OrganizationsList extends Component {
|
||||
constructor (props) {
|
||||
@@ -39,10 +39,8 @@ class OrganizationsList extends Component {
|
||||
selected: [],
|
||||
};
|
||||
|
||||
this.getQueryParams = this.getQueryParams.bind(this);
|
||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.updateUrl = this.updateUrl.bind(this);
|
||||
this.fetchOptionsOrganizations = this.fetchOptionsOrganizations.bind(this);
|
||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||
@@ -77,16 +75,6 @@ class OrganizationsList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getQueryParams () {
|
||||
const { location } = this.props;
|
||||
const searchParams = parseQueryString(location.search.substring(1));
|
||||
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...searchParams,
|
||||
};
|
||||
}
|
||||
|
||||
async handleOrgDelete () {
|
||||
const { selected } = this.state;
|
||||
const { api, handleHttpError } = this.props;
|
||||
@@ -101,25 +89,14 @@ class OrganizationsList extends Component {
|
||||
errorHandled = handleHttpError(err);
|
||||
} finally {
|
||||
if (!errorHandled) {
|
||||
const queryParams = this.getQueryParams();
|
||||
this.fetchOrganizations(queryParams);
|
||||
this.fetchOrganizations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUrl (queryParams) {
|
||||
const { history, location } = this.props;
|
||||
const pathname = '/organizations';
|
||||
const search = `?${encodeQueryString(queryParams)}`;
|
||||
|
||||
if (search !== location.search) {
|
||||
history.replace({ pathname, search });
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOrganizations () {
|
||||
const { api, handleHttpError } = this.props;
|
||||
const params = this.getQueryParams();
|
||||
const { api, handleHttpError, location } = this.props;
|
||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||
|
||||
this.setState({ error: false, isLoading: true });
|
||||
|
||||
@@ -185,7 +162,7 @@ class OrganizationsList extends Component {
|
||||
items={organizations}
|
||||
itemCount={itemCount}
|
||||
itemName="organization"
|
||||
queryParams={this.getQueryParams()}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={COLUMNS}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
|
||||
@@ -47,3 +47,9 @@ export const Organization = shape({
|
||||
created: string,
|
||||
modified: string,
|
||||
});
|
||||
|
||||
export const QSConfig = shape({
|
||||
defaultParams: shape().isRequired,
|
||||
namespace: string,
|
||||
integerFields: arrayOf(string).isRequired,
|
||||
});
|
||||
|
||||
@@ -40,3 +40,74 @@ export const parseQueryString = (queryString, integerFields = ['page', 'page_siz
|
||||
|
||||
return Object.assign(...keyValuePairs.map(([k, v]) => ({ [k]: v })));
|
||||
};
|
||||
|
||||
export function getQSConfig (
|
||||
namespace,
|
||||
defaultParams = { page: 1, page_size: 5, order_by: 'name' },
|
||||
integerFields = ['page', 'page_size']
|
||||
) {
|
||||
if (!namespace) {
|
||||
throw new Error('a QS namespace is required');
|
||||
}
|
||||
return {
|
||||
defaultParams,
|
||||
namespace,
|
||||
integerFields,
|
||||
};
|
||||
}
|
||||
|
||||
export function encodeNamespacedQueryString (config, params) {
|
||||
return encodeQueryString(namespaceParams(config.namespace, params));
|
||||
}
|
||||
|
||||
export function parseNamespacedQueryString (config, queryString, includeDefaults = true) {
|
||||
const integerFields = prependNamespaceToArray(config.namespace, config.integerFields);
|
||||
const parsed = parseQueryString(queryString, integerFields);
|
||||
|
||||
const namespace = {};
|
||||
Object.keys(parsed).forEach(field => {
|
||||
if (namespaceMatches(config.namespace, field)) {
|
||||
let fieldname = field;
|
||||
if (config.namespace) {
|
||||
fieldname = field.substr(config.namespace.length + 1);
|
||||
}
|
||||
namespace[fieldname] = parsed[field];
|
||||
}
|
||||
});
|
||||
return {
|
||||
...includeDefaults ? config.defaultParams : {},
|
||||
...namespace,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateNamespacedQueryString (config, queryString, newParams) {
|
||||
const params = parseQueryString(queryString);
|
||||
return encodeQueryString({
|
||||
...params,
|
||||
...namespaceParams(config.namespace, newParams),
|
||||
});
|
||||
}
|
||||
|
||||
function namespaceParams (ns, params) {
|
||||
if (!ns) return params;
|
||||
|
||||
const namespaced = {};
|
||||
Object.keys(params).forEach(key => {
|
||||
namespaced[`${ns}.${key}`] = params[key];
|
||||
});
|
||||
return namespaced;
|
||||
}
|
||||
|
||||
function namespaceMatches (namespace, fieldname) {
|
||||
if (!namespace) {
|
||||
return !fieldname.includes('.');
|
||||
}
|
||||
return fieldname.startsWith(`${namespace}.`);
|
||||
}
|
||||
|
||||
function prependNamespaceToArray (namespace, arr) {
|
||||
if (!namespace) {
|
||||
return arr;
|
||||
}
|
||||
return arr.map(f => `${namespace}.${f}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user