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:
Keith Grant
2019-05-15 10:06:14 -04:00
committed by GitHub
parent d59975c1ad
commit 4407aeac20
19 changed files with 2656 additions and 2648 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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));

View File

@@ -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 };

View File

@@ -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 },

View File

@@ -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

View File

@@ -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>

View File

@@ -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}

View File

@@ -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,
});

View File

@@ -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}`);
}