update organizations structure and add unstyled sub routes and breadcrumbs

This commit is contained in:
John Mitchell
2018-11-29 14:12:33 -05:00
parent 1e7ab9deed
commit 12c8267b12
14 changed files with 594 additions and 53 deletions

View File

@@ -0,0 +1,78 @@
import React, { Fragment } from 'react';
import {
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
import {
Link
} from 'react-router-dom';
import getTabName from '../utils';
const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location }) => {
const { light } = PageSectionVariants;
let breadcrumb = '';
if (parentObj !== 'loading') {
const generateCrumb = (noLastLink = false) => (
<Fragment>
{parentObj
.map(({ url, name }, index) => {
let elem;
if (noLastLink && parentObj.length - 1 === index) {
elem = (<Fragment key={name}>{name}</Fragment>);
} else {
elem = (
<Link
key={name}
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
>
{name}
</Link>
);
}
return elem;
})
.reduce((prev, curr) => [prev, ' > ', curr])}
</Fragment>
);
if (currentTab && currentTab !== 'details') {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > '}
{getTabName(currentTab)}
</Fragment>
);
} else if (location.pathname.indexOf('edit') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > edit'}
</Fragment>
);
} else if (location.pathname.indexOf('add') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > add'}
</Fragment>
);
} else {
breadcrumb = (
<Fragment>
{generateCrumb(true)}
</Fragment>
);
}
}
return (
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">{breadcrumb}</Title>
</PageSection>
);
};
export default OrganizationBreadcrumb;

View File

