Merge pull request #78 from jorilallo/jori/stores-refactor
Collections store refactor and layout changes
This commit is contained in:
commit
d853821186
|
@ -11,6 +11,7 @@
|
|||
"flowtype"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": 2,
|
||||
// // Bring back after we remove CSS Modules 100%
|
||||
// "import/order": "warn",
|
||||
// Prettier automatically uses the least amount of parens possible, so this
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import DocumentLink from './components/DocumentLink';
|
||||
import moment from 'moment';
|
||||
|
||||
import styles from './Collection.scss';
|
||||
// import classNames from 'classnames/bind';
|
||||
// const cx = classNames.bind(styles);
|
||||
|
||||
@observer class Collection extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -24,7 +21,18 @@ import styles from './Collection.scss';
|
|||
</h2>
|
||||
{data.recentDocuments.length > 0
|
||||
? data.recentDocuments.map(document => {
|
||||
return <DocumentLink document={document} key={document.id} />;
|
||||
return (
|
||||
<Link
|
||||
key={document.id}
|
||||
to={document.url}
|
||||
className={styles.link}
|
||||
>
|
||||
<h3 className={styles.title}>{document.title}</h3>
|
||||
<span className={styles.timestamp}>
|
||||
{moment(document.updatedAt).fromNow()}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
: <div className={styles.description}>
|
||||
No documents. Why not
|
||||
|
|
|
@ -17,3 +17,25 @@
|
|||
.description {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: $textColor;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ const DocumentLink = styled(Link)`
|
|||
}
|
||||
`;
|
||||
|
||||
// $FlowIssue
|
||||
const TruncatedMarkdown = styled(Markdown)`
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
|
|
@ -12,13 +12,15 @@ import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
|||
|
||||
const onlyInCode = node => node.type === 'code';
|
||||
|
||||
type CreatePluginsOptions = {
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
};
|
||||
|
||||
const createPlugins = ({
|
||||
onImageUploadStart,
|
||||
onImageUploadStop,
|
||||
}: {
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
}) => {
|
||||
}: CreatePluginsOptions) => {
|
||||
return [
|
||||
PasteLinkify({
|
||||
type: 'link',
|
||||
|
|
|
@ -6,26 +6,23 @@ import styled from 'styled-components';
|
|||
import { observer, inject } from 'mobx-react';
|
||||
import _ from 'lodash';
|
||||
import keydown from 'react-keydown';
|
||||
import classNames from 'classnames/bind';
|
||||
import searchIcon from 'assets/icons/search.svg';
|
||||
import { Flex } from 'reflexbox';
|
||||
import { textColor } from 'styles/constants.scss';
|
||||
import styles from './Layout.scss';
|
||||
import { textColor, headerHeight } from 'styles/constants.scss';
|
||||
|
||||
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import UserStore from 'stores/UserStore';
|
||||
|
||||
const cx = classNames.bind(styles);
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
history: Object,
|
||||
children?: ?React.Element<any>,
|
||||
actions?: ?React.Element<any>,
|
||||
title?: ?React.Element<any>,
|
||||
titleText?: string,
|
||||
loading?: boolean,
|
||||
user: UserStore,
|
||||
auth: AuthStore,
|
||||
search: ?boolean,
|
||||
notifications?: React.Element<any>,
|
||||
};
|
||||
|
@ -39,29 +36,27 @@ type Props = {
|
|||
|
||||
@keydown(['/', 't'])
|
||||
search() {
|
||||
if (!this.props.user) return;
|
||||
_.defer(() => this.props.history.push('/search'));
|
||||
if (this.props.auth.isAuthenticated)
|
||||
_.defer(() => this.props.history.push('/search'));
|
||||
}
|
||||
|
||||
@keydown(['d'])
|
||||
dashboard() {
|
||||
if (!this.props.user) return;
|
||||
_.defer(() => this.props.history.push('/'));
|
||||
if (this.props.auth.isAuthenticated)
|
||||
_.defer(() => this.props.history.push('/'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const user = this.props.user;
|
||||
handleLogout = () => {
|
||||
this.props.auth.logout(() => this.props.history.push('/'));
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
user.logout(() => this.props.history.push('/'));
|
||||
};
|
||||
render() {
|
||||
const { auth, user } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Container column auto>
|
||||
<Helmet
|
||||
title={
|
||||
this.props.titleText ? `${this.props.titleText} - Atlas` : 'Atlas'
|
||||
}
|
||||
title="Atlas"
|
||||
meta={[
|
||||
{
|
||||
name: 'viewport',
|
||||
|
@ -74,26 +69,27 @@ type Props = {
|
|||
|
||||
{this.props.notifications}
|
||||
|
||||
<div className={cx(styles.header)}>
|
||||
<div className={styles.headerLeft}>
|
||||
<Link to="/" className={styles.team}>Atlas</Link>
|
||||
<span className={styles.title}>
|
||||
<Header>
|
||||
<Flex align="center">
|
||||
<LogoLink to="/">Atlas</LogoLink>
|
||||
<Title>
|
||||
{this.props.title}
|
||||
</span>
|
||||
</div>
|
||||
<Flex className={styles.headerRight}>
|
||||
</Title>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex>
|
||||
<Flex align="center" className={styles.actions}>
|
||||
<Flex align="center">
|
||||
{this.props.actions}
|
||||
</Flex>
|
||||
{user.user &&
|
||||
{auth.authenticated &&
|
||||
user &&
|
||||
<Flex>
|
||||
{this.props.search &&
|
||||
<Flex>
|
||||
<Link to="/search">
|
||||
<div className={styles.search} title="Search (/)">
|
||||
<img src={searchIcon} alt="Search" />
|
||||
</div>
|
||||
<Search title="Search (/)">
|
||||
<SearchIcon src={searchIcon} alt="Search" />
|
||||
</Search>
|
||||
</Link>
|
||||
</Flex>}
|
||||
<DropdownMenu label={<Avatar src={user.user.avatarUrl} />}>
|
||||
|
@ -108,21 +104,72 @@ type Props = {
|
|||
<MenuLink to="/developers">
|
||||
<MenuItem>API</MenuItem>
|
||||
</MenuLink>
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
|
||||
</DropdownMenu>
|
||||
</Flex>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<div className={cx(styles.content)}>
|
||||
<Content auto justify="center">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
height: ${headerHeight};
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const LogoLink = styled(Link)`
|
||||
font-family: 'Atlas Grotesk';
|
||||
font-weight: bold;
|
||||
color: ${textColor};
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
color: #ccc;
|
||||
|
||||
a {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: ${textColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const Search = styled(Flex)`
|
||||
margin: 0 5px;
|
||||
padding: 15px 5px 0 5px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const SearchIcon = styled.img`
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
const Avatar = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
@ -133,4 +180,9 @@ const MenuLink = styled(Link)`
|
|||
color: ${textColor};
|
||||
`;
|
||||
|
||||
export default withRouter(inject('user')(Layout));
|
||||
const Content = styled(Flex)`
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('user', 'auth')(Layout));
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
height: $headerHeight;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.team {
|
||||
font-family: 'Atlas Grotesk';
|
||||
font-weight: bold;
|
||||
color: $textColor;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ccc;
|
||||
|
||||
a {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $textColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 0 5px;
|
||||
padding: 15px 5px 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
};
|
||||
|
||||
const PageTitle = ({ title }: Props) => <Helmet title={`${title} - Atlas`} />;
|
||||
|
||||
export default PageTitle;
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
import PageTitle from './PageTitle';
|
||||
export default PageTitle;
|
|
@ -1,12 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import UserStore from 'stores/UserStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
children: React.Element<any>,
|
||||
scopes?: Array<string>,
|
||||
user: UserStore,
|
||||
auth: AuthStore,
|
||||
redirectUri: string,
|
||||
};
|
||||
|
||||
|
@ -28,7 +28,7 @@ type Props = {
|
|||
client_id: SLACK_KEY,
|
||||
scope: this.props.scopes ? this.props.scopes.join(' ') : '',
|
||||
redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
|
||||
state: this.props.user.getOauthState(),
|
||||
state: this.props.auth.getOauthState(),
|
||||
};
|
||||
|
||||
const urlParams = Object.keys(params)
|
||||
|
@ -45,4 +45,4 @@ type Props = {
|
|||
}
|
||||
}
|
||||
|
||||
export default inject('user')(SlackAuthLink);
|
||||
export default inject('auth')(SlackAuthLink);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Flex } from 'reflexbox';
|
||||
|
||||
import stores from 'stores';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'styles/base.scss';
|
||||
|
@ -21,7 +22,7 @@ import 'styles/hljs-github-gist.scss';
|
|||
|
||||
import Home from 'scenes/Home';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
import Atlas from 'scenes/Atlas';
|
||||
import Collection from 'scenes/Collection';
|
||||
import Document from 'scenes/Document';
|
||||
import Search from 'scenes/Search';
|
||||
import Settings from 'scenes/Settings';
|
||||
|
@ -37,13 +38,37 @@ if (__DEV__) {
|
|||
DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
let authenticatedStores;
|
||||
|
||||
type AuthProps = {
|
||||
children?: React.Element<any>,
|
||||
};
|
||||
|
||||
const Auth = ({ children }: AuthProps) => {
|
||||
if (stores.user.authenticated) {
|
||||
return <Flex auto>{children}</Flex>;
|
||||
if (stores.auth.authenticated && stores.auth.team) {
|
||||
// Only initialize stores once. Kept in global scope
|
||||
// because otherwise they will get overriden on route
|
||||
// change
|
||||
if (!authenticatedStores) {
|
||||
// Stores for authenticated user
|
||||
const user = stores.auth.getUserStore();
|
||||
authenticatedStores = {
|
||||
user,
|
||||
collections: new CollectionsStore({
|
||||
teamId: user.team.id,
|
||||
}),
|
||||
};
|
||||
|
||||
authenticatedStores.collections.fetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex auto>
|
||||
<Provider {...authenticatedStores}>
|
||||
{children}
|
||||
</Provider>
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
|
@ -64,10 +89,14 @@ render(
|
|||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
|
||||
<Route exact path="/auth/slack" component={SlackAuth} />
|
||||
<Route exact path="/auth/slack/commands" component={SlackAuth} />
|
||||
<Route exact path="/auth/error" component={ErrorAuth} />
|
||||
|
||||
<Auth>
|
||||
<Switch>
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route exact path="/collections/:id" component={Atlas} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path="/d/:id" component={Document} />
|
||||
<Route exact path="/d/:id/:edit" component={Document} />
|
||||
<Route
|
||||
|
@ -81,10 +110,6 @@ render(
|
|||
<Route exact path="/search/:query" component={Search} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
|
||||
<Route exact path="/auth/slack" component={SlackAuth} />
|
||||
<Route exact path="/auth/slack/commands" component={SlackAuth} />
|
||||
<Route exact path="/auth/error" component={ErrorAuth} />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/keyboard-shortcuts"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { extendObservable, action, computed, runInAction } from 'mobx';
|
||||
import { extendObservable, action, runInAction } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import keydown from 'react-keydown';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import _ from 'lodash';
|
||||
|
||||
// TODO move here argh
|
||||
import store from './AtlasStore';
|
||||
|
||||
import Layout, { Title } from 'components/Layout';
|
||||
import PreviewLoading from 'components/PreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import DocumentList from 'components/DocumentList';
|
||||
import Divider from 'components/Divider';
|
||||
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
|
||||
import { Flex } from 'reflexbox';
|
||||
|
||||
import styles from './Atlas.scss';
|
||||
|
||||
type Props = {
|
||||
params: Object,
|
||||
history: Object,
|
||||
match: Object,
|
||||
keydown: Object,
|
||||
};
|
||||
|
||||
@keydown(['c'])
|
||||
@observer
|
||||
class Atlas extends React.Component {
|
||||
props: Props;
|
||||
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.match.params;
|
||||
store.fetchCollection(id, data => {
|
||||
// Forward directly to root document
|
||||
if (data.type === 'atlas') {
|
||||
this.props.history.replace(data.navigationTree.url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps = (nextProps: Props) => {
|
||||
const key = nextProps.keydown.event;
|
||||
if (key) {
|
||||
if (key.key === 'c') {
|
||||
_.defer(this.onCreate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onCreate = (event: Event) => {
|
||||
if (event) event.preventDefault();
|
||||
store.collection && this.props.history.push(`${store.collection.url}/new`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const collection = store.collection;
|
||||
|
||||
let actions;
|
||||
let title;
|
||||
let titleText;
|
||||
|
||||
if (collection) {
|
||||
actions = (
|
||||
<Flex>
|
||||
<DropdownMenu label={<MoreIcon />}>
|
||||
<MenuItem onClick={this.onCreate}>
|
||||
New document
|
||||
</MenuItem>
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
);
|
||||
title = <Title content={collection.name} />;
|
||||
titleText = collection.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout actions={actions} title={title} titleText={titleText}>
|
||||
<CenteredContent>
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="fadeIn"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={0}
|
||||
transitionEnterTimeout={0}
|
||||
transitionLeaveTimeout={0}
|
||||
>
|
||||
{store.isFetching
|
||||
? <PreviewLoading />
|
||||
: collection &&
|
||||
<div className={styles.container}>
|
||||
<div className={styles.atlasDetails}>
|
||||
<h2>{collection.name}</h2>
|
||||
<blockquote>
|
||||
{collection.description}
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<DocumentList
|
||||
documents={collection.recentDocuments}
|
||||
preview
|
||||
/>
|
||||
</div>}
|
||||
</ReactCSSTransitionGroup>
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Atlas;
|
|
@ -1,17 +0,0 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.atlasDetails {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
blockquote {
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
import Atlas from './Atlas';
|
||||
export default Atlas;
|
|
@ -0,0 +1,65 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { Redirect } from 'react-router';
|
||||
import _ from 'lodash';
|
||||
import { notFoundUrl } from 'utils/routeHelpers';
|
||||
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PreviewLoading from 'components/PreviewLoading';
|
||||
|
||||
type Props = {
|
||||
collections: CollectionsStore,
|
||||
match: Object,
|
||||
};
|
||||
|
||||
type State = {
|
||||
redirectUrl: ?string,
|
||||
};
|
||||
|
||||
@observer class Collection extends React.Component {
|
||||
props: Props;
|
||||
state: State;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
redirectUrl: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.match.params;
|
||||
this.props.collections
|
||||
.getById(id)
|
||||
.then(collection => {
|
||||
if (collection.type !== 'atlas')
|
||||
throw new Error('TODO code up non-atlas collections');
|
||||
|
||||
this.setState({
|
||||
redirectUrl: collection.navigationTree.url,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
redirectUrl: notFoundUrl(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
{this.state.redirectUrl && <Redirect to={this.state.redirectUrl} />}
|
||||
|
||||
<CenteredContent>
|
||||
<PreviewLoading />
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default inject('collections')(Collection);
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
import Collection from './Collection';
|
||||
export default Collection;
|
|
@ -1,10 +1,9 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Flex } from 'reflexbox';
|
||||
|
||||
import DashboardStore from './DashboardStore';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import Collection from 'components/Collection';
|
||||
|
@ -12,37 +11,24 @@ import PreviewLoading from 'components/PreviewLoading';
|
|||
import CenteredContent from 'components/CenteredContent';
|
||||
|
||||
type Props = {
|
||||
user: Object,
|
||||
router: Object,
|
||||
collections: CollectionsStore,
|
||||
};
|
||||
|
||||
@withRouter
|
||||
@inject('user')
|
||||
@observer
|
||||
class Dashboard extends React.Component {
|
||||
@observer class Dashboard extends React.Component {
|
||||
props: Props;
|
||||
store: DashboardStore;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.store = new DashboardStore({
|
||||
team: props.user.team,
|
||||
router: props.router,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { collections } = this.props;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<CenteredContent>
|
||||
<Flex column auto>
|
||||
{this.store.isFetching
|
||||
{!collections.isLoaded
|
||||
? <PreviewLoading />
|
||||
: this.store.collections &&
|
||||
this.store.collections.map(collection => (
|
||||
<Collection key={collection.id} data={collection} />
|
||||
))}
|
||||
: collections.data.map(collection => (
|
||||
<Collection key={collection.id} data={collection} />
|
||||
))}
|
||||
</Flex>
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
|
@ -50,4 +36,4 @@ class Dashboard extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
export default inject('collections')(Dashboard);
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { Pagination } from 'types';
|
||||
import Collection from 'models/Collection';
|
||||
|
||||
type Options = {
|
||||
team: Object,
|
||||
router: Object,
|
||||
};
|
||||
|
||||
class DashboardStore {
|
||||
team: Object;
|
||||
router: Object;
|
||||
@observable collections: Array<Collection>;
|
||||
@observable pagination: Pagination;
|
||||
|
||||
@observable isFetching: boolean = true;
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action fetchCollections = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post('/collections.list', { id: this.team.id });
|
||||
invariant(
|
||||
res && res.data && res.pagination,
|
||||
'API response should be available'
|
||||
);
|
||||
const { data, pagination } = res;
|
||||
runInAction('fetchCollections', () => {
|
||||
this.collections = data.map(collection => new Collection(collection));
|
||||
this.pagination = pagination;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Something went wrong');
|
||||
}
|
||||
this.isFetching = false;
|
||||
};
|
||||
|
||||
constructor(options: Options) {
|
||||
this.team = options.team;
|
||||
this.router = options.router;
|
||||
this.fetchCollections();
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardStore;
|
|
@ -14,6 +14,7 @@ import Layout, { HeaderAction, SaveAction } from 'components/Layout';
|
|||
import PublishingInfo from 'components/PublishingInfo';
|
||||
import PreviewLoading from 'components/PreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
const DISCARD_CHANGES = `
|
||||
You have unsaved changes.
|
||||
|
@ -103,9 +104,7 @@ type Props = {
|
|||
/>
|
||||
);
|
||||
|
||||
const titleText =
|
||||
this.store.document &&
|
||||
`${get(this.store, 'document.collection.name')} - ${get(this.store, 'document.title')}`;
|
||||
const titleText = this.store.document && get(this.store, 'document.title');
|
||||
|
||||
const actions = (
|
||||
<Flex>
|
||||
|
@ -130,11 +129,11 @@ type Props = {
|
|||
<Layout
|
||||
actions={actions}
|
||||
title={title}
|
||||
titleText={titleText}
|
||||
loading={this.store.isSaving || this.store.isUploading}
|
||||
search={false}
|
||||
fixed
|
||||
>
|
||||
<PageTitle title={titleText} />
|
||||
<Prompt when={this.store.hasPendingChanges} message={DISCARD_CHANGES} />
|
||||
{this.store.isFetching &&
|
||||
<CenteredContent>
|
||||
|
|
|
@ -4,11 +4,13 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import Layout from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
class Error404 extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Layout titleText="Not Found">
|
||||
<Layout>
|
||||
<PageTitle title="Not found" />
|
||||
<CenteredContent>
|
||||
<h1>Not Found</h1>
|
||||
|
||||
|
|
|
@ -4,11 +4,13 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import Layout from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
class ErrorAuth extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Layout titleText="Not Found">
|
||||
<Layout>
|
||||
<PageTitle title="Authentication error" />
|
||||
<CenteredContent>
|
||||
<h1>Authentication failed</h1>
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { observer } from 'mobx-react';
|
|||
import Layout, { Title } from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import { DocumentHtml } from 'components/Document';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
import { convertToMarkdown } from 'utils/markdown';
|
||||
|
||||
|
@ -20,11 +21,8 @@ type Props = {
|
|||
const { title, content } = this.props;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={<Title content={title} />}
|
||||
titleText={title}
|
||||
search={false}
|
||||
>
|
||||
<Layout title={<Title content={title} />} search={false}>
|
||||
<PageTitle title={title} />
|
||||
<CenteredContent>
|
||||
<DocumentHtml html={convertToMarkdown(content)} />
|
||||
</CenteredContent>
|
||||
|
|
|
@ -2,22 +2,23 @@
|
|||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
import { Flex } from 'reflexbox';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import SlackAuthLink from 'components/SlackAuthLink';
|
||||
import Alert from 'components/Alert';
|
||||
|
||||
import styles from './Home.scss';
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
location: Object,
|
||||
};
|
||||
|
||||
@inject('user')
|
||||
@observer
|
||||
export default class Home extends React.Component {
|
||||
static propTypes = {
|
||||
user: React.PropTypes.object.isRequired,
|
||||
location: React.PropTypes.object.isRequired,
|
||||
};
|
||||
@observer class Home extends React.Component {
|
||||
props: Props;
|
||||
|
||||
get notifications(): React.Element<any>[] {
|
||||
const notifications = [];
|
||||
|
@ -40,17 +41,17 @@ export default class Home extends React.Component {
|
|||
return (
|
||||
<Flex auto>
|
||||
<Layout notifications={this.notifications}>
|
||||
{this.props.user.authenticated && <Redirect to="/dashboard" />}
|
||||
{this.props.auth.authenticated && <Redirect to="/dashboard" />}
|
||||
|
||||
<CenteredContent>
|
||||
{showLandingPageCopy &&
|
||||
<div className={styles.intro}>
|
||||
<h1 className={styles.title}>Simple, fast, markdown.</h1>
|
||||
<p className={styles.copy}>
|
||||
<div>
|
||||
<Title>Simple, fast, markdown.</Title>
|
||||
<Copy>
|
||||
We're building a modern wiki for engineering teams.
|
||||
</p>
|
||||
</Copy>
|
||||
</div>}
|
||||
<div className={styles.action}>
|
||||
<div>
|
||||
<SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
|
||||
<img
|
||||
alt="Sign in with Slack"
|
||||
|
@ -67,3 +68,15 @@ export default class Home extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 36px;
|
||||
margin-bottom: 24px;
|
||||
}`;
|
||||
|
||||
const Copy = styled.p`
|
||||
font-size: 20px;
|
||||
margin-bottom: 36px;
|
||||
}`;
|
||||
|
||||
export default inject('auth')(Home);
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
.title {
|
||||
font-size: 36px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
font-size: 20px;
|
||||
margin-bottom: 36px;
|
||||
}
|
|
@ -13,6 +13,7 @@ import SearchStore from './SearchStore';
|
|||
import Layout, { Title } from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
type Props = {
|
||||
history: Object,
|
||||
|
@ -56,12 +57,8 @@ type Props = {
|
|||
const title = <Title content="Search" />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={title}
|
||||
titleText="Search"
|
||||
search={false}
|
||||
loading={this.store.isFetching}
|
||||
>
|
||||
<Layout title={title} search={false} loading={this.store.isFetching}>
|
||||
<PageTitle title="Search" />
|
||||
<CenteredContent>
|
||||
{this.props.notFound &&
|
||||
<div>
|
||||
|
|
|
@ -11,6 +11,7 @@ import SettingsStore from './SettingsStore';
|
|||
import Layout, { Title } from 'components/Layout';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import SlackAuthLink from 'components/SlackAuthLink';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
@observer class Settings extends React.Component {
|
||||
store: SettingsStore;
|
||||
|
@ -25,12 +26,8 @@ import SlackAuthLink from 'components/SlackAuthLink';
|
|||
const showSlackSettings = DEPLOYMENT === 'hosted';
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={title}
|
||||
titleText="Settings"
|
||||
search={false}
|
||||
loading={this.store.isFetching}
|
||||
>
|
||||
<Layout title={title} search={false} loading={this.store.isFetching}>
|
||||
<PageTitle title="Settings" />
|
||||
<CenteredContent>
|
||||
{showSlackSettings &&
|
||||
<div className={styles.section}>
|
||||
|
|
|
@ -5,22 +5,24 @@ import queryString from 'query-string';
|
|||
import { observer, inject } from 'mobx-react';
|
||||
import { client } from 'utils/ApiClient';
|
||||
|
||||
import UserStore from 'stores/UserStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
user: UserStore,
|
||||
auth: AuthStore,
|
||||
location: Object,
|
||||
};
|
||||
|
||||
@inject('user')
|
||||
@observer
|
||||
class SlackAuth extends React.Component {
|
||||
type State = {
|
||||
redirectTo: string,
|
||||
};
|
||||
|
||||
@observer class SlackAuth extends React.Component {
|
||||
props: Props;
|
||||
state: State;
|
||||
state = {};
|
||||
|
||||
state: { redirectTo: string };
|
||||
|
||||
// $FlowFixMe not sure why this breaks
|
||||
componentDidMount = async () => {
|
||||
// $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803
|
||||
async componentDidMount(): void {
|
||||
const { error, code, state } = queryString.parse(
|
||||
this.props.location.search
|
||||
);
|
||||
|
@ -46,13 +48,13 @@ class SlackAuth extends React.Component {
|
|||
const redirectTo = sessionStorage.getItem('redirectTo');
|
||||
sessionStorage.removeItem('redirectTo');
|
||||
|
||||
const { success } = await this.props.user.authWithSlack(code, state);
|
||||
const { success } = await this.props.auth.authWithSlack(code, state);
|
||||
success
|
||||
? this.setState({ redirectTo: redirectTo || '/dashboard' })
|
||||
: this.setState({ redirectTo: '/auth/error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -63,4 +65,4 @@ class SlackAuth extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default SlackAuth;
|
||||
export default inject('auth')(SlackAuth);
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
// @flow
|
||||
import { observable, action, computed, autorunAsync } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import UserStore from 'stores/UserStore';
|
||||
import type { User, Team } from 'types';
|
||||
|
||||
const AUTH_STORE = 'AUTH_STORE';
|
||||
|
||||
class AuthStore {
|
||||
@observable user: ?User;
|
||||
@observable team: ?Team;
|
||||
@observable token: ?string;
|
||||
@observable oauthState: string;
|
||||
@observable isLoading: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get authenticated(): boolean {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
@computed get asJson(): string {
|
||||
return JSON.stringify({
|
||||
user: this.user,
|
||||
team: this.team,
|
||||
token: this.token,
|
||||
oauthState: this.oauthState,
|
||||
});
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action logout = (cb: Function) => {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
cb();
|
||||
};
|
||||
|
||||
@action getOauthState = () => {
|
||||
const state = Math.random().toString(36).substring(7);
|
||||
this.oauthState = state;
|
||||
return this.oauthState;
|
||||
};
|
||||
|
||||
@action authWithSlack = async (code: string, state: string) => {
|
||||
if (state !== this.oauthState) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await client.post('/auth.slack', { code });
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
invariant(
|
||||
res && res.data && res.data.user && res.data.team && res.data.accessToken,
|
||||
'All values should be available'
|
||||
);
|
||||
this.user = res.data.user;
|
||||
this.team = res.data.team;
|
||||
this.token = res.data.accessToken;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
getUserStore(): UserStore {
|
||||
invariant(
|
||||
this.user && this.team,
|
||||
'Tried to create a user store without data'
|
||||
);
|
||||
return new UserStore({
|
||||
user: this.user,
|
||||
team: this.team,
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Rehydrate
|
||||
const data = JSON.parse(localStorage.getItem(AUTH_STORE) || '{}');
|
||||
this.user = data.user;
|
||||
this.team = data.team;
|
||||
this.token = data.token;
|
||||
this.oauthState = data.oauthState;
|
||||
|
||||
autorunAsync(() => {
|
||||
localStorage.setItem(AUTH_STORE, this.asJson);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthStore;
|
|
@ -0,0 +1,53 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction, ObservableArray } from 'mobx';
|
||||
import ApiClient, { client } from 'utils/ApiClient';
|
||||
import _ from 'lodash';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import stores from 'stores';
|
||||
import Collection from 'models/Collection';
|
||||
import ErrorsStore from 'stores/ErrorsStore';
|
||||
|
||||
type Options = {
|
||||
teamId: string,
|
||||
};
|
||||
|
||||
class CollectionsStore {
|
||||
@observable data: ObservableArray<Collection> = observable.array([]);
|
||||
@observable isLoaded: boolean = false;
|
||||
|
||||
client: ApiClient;
|
||||
teamId: string;
|
||||
errors: ErrorsStore;
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action fetch = async (): Promise<*> => {
|
||||
try {
|
||||
const res = await this.client.post('/collections.list', {
|
||||
id: this.teamId,
|
||||
});
|
||||
invariant(res && res.data, 'Collection list not available');
|
||||
const { data } = res;
|
||||
runInAction('CollectionsStore#fetch', () => {
|
||||
this.data.replace(data.map(collection => new Collection(collection)));
|
||||
this.isLoaded = true;
|
||||
});
|
||||
} catch (e) {
|
||||
this.errors.add('Failed to load collections');
|
||||
}
|
||||
};
|
||||
|
||||
@action getById = async (id: string): Promise<Collection> => {
|
||||
if (!this.isLoaded) await this.fetch();
|
||||
return _.find(this.data, { id });
|
||||
};
|
||||
|
||||
constructor(options: Options) {
|
||||
this.client = client;
|
||||
this.errors = stores.errors;
|
||||
this.teamId = options.teamId;
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionsStore;
|
|
@ -0,0 +1,54 @@
|
|||
/* eslint-disable */
|
||||
import CollectionsStore from './CollectionsStore';
|
||||
|
||||
jest.mock('utils/ApiClient', () => ({
|
||||
client: { post: {} },
|
||||
}));
|
||||
jest.mock('stores', () => ({ errors: {} }));
|
||||
|
||||
describe('CollectionsStore', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new CollectionsStore({
|
||||
teamId: 123,
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetch', () => {
|
||||
test('should load stores', async () => {
|
||||
store.client = {
|
||||
post: jest.fn(() => ({
|
||||
data: [
|
||||
{
|
||||
name: 'New collection',
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
await store.fetch();
|
||||
|
||||
expect(store.client.post).toHaveBeenCalledWith('/collections.list', {
|
||||
id: 123,
|
||||
});
|
||||
expect(store.data.length).toBe(1);
|
||||
expect(store.data[0].name).toBe('New collection');
|
||||
});
|
||||
|
||||
test('should report errors', async () => {
|
||||
store.client = {
|
||||
post: jest.fn(() => Promise.reject),
|
||||
};
|
||||
store.errors = {
|
||||
add: jest.fn(),
|
||||
};
|
||||
|
||||
await store.fetch();
|
||||
|
||||
expect(store.errors.add).toHaveBeenCalledWith(
|
||||
'Failed to load collections'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { observable, action, computed } from 'mobx';
|
||||
import { observable, action, computed, autorunAsync } from 'mobx';
|
||||
|
||||
const UI_STORE = 'UI_STORE';
|
||||
|
||||
|
@ -24,8 +24,11 @@ class UiStore {
|
|||
// Rehydrate
|
||||
const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}');
|
||||
this.sidebar = data.sidebar;
|
||||
|
||||
autorunAsync(() => {
|
||||
localStorage.setItem(UI_STORE, this.asJson);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default UiStore;
|
||||
export { UI_STORE };
|
||||
|
|
|
@ -1,87 +1,32 @@
|
|||
// @flow
|
||||
import { observable, action, computed } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import { observable, computed } from 'mobx';
|
||||
import type { User, Team } from 'types';
|
||||
|
||||
const USER_STORE = 'USER_STORE';
|
||||
type Options = {
|
||||
user: User,
|
||||
team: Team,
|
||||
};
|
||||
|
||||
class UserStore {
|
||||
@observable user: ?User;
|
||||
@observable team: ?Team;
|
||||
|
||||
@observable token: ?string;
|
||||
@observable oauthState: string;
|
||||
@observable user: User;
|
||||
@observable team: Team;
|
||||
|
||||
@observable isLoading: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get authenticated(): boolean {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
@computed get asJson(): string {
|
||||
return JSON.stringify({
|
||||
user: this.user,
|
||||
team: this.team,
|
||||
token: this.token,
|
||||
oauthState: this.oauthState,
|
||||
});
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action logout = (cb: Function) => {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
cb();
|
||||
};
|
||||
|
||||
@action getOauthState = () => {
|
||||
const state = Math.random().toString(36).substring(7);
|
||||
this.oauthState = state;
|
||||
return this.oauthState;
|
||||
};
|
||||
|
||||
@action authWithSlack = async (code: string, state: string) => {
|
||||
if (state !== this.oauthState) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await client.post('/auth.slack', { code });
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
invariant(
|
||||
res && res.data && res.data.user && res.data.team && res.data.accessToken,
|
||||
'All values should be available'
|
||||
);
|
||||
this.user = res.data.user;
|
||||
this.team = res.data.team;
|
||||
this.token = res.data.accessToken;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(options: Options) {
|
||||
// Rehydrate
|
||||
const data = JSON.parse(localStorage.getItem(USER_STORE) || '{}');
|
||||
this.user = data.user;
|
||||
this.team = data.team;
|
||||
this.token = data.token;
|
||||
this.oauthState = data.oauthState;
|
||||
this.user = options.user;
|
||||
this.team = options.team;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserStore;
|
||||
export { USER_STORE };
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
// @flow
|
||||
import { autorunAsync } from 'mobx';
|
||||
import UserStore, { USER_STORE } from './UserStore';
|
||||
import UiStore, { UI_STORE } from './UiStore';
|
||||
import AuthStore from './AuthStore';
|
||||
import UiStore from './UiStore';
|
||||
import ErrorsStore from './ErrorsStore';
|
||||
|
||||
const stores = {
|
||||
user: new UserStore(),
|
||||
user: null, // Including for Layout
|
||||
auth: new AuthStore(),
|
||||
ui: new UiStore(),
|
||||
errors: new ErrorsStore(),
|
||||
};
|
||||
|
||||
// Persist stores to localStorage
|
||||
// TODO: move to store constructors
|
||||
autorunAsync(() => {
|
||||
localStorage.setItem(USER_STORE, stores.user.asJson);
|
||||
localStorage.setItem(UI_STORE, stores.ui.asJson);
|
||||
});
|
||||
|
||||
export default stores;
|
||||
|
|
|
@ -10,4 +10,6 @@ $headerHeight: 42px;
|
|||
:export {
|
||||
textColor: $textColor;
|
||||
actionColor: $actionColor;
|
||||
|
||||
headerHeight: $headerHeight;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import _ from 'lodash';
|
||||
import invariant from 'invariant';
|
||||
import stores from 'stores';
|
||||
|
||||
type Options = {
|
||||
|
@ -39,9 +40,9 @@ class ApiClient {
|
|||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
if (stores.user.authenticated) {
|
||||
// $FlowFixMe this is not great, need to refactor
|
||||
headers.set('Authorization', `Bearer ${stores.user.token}`);
|
||||
if (stores.auth.authenticated) {
|
||||
invariant(stores.auth.token, 'JWT token not set properly');
|
||||
headers.set('Authorization', `Bearer ${stores.auth.token}`);
|
||||
}
|
||||
|
||||
// Construct request
|
||||
|
@ -62,15 +63,10 @@ class ApiClient {
|
|||
return response;
|
||||
}
|
||||
|
||||
// // Handle 404
|
||||
// if (response.status === 404) {
|
||||
// // return browserHistory.push('/404');
|
||||
// }
|
||||
|
||||
// // Handle 401, log out user
|
||||
// if (response.status === 401) {
|
||||
// return stores.user.logout();
|
||||
// }
|
||||
// Handle 401, log out user
|
||||
if (response.status === 401) {
|
||||
return stores.auth.logout(() => (window.location = '/'));
|
||||
}
|
||||
|
||||
// Handle failed responses
|
||||
const error = {};
|
||||
|
@ -112,5 +108,4 @@ class ApiClient {
|
|||
export default ApiClient;
|
||||
|
||||
// In case you don't want to always initiate, just import with `import { client } ...`
|
||||
const client = new ApiClient();
|
||||
export { client };
|
||||
export const client = new ApiClient();
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
// @flow
|
||||
|
||||
export function homeUrl() {
|
||||
export function homeUrl(): string {
|
||||
return '/dashboard';
|
||||
}
|
||||
|
||||
export function newCollectionUrl() {
|
||||
export function newCollectionUrl(): string {
|
||||
return '/collections/new';
|
||||
}
|
||||
|
||||
export function searchUrl(query: string) {
|
||||
export function searchUrl(query: string): string {
|
||||
if (query) return `/search/${query}`;
|
||||
return `/search`;
|
||||
}
|
||||
|
||||
export function notFoundUrl(): string {
|
||||
return '/404';
|
||||
}
|
||||
|
|
|
@ -41,8 +41,7 @@
|
|||
"json"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"server"
|
||||
"node_modules"
|
||||
],
|
||||
"modulePaths": [
|
||||
"frontend"
|
||||
|
@ -188,4 +187,4 @@
|
|||
"react-addons-test-utils": "^15.3.1",
|
||||
"react-test-renderer": "^15.3.1"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue