Flow for all the files

This commit is contained in:
Jori Lallo 2017-05-11 17:23:56 -07:00
parent a98199599a
commit 0a76d6af9e
110 changed files with 512 additions and 269 deletions

View File

@ -3,16 +3,34 @@
"extends": [ "extends": [
"react-app", "react-app",
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings" "plugin:import/warnings",
"plugin:flowtype/recommended"
], ],
"plugins": [ "plugins": [
"prettier" "prettier",
"flowtype",
], ],
"rules": { "rules": {
"import/order": "warn", "import/order": "warn",
// Prettier automatically uses the least amount of parens possible, so this // Prettier automatically uses the least amount of parens possible, so this
// does more harm than good. // does more harm than good.
"no-mixed-operators": "off", "no-mixed-operators": "off",
// Flow
"flowtype/require-valid-file-annotation": [
2,
"always",
{
"annotationStyle": "line"
}
],
"flowtype/space-after-type-colon": [
2,
"always"
],
"flowtype/space-before-type-colon": [
2,
"never"
],
// Enforce that code is formatted with prettier. // Enforce that code is formatted with prettier.
"prettier/prettier": [ "prettier/prettier": [
"error", "error",
@ -23,7 +41,10 @@
] ]
}, },
"settings": { "settings": {
"import/resolver": "webpack" "import/resolver": "webpack",
"flowtype": {
"onlyFilesWithFlowAnnotation": false
}
}, },
"env": { "env": {
"jest": true "jest": true
@ -33,6 +54,7 @@
"SLACK_KEY": true, "SLACK_KEY": true,
"SLACK_REDIRECT_URI": true, "SLACK_REDIRECT_URI": true,
"DEPLOYMENT": true, "DEPLOYMENT": true,
"BASE_URL": true,
"afterAll": true "afterAll": true
} }
} }

View File

@ -1,7 +1,11 @@
[include]
.*/frontend/.*
[ignore] [ignore]
.*/node_modules/styled-components/.* .*/node_modules/styled-components/.*
.*/node_modules/react-side-effect/.* .*/node_modules/react-side-effect/.*
.*/node_modules/fbjs/.* .*/node_modules/fbjs/.*
.*/node_modules/config-chain/.*
[libs] [libs]
@ -12,8 +16,11 @@ module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=frontend module.system.node.resolve_dirname=frontend
module.name_mapper='^\(.*\)\.s?css$' -> 'empty/object' module.name_mapper='^\(.*\)\.s?css$' -> 'empty/object'
module.name_mapper='^\(.*\)\.md$' -> 'empty/object'
module.file_ext=.js module.file_ext=.js
module.file_ext=.scss module.file_ext=.scss
module.file_ext=.md
module.file_ext=.json
esproposal.decorators=ignore esproposal.decorators=ignore
esproposal.class_static_fields=enable esproposal.class_static_fields=enable

View File

@ -1 +0,0 @@
yarn flow

6
flow-typed/globals.js vendored Normal file
View File

@ -0,0 +1,6 @@
// @flow
declare var __DEV__: string;
declare var SLACK_REDIRECT_URI: string;
declare var SLACK_KEY: string;
declare var BASE_URL: string;
declare var DEPLOYMENT: string;

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Flex } from 'reflexbox'; import { Flex } from 'reflexbox';
import classNames from 'classnames/bind'; import classNames from 'classnames/bind';

View File

@ -1,2 +1,3 @@
// @flow
import Alert from './Alert'; import Alert from './Alert';
export default Alert; export default Alert;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import Link from 'react-router/lib/Link'; import Link from 'react-router/lib/Link';

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';

View File

@ -1,2 +1,3 @@
// @flow
import DocumentLink from './DocumentLink'; import DocumentLink from './DocumentLink';
export default DocumentLink; export default DocumentLink;

View File

@ -1,2 +1,3 @@
// @flow
import AtlasPreview from './AtlasPreview'; import AtlasPreview from './AtlasPreview';
export default AtlasPreview; export default AtlasPreview;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components';

View File

@ -1,2 +1,3 @@
// @flow
import AtlasPreviewLoading from './AtlasPreviewLoading'; import AtlasPreviewLoading from './AtlasPreviewLoading';
export default AtlasPreviewLoading; export default AtlasPreviewLoading;

View File

@ -1,2 +0,0 @@
import Button from './Button';
export default Button;

View File