@@ -0,0 +1,140 @@
import React, { Fragment } from 'react';
import {
Card,
CardHeader,
CardBody,
PageSection,
PageSectionVariants,
ToolbarGroup,
ToolbarItem,
ToolbarSection,
} from '@patternfly/react-core';
import {
Switch,
Link,
Route
} from 'react-router-dom';
import getTabName from '../utils';
import '../tabs.scss';
const DetailTab = ({ location, match, tab, currentTab, children, breadcrumb }) => {
const tabClasses = () => {
let classes = 'at-c-tabs__tab';
if (tab === currentTab) {
classes += ' at-m-selected';
}
return classes;
};
const updateTab = () => {
const params = new URLSearchParams(location.search);
if (params.get('tab') !== undefined) {
params.set('tab', tab);
} else {
params.append('tab', tab);
}
return `?${params.toString()}`;
};
return (
<ToolbarItem className={tabClasses()}>
<Link to={{ pathname: `${match.url}`, search: updateTab(), state: { breadcrumb } }}>
{children}
</Link>
</ToolbarItem>
);
};
const OrganizationDetail = ({
location,
match,
parentBreadcrumbObj,
organization,
params,
currentTab
}) => {
// TODO: set objectName by param or through grabbing org detail get from api
const { medium } = PageSectionVariants;
const deleteResourceView = () => (
<Fragment>
{`deleting ${currentTab} association with orgs `}
<Link to={{ pathname: `${match.url}`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`confirm removal of ${currentTab}/cancel and go back to ${currentTab} view.`}
</Link>
</Fragment>
);
const addResourceView = () => (
<Fragment>
{`adding ${currentTab} `}
<Link to={{ pathname: `${match.url}`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`save/cancel and go back to ${currentTab} view`}
</Link>
</Fragment>
);
const resourceView = () => (
<Fragment>
{`${currentTab} detail view `}
<Link to={{ pathname: `${match.url}/add-resource`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`add ${currentTab}`}
</Link>
{' '}
<Link to={{ pathname: `${match.url}/delete-resources`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`delete ${currentTab}`}
</Link>
</Fragment>
);
const detailTabs = (tabs) => (
<ToolbarSection aria-label="Organization detail tabs">
<ToolbarGroup className="at-c-tabs">
{tabs.map(tab => (
<DetailTab
key={tab}
tab={tab}
location={location}
match={match}
currentTab={currentTab}
breadcrumb={parentBreadcrumbObj}
>
{getTabName(tab)}
</DetailTab>
))}
</ToolbarGroup>
</ToolbarSection>
);
return (
<PageSection variant={medium}>
<Card className="at-c-orgPane">
<CardHeader>
{detailTabs(['details', 'users', 'teams', 'admins', 'notifications'])}
</CardHeader>
<CardBody>
{(currentTab && currentTab !== 'details') ? (
<Switch>
<Route path={`${match.path}/delete-resources`} component={() => deleteResourceView()} />
<Route path={`${match.path}/add-resource`} component={() => addResourceView()} />
<Route path={`${match.path}`} component={() => resourceView()} />
</Switch>
) : (
<Fragment>
{'detail view '}
<Link to={{ pathname: `${match.url}/edit`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{'edit'}
</Link>
</Fragment>
)}
</CardBody>
</Card>
</PageSection>
);
};
export default OrganizationDetail;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import {
Card,
CardBody,
PageSection,
PageSectionVariants
} from '@patternfly/react-core';
import {
Link
} from 'react-router-dom';
const OrganizationEdit = ({ match, parentBreadcrumbObj, organization }) => {
const { medium } = PageSectionVariants;
return (
<PageSection variant={medium}>
<Card className="at-c-orgPane">
<CardBody>
{'edit view '}
<Link to={{ pathname: `/organizations/${match.params.id}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{'save/cancel and go back to view'}
</Link>
</CardBody>
</Card>
</PageSection>
);
};
export default OrganizationEdit;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import {
Badge,
Checkbox,
} from '@patternfly/react-core';
import {
Link
} from 'react-router-dom';
export default ({
itemId,
name,
userCount,
teamCount,
adminCount,
isSelected,
onSelect,
detailUrl,
parentBreadcrumb
}) => (
<li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1">
<div className="pf-c-data-list__check">
<Checkbox
checked={isSelected}
onChange={onSelect}
aria-label={`select organization ${itemId}`}
id={`select-organization-${itemId}`}
/>
</div>
<div className="pf-c-data-list__cell">
<span id="check-action-item1">
<Link
to={{
pathname: detailUrl,
state: { breadcrumb: [parentBreadcrumb, { name, url: detailUrl }] }
}}
>
{name}
</Link>
</span>
</div>
<div className="pf-c-data-list__cell">
<Link to={`${detailUrl}?tab=users`}>
Users
</Link>
<Badge isRead>
{' '}
{userCount}
{' '}
</Badge>
<Link to={`${detailUrl}?tab=teams`}>
Teams
</Link>
<Badge isRead>
{' '}
{teamCount}
{' '}
</Badge>
<Link to={`${detailUrl}?tab=admins`}>
Admins
</Link>
<Badge isRead>
{' '}
{adminCount}
{' '}
</Badge>
</div>
<div className="pf-c-data-list__cell" />
</li>
);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import OrganizationAdd from './views/Organization.add';
import OrganizationView from './views/Organization.view';
import OrganizationsList from './views/Organizations.list';
const Organizations = ({ match }) => (
<Switch>
<Route path={`${match.path}/add`} component={OrganizationAdd} />
<Route path={`${match.path}/:id`} component={OrganizationView} />
<Route path={`${match.path}`} component={OrganizationsList} />
</Switch>
);
export default Organizations;

View File

@@ -0,0 +1,18 @@
.at-c-tabs {
padding: 0 5px !important;
margin: 0 -10px !important;
.at-c-tabs__tab {
margin: 0 5px;
}
.at-c-tabs__tab.at-m-selected {
text-decoration: underline;
}
}
.at-c-orgPane {
a {
display: block;
}
}

View File

@@ -0,0 +1,17 @@
const getTabName = (tab) => {
let tabName = '';
if (tab === 'details') {
tabName = 'Details';
} else if (tab === 'users') {
tabName = 'Users';
} else if (tab === 'teams') {
tabName = 'Teams';
} else if (tab === 'admins') {
tabName = 'Admins';
} else if (tab === 'notifications') {
tabName = 'Notifications';
}
return tabName;
};
export default getTabName;

View File

@@ -0,0 +1,21 @@
import React, { Fragment } from 'react';
import {
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
const { light, medium } = PageSectionVariants;
const OrganizationView = () => (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">Organization Add</Title>
</PageSection>
<PageSection variant={medium}>
This is the add view
</PageSection>
</Fragment>
);
export default OrganizationView;

View File

@@ -0,0 +1,120 @@
import React, { Component, Fragment } from 'react';
import {
Switch,
Route
} from 'react-router-dom';
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
import OrganizationDetail from '../components/OrganizationDetail';
import OrganizationEdit from '../components/OrganizationEdit';
import api from '../../../api';
import { API_ORGANIZATIONS } from '../../../endpoints';
class OrganizationView extends Component {
constructor (props) {
super(props);
let { breadcrumb: parentBreadcrumbObj, organization } = props.location.state || {};
if (!parentBreadcrumbObj) {
parentBreadcrumbObj = 'loading';
}
if (!organization) {
organization = 'loading';
}
this.state = {
parentBreadcrumbObj,
organization,
error: false,
loading: false,
mounted: false
};
}
componentDidMount () {
this.setState({ mounted: true }, () => {
const { organization } = this.state;
if (organization === 'loading') {
this.fetchOrganization();
}
});
}
componentWillUnmount () {
this.setState({ mounted: false });
}
async fetchOrganization () {
const { mounted } = this.state;
if (mounted) {
this.setState({ error: false, loading: true });
const { match } = this.props;
const { parentBreadcrumbObj, organization } = this.state;
try {
const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
if (organization === 'loading') {
this.setState({ organization: data });
}
const { name } = data;
if (parentBreadcrumbObj === 'loading') {
this.setState({ parentBreadcrumbObj: [{ name: 'Organizations', url: '/organizations' }, { name, url: match.url }] });
}
} catch (err) {
this.setState({ error: true });
} finally {
this.setState({ loading: false });
}
}
}
render () {
const { location, match } = this.props;
const { parentBreadcrumbObj, organization, error, loading } = this.state;
const params = new URLSearchParams(location.search);
const currentTab = params.get('tab') || 'details';
return (
<Fragment>
<OrganizationBreadcrumb
parentObj={parentBreadcrumbObj}
currentTab={currentTab}
location={location}
organization={organization}
/>
<Switch>
<Route
path={`${match.path}/edit`}
component={() => (
<OrganizationEdit
location={location}
match={match}
parentBreadcrumbObj={parentBreadcrumbObj}
organization={organization}
params={params}
currentTab={currentTab}
/>
)}
/>
<Route
path={`${match.path}`}
component={() => (
<OrganizationDetail
location={location}
match={match}
parentBreadcrumbObj={parentBreadcrumbObj}
organization={organization}
params={params}
currentTab={currentTab}
/>
)}
/>
</Switch>
{error ? 'error!' : ''}
{loading ? 'loading...' : ''}
</Fragment>
);
}
}
export default OrganizationView;

View File

@@ -11,17 +11,17 @@ import {
Title,
} from '@patternfly/react-core';
import DataListToolbar from '../components/DataListToolbar';
import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
import Pagination from '../components/Pagination';
import Pagination from '../../../components/Pagination';
import api from '../api';
import { API_ORGANIZATIONS } from '../endpoints';
import api from '../../../api';
import { API_ORGANIZATIONS } from '../../../endpoints';
import {
encodeQueryString,
parseQueryString,
} from '../qs';
} from '../../../qs';
class Organizations extends Component {
columns = [
@@ -58,7 +58,6 @@ class Organizations extends Component {
componentDidMount () {
const queryParams = this.getQueryParams();
this.fetchOrganizations(queryParams);
}
@@ -122,7 +121,6 @@ class Organizations extends Component {
updateUrl (queryParams) {
const { history, location } = this.props;
const pathname = '/organizations';
const search = `?${encodeQueryString(queryParams)}`;
@@ -185,6 +183,8 @@ class Organizations extends Component {
results,
selected,
} = this.state;
const { match } = this.props;
const parentBreadcrumb = { name: 'Organizations', url: match.url };
return (
<Fragment>
@@ -193,6 +193,7 @@ class Organizations extends Component {
</PageSection>
<PageSection variant={medium}>
<DataListToolbar
addUrl={`${match.url}/add`}
isAllSelected={selected.length === results.length}
sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder}
@@ -207,6 +208,8 @@ class Organizations extends Component {
key={o.id}
itemId={o.id}
name={o.name}
detailUrl={`${match.url}/${o.id}`}
parentBreadcrumb={parentBreadcrumb}
userCount={o.summary_fields.related_field_counts.users}
teamCount={o.summary_fields.related_field_counts.teams}
adminCount={o.summary_fields.related_field_counts.admins}