From 201f90030c3eff1216008e2cde57f74ea88b9485 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Fri, 26 May 2017 12:56:10 -0700 Subject: [PATCH] Refactored collections store and layout components --- .eslintrc | 1 + frontend/components/Collection/Collection.js | 18 ++- .../components/Collection/Collection.scss | 22 ++++ frontend/components/Editor/plugins.js | 10 +- frontend/components/Layout/Layout.js | 99 ++++++++++++---- frontend/components/Layout/Layout.scss | 72 ----------- frontend/components/PageTitle/PageTitle.js | 11 ++ frontend/components/PageTitle/index.js | 3 + frontend/index.js | 39 ++++-- frontend/models/Collection.js | 2 +- frontend/scenes/Atlas/Atlas.js | 112 ------------------ frontend/scenes/Atlas/Atlas.scss | 17 --- frontend/scenes/Atlas/index.js | 3 - frontend/scenes/Collection/Collection.js | 65 ++++++++++ .../CollectionStore.js} | 0 frontend/scenes/Collection/index.js | 3 + frontend/scenes/Dashboard/Dashboard.js | 34 ++---- frontend/scenes/Dashboard/DashboardStore.js | 50 -------- frontend/scenes/Document/Document.js | 7 +- frontend/scenes/Error404/Error404.js | 4 +- frontend/scenes/ErrorAuth/ErrorAuth.js | 4 +- frontend/scenes/Flatpage/Flatpage.js | 8 +- frontend/scenes/Search/Search.js | 9 +- frontend/scenes/Settings/Settings.js | 9 +- frontend/scenes/SlackAuth/SlackAuth.js | 3 +- frontend/stores/CollectionsStore.js | 59 +++++++++ frontend/stores/CollectionsStore.test.js | 54 +++++++++ frontend/styles/constants.scss | 2 + frontend/utils/routeHelpers.js | 10 +- 29 files changed, 382 insertions(+), 348 deletions(-) delete mode 100644 frontend/components/Layout/Layout.scss create mode 100644 frontend/components/PageTitle/PageTitle.js create mode 100644 frontend/components/PageTitle/index.js delete mode 100644 frontend/scenes/Atlas/Atlas.js delete mode 100644 frontend/scenes/Atlas/Atlas.scss delete mode 100644 frontend/scenes/Atlas/index.js create mode 100644 frontend/scenes/Collection/Collection.js rename frontend/scenes/{Atlas/AtlasStore.js => Collection/CollectionStore.js} (100%) create mode 100644 frontend/scenes/Collection/index.js delete mode 100644 frontend/scenes/Dashboard/DashboardStore.js create mode 100644 frontend/stores/CollectionsStore.js create mode 100644 frontend/stores/CollectionsStore.test.js diff --git a/.eslintrc b/.eslintrc index 9b509975..00a1c024 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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 diff --git a/frontend/components/Collection/Collection.js b/frontend/components/Collection/Collection.js index 4d3f7b6a..c1e4fe92 100644 --- a/frontend/components/Collection/Collection.js +++ b/frontend/components/Collection/Collection.js @@ -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'; {data.recentDocuments.length > 0 ? data.recentDocuments.map(document => { - return ; + return ( + +

{document.title}