@ -3,9 +3,9 @@ import React from 'react';
import styles from './CenteredContent.scss'; import styles from './CenteredContent.scss';
type Props = { type Props = {
children: any, children?: React.Element<any>,
style: Object, style?: Object,
maxWidth: string, maxWidth?: string,
}; };
const CenteredContent = (props: Props) => { const CenteredContent = (props: Props) => {

View File

@ -1,2 +1,3 @@
// @flow
import CenteredContent from './CenteredContent'; import CenteredContent from './CenteredContent';
export default CenteredContent; export default CenteredContent;

View File

@ -1,8 +1,9 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './Divider.scss'; import styles from './Divider.scss';
const Divider = props => { const Divider = () => {
return <div className={styles.divider}><span /></div>; return <div className={styles.divider}><span /></div>;
}; };

View File

@ -1,2 +1,3 @@
// @flow
import Divider from './Divider'; import Divider from './Divider';
export default Divider; export default Divider;

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -18,9 +19,11 @@ import styles from './DocumentHtml.scss';
}; };
setExternalLinks = () => { setExternalLinks = () => {
// $FlowFixMe
const links = ReactDOM.findDOMNode(this).querySelectorAll('a'); const links = ReactDOM.findDOMNode(this).querySelectorAll('a');
links.forEach(link => { links.forEach(link => {
if (link.hostname !== window.location.hostname) { if (link.hostname !== window.location.hostname) {
// $FlowFixMe
link.target = '_blank'; // eslint-disable-line no-param-reassign link.target = '_blank'; // eslint-disable-line no-param-reassign
} }
}); });

View File

@ -1,2 +1,3 @@
// @flow
import DocumentHtml from './DocumentHtml'; import DocumentHtml from './DocumentHtml';
export default DocumentHtml; export default DocumentHtml;

View File

@ -1,3 +1,4 @@
// @flow
import Document from './Document'; import Document from './Document';
import DocumentHtml from './components/DocumentHtml'; import DocumentHtml from './components/DocumentHtml';

View File

@ -1,2 +1,3 @@
// @flow
import DocumentList from './DocumentList'; import DocumentList from './DocumentList';
export default DocumentList; export default DocumentList;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { toJS } from 'mobx'; import { toJS } from 'mobx';
import { Link } from 'react-router'; import { Link } from 'react-router';

View File

@ -1,2 +1,3 @@
// @flow
import DocumentPreview from './DocumentPreview'; import DocumentPreview from './DocumentPreview';
export default DocumentPreview; export default DocumentPreview;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './MoreIcon.scss'; import styles from './MoreIcon.scss';

View File

@ -1,2 +1,3 @@
// @flow
import MoreIcon from './MoreIcon'; import MoreIcon from './MoreIcon';
export default MoreIcon; export default MoreIcon;

View File

@ -1,3 +1,4 @@
// @flow
import DropdownMenu, { MenuItem } from './DropdownMenu'; import DropdownMenu, { MenuItem } from './DropdownMenu';
import MoreIcon from './components/MoreIcon'; import MoreIcon from './components/MoreIcon';
export default DropdownMenu; export default DropdownMenu;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { browserHistory, Link } from 'react-router'; import { browserHistory, Link } from 'react-router';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
@ -10,24 +11,25 @@ import { Flex } from 'reflexbox';
import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
import UserStore from 'stores/UserStore';
import styles from './Layout.scss'; import styles from './Layout.scss';
import classNames from 'classnames/bind'; import classNames from 'classnames/bind';
const cx = classNames.bind(styles); const cx = classNames.bind(styles);
@inject('user') type Props = {
@observer children?: ?React.Element<any>,
class Layout extends React.Component { actions?: ?React.Element<any>,
static propTypes = { title?: ?React.Element<any>,
children: React.PropTypes.node, titleText?: string,
actions: React.PropTypes.node, loading?: boolean,
title: React.PropTypes.node, user: UserStore,
titleText: React.PropTypes.node, search: ?boolean,
loading: React.PropTypes.bool, notifications?: React.Element<any>,
user: React.PropTypes.object.isRequired, };
search: React.PropTypes.bool,
notifications: React.PropTypes.node, @observer class Layout extends React.Component {
}; props: Props;
static defaultProps = { static defaultProps = {
search: true, search: true,
@ -114,4 +116,4 @@ const Avatar = styled.img`
border-radius: 50%; border-radius: 50%;
`; `;
export default Layout; export default inject('user')(Layout);

View File

@ -1,8 +1,11 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './HeaderAction.scss'; import styles from './HeaderAction.scss';
const HeaderAction = props => { type Props = { onClick?: ?Function, children?: ?React.Element<any> };
const HeaderAction = (props: Props) => {
return ( return (
<div onClick={props.onClick} className={styles.container}> <div onClick={props.onClick} className={styles.container}>
{props.children} {props.children}
@ -10,8 +13,4 @@ const HeaderAction = props => {
); );
}; };
HeaderAction.propTypes = {
onClick: React.PropTypes.func,
};
export default HeaderAction; export default HeaderAction;

View File

@ -1,2 +1,3 @@
// @flow
import HeaderAction from './HeaderAction'; import HeaderAction from './HeaderAction';
export default HeaderAction; export default HeaderAction;

View File

@ -1,14 +1,17 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@observer class SaveAction extends React.Component { type Props = {
static propTypes = { onClick: Function,
onClick: React.PropTypes.func.isRequired, disabled?: boolean,
disabled: React.PropTypes.bool, isNew?: boolean,
isNew: React.PropTypes.bool, };
};
onClick = event => { @observer class SaveAction extends React.Component {
props: Props;
onClick = (event: MouseEvent) => {
if (this.props.disabled) return; if (this.props.disabled) return;
event.preventDefault(); event.preventDefault();

View File

@ -1,2 +1,3 @@
// @flow
import SaveAction from './SaveAction'; import SaveAction from './SaveAction';
export default SaveAction; export default SaveAction;

View File

@ -4,9 +4,9 @@ import _ from 'lodash';
import styled from 'styled-components'; import styled from 'styled-components';
type Props = { type Props = {
children: string, content: string,
truncate?: number, truncate?: number,
placeholder: string, placeholder?: ?string,
}; };
class Title extends React.Component { class Title extends React.Component {
@ -15,9 +15,9 @@ class Title extends React.Component {
render() { render() {
let title; let title;
if (this.props.truncate) { if (this.props.truncate) {
title = _.truncate(this.props.children, this.props.truncate); title = _.truncate(this.props.content, this.props.truncate);
} else { } else {
title = this.props.children; title = this.props.content;
} }
let usePlaceholder; let usePlaceholder;
@ -29,7 +29,7 @@ class Title extends React.Component {
return ( return (
<span> <span>
{title && <span>&nbsp;/&nbsp;</span>} {title && <span>&nbsp;/&nbsp;</span>}
<TitleText title={this.props.children} untitled={usePlaceholder}> <TitleText title={this.props.content} untitled={usePlaceholder}>
{title} {title}
</TitleText> </TitleText>
</span> </span>

View File

@ -1,3 +1,4 @@
// @flow
import Layout from './Layout'; import Layout from './Layout';
import Title from './components/Title'; import Title from './components/Title';
import HeaderAction from './components/HeaderAction'; import HeaderAction from './components/HeaderAction';

View File

@ -1,8 +1,9 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './LoadingIndicator.scss'; import styles from './LoadingIndicator.scss';
const LoadingIndicator = props => { const LoadingIndicator = () => {
return ( return (
<div className={styles.loading}> <div className={styles.loading}>
<div className={styles.loader} /> <div className={styles.loader} />

View File

@ -1,2 +1,3 @@
// @flow
import LoadingIndicator from './LoadingIndicator'; import LoadingIndicator from './LoadingIndicator';
export default LoadingIndicator; export default LoadingIndicator;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import Codemirror from 'react-codemirror'; import Codemirror from 'react-codemirror';
@ -28,13 +29,13 @@ import { client } from 'utils/ApiClient';
toggleUploadingIndicator: React.PropTypes.func, toggleUploadingIndicator: React.PropTypes.func,
}; };
onChange = newText => { onChange = (newText: string) => {
if (newText !== this.props.text) { if (newText !== this.props.text) {
this.props.onChange(newText); this.props.onChange(newText);
} }
}; };
onDropAccepted = files => { onDropAccepted = (files: Object[]) => {
const file = files[0]; const file = files[0];
const editor = this.getEditorInstance(); const editor = this.getEditorInstance();
@ -62,6 +63,7 @@ import { client } from 'utils/ApiClient';
filename: file.name, filename: file.name,
}) })
.then(response => { .then(response => {
// $FlowFixMe need to augment ApiClient
const data = response.data; const data = response.data;
// Upload using FormData API // Upload using FormData API
const formData = new FormData(); const formData = new FormData();
@ -77,7 +79,7 @@ import { client } from 'utils/ApiClient';
} }
fetch(data.uploadUrl, { fetch(data.uploadUrl, {
method: 'post', method: 'POST',
body: formData, body: formData,
}) })
.then(_s3Response => { .then(_s3Response => {

View File

@ -1,8 +1,9 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './ClickablePadding.scss'; import styles from './ClickablePadding.scss';
const ClickablePadding = props => { const ClickablePadding = (props: { onClick: Function }) => {
return <div className={styles.container} onClick={props.onClick}>&nbsp;</div>; return <div className={styles.container} onClick={props.onClick}>&nbsp;</div>;
}; };

View File

@ -1,2 +1,3 @@
// @flow
import ClickablePadding from './ClickablePadding'; import ClickablePadding from './ClickablePadding';
export default ClickablePadding; export default ClickablePadding;

View File

@ -1,2 +1,3 @@
// @flow
import MarkdownEditor from './MarkdownEditor'; import MarkdownEditor from './MarkdownEditor';
export default MarkdownEditor; export default MarkdownEditor;

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import moment from 'moment'; import moment from 'moment';
import styled from 'styled-components'; import styled from 'styled-components';

View File

@ -1,2 +1,3 @@
// @flow
import PublishingInfo from './PublishingInfo'; import PublishingInfo from './PublishingInfo';
export default PublishingInfo; export default PublishingInfo;

View File

@ -3,15 +3,15 @@ import React from 'react';
import { observer, inject } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import UserStore from 'stores/UserStore'; import UserStore from 'stores/UserStore';
@inject('user') type Props = {
@observer children: React.Element<any>,
class SlackAuthLink extends React.Component { scopes?: Array<string>,
props: {
children: any,
scopes: Array<string>,
user: UserStore, user: UserStore,
redirectUri: string, redirectUri: string,
}; };
@observer class SlackAuthLink extends React.Component {
props: Props;
static defaultProps = { static defaultProps = {
scopes: [ scopes: [
@ -25,10 +25,8 @@ class SlackAuthLink extends React.Component {
slackUrl = () => { slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize'; const baseUrl = 'https://slack.com/oauth/authorize';
const params = { const params = {
// $FlowIssue global variable
client_id: SLACK_KEY, client_id: SLACK_KEY,
scope: this.props.scopes.join(' '), scope: this.props.scopes ? this.props.scopes.join(' ') : '',
// $FlowIssue global variable
redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI, redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
state: this.props.user.getOauthState(), state: this.props.user.getOauthState(),
}; };
@ -47,4 +45,4 @@ class SlackAuthLink extends React.Component {
} }
} }
export default SlackAuthLink; export default inject('user')(SlackAuthLink);

View File

@ -1,2 +1,3 @@
// @flow
import SlackAuthLink from './SlackAuthLink'; import SlackAuthLink from './SlackAuthLink';
export default SlackAuthLink; export default SlackAuthLink;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import history from 'utils/History'; import history from 'utils/History';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
const Tree = require('js-tree'); const Tree = require('js-tree');
const proto = Tree.prototype; const proto = Tree.prototype;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
const React = require('react'); const React = require('react');
const Tree = require('./Tree'); const Tree = require('./Tree');
const Node = require('./Node'); const Node = require('./Node');

View File

@ -1,2 +1,3 @@
// @flow
import UiTree from './UiTree'; import UiTree from './UiTree';
export default UiTree; export default UiTree;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Provider } from 'mobx-react'; import { Provider } from 'mobx-react';
@ -115,7 +116,7 @@ render(
</Route> </Route>
</Router> </Router>
</Provider> </Provider>
{__DEV__ && <DevTools position={{ bottom: 0, right: 0 }} />} {DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
</div>, </div>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';

View File

@ -1,10 +1,12 @@
import React, { PropTypes } from 'react'; // @flow
import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import keydown from 'react-keydown'; import keydown from 'react-keydown';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import _ from 'lodash'; import _ from 'lodash';
// TODO move here argh
import store from './AtlasStore'; import store from './AtlasStore';
import Layout, { Title } from 'components/Layout'; import Layout, { Title } from 'components/Layout';
@ -17,12 +19,15 @@ import { Flex } from 'reflexbox';
import styles from './Atlas.scss'; import styles from './Atlas.scss';
type Props = {
params: Object,
keydown: Object,
};
@keydown(['c']) @keydown(['c'])
@observer @observer
class Atlas extends React.Component { class Atlas extends React.Component {
static propTypes = { props: Props;
params: PropTypes.object.isRequired,
};
componentDidMount = () => { componentDidMount = () => {
const { id } = this.props.params; const { id } = this.props.params;
@ -34,7 +39,7 @@ class Atlas extends React.Component {
}); });
}; };
componentWillReceiveProps = nextProps => { componentWillReceiveProps = (nextProps: Props) => {
const key = nextProps.keydown.event; const key = nextProps.keydown.event;
if (key) { if (key) {
if (key.key === 'c') { if (key.key === 'c') {
@ -43,9 +48,9 @@ class Atlas extends React.Component {
} }
}; };
onCreate = event => { onCreate = (event: Event) => {
if (event) event.preventDefault(); if (event) event.preventDefault();
browserHistory.push(`${store.collection.url}/new`); store.collection && browserHistory.push(`${store.collection.url}/new`);
}; };
render() { render() {
@ -65,7 +70,7 @@ class Atlas extends React.Component {
</DropdownMenu> </DropdownMenu>
</Flex> </Flex>
); );
title = <Title>{collection.name}</Title>; title = <Title content={collection.name} />;
titleText = collection.name; titleText = collection.name;
} }
@ -81,7 +86,8 @@ class Atlas extends React.Component {
> >
{store.isFetching {store.isFetching
? <AtlasPreviewLoading /> ? <AtlasPreviewLoading />
: <div className={styles.container}> : collection &&
<div className={styles.container}>
<div className={styles.atlasDetails}> <div className={styles.atlasDetails}>
<h2>{collection.name}</h2> <h2>{collection.name}</h2>
<blockquote> <blockquote>

View File

@ -1,19 +1,29 @@
import { observable, action } from 'mobx'; // @flow
import { observable, action, computed } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { Collection } from 'types';
const store = new class AtlasStore { const store = new class AtlasStore {
@observable collection; @observable collection: ?(Collection & { recentDocuments?: Object[] });
@observable isFetching = true; @observable isFetching = true;
/* Computed */
@computed get isLoaded(): boolean {
return !this.isFetching && !!this.collection;
}
/* Actions */ /* Actions */
@action fetchCollection = async (id, successCallback) => { @action fetchCollection = async (id: string, successCallback: Function) => {
this.isFetching = true; this.isFetching = true;
this.collection = null; this.collection = null;
try { try {
const res = await client.get('/collections.info', { id }); const res = await client.get('/collections.info', { id });
invariant(res && res.data, 'Data should be available');
const { data } = res; const { data } = res;
this.collection = data; this.collection = data;
successCallback(data); successCallback(data);

View File

@ -1,2 +1,3 @@
// @flow
import Atlas from './Atlas'; import Atlas from './Atlas';
export default Atlas; export default Atlas;

View File

@ -1,5 +1,6 @@
// @flow // @flow
import { observable, action, runInAction } from 'mobx'; import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { Pagination, Collection } from 'types'; import type { Pagination, Collection } from 'types';
@ -23,6 +24,10 @@ class DashboardStore {
try { try {
const res = await client.post('/collections.list', { id: this.team.id }); 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; const { data, pagination } = res;
runInAction('fetchCollections', () => { runInAction('fetchCollections', () => {
this.collections = data; this.collections = data;

View File

@ -1,2 +1,3 @@
// @flow
import Dashboard from './Dashboard'; import Dashboard from './Dashboard';
export default Dashboard; export default Dashboard;

View File

@ -1,21 +1,29 @@
// @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { browserHistory, withRouter } from 'react-router'; import { browserHistory, withRouter } from 'react-router';
import keydown from 'react-keydown'; import keydown from 'react-keydown';
import { Flex } from 'reflexbox';
import DocumentEditStore, { DOCUMENT_EDIT_SETTINGS } from './DocumentEditStore'; import DocumentEditStore, { DOCUMENT_EDIT_SETTINGS } from './DocumentEditStore';
import EditorLoader from './components/EditorLoader';
import Layout, { Title, HeaderAction, SaveAction } from 'components/Layout'; import Layout, { Title, HeaderAction, SaveAction } from 'components/Layout';
import { Flex } from 'reflexbox';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading'; import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu'; import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
import EditorLoader from './components/EditorLoader';
const DISREGARD_CHANGES = `You have unsaved changes. const DISREGARD_CHANGES = `You have unsaved changes.
Are you sure you want to disgard them?`; Are you sure you want to disgard them?`;
type Props = {
route: Object,
router: Object,
params: Object,
keydown: Object,
};
@keydown([ @keydown([
'cmd+enter', 'cmd+enter',
'ctrl+enter', 'ctrl+enter',
@ -27,13 +35,10 @@ Are you sure you want to disgard them?`;
@withRouter @withRouter
@observer @observer
class DocumentEdit extends Component { class DocumentEdit extends Component {
static propTypes = { store: DocumentEditStore;
route: React.PropTypes.object.isRequired, props: Props;
router: React.PropTypes.object.isRequired,
params: React.PropTypes.object,
};
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.store = new DocumentEditStore( this.store = new DocumentEditStore(
JSON.parse(localStorage[DOCUMENT_EDIT_SETTINGS] || '{}') JSON.parse(localStorage[DOCUMENT_EDIT_SETTINGS] || '{}')
@ -60,6 +65,7 @@ class DocumentEdit extends Component {
// Load editor async // Load editor async
EditorLoader().then(({ Editor }) => { EditorLoader().then(({ Editor }) => {
// $FlowIssue we can remove after moving to new editor
this.setState({ Editor }); this.setState({ Editor });
}); });
@ -72,7 +78,7 @@ class DocumentEdit extends Component {
}); });
}; };
componentWillReceiveProps = nextProps => { componentWillReceiveProps = (nextProps: Props) => {
const key = nextProps.keydown.event; const key = nextProps.keydown.event;
if (key) { if (key) {
@ -109,7 +115,7 @@ class DocumentEdit extends Component {
browserHistory.goBack(); browserHistory.goBack();
}; };
onScroll = scrollTop => { onScroll = (scrollTop: number) => {
this.setState({ this.setState({
scrollTop, scrollTop,
}); });
@ -119,10 +125,9 @@ class DocumentEdit extends Component {
const title = ( const title = (
<Title <Title
truncate={60} truncate={60}
placeholder={!this.store.isFetching && 'Untitled document'} placeholder={!this.store.isFetching ? 'Untitled document' : null}
> content={this.store.title}
{this.store.title} />
</Title>
); );
const titleText = this.store.title; const titleText = this.store.title;

View File

@ -1,7 +1,10 @@
// @flow
import { observable, action, toJS, autorun } from 'mobx'; import { observable, action, toJS, autorun } from 'mobx';
import { client } from 'utils/ApiClient';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import emojify from 'utils/emojify'; import emojify from 'utils/emojify';
import type { Document } from 'types';
const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS'; const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS';
@ -22,17 +25,17 @@ const parseHeader = text => {
class DocumentEditStore { class DocumentEditStore {
@observable documentId = null; @observable documentId = null;
@observable collectionId = null; @observable collectionId = null;
@observable parentDocument; @observable parentDocument: ?Document;
@observable title; @observable title: string;
@observable text; @observable text: string;
@observable hasPendingChanges = false; @observable hasPendingChanges = false;
@observable newDocument; @observable newDocument: ?boolean;
@observable newChildDocument; @observable newChildDocument: ?boolean;
@observable preview; @observable preview: ?boolean = false;
@observable isFetching; @observable isFetching: boolean = false;
@observable isSaving; @observable isSaving: boolean = false;
@observable isUploading; @observable isUploading: boolean = false;
/* Actions */ /* Actions */
@ -40,17 +43,18 @@ class DocumentEditStore {
this.isFetching = true; this.isFetching = true;
try { try {
const data = await client.get( const res = await client.get(
'/documents.info', '/documents.info',
{ {
id: this.documentId, id: this.documentId,
}, },
{ cache: true } { cache: true }
); );
invariant(res && res.data, 'Data shoule be available');
if (this.newChildDocument) { if (this.newChildDocument) {
this.parentDocument = data.data; this.parentDocument = res.data;
} else { } else {
const { title, text } = data.data; const { title, text } = res.data;
this.title = title; this.title = title;
this.text = text; this.text = text;
} }
@ -66,17 +70,19 @@ class DocumentEditStore {
this.isSaving = true; this.isSaving = true;
try { try {
const data = await client.post( const res = await client.post(
'/documents.create', '/documents.create',
{ {
parentDocument: this.parentDocument && this.parentDocument.id, parentDocument: this.parentDocument && this.parentDocument.id,
// $FlowFixMe this logic will probably get rewritten soon anyway
collection: this.collectionId || this.parentDocument.collection.id, collection: this.collectionId || this.parentDocument.collection.id,
title: this.title || 'Untitled document', title: this.title || 'Untitled document',
text: this.text, text: this.text,
}, },
{ cache: true } { cache: true }
); );
const { url } = data.data; invariant(res && res.data, 'Data shoule be available');
const { url } = res.data;
this.hasPendingChanges = false; this.hasPendingChanges = false;
browserHistory.push(url); browserHistory.push(url);
@ -92,7 +98,7 @@ class DocumentEditStore {
this.isSaving = true; this.isSaving = true;
try { try {
const data = await client.post( const res = await client.post(
'/documents.update', '/documents.update',
{ {
id: this.documentId, id: this.documentId,
@ -101,7 +107,8 @@ class DocumentEditStore {
}, },
{ cache: true } { cache: true }
); );
const { url } = data.data; invariant(res && res.data, 'Data shoule be available');
const { url } = res.data;
this.hasPendingChanges = false; this.hasPendingChanges = false;
browserHistory.push(url); browserHistory.push(url);
@ -111,17 +118,17 @@ class DocumentEditStore {
this.isSaving = false; this.isSaving = false;
}; };
@action updateText = text => { @action updateText = (text: string) => {
this.text = text; this.text = text;
this.title = parseHeader(text); this.title = parseHeader(text);
this.hasPendingChanges = true; this.hasPendingChanges = true;
}; };
@action updateTitle = title => { @action updateTitle = (title: string) => {
this.title = title; this.title = title;
}; };
@action replaceText = args => { @action replaceText = (args: { original: string, new: string }) => {
this.text = this.text.replace(args.original, args.new); this.text = this.text.replace(args.original, args.new);
this.hasPendingChanges = true; this.hasPendingChanges = true;
}; };
@ -147,7 +154,7 @@ class DocumentEditStore {
}); });
}; };
constructor(settings) { constructor(settings: { preview: ?boolean }) {
// Rehydrate settings // Rehydrate settings
this.preview = settings.preview; this.preview = settings.preview;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { convertToMarkdown } from 'utils/markdown'; import { convertToMarkdown } from 'utils/markdown';

View File

@ -1,5 +1,7 @@
// @flow
export default () => { export default () => {
return new Promise(resolve => { return new Promise(resolve => {
// $FlowIssue this is available with webpack
require.ensure([], () => { require.ensure([], () => {
resolve({ resolve({
Editor: require('./Editor').default, Editor: require('./Editor').default,

View File

@ -1,18 +1,21 @@
// @flow
import React from 'react'; import React from 'react';
import styles from '../DocumentEdit.scss'; import styles from '../DocumentEdit.scss';
import classNames from 'classnames/bind'; import classNames from 'classnames/bind';
const cx = classNames.bind(styles); const cx = classNames.bind(styles);
class EditorPane extends React.Component { type Props = {
static propTypes = { children?: ?React.Element<any>,
children: React.PropTypes.node.isRequired, onScroll?: Function,
onScroll: React.PropTypes.func.isRequired, scrollTop?: ?number,
scrollTop: React.PropTypes.number, fullWidth?: ?boolean,
fullWidth: React.PropTypes.bool, };
};
componentWillReceiveProps = nextProps => { class EditorPane extends React.Component {
props: Props;
componentWillReceiveProps = (nextProps: Props) => {
if (nextProps.scrollTop) { if (nextProps.scrollTop) {
this.scrollToPosition(nextProps.scrollTop); this.scrollToPosition(nextProps.scrollTop);
} }
@ -26,15 +29,16 @@ class EditorPane extends React.Component {
this.refs.pane.removeEventListener('scroll', this.handleScroll); this.refs.pane.removeEventListener('scroll', this.handleScroll);
}; };
handleScroll = e => { handleScroll = (e: Event) => {
setTimeout(() => { setTimeout(() => {
const element = this.refs.pane; const element = this.refs.pane;
const contentEl = this.refs.content; const contentEl = this.refs.content;
this.props.onScroll &&
this.props.onScroll(element.scrollTop / contentEl.offsetHeight); this.props.onScroll(element.scrollTop / contentEl.offsetHeight);
}, 50); }, 50);
}; };
scrollToPosition = percentage => { scrollToPosition = (percentage: number) => {
const contentEl = this.refs.content; const contentEl = this.refs.content;
// Push to edges // Push to edges

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { DocumentHtml } from 'components/Document'; import { DocumentHtml } from 'components/Document';
@ -6,7 +7,11 @@ import styles from './Preview.scss';
import classNames from 'classnames/bind'; import classNames from 'classnames/bind';
const cx = classNames.bind(styles); const cx = classNames.bind(styles);
const Preview = props => { type Props = {
html: ?string,
};
const Preview = (props: Props) => {
return ( return (
<div className={cx(styles.container)}> <div className={cx(styles.container)}>
<DocumentHtml html={props.html} /> <DocumentHtml html={props.html} />
@ -14,8 +19,4 @@ const Preview = props => {
); );
}; };
Preview.propTypes = {
html: React.PropTypes.string.isRequired,
};
export default Preview; export default Preview;

View File

@ -1,2 +1,3 @@
// @flow
import Preview from './Preview'; import Preview from './Preview';
export default Preview; export default Preview;

View File

@ -1,2 +1,3 @@
// @flow
import DocumentEdit from './DocumentEdit'; import DocumentEdit from './DocumentEdit';
export default DocumentEdit; export default DocumentEdit;

View File

@ -1,4 +1,6 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import invariant from 'invariant';
import { Link, browserHistory } from 'react-router'; import { Link, browserHistory } from 'react-router';
import { observer, inject } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import { toJS } from 'mobx'; import { toJS } from 'mobx';
@ -6,6 +8,7 @@ import keydown from 'react-keydown';
import _ from 'lodash'; import _ from 'lodash';
import DocumentSceneStore, { DOCUMENT_PREFERENCES } from './DocumentSceneStore'; import DocumentSceneStore, { DOCUMENT_PREFERENCES } from './DocumentSceneStore';
import UiStore from 'stores/UiStore';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading'; import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
@ -16,13 +19,20 @@ import { Flex } from 'reflexbox';
import Sidebar from './components/Sidebar'; import Sidebar from './components/Sidebar';
import styles from './DocumentScene.scss'; import styles from './DocumentScene.scss';
// import classNames from 'classnames/bind';
// const cx = classNames.bind(styles); type Props = {
ui: UiStore,
routeParams: Object,
params: Object,
location: Object,
keydown: Object,
};
@keydown(['cmd+/', 'ctrl+/', 'c', 'e']) @keydown(['cmd+/', 'ctrl+/', 'c', 'e'])
@inject('ui') @inject('ui')
@observer @observer
class DocumentScene extends React.Component { class DocumentScene extends React.Component {
store: DocumentSceneStore;
static propTypes = { static propTypes = {
ui: PropTypes.object.isRequired, ui: PropTypes.object.isRequired,
routeParams: PropTypes.object, routeParams: PropTypes.object,
@ -30,7 +40,7 @@ class DocumentScene extends React.Component {
location: PropTypes.object.isRequired, location: PropTypes.object.isRequired,
}; };
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.store = new DocumentSceneStore( this.store = new DocumentSceneStore(
JSON.parse(localStorage[DOCUMENT_PREFERENCES] || '{}') JSON.parse(localStorage[DOCUMENT_PREFERENCES] || '{}')
@ -41,15 +51,16 @@ class DocumentScene extends React.Component {
didScroll: false, didScroll: false,
}; };
componentDidMount = async () => { componentDidMount = () => {
const { id } = this.props.routeParams; const { id } = this.props.routeParams;
await this.store.fetchDocument(id, { this.store
.fetchDocument(id, {
replaceUrl: !this.props.location.hash, replaceUrl: !this.props.location.hash,
}); })
this.scrollTohash(); .then(() => this.scrollTohash());
}; };
componentWillReceiveProps = async nextProps => { componentWillReceiveProps = (nextProps: Props) => {
const key = nextProps.keydown.event; const key = nextProps.keydown.event;
if (key) { if (key) {
if (key.key === '/' && (key.metaKey || key.ctrl.Key)) { if (key.key === '/' && (key.metaKey || key.ctrl.Key)) {
@ -57,7 +68,7 @@ class DocumentScene extends React.Component {
} }
if (key.key === 'c') { if (key.key === 'c') {
_.defer(this.onCreate); _.defer(this.onCreateDocument);
} }
if (key.key === 'e') { if (key.key === 'e') {
@ -69,31 +80,38 @@ class DocumentScene extends React.Component {
const oldId = this.props.params.id; const oldId = this.props.params.id;
const newId = nextProps.params.id; const newId = nextProps.params.id;
if (oldId !== newId) { if (oldId !== newId) {
await this.store.fetchDocument(newId, { this.store
.fetchDocument(newId, {
softLoad: true, softLoad: true,
replaceUrl: !this.props.location.hash, replaceUrl: !this.props.location.hash,
}); })
.then(() => this.scrollTohash());
} }
this.scrollTohash();
}; };
onEdit = () => { onEdit = () => {
invariant(this.store.document, 'Document is not available');
const url = `${this.store.document.url}/edit`; const url = `${this.store.document.url}/edit`;
browserHistory.push(url); browserHistory.push(url);
}; };
onCreateDocument = () => { onCreateDocument = () => {
invariant(this.store.collectionTree, 'collectionTree is not available');
browserHistory.push(`${this.store.collectionTree.url}/new`); browserHistory.push(`${this.store.collectionTree.url}/new`);
}; };
onCreateChild = () => { onCreateChild = () => {
invariant(this.store.document, 'Document is not available');
browserHistory.push(`${this.store.document.url}/new`); browserHistory.push(`${this.store.document.url}/new`);
}; };
onDelete = () => { onDelete = () => {
let msg; let msg;
if (this.store.document.collection.type === 'atlas') { if (
this.store.document &&
this.store.document.collection &&
this.store.document.collection.type === 'atlas'
) {
msg = msg =
"Are you sure you want to delete this document and all it's child documents (if any)?"; "Are you sure you want to delete this document and all it's child documents (if any)?";
} else { } else {
@ -107,11 +125,13 @@ class DocumentScene extends React.Component {
onExport = () => { onExport = () => {
const doc = this.store.document; const doc = this.store.document;
if (doc) {
const a = document.createElement('a'); const a = document.createElement('a');
a.textContent = 'download'; a.textContent = 'download';
a.download = `${doc.title}.md`; a.download = `${doc.title}.md`;
a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(doc.text)}`; a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(doc.text)}`;
a.click(); a.click();
}
}; };
scrollTohash = () => { scrollTohash = () => {
@ -130,6 +150,7 @@ class DocumentScene extends React.Component {
const { sidebar } = this.props.ui; const { sidebar } = this.props.ui;
const doc = this.store.document; const doc = this.store.document;
if (!doc) return;
const allowDelete = const allowDelete =
doc && doc &&
doc.collection.type === 'atlas' && doc.collection.type === 'atlas' &&

View File

@ -1,5 +1,7 @@
// @flow
import _ from 'lodash'; import _ from 'lodash';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import invariant from 'invariant';
import { import {
observable, observable,
action, action,
@ -9,26 +11,36 @@ import {
autorunAsync, autorunAsync,
} from 'mobx'; } from 'mobx';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { Document as DocumentType, Collection } from 'types';
const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES'; const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES';
class DocumentSceneStore { type Document = {
@observable document; collection: Collection,
@observable collapsedNodes = []; } & DocumentType;
@observable isFetching = true; class DocumentSceneStore {
@observable updatingContent = false; @observable document: ?Document;
@observable updatingStructure = false; @observable collapsedNodes: string[] = [];
@observable isDeleting;
@observable isFetching: boolean = true;
@observable updatingContent: boolean = false;
@observable updatingStructure: boolean = false;
@observable isDeleting: boolean = false;
/* Computed */ /* Computed */
@computed get isCollection() { @computed get isCollection(): boolean {
return this.document && this.document.collection.type === 'atlas'; return !!this.document && this.document.collection.type === 'atlas';
} }
@computed get collectionTree() { @computed get collectionTree(): ?Object {
if (!this.document || this.document.collection.type !== 'atlas') return; if (
!this.document ||
this.document.collection ||
this.document.collection.type !== 'atlas'
)
return;
const tree = this.document.collection.navigationTree; const tree = this.document.collection.navigationTree;
const collapseNodes = node => { const collapseNodes = node => {
@ -45,7 +57,10 @@ class DocumentSceneStore {
/* Actions */ /* Actions */
@action fetchDocument = async (id, options = {}) => { @action fetchDocument = async (
id: string,
options: { softLoad?: boolean, replaceUrl?: boolean } = {}
) => {
options = { options = {
softLoad: false, softLoad: false,
replaceUrl: true, replaceUrl: true,
@ -57,6 +72,7 @@ class DocumentSceneStore {
try { try {
const res = await client.get('/documents.info', { id }); const res = await client.get('/documents.info', { id });
invariant(res && res.data, 'data should be available');
const { data } = res; const { data } = res;
runInAction('fetchDocument', () => { runInAction('fetchDocument', () => {
this.document = data; this.document = data;
@ -70,10 +86,12 @@ class DocumentSceneStore {
}; };
@action deleteDocument = async () => { @action deleteDocument = async () => {
if (!this.document) return;
this.isFetching = true; this.isFetching = true;
try { try {
await client.post('/documents.delete', { id: this.document.id }); await client.post('/documents.delete', { id: this.document.id });
// $FlowFixMe don't be stupid
browserHistory.push(this.document.collection.url); browserHistory.push(this.document.collection.url);
} catch (e) { } catch (e) {
console.error('Something went wrong'); console.error('Something went wrong');
@ -81,8 +99,9 @@ class DocumentSceneStore {
this.isFetching = false; this.isFetching = false;
}; };
@action updateNavigationTree = async tree => { @action updateNavigationTree = async (tree: Object) => {
// Only update when tree changes // Only update when tree changes
// $FlowFixMe don't be stupid
if (_.isEqual(toJS(tree), toJS(this.document.collection.navigationTree))) { if (_.isEqual(toJS(tree), toJS(this.document.collection.navigationTree))) {
return true; return true;
} }
@ -91,11 +110,14 @@ class DocumentSceneStore {
try { try {
const res = await client.post('/collections.updateNavigationTree', { const res = await client.post('/collections.updateNavigationTree', {
// $FlowFixMe don't be stupid
id: this.document.collection.id, id: this.document.collection.id,
tree, tree,
}); });
invariant(res && res.data, 'data should be available');
runInAction('updateNavigationTree', () => { runInAction('updateNavigationTree', () => {
const { data } = res; const { data } = res;
// $FlowFixMe don't be stupid
this.document.collection = data; this.document.collection = data;
}); });
} catch (e) { } catch (e) {
@ -104,7 +126,7 @@ class DocumentSceneStore {
this.updatingStructure = false; this.updatingStructure = false;
}; };
@action onNodeCollapse = nodeId => { @action onNodeCollapse = (nodeId: string) => {
if (_.indexOf(this.collapsedNodes, nodeId) >= 0) { if (_.indexOf(this.collapsedNodes, nodeId) >= 0) {
this.collapsedNodes = _.without(this.collapsedNodes, nodeId); this.collapsedNodes = _.without(this.collapsedNodes, nodeId);
} else { } else {
@ -120,7 +142,7 @@ class DocumentSceneStore {
}); });
}; };
constructor(settings, options) { constructor(settings: { collapsedNodes: string[] }) {
// Rehydrate settings // Rehydrate settings
this.collapsedNodes = settings.collapsedNodes || []; this.collapsedNodes = settings.collapsedNodes || [];

View File

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'; // @flow
import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Flex } from 'reflexbox'; import { Flex } from 'reflexbox';
@ -11,22 +12,25 @@ const cx = classNames.bind(styles);
import SidebarStore from './SidebarStore'; import SidebarStore from './SidebarStore';
@observer class Sidebar extends React.Component { type Props = {
static propTypes = { open?: boolean,
open: PropTypes.bool, onToggle: Function,
onToggle: PropTypes.func.isRequired, navigationTree: Object,
navigationTree: PropTypes.object.isRequired, onNavigationUpdate: Function,
onNavigationUpdate: PropTypes.func.isRequired, onNodeCollapse: Function,
onNodeCollapse: PropTypes.func.isRequired, };
};
constructor(props) { @observer class Sidebar extends React.Component {
props: Props;
store: SidebarStore;
constructor(props: Props) {
super(props); super(props);
this.store = new SidebarStore(); this.store = new SidebarStore();
} }
toggleEdit = e => { toggleEdit = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.store.toggleEdit(); this.store.toggleEdit();
}; };

View File

@ -1,3 +1,4 @@
// @flow
import { observable, action } from 'mobx'; import { observable, action } from 'mobx';
class SidebarStore { class SidebarStore {

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import styles from './Separator.scss'; import styles from './Separator.scss';

View File

@ -1,2 +1,3 @@
// @flow
import Separator from './Separator'; import Separator from './Separator';
export default Separator; export default Separator;

View File

@ -1,2 +1,3 @@
// @flow
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
export default Sidebar; export default Sidebar;

View File

@ -1,2 +1,3 @@
// @flow
import DocumentScene from './DocumentScene'; import DocumentScene from './DocumentScene';
export default DocumentScene; export default DocumentScene;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';

View File

@ -1,2 +1,3 @@
// @flow
import Error404 from './Error404'; import Error404 from './Error404';
export default Error404; export default Error404;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';

View File

@ -1,2 +1,3 @@
// @flow
import ErrorAuth from './ErrorAuth'; import ErrorAuth from './ErrorAuth';
export default ErrorAuth; export default ErrorAuth;

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -16,7 +17,11 @@ import { convertToMarkdown } from 'utils/markdown';
const { title, content } = this.props.route; const { title, content } = this.props.route;
return ( return (
<Layout title={<Title>{title}</Title>} titleText={title} search={false}> <Layout
title={<Title content={title} />}
titleText={title}
search={false}
>
<CenteredContent> <CenteredContent>
<DocumentHtml html={convertToMarkdown(content)} /> <DocumentHtml html={convertToMarkdown(content)} />
</CenteredContent> </CenteredContent>

View File

@ -1,2 +1,3 @@
// @flow
import Flatpage from './Flatpage'; import Flatpage from './Flatpage';
export default Flatpage; export default Flatpage;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer, inject } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
@ -24,7 +25,7 @@ export default class Home extends React.Component {
} }
}; };
get notifications() { get notifications(): React.Element<any>[] {
const notifications = []; const notifications = [];
const { state } = this.props.location; const { state } = this.props.location;
@ -54,7 +55,7 @@ export default class Home extends React.Component {
</p> </p>
</div>} </div>}
<div className={styles.action}> <div className={styles.action}>
<SlackAuthLink redirectUri={`${URL}/auth/slack`}> <SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
<img <img
alt="Sign in with Slack" alt="Sign in with Slack"
height="40" height="40"

View File

@ -1,2 +1,3 @@
// @flow
import Home from './Home'; import Home from './Home';
export default Home; export default Home;

View File

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'; // @flow
import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import _ from 'lodash'; import _ from 'lodash';
import { Flex } from 'reflexbox'; import { Flex } from 'reflexbox';
@ -11,13 +12,16 @@ import Layout, { Title } from 'components/Layout';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import DocumentPreview from 'components/DocumentPreview'; import DocumentPreview from 'components/DocumentPreview';
@observer class Search extends React.Component { type Props = {
static propTypes = { route: Object,
route: PropTypes.object.isRequired, routeParams: Object,
routeParams: PropTypes.object.isRequired, };
};
constructor(props) { @observer class Search extends React.Component {
props: Props;
store: SearchStore;
constructor(props: Props) {
super(props); super(props);
this.store = new SearchStore(); this.store = new SearchStore();
} }
@ -31,7 +35,7 @@ import DocumentPreview from 'components/DocumentPreview';
} }
}; };
get viewNotFound() { get viewNotFound(): boolean {
const { sceneType } = this.props.route; const { sceneType } = this.props.route;
return sceneType === 'notFound'; return sceneType === 'notFound';
} }
@ -40,11 +44,7 @@ import DocumentPreview from 'components/DocumentPreview';
const search = _.debounce(searchTerm => { const search = _.debounce(searchTerm => {
this.store.search(searchTerm); this.store.search(searchTerm);
}, 250); }, 250);
const title = ( const title = <Title content="Search" />;
<Title>
Search
</Title>
);
return ( return (
<Layout <Layout

View File

@ -1,23 +1,26 @@
// @flow
import { observable, action, runInAction } from 'mobx'; import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { Pagination, Document } from 'types';
class SearchStore { class SearchStore {
@observable documents; @observable documents: ?(Document[]);
@observable pagination; @observable pagination: Pagination;
@observable selectedDocument; @observable searchTerm: ?string = null;
@observable searchTerm;
@observable isFetching = false; @observable isFetching = false;
/* Actions */ /* Actions */
@action search = async query => { @action search = async (query: string) => {
this.searchTerm = query; this.searchTerm = query;
this.isFetching = true; this.isFetching = true;
if (query) { if (query) {
try { try {
const res = await client.get('/documents.search', { query }); const res = await client.get('/documents.search', { query });
invariant(res && res.data && res.pagination, 'API response');
const { data, pagination } = res; const { data, pagination } = res;
runInAction('search document', () => { runInAction('search document', () => {
this.documents = data; this.documents = data;

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -8,8 +9,8 @@ import styles from './SearchField.scss';
onChange: PropTypes.func, onChange: PropTypes.func,
}; };
onChange = event => { onChange = (event: SyntheticEvent) => {
this.props.onChange(event.currentTarget.value); event.currentTarget.value && this.props.onChange(event.currentTarget.value);
}; };
render() { render() {

View File

@ -1,2 +1,3 @@
// @flow
import SearchField from './SearchField'; import SearchField from './SearchField';
export default SearchField; export default SearchField;

View File

@ -1,2 +1,3 @@
// @flow
import Search from './Search'; import Search from './Search';
export default Search; export default Search;

View File

@ -13,7 +13,7 @@ import CenteredContent from 'components/CenteredContent';
import SlackAuthLink from 'components/SlackAuthLink'; import SlackAuthLink from 'components/SlackAuthLink';
@observer class Settings extends React.Component { @observer class Settings extends React.Component {
store = SettingsStore; store: SettingsStore;
constructor() { constructor() {
super(); super();
@ -21,13 +21,7 @@ import SlackAuthLink from 'components/SlackAuthLink';
} }
render() { render() {
const title = ( const title = <Title content="Settings" />;
<Title>
Settings
</Title>
);
// $FlowIssue global variable
const showSlackSettings = DEPLOYMENT === 'hosted'; const showSlackSettings = DEPLOYMENT === 'hosted';
return ( return (
@ -48,8 +42,7 @@ import SlackAuthLink from 'components/SlackAuthLink';
<SlackAuthLink <SlackAuthLink
scopes={['commands']} scopes={['commands']}
redirectUri={// $FlowIssue URL is a global variable redirectUri={`${BASE_URL}/auth/slack/commands`}
`${URL}/auth/slack/commands`}
> >
<img <img
alt="Add to Slack" alt="Add to Slack"
@ -71,7 +64,8 @@ import SlackAuthLink from 'components/SlackAuthLink';
{this.store.apiKeys && {this.store.apiKeys &&
<table className={styles.apiKeyTable}> <table className={styles.apiKeyTable}>
<tbody> <tbody>
{this.store.apiKeys.map(key => ( {this.store.apiKeys &&
this.store.apiKeys.map(key => (
<ApiKeyRow <ApiKeyRow
id={key.id} id={key.id}
key={key.id} key={key.id}
@ -106,10 +100,10 @@ class InlineForm extends React.Component {
placeholder: string, placeholder: string,
buttonLabel: string, buttonLabel: string,
name: string, name: string,
value: string, value: ?string,
onChange: Function, onChange: Function,
onSubmit: Function, onSubmit: Function,
disabled?: boolean, disabled?: ?boolean,
}; };
validationTimeout: number; validationTimeout: number;

View File

@ -1,18 +1,23 @@
// @flow
import { observable, action, runInAction } from 'mobx'; import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { ApiKey } from 'types';
class SearchStore { class SearchStore {
@observable apiKeys = []; @observable apiKeys: ApiKey[] = [];
@observable keyName; @observable keyName: ?string;
@observable isFetching; @observable isFetching: boolean = false;
@action fetchApiKeys = async () => { @action fetchApiKeys = async () => {
this.isFetching = true; this.isFetching = true;
try { try {
const res = await client.post('/apiKeys.list'); const res = await client.post('/apiKeys.list');
invariant(res && res.data, 'Data shoule be available');
const { data } = res; const { data } = res;
runInAction('fetchApiKeys', () => { runInAction('fetchApiKeys', () => {
this.apiKeys = data; this.apiKeys = data;
}); });
@ -27,8 +32,9 @@ class SearchStore {
try { try {
const res = await client.post('/apiKeys.create', { const res = await client.post('/apiKeys.create', {
name: `${this.keyName}` || 'Untitled key', name: this.keyName ? this.keyName : 'Untitled key',
}); });
invariant(res && res.data, 'Data shoule be available');
const { data } = res; const { data } = res;
runInAction('createApiKey', () => { runInAction('createApiKey', () => {
this.apiKeys.push(data); this.apiKeys.push(data);
@ -40,7 +46,7 @@ class SearchStore {
this.isFetching = false; this.isFetching = false;
}; };
@action deleteApiKey = async id => { @action deleteApiKey = async (id: string) => {
this.isFetching = true; this.isFetching = true;
try { try {
@ -56,7 +62,7 @@ class SearchStore {
this.isFetching = false; this.isFetching = false;
}; };
@action setKeyName = value => { @action setKeyName = (value: SyntheticInputEvent) => {
this.keyName = value.target.value; this.keyName = value.target.value;
}; };

View File

@ -1,3 +1,4 @@
// @flow
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import styles from './ApiKeyRow.scss'; import styles from './ApiKeyRow.scss';

View File

@ -1,2 +1,3 @@
// @flow
import ApiKeyRow from './ApiKeyRow'; import ApiKeyRow from './ApiKeyRow';
export default ApiKeyRow; export default ApiKeyRow;

View File

@ -1,2 +1,3 @@
// @flow
import Settings from './Settings'; import Settings from './Settings';
export default Settings; export default Settings;

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import { observer, inject } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
@ -12,6 +13,7 @@ class SlackAuth extends React.Component {
route: React.PropTypes.object.isRequired, route: React.PropTypes.object.isRequired,
}; };
// $FlowIssue wtf
componentDidMount = async () => { componentDidMount = async () => {
const { error, code, state } = this.props.location.query; const { error, code, state } = this.props.location.query;
@ -22,6 +24,7 @@ class SlackAuth extends React.Component {
} else { } else {
browserHistory.push('/auth/error'); browserHistory.push('/auth/error');
} }
// $FlowIssue wtf
return; return;
} }

View File

@ -1,2 +1,3 @@
// @flow
import SlackAuth from './SlackAuth'; import SlackAuth from './SlackAuth';
export default SlackAuth; export default SlackAuth;

View File

@ -1,5 +1,6 @@
import api from './api.markdown'; // @flow
import keyboard from './keyboard.markdown'; import api from './api.md';
import keyboard from './keyboard.md';
export default { export default {
api, api,

View File

@ -1,13 +1,14 @@
// @flow
import { observable, action, computed } from 'mobx'; import { observable, action, computed } from 'mobx';
const UI_STORE = 'UI_STORE'; const UI_STORE = 'UI_STORE';
class UiStore { class UiStore {
@observable sidebar; @observable sidebar: boolean = false;
/* Computed */ /* Computed */
@computed get asJson() { @computed get asJson(): string {
return JSON.stringify({ return JSON.stringify({
sidebar: this.sidebar, sidebar: this.sidebar,
}); });
@ -15,7 +16,7 @@ class UiStore {
/* Actions */ /* Actions */
@action toggleSidebar = () => { @action toggleSidebar = (): void => {
this.sidebar = !this.sidebar; this.sidebar = !this.sidebar;
}; };

View File

@ -1,25 +1,28 @@
// @flow
import { observable, action, computed } from 'mobx'; import { observable, action, computed } from 'mobx';
import invariant from 'invariant';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import type { User, Team } from 'types';
const USER_STORE = 'USER_STORE'; const USER_STORE = 'USER_STORE';
class UserStore { class UserStore {
@observable user; @observable user: ?User;
@observable team; @observable team: ?Team;
@observable token; @observable token: ?string;
@observable oauthState; @observable oauthState: string;
@observable isLoading; @observable isLoading: boolean = false;
/* Computed */ /* Computed */
@computed get authenticated() { @computed get authenticated(): boolean {
return !!this.token; return !!this.token;
} }
@computed get asJson() { @computed get asJson(): string {
return JSON.stringify({ return JSON.stringify({
user: this.user, user: this.user,
team: this.team, team: this.team,
@ -42,7 +45,11 @@ class UserStore {
return this.oauthState; return this.oauthState;
}; };
@action authWithSlack = async (code, state, redirectTo) => { @action authWithSlack = async (
code: string,
state: string,
redirectTo: ?string
) => {
if (state !== this.oauthState) { if (state !== this.oauthState) {
browserHistory.push('/auth-error'); browserHistory.push('/auth-error');
return; return;
@ -56,6 +63,10 @@ class UserStore {
return; return;
} }
invariant(
res && res.data && res.data.user && res.data.team && res.data.accessToken,
'All values should be available'
);
this.user = res.data.user; this.user = res.data.user;
this.team = res.data.team; this.team = res.data.team;
this.token = res.data.accessToken; this.token = res.data.accessToken;

View File

@ -1,3 +1,4 @@
// @flow
import { autorunAsync } from 'mobx'; import { autorunAsync } from 'mobx';
import UserStore, { USER_STORE } from './UserStore'; import UserStore, { USER_STORE } from './UserStore';
import UiStore, { UI_STORE } from './UiStore'; import UiStore, { UI_STORE } from './UiStore';

View File

@ -6,12 +6,18 @@ export type User = {
username: string, username: string,
}; };
export type Team = {
id: string,
name: string,
};
export type Collection = { export type Collection = {
createdAt: string, createdAt: string,
description: ?string, description: ?string,
id: string, id: string,
name: string, name: string,
type: 'atlas' | 'journal', type: 'atlas' | 'journal',
navigationTree: Object, // TODO
updatedAt: string, updatedAt: string,
url: string, url: string,
}; };
@ -37,3 +43,9 @@ export type Pagination = {
nextPath: string, nextPath: string,
offset: number, offset: number,
}; };
export type ApiKey = {
id: string,
name: ?string,
secret: string,
};

Some files were not shown because too many files have changed in this diff Show More