Refactored collections store and layout components
This commit is contained in:
parent
1247fd1cd2
commit
201f90030c
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,24 +6,19 @@ 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);
|
||||
|
||||
type Props = {
|
||||
history: Object,
|
||||
children?: ?React.Element<any>,
|
||||
actions?: ?React.Element<any>,
|
||||
title?: ?React.Element<any>,
|
||||
titleText?: string,
|
||||
loading?: boolean,
|
||||
user: UserStore,
|
||||
search: ?boolean,
|
||||
|
@ -57,11 +52,9 @@ type 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,16 +67,16 @@ 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 &&
|
||||
|
@ -91,9 +84,9 @@ type Props = {
|
|||
{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} />}>
|
||||
|
@ -113,16 +106,67 @@ type Props = {
|
|||
</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 +177,9 @@ const MenuLink = styled(Link)`
|
|||
color: ${textColor};
|
||||
`;
|
||||
|
||||
const Content = styled(Flex)`
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('user')(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;
|
|
@ -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,35 @@ 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.user.authenticated && stores.user.team) {
|
||||
// Only initialize stores once. Kept in global scope
|
||||
// because otherwise they will get overriden on route
|
||||
// change
|
||||
if (!authenticatedStores) {
|
||||
// Stores for authenticated user
|
||||
authenticatedStores = {
|
||||
collections: new CollectionsStore({
|
||||
teamId: stores.user.team.id,
|
||||
}),
|
||||
};
|
||||
|
||||
authenticatedStores.collections.fetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex auto>
|
||||
<Provider {...authenticatedStores}>
|
||||
{children}
|
||||
</Provider>
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
|
@ -64,10 +87,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 +108,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 AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
|
||||
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>
|
||||
<AtlasPreviewLoading />
|
||||
</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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -17,7 +17,8 @@ type Props = {
|
|||
class SlackAuth extends React.Component {
|
||||
props: Props;
|
||||
|
||||
state: { redirectTo: string };
|
||||
state: { redirectTo?: string };
|
||||
state = {};
|
||||
|
||||
// $FlowFixMe not sure why this breaks
|
||||
componentDidMount = async () => {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction, ObservableArray } from 'mobx';
|
||||
import ApiClient, { client } from 'utils/ApiClient';
|
||||
import _ from 'lodash';
|
||||
|
||||
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,
|
||||
});
|
||||
const { data } = res;
|
||||
runInAction('CollectionsStore#fetch', () => {
|
||||
this.data.replace(
|
||||
data.map(
|
||||
collection =>
|
||||
new Collection({
|
||||
...collection,
|
||||
errors: this.errors,
|
||||
})
|
||||
)
|
||||
);
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,4 +10,6 @@ $headerHeight: 42px;
|
|||
:export {
|
||||
textColor: $textColor;
|
||||
actionColor: $actionColor;
|
||||
|
||||
headerHeight: $headerHeight;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Reference in New Issue