+ + {moment(document.updatedAt).fromNow()} + + + ); }) :
No documents. Why not diff --git a/frontend/components/Collection/Collection.scss b/frontend/components/Collection/Collection.scss index 0f0dd2e0..ca141076 100644 --- a/frontend/components/Collection/Collection.scss +++ b/frontend/components/Collection/Collection.scss @@ -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; +} diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index a6940899..bf6d8b7a 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -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', diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 909f4253..a234f717 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -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, actions?: ?React.Element, title?: ?React.Element, - titleText?: string, loading?: boolean, user: UserStore, search: ?boolean, @@ -57,11 +52,9 @@ type Props = { }; return ( -
+ -
- Atlas - +
+ + Atlas + {this.props.title} - </span> - </div> - <Flex className={styles.headerRight}> + + + - + {this.props.actions} {user.user && @@ -91,9 +84,9 @@ type Props = { {this.props.search && -
- Search -
+ + +
} }> @@ -113,16 +106,67 @@ type Props = {
}
-
+ -
+ {this.props.children} -
-
+ + ); } } +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)); diff --git a/frontend/components/Layout/Layout.scss b/frontend/components/Layout/Layout.scss deleted file mode 100644 index e720b670..00000000 --- a/frontend/components/Layout/Layout.scss +++ /dev/null @@ -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; -} diff --git a/frontend/components/PageTitle/PageTitle.js b/frontend/components/PageTitle/PageTitle.js new file mode 100644 index 00000000..1b3526a8 --- /dev/null +++ b/frontend/components/PageTitle/PageTitle.js @@ -0,0 +1,11 @@ +// @flow +import React from 'react'; +import Helmet from 'react-helmet'; + +type Props = { + title: string, +}; + +const PageTitle = ({ title }: Props) => ; + +export default PageTitle; diff --git a/frontend/components/PageTitle/index.js b/frontend/components/PageTitle/index.js new file mode 100644 index 00000000..f9286c54 --- /dev/null +++ b/frontend/components/PageTitle/index.js @@ -0,0 +1,3 @@ +// @flow +import PageTitle from './PageTitle'; +export default PageTitle; diff --git a/frontend/index.js b/frontend/index.js index 425c2b1a..96436d86 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -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, }; const Auth = ({ children }: AuthProps) => { - if (stores.user.authenticated) { - return {children}; + 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 ( + + + {children} + + + ); } else { return ; } @@ -64,10 +87,14 @@ render( + + + + - + - - - - { - 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 = ( - - }> - - New document - - - - ); - title = ; - 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; diff --git a/frontend/scenes/Atlas/Atlas.scss b/frontend/scenes/Atlas/Atlas.scss deleted file mode 100644 index 6c2e2a19..00000000 --- a/frontend/scenes/Atlas/Atlas.scss +++ /dev/null @@ -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; - } -} diff --git a/frontend/scenes/Atlas/index.js b/frontend/scenes/Atlas/index.js deleted file mode 100644 index f28d358e..00000000 --- a/frontend/scenes/Atlas/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import Atlas from './Atlas'; -export default Atlas; diff --git a/frontend/scenes/Collection/Collection.js b/frontend/scenes/Collection/Collection.js new file mode 100644 index 00000000..1b4e8459 --- /dev/null +++ b/frontend/scenes/Collection/Collection.js @@ -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); diff --git a/frontend/scenes/Atlas/AtlasStore.js b/frontend/scenes/Collection/CollectionStore.js similarity index 100% rename from frontend/scenes/Atlas/AtlasStore.js rename to frontend/scenes/Collection/CollectionStore.js diff --git a/frontend/scenes/Collection/index.js b/frontend/scenes/Collection/index.js new file mode 100644 index 00000000..c25fbb86 --- /dev/null +++ b/frontend/scenes/Collection/index.js @@ -0,0 +1,3 @@ +// @flow +import Collection from './Collection'; +export default Collection; diff --git a/frontend/scenes/Dashboard/Dashboard.js b/frontend/scenes/Dashboard/Dashboard.js index ddf38f84..57ed95bb 100644 --- a/frontend/scenes/Dashboard/Dashboard.js +++ b/frontend/scenes/Dashboard/Dashboard.js @@ -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); diff --git a/frontend/scenes/Dashboard/DashboardStore.js b/frontend/scenes/Dashboard/DashboardStore.js deleted file mode 100644 index 35071254..00000000 --- a/frontend/scenes/Dashboard/DashboardStore.js +++ /dev/null @@ -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; diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 686e37f1..77e14a78 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -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> diff --git a/frontend/scenes/Error404/Error404.js b/frontend/scenes/Error404/Error404.js index bc1c767f..d7865b16 100644 --- a/frontend/scenes/Error404/Error404.js +++ b/frontend/scenes/Error404/Error404.js @@ -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> diff --git a/frontend/scenes/ErrorAuth/ErrorAuth.js b/frontend/scenes/ErrorAuth/ErrorAuth.js index e5746fe6..c9cf6f2b 100644 --- a/frontend/scenes/ErrorAuth/ErrorAuth.js +++ b/frontend/scenes/ErrorAuth/ErrorAuth.js @@ -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> diff --git a/frontend/scenes/Flatpage/Flatpage.js b/frontend/scenes/Flatpage/Flatpage.js index 0abd0b53..73a88a62 100644 --- a/frontend/scenes/Flatpage/Flatpage.js +++ b/frontend/scenes/Flatpage/Flatpage.js @@ -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> diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index f7c51784..2365f4ec 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -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> diff --git a/frontend/scenes/Settings/Settings.js b/frontend/scenes/Settings/Settings.js index 49df4a97..e16ac6e9 100644 --- a/frontend/scenes/Settings/Settings.js +++ b/frontend/scenes/Settings/Settings.js @@ -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}> diff --git a/frontend/scenes/SlackAuth/SlackAuth.js b/frontend/scenes/SlackAuth/SlackAuth.js index e29af7bf..8dffa35e 100644 --- a/frontend/scenes/SlackAuth/SlackAuth.js +++ b/frontend/scenes/SlackAuth/SlackAuth.js @@ -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 () => { diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js new file mode 100644 index 00000000..053e7121 --- /dev/null +++ b/frontend/stores/CollectionsStore.js @@ -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; diff --git a/frontend/stores/CollectionsStore.test.js b/frontend/stores/CollectionsStore.test.js new file mode 100644 index 00000000..2694a9dc --- /dev/null +++ b/frontend/stores/CollectionsStore.test.js @@ -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' + ); + }); + }); +}); diff --git a/frontend/styles/constants.scss b/frontend/styles/constants.scss index 8596344e..5b8eec4c 100644 --- a/frontend/styles/constants.scss +++ b/frontend/styles/constants.scss @@ -10,4 +10,6 @@ $headerHeight: 42px; :export { textColor: $textColor; actionColor: $actionColor; + + headerHeight: $headerHeight; } diff --git a/frontend/utils/routeHelpers.js b/frontend/utils/routeHelpers.js index 0b9fcecd..d7a3b14b 100644 --- a/frontend/utils/routeHelpers.js +++ b/frontend/utils/routeHelpers.js @@ -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'; +}