Flow for all the files
This commit is contained in:
parent
a98199599a
commit
0a76d6af9e
28
.eslintrc
28
.eslintrc
|
@ -3,16 +3,34 @@
|
|||
"extends": [
|
||||
"react-app",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
"prettier",
|
||||
"flowtype",
|
||||
],
|
||||
"rules": {
|
||||
"import/order": "warn",
|
||||
// Prettier automatically uses the least amount of parens possible, so this
|
||||
// does more harm than good.
|
||||
"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.
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
|
@ -23,7 +41,10 @@
|
|||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "webpack"
|
||||
"import/resolver": "webpack",
|
||||
"flowtype": {
|
||||
"onlyFilesWithFlowAnnotation": false
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
|
@ -33,6 +54,7 @@
|
|||
"SLACK_KEY": true,
|
||||
"SLACK_REDIRECT_URI": true,
|
||||
"DEPLOYMENT": true,
|
||||
"BASE_URL": true,
|
||||
"afterAll": true
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
[include]
|
||||
.*/frontend/.*
|
||||
|
||||
[ignore]
|
||||
.*/node_modules/styled-components/.*
|
||||
.*/node_modules/react-side-effect/.*
|
||||
.*/node_modules/fbjs/.*
|
||||
.*/node_modules/config-chain/.*
|
||||
|
||||
[libs]
|
||||
|
||||
|
@ -12,8 +16,11 @@ module.system.node.resolve_dirname=node_modules
|
|||
module.system.node.resolve_dirname=frontend
|
||||
|
||||
module.name_mapper='^\(.*\)\.s?css$' -> 'empty/object'
|
||||
module.name_mapper='^\(.*\)\.md$' -> 'empty/object'
|
||||
module.file_ext=.js
|
||||
module.file_ext=.scss
|
||||
module.file_ext=.md
|
||||
module.file_ext=.json
|
||||
|
||||
esproposal.decorators=ignore
|
||||
esproposal.class_static_fields=enable
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
yarn flow
|
|
@ -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;
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Flex } from 'reflexbox';
|
||||
import classNames from 'classnames/bind';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Alert from './Alert';
|
||||
export default Alert;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Link from 'react-router/lib/Link';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentLink from './DocumentLink';
|
||||
export default DocumentLink;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import AtlasPreview from './AtlasPreview';
|
||||
export default AtlasPreview;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import AtlasPreviewLoading from './AtlasPreviewLoading';
|
||||
export default AtlasPreviewLoading;
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
import Button from './Button';
|
||||
export default Button;
|
|
@ -3,9 +3,9 @@ import React from 'react';
|
|||
import styles from './CenteredContent.scss';
|
||||
|
||||
type Props = {
|
||||
children: any,
|
||||
style: Object,
|
||||
maxWidth: string,
|
||||
children?: React.Element<any>,
|
||||
style?: Object,
|
||||
maxWidth?: string,
|
||||
};
|
||||
|
||||
const CenteredContent = (props: Props) => {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import CenteredContent from './CenteredContent';
|
||||
export default CenteredContent;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Divider.scss';
|
||||
|
||||
const Divider = props => {
|
||||
const Divider = () => {
|
||||
return <div className={styles.divider}><span /></div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Divider from './Divider';
|
||||
export default Divider;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
|
@ -18,9 +19,11 @@ import styles from './DocumentHtml.scss';
|
|||
};
|
||||
|
||||
setExternalLinks = () => {
|
||||
// $FlowFixMe
|
||||
const links = ReactDOM.findDOMNode(this).querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
if (link.hostname !== window.location.hostname) {
|
||||
// $FlowFixMe
|
||||
link.target = '_blank'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentHtml from './DocumentHtml';
|
||||
export default DocumentHtml;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import Document from './Document';
|
||||
import DocumentHtml from './components/DocumentHtml';
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentList from './DocumentList';
|
||||
export default DocumentList;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { toJS } from 'mobx';
|
||||
import { Link } from 'react-router';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentPreview from './DocumentPreview';
|
||||
export default DocumentPreview;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { browserHistory } from 'react-router';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './MoreIcon.scss';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import MoreIcon from './MoreIcon';
|
||||
export default MoreIcon;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import DropdownMenu, { MenuItem } from './DropdownMenu';
|
||||
import MoreIcon from './components/MoreIcon';
|
||||
export default DropdownMenu;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { browserHistory, Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
|
@ -10,24 +11,25 @@ import { Flex } from 'reflexbox';
|
|||
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import UserStore from 'stores/UserStore';
|
||||
|
||||
import styles from './Layout.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
const cx = classNames.bind(styles);
|
||||
|
||||
@inject('user')
|
||||
@observer
|
||||
class Layout extends React.Component {
|
||||
static propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
actions: React.PropTypes.node,
|
||||
title: React.PropTypes.node,
|
||||
titleText: React.PropTypes.node,
|
||||
loading: React.PropTypes.bool,
|
||||
user: React.PropTypes.object.isRequired,
|
||||
search: React.PropTypes.bool,
|
||||
notifications: React.PropTypes.node,
|
||||
};
|
||||
type Props = {
|
||||
children?: ?React.Element<any>,
|
||||
actions?: ?React.Element<any>,
|
||||
title?: ?React.Element<any>,
|
||||
titleText?: string,
|
||||
loading?: boolean,
|
||||
user: UserStore,
|
||||
search: ?boolean,
|
||||
notifications?: React.Element<any>,
|
||||
};
|
||||
|
||||
@observer class Layout extends React.Component {
|
||||
props: Props;
|
||||
|
||||
static defaultProps = {
|
||||
search: true,
|
||||
|
@ -114,4 +116,4 @@ const Avatar = styled.img`
|
|||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
export default Layout;
|
||||
export default inject('user')(Layout);
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './HeaderAction.scss';
|
||||
|
||||
const HeaderAction = props => {
|
||||
type Props = { onClick?: ?Function, children?: ?React.Element<any> };
|
||||
|
||||
const HeaderAction = (props: Props) => {
|
||||
return (
|
||||
<div onClick={props.onClick} className={styles.container}>
|
||||
{props.children}
|
||||
|
@ -10,8 +13,4 @@ const HeaderAction = props => {
|
|||
);
|
||||
};
|
||||
|
||||
HeaderAction.propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
};
|
||||
|
||||
export default HeaderAction;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import HeaderAction from './HeaderAction';
|
||||
export default HeaderAction;
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
@observer class SaveAction extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
disabled: React.PropTypes.bool,
|
||||
isNew: React.PropTypes.bool,
|
||||
};
|
||||
type Props = {
|
||||
onClick: Function,
|
||||
disabled?: boolean,
|
||||
isNew?: boolean,
|
||||
};
|
||||
|
||||
onClick = event => {
|
||||
@observer class SaveAction extends React.Component {
|
||||
props: Props;
|
||||
|
||||
onClick = (event: MouseEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import SaveAction from './SaveAction';
|
||||
export default SaveAction;
|
||||
|
|
|
@ -4,9 +4,9 @@ import _ from 'lodash';
|
|||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
children: string,
|
||||
content: string,
|
||||
truncate?: number,
|
||||
placeholder: string,
|
||||
placeholder?: ?string,
|
||||
};
|
||||
|
||||
class Title extends React.Component {
|
||||
|
@ -15,9 +15,9 @@ class Title extends React.Component {
|
|||
render() {
|
||||
let title;
|
||||
if (this.props.truncate) {
|
||||
title = _.truncate(this.props.children, this.props.truncate);
|
||||
title = _.truncate(this.props.content, this.props.truncate);
|
||||
} else {
|
||||
title = this.props.children;
|
||||
title = this.props.content;
|
||||
}
|
||||
|
||||
let usePlaceholder;
|
||||
|
@ -29,7 +29,7 @@ class Title extends React.Component {
|
|||
return (
|
||||
<span>
|
||||
{title && <span> / </span>}
|
||||
<TitleText title={this.props.children} untitled={usePlaceholder}>
|
||||
<TitleText title={this.props.content} untitled={usePlaceholder}>
|
||||
{title}
|
||||
</TitleText>
|
||||
</span>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import Layout from './Layout';
|
||||
import Title from './components/Title';
|
||||
import HeaderAction from './components/HeaderAction';
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './LoadingIndicator.scss';
|
||||
|
||||
const LoadingIndicator = props => {
|
||||
const LoadingIndicator = () => {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.loader} />
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
export default LoadingIndicator;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Codemirror from 'react-codemirror';
|
||||
|
@ -28,13 +29,13 @@ import { client } from 'utils/ApiClient';
|
|||
toggleUploadingIndicator: React.PropTypes.func,
|
||||
};
|
||||
|
||||
onChange = newText => {
|
||||
onChange = (newText: string) => {
|
||||
if (newText !== this.props.text) {
|
||||
this.props.onChange(newText);
|
||||
}
|
||||
};
|
||||
|
||||
onDropAccepted = files => {
|
||||
onDropAccepted = (files: Object[]) => {
|
||||
const file = files[0];
|
||||
const editor = this.getEditorInstance();
|
||||
|
||||
|
@ -62,6 +63,7 @@ import { client } from 'utils/ApiClient';
|
|||
filename: file.name,
|
||||
})
|
||||
.then(response => {
|
||||
// $FlowFixMe need to augment ApiClient
|
||||
const data = response.data;
|
||||
// Upload using FormData API
|
||||
const formData = new FormData();
|
||||
|
@ -77,7 +79,7 @@ import { client } from 'utils/ApiClient';
|
|||
}
|
||||
|
||||
fetch(data.uploadUrl, {
|
||||
method: 'post',
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(_s3Response => {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './ClickablePadding.scss';
|
||||
|
||||
const ClickablePadding = props => {
|
||||
const ClickablePadding = (props: { onClick: Function }) => {
|
||||
return <div className={styles.container} onClick={props.onClick}> </div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import ClickablePadding from './ClickablePadding';
|
||||
export default ClickablePadding;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import MarkdownEditor from './MarkdownEditor';
|
||||
export default MarkdownEditor;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import PublishingInfo from './PublishingInfo';
|
||||
export default PublishingInfo;
|
||||
|
|
|
@ -3,15 +3,15 @@ import React from 'react';
|
|||
import { observer, inject } from 'mobx-react';
|
||||
import UserStore from 'stores/UserStore';
|
||||
|
||||
@inject('user')
|
||||
@observer
|
||||
class SlackAuthLink extends React.Component {
|
||||
props: {
|
||||
children: any,
|
||||
scopes: Array<string>,
|
||||
user: UserStore,
|
||||
redirectUri: string,
|
||||
};
|
||||
type Props = {
|
||||
children: React.Element<any>,
|
||||
scopes?: Array<string>,
|
||||
user: UserStore,
|
||||
redirectUri: string,
|
||||
};
|
||||
|
||||
@observer class SlackAuthLink extends React.Component {
|
||||
props: Props;
|
||||
|
||||
static defaultProps = {
|
||||
scopes: [
|
||||
|
@ -25,10 +25,8 @@ class SlackAuthLink extends React.Component {
|
|||
slackUrl = () => {
|
||||
const baseUrl = 'https://slack.com/oauth/authorize';
|
||||
const params = {
|
||||
// $FlowIssue global variable
|
||||
client_id: SLACK_KEY,
|
||||
scope: this.props.scopes.join(' '),
|
||||
// $FlowIssue global variable
|
||||
scope: this.props.scopes ? this.props.scopes.join(' ') : '',
|
||||
redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
|
||||
state: this.props.user.getOauthState(),
|
||||
};
|
||||
|
@ -47,4 +45,4 @@ class SlackAuthLink extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default SlackAuthLink;
|
||||
export default inject('user')(SlackAuthLink);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import SlackAuthLink from './SlackAuthLink';
|
||||
export default SlackAuthLink;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import history from 'utils/History';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
const Tree = require('js-tree');
|
||||
const proto = Tree.prototype;
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
const React = require('react');
|
||||
const Tree = require('./Tree');
|
||||
const Node = require('./Node');
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import UiTree from './UiTree';
|
||||
export default UiTree;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
|
@ -115,7 +116,7 @@ render(
|
|||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
{__DEV__ && <DevTools position={{ bottom: 0, right: 0 }} />}
|
||||
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
|
||||
</div>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { browserHistory } from 'react-router';
|
||||
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';
|
||||
|
@ -17,12 +19,15 @@ import { Flex } from 'reflexbox';
|
|||
|
||||
import styles from './Atlas.scss';
|
||||
|
||||
type Props = {
|
||||
params: Object,
|
||||
keydown: Object,
|
||||
};
|
||||
|
||||
@keydown(['c'])
|
||||
@observer
|
||||
class Atlas extends React.Component {
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
props: Props;
|
||||
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.params;
|
||||
|
@ -34,7 +39,7 @@ class Atlas extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps = nextProps => {
|
||||
componentWillReceiveProps = (nextProps: Props) => {
|
||||
const key = nextProps.keydown.event;
|
||||
if (key) {
|
||||
if (key.key === 'c') {
|
||||
|
@ -43,9 +48,9 @@ class Atlas extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onCreate = event => {
|
||||
onCreate = (event: Event) => {
|
||||
if (event) event.preventDefault();
|
||||
browserHistory.push(`${store.collection.url}/new`);
|
||||
store.collection && browserHistory.push(`${store.collection.url}/new`);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -65,7 +70,7 @@ class Atlas extends React.Component {
|
|||
</DropdownMenu>
|
||||
</Flex>
|
||||
);
|
||||
title = <Title>{collection.name}</Title>;
|
||||
title = <Title content={collection.name} />;
|
||||
titleText = collection.name;
|
||||
}
|
||||
|
||||
|
@ -81,21 +86,22 @@ class Atlas extends React.Component {
|
|||
>
|
||||
{store.isFetching
|
||||
? <AtlasPreviewLoading />
|
||||
: <div className={styles.container}>
|
||||
<div className={styles.atlasDetails}>
|
||||
<h2>{collection.name}</h2>
|
||||
<blockquote>
|
||||
{collection.description}
|
||||
</blockquote>
|
||||
</div>
|
||||
: collection &&
|
||||
<div className={styles.container}>
|
||||
<div className={styles.atlasDetails}>
|
||||
<h2>{collection.name}</h2>
|
||||
<blockquote>
|
||||
{collection.description}
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
|
||||
<DocumentList
|
||||
documents={collection.recentDocuments}
|
||||
preview
|
||||
/>
|
||||
</div>}
|
||||
<DocumentList
|
||||
documents={collection.recentDocuments}
|
||||
preview
|
||||
/>
|
||||
</div>}
|
||||
</ReactCSSTransitionGroup>
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
|
|
|
@ -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 type { Collection } from 'types';
|
||||
|
||||
const store = new class AtlasStore {
|
||||
@observable collection;
|
||||
@observable collection: ?(Collection & { recentDocuments?: Object[] });
|
||||
|
||||
@observable isFetching = true;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get isLoaded(): boolean {
|
||||
return !this.isFetching && !!this.collection;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action fetchCollection = async (id, successCallback) => {
|
||||
@action fetchCollection = async (id: string, successCallback: Function) => {
|
||||
this.isFetching = true;
|
||||
this.collection = null;
|
||||
|
||||
try {
|
||||
const res = await client.get('/collections.info', { id });
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
const { data } = res;
|
||||
this.collection = data;
|
||||
successCallback(data);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Atlas from './Atlas';
|
||||
export default Atlas;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { Pagination, Collection } from 'types';
|
||||
|
||||
|
@ -23,6 +24,10 @@ class DashboardStore {
|
|||
|
||||
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;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { browserHistory, withRouter } from 'react-router';
|
||||
import keydown from 'react-keydown';
|
||||
import { Flex } from 'reflexbox';
|
||||
|
||||
import DocumentEditStore, { DOCUMENT_EDIT_SETTINGS } from './DocumentEditStore';
|
||||
import EditorLoader from './components/EditorLoader';
|
||||
|
||||
import Layout, { Title, HeaderAction, SaveAction } from 'components/Layout';
|
||||
import { Flex } from 'reflexbox';
|
||||
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
|
||||
|
||||
import EditorLoader from './components/EditorLoader';
|
||||
|
||||
const DISREGARD_CHANGES = `You have unsaved changes.
|
||||
Are you sure you want to disgard them?`;
|
||||
|
||||
type Props = {
|
||||
route: Object,
|
||||
router: Object,
|
||||
params: Object,
|
||||
keydown: Object,
|
||||
};
|
||||
|
||||
@keydown([
|
||||
'cmd+enter',
|
||||
'ctrl+enter',
|
||||
|
@ -27,13 +35,10 @@ Are you sure you want to disgard them?`;
|
|||
@withRouter
|
||||
@observer
|
||||
class DocumentEdit extends Component {
|
||||
static propTypes = {
|
||||
route: React.PropTypes.object.isRequired,
|
||||
router: React.PropTypes.object.isRequired,
|
||||
params: React.PropTypes.object,
|
||||
};
|
||||
store: DocumentEditStore;
|
||||
props: Props;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.store = new DocumentEditStore(
|
||||
JSON.parse(localStorage[DOCUMENT_EDIT_SETTINGS] || '{}')
|
||||
|
@ -60,6 +65,7 @@ class DocumentEdit extends Component {
|
|||
|
||||
// Load editor async
|
||||
EditorLoader().then(({ Editor }) => {
|
||||
// $FlowIssue we can remove after moving to new editor
|
||||
this.setState({ Editor });
|
||||
});
|
||||
|
||||
|
@ -72,7 +78,7 @@ class DocumentEdit extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps = nextProps => {
|
||||
componentWillReceiveProps = (nextProps: Props) => {
|
||||
const key = nextProps.keydown.event;
|
||||
|
||||
if (key) {
|
||||
|
@ -109,7 +115,7 @@ class DocumentEdit extends Component {
|
|||
browserHistory.goBack();
|
||||
};
|
||||
|
||||
onScroll = scrollTop => {
|
||||
onScroll = (scrollTop: number) => {
|
||||
this.setState({
|
||||
scrollTop,
|
||||
});
|
||||
|
@ -119,10 +125,9 @@ class DocumentEdit extends Component {
|
|||
const title = (
|
||||
<Title
|
||||
truncate={60}
|
||||
placeholder={!this.store.isFetching && 'Untitled document'}
|
||||
>
|
||||
{this.store.title}
|
||||
</Title>
|
||||
placeholder={!this.store.isFetching ? 'Untitled document' : null}
|
||||
content={this.store.title}
|
||||
/>
|
||||
);
|
||||
|
||||
const titleText = this.store.title;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// @flow
|
||||
import { observable, action, toJS, autorun } from 'mobx';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import { browserHistory } from 'react-router';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import emojify from 'utils/emojify';
|
||||
import type { Document } from 'types';
|
||||
|
||||
const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS';
|
||||
|
||||
|
@ -22,17 +25,17 @@ const parseHeader = text => {
|
|||
class DocumentEditStore {
|
||||
@observable documentId = null;
|
||||
@observable collectionId = null;
|
||||
@observable parentDocument;
|
||||
@observable title;
|
||||
@observable text;
|
||||
@observable parentDocument: ?Document;
|
||||
@observable title: string;
|
||||
@observable text: string;
|
||||
@observable hasPendingChanges = false;
|
||||
@observable newDocument;
|
||||
@observable newChildDocument;
|
||||
@observable newDocument: ?boolean;
|
||||
@observable newChildDocument: ?boolean;
|
||||
|
||||
@observable preview;
|
||||
@observable isFetching;
|
||||
@observable isSaving;
|
||||
@observable isUploading;
|
||||
@observable preview: ?boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@observable isSaving: boolean = false;
|
||||
@observable isUploading: boolean = false;
|
||||
|
||||
/* Actions */
|
||||
|
||||
|
@ -40,17 +43,18 @@ class DocumentEditStore {
|
|||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const data = await client.get(
|
||||
const res = await client.get(
|
||||
'/documents.info',
|
||||
{
|
||||
id: this.documentId,
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
invariant(res && res.data, 'Data shoule be available');
|
||||
if (this.newChildDocument) {
|
||||
this.parentDocument = data.data;
|
||||
this.parentDocument = res.data;
|
||||
} else {
|
||||
const { title, text } = data.data;
|
||||
const { title, text } = res.data;
|
||||
this.title = title;
|
||||
this.text = text;
|
||||
}
|
||||
|
@ -66,17 +70,19 @@ class DocumentEditStore {
|
|||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const data = await client.post(
|
||||
const res = await client.post(
|
||||
'/documents.create',
|
||||
{
|
||||
parentDocument: this.parentDocument && this.parentDocument.id,
|
||||
// $FlowFixMe this logic will probably get rewritten soon anyway
|
||||
collection: this.collectionId || this.parentDocument.collection.id,
|
||||
title: this.title || 'Untitled document',
|
||||
text: this.text,
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
const { url } = data.data;
|
||||
invariant(res && res.data, 'Data shoule be available');
|
||||
const { url } = res.data;
|
||||
|
||||
this.hasPendingChanges = false;
|
||||
browserHistory.push(url);
|
||||
|
@ -92,7 +98,7 @@ class DocumentEditStore {
|
|||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const data = await client.post(
|
||||
const res = await client.post(
|
||||
'/documents.update',
|
||||
{
|
||||
id: this.documentId,
|
||||
|
@ -101,7 +107,8 @@ class DocumentEditStore {
|
|||
},
|
||||
{ cache: true }
|
||||
);
|
||||
const { url } = data.data;
|
||||
invariant(res && res.data, 'Data shoule be available');
|
||||
const { url } = res.data;
|
||||
|
||||
this.hasPendingChanges = false;
|
||||
browserHistory.push(url);
|
||||
|
@ -111,17 +118,17 @@ class DocumentEditStore {
|
|||
this.isSaving = false;
|
||||
};
|
||||
|
||||
@action updateText = text => {
|
||||
@action updateText = (text: string) => {
|
||||
this.text = text;
|
||||
this.title = parseHeader(text);
|
||||
this.hasPendingChanges = true;
|
||||
};
|
||||
|
||||
@action updateTitle = title => {
|
||||
@action updateTitle = (title: string) => {
|
||||
this.title = title;
|
||||
};
|
||||
|
||||
@action replaceText = args => {
|
||||
@action replaceText = (args: { original: string, new: string }) => {
|
||||
this.text = this.text.replace(args.original, args.new);
|
||||
this.hasPendingChanges = true;
|
||||
};
|
||||
|
@ -147,7 +154,7 @@ class DocumentEditStore {
|
|||
});
|
||||
};
|
||||
|
||||
constructor(settings) {
|
||||
constructor(settings: { preview: ?boolean }) {
|
||||
// Rehydrate settings
|
||||
this.preview = settings.preview;
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { convertToMarkdown } from 'utils/markdown';
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @flow
|
||||
export default () => {
|
||||
return new Promise(resolve => {
|
||||
// $FlowIssue this is available with webpack
|
||||
require.ensure([], () => {
|
||||
resolve({
|
||||
Editor: require('./Editor').default,
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from '../DocumentEdit.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
const cx = classNames.bind(styles);
|
||||
|
||||
class EditorPane extends React.Component {
|
||||
static propTypes = {
|
||||
children: React.PropTypes.node.isRequired,
|
||||
onScroll: React.PropTypes.func.isRequired,
|
||||
scrollTop: React.PropTypes.number,
|
||||
fullWidth: React.PropTypes.bool,
|
||||
};
|
||||
type Props = {
|
||||
children?: ?React.Element<any>,
|
||||
onScroll?: Function,
|
||||
scrollTop?: ?number,
|
||||
fullWidth?: ?boolean,
|
||||
};
|
||||
|
||||
componentWillReceiveProps = nextProps => {
|
||||
class EditorPane extends React.Component {
|
||||
props: Props;
|
||||
|
||||
componentWillReceiveProps = (nextProps: Props) => {
|
||||
if (nextProps.scrollTop) {
|
||||
this.scrollToPosition(nextProps.scrollTop);
|
||||
}
|
||||
|
@ -26,15 +29,16 @@ class EditorPane extends React.Component {
|
|||
this.refs.pane.removeEventListener('scroll', this.handleScroll);
|
||||
};
|
||||
|
||||
handleScroll = e => {
|
||||
handleScroll = (e: Event) => {
|
||||
setTimeout(() => {
|
||||
const element = this.refs.pane;
|
||||
const contentEl = this.refs.content;
|
||||
this.props.onScroll(element.scrollTop / contentEl.offsetHeight);
|
||||
this.props.onScroll &&
|
||||
this.props.onScroll(element.scrollTop / contentEl.offsetHeight);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
scrollToPosition = percentage => {
|
||||
scrollToPosition = (percentage: number) => {
|
||||
const contentEl = this.refs.content;
|
||||
|
||||
// Push to edges
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import { DocumentHtml } from 'components/Document';
|
||||
|
@ -6,7 +7,11 @@ import styles from './Preview.scss';
|
|||
import classNames from 'classnames/bind';
|
||||
const cx = classNames.bind(styles);
|
||||
|
||||
const Preview = props => {
|
||||
type Props = {
|
||||
html: ?string,
|
||||
};
|
||||
|
||||
const Preview = (props: Props) => {
|
||||
return (
|
||||
<div className={cx(styles.container)}>
|
||||
<DocumentHtml html={props.html} />
|
||||
|
@ -14,8 +19,4 @@ const Preview = props => {
|
|||
);
|
||||
};
|
||||
|
||||
Preview.propTypes = {
|
||||
html: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Preview from './Preview';
|
||||
export default Preview;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentEdit from './DocumentEdit';
|
||||
export default DocumentEdit;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import invariant from 'invariant';
|
||||
import { Link, browserHistory } from 'react-router';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
|
@ -6,6 +8,7 @@ import keydown from 'react-keydown';
|
|||
import _ from 'lodash';
|
||||
|
||||
import DocumentSceneStore, { DOCUMENT_PREFERENCES } from './DocumentSceneStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
|
@ -16,13 +19,20 @@ import { Flex } from 'reflexbox';
|
|||
import Sidebar from './components/Sidebar';
|
||||
|
||||
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'])
|
||||
@inject('ui')
|
||||
@observer
|
||||
class DocumentScene extends React.Component {
|
||||
store: DocumentSceneStore;
|
||||
static propTypes = {
|
||||
ui: PropTypes.object.isRequired,
|
||||
routeParams: PropTypes.object,
|
||||
|
@ -30,7 +40,7 @@ class DocumentScene extends React.Component {
|
|||
location: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.store = new DocumentSceneStore(
|
||||
JSON.parse(localStorage[DOCUMENT_PREFERENCES] || '{}')
|
||||
|
@ -41,15 +51,16 @@ class DocumentScene extends React.Component {
|
|||
didScroll: false,
|
||||
};
|
||||
|
||||
componentDidMount = async () => {
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.routeParams;
|
||||
await this.store.fetchDocument(id, {
|
||||
replaceUrl: !this.props.location.hash,
|
||||
});
|
||||
this.scrollTohash();
|
||||
this.store
|
||||
.fetchDocument(id, {
|
||||
replaceUrl: !this.props.location.hash,
|
||||
})
|
||||
.then(() => this.scrollTohash());
|
||||
};
|
||||
|
||||
componentWillReceiveProps = async nextProps => {
|
||||
componentWillReceiveProps = (nextProps: Props) => {
|
||||
const key = nextProps.keydown.event;
|
||||
if (key) {
|
||||
if (key.key === '/' && (key.metaKey || key.ctrl.Key)) {
|
||||
|
@ -57,7 +68,7 @@ class DocumentScene extends React.Component {
|
|||
}
|
||||
|
||||
if (key.key === 'c') {
|
||||
_.defer(this.onCreate);
|
||||
_.defer(this.onCreateDocument);
|
||||
}
|
||||
|
||||
if (key.key === 'e') {
|
||||
|
@ -69,31 +80,38 @@ class DocumentScene extends React.Component {
|
|||
const oldId = this.props.params.id;
|
||||
const newId = nextProps.params.id;
|
||||
if (oldId !== newId) {
|
||||
await this.store.fetchDocument(newId, {
|
||||
softLoad: true,
|
||||
replaceUrl: !this.props.location.hash,
|
||||
});
|
||||
this.store
|
||||
.fetchDocument(newId, {
|
||||
softLoad: true,
|
||||
replaceUrl: !this.props.location.hash,
|
||||
})
|
||||
.then(() => this.scrollTohash());
|
||||
}
|
||||
|
||||
this.scrollTohash();
|
||||
};
|
||||
|
||||
onEdit = () => {
|
||||
invariant(this.store.document, 'Document is not available');
|
||||
const url = `${this.store.document.url}/edit`;
|
||||
browserHistory.push(url);
|
||||
};
|
||||
|
||||
onCreateDocument = () => {
|
||||
invariant(this.store.collectionTree, 'collectionTree is not available');
|
||||
browserHistory.push(`${this.store.collectionTree.url}/new`);
|
||||
};
|
||||
|
||||
onCreateChild = () => {
|
||||
invariant(this.store.document, 'Document is not available');
|
||||
browserHistory.push(`${this.store.document.url}/new`);
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
let msg;
|
||||
if (this.store.document.collection.type === 'atlas') {
|
||||
if (
|
||||
this.store.document &&
|
||||
this.store.document.collection &&
|
||||
this.store.document.collection.type === 'atlas'
|
||||
) {
|
||||
msg =
|
||||
"Are you sure you want to delete this document and all it's child documents (if any)?";
|
||||
} else {
|
||||
|
@ -107,11 +125,13 @@ class DocumentScene extends React.Component {
|
|||
|
||||
onExport = () => {
|
||||
const doc = this.store.document;
|
||||
const a = document.createElement('a');
|
||||
a.textContent = 'download';
|
||||
a.download = `${doc.title}.md`;
|
||||
a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(doc.text)}`;
|
||||
a.click();
|
||||
if (doc) {
|
||||
const a = document.createElement('a');
|
||||
a.textContent = 'download';
|
||||
a.download = `${doc.title}.md`;
|
||||
a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(doc.text)}`;
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
scrollTohash = () => {
|
||||
|
@ -130,6 +150,7 @@ class DocumentScene extends React.Component {
|
|||
const { sidebar } = this.props.ui;
|
||||
|
||||
const doc = this.store.document;
|
||||
if (!doc) return;
|
||||
const allowDelete =
|
||||
doc &&
|
||||
doc.collection.type === 'atlas' &&
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @flow
|
||||
import _ from 'lodash';
|
||||
import { browserHistory } from 'react-router';
|
||||
import invariant from 'invariant';
|
||||
import {
|
||||
observable,
|
||||
action,
|
||||
|
@ -9,26 +11,36 @@ import {
|
|||
autorunAsync,
|
||||
} from 'mobx';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { Document as DocumentType, Collection } from 'types';
|
||||
|
||||
const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES';
|
||||
|
||||
class DocumentSceneStore {
|
||||
@observable document;
|
||||
@observable collapsedNodes = [];
|
||||
type Document = {
|
||||
collection: Collection,
|
||||
} & DocumentType;
|
||||
|
||||
@observable isFetching = true;
|
||||
@observable updatingContent = false;
|
||||
@observable updatingStructure = false;
|
||||
@observable isDeleting;
|
||||
class DocumentSceneStore {
|
||||
@observable document: ?Document;
|
||||
@observable collapsedNodes: string[] = [];
|
||||
|
||||
@observable isFetching: boolean = true;
|
||||
@observable updatingContent: boolean = false;
|
||||
@observable updatingStructure: boolean = false;
|
||||
@observable isDeleting: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get isCollection() {
|
||||
return this.document && this.document.collection.type === 'atlas';
|
||||
@computed get isCollection(): boolean {
|
||||
return !!this.document && this.document.collection.type === 'atlas';
|
||||
}
|
||||
|
||||
@computed get collectionTree() {
|
||||
if (!this.document || this.document.collection.type !== 'atlas') return;
|
||||
@computed get collectionTree(): ?Object {
|
||||
if (
|
||||
!this.document ||
|
||||
this.document.collection ||
|
||||
this.document.collection.type !== 'atlas'
|
||||
)
|
||||
return;
|
||||
const tree = this.document.collection.navigationTree;
|
||||
|
||||
const collapseNodes = node => {
|
||||
|
@ -45,7 +57,10 @@ class DocumentSceneStore {
|
|||
|
||||
/* Actions */
|
||||
|
||||
@action fetchDocument = async (id, options = {}) => {
|
||||
@action fetchDocument = async (
|
||||
id: string,
|
||||
options: { softLoad?: boolean, replaceUrl?: boolean } = {}
|
||||
) => {
|
||||
options = {
|
||||
softLoad: false,
|
||||
replaceUrl: true,
|
||||
|
@ -57,6 +72,7 @@ class DocumentSceneStore {
|
|||
|
||||
try {
|
||||
const res = await client.get('/documents.info', { id });
|
||||
invariant(res && res.data, 'data should be available');
|
||||
const { data } = res;
|
||||
runInAction('fetchDocument', () => {
|
||||
this.document = data;
|
||||
|
@ -70,10 +86,12 @@ class DocumentSceneStore {
|
|||
};
|
||||
|
||||
@action deleteDocument = async () => {
|
||||
if (!this.document) return;
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
await client.post('/documents.delete', { id: this.document.id });
|
||||
// $FlowFixMe don't be stupid
|
||||
browserHistory.push(this.document.collection.url);
|
||||
} catch (e) {
|
||||
console.error('Something went wrong');
|
||||
|
@ -81,8 +99,9 @@ class DocumentSceneStore {
|
|||
this.isFetching = false;
|
||||
};
|
||||
|
||||
@action updateNavigationTree = async tree => {
|
||||
@action updateNavigationTree = async (tree: Object) => {
|
||||
// Only update when tree changes
|
||||
// $FlowFixMe don't be stupid
|
||||
if (_.isEqual(toJS(tree), toJS(this.document.collection.navigationTree))) {
|
||||
return true;
|
||||
}
|
||||
|
@ -91,11 +110,14 @@ class DocumentSceneStore {
|
|||
|
||||
try {
|
||||
const res = await client.post('/collections.updateNavigationTree', {
|
||||
// $FlowFixMe don't be stupid
|
||||
id: this.document.collection.id,
|
||||
tree,
|
||||
});
|
||||
invariant(res && res.data, 'data should be available');
|
||||
runInAction('updateNavigationTree', () => {
|
||||
const { data } = res;
|
||||
// $FlowFixMe don't be stupid
|
||||
this.document.collection = data;
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -104,7 +126,7 @@ class DocumentSceneStore {
|
|||
this.updatingStructure = false;
|
||||
};
|
||||
|
||||
@action onNodeCollapse = nodeId => {
|
||||
@action onNodeCollapse = (nodeId: string) => {
|
||||
if (_.indexOf(this.collapsedNodes, nodeId) >= 0) {
|
||||
this.collapsedNodes = _.without(this.collapsedNodes, nodeId);
|
||||
} else {
|
||||
|
@ -120,7 +142,7 @@ class DocumentSceneStore {
|
|||
});
|
||||
};
|
||||
|
||||
constructor(settings, options) {
|
||||
constructor(settings: { collapsedNodes: string[] }) {
|
||||
// Rehydrate settings
|
||||
this.collapsedNodes = settings.collapsedNodes || [];
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { Flex } from 'reflexbox';
|
||||
|
@ -11,22 +12,25 @@ const cx = classNames.bind(styles);
|
|||
|
||||
import SidebarStore from './SidebarStore';
|
||||
|
||||
@observer class Sidebar extends React.Component {
|
||||
static propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
navigationTree: PropTypes.object.isRequired,
|
||||
onNavigationUpdate: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
};
|
||||
type Props = {
|
||||
open?: boolean,
|
||||
onToggle: Function,
|
||||
navigationTree: Object,
|
||||
onNavigationUpdate: Function,
|
||||
onNodeCollapse: Function,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@observer class Sidebar extends React.Component {
|
||||
props: Props;
|
||||
store: SidebarStore;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.store = new SidebarStore();
|
||||
}
|
||||
|
||||
toggleEdit = e => {
|
||||
toggleEdit = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.store.toggleEdit();
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
class SidebarStore {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Separator.scss';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Separator from './Separator';
|
||||
export default Separator;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Sidebar from './Sidebar';
|
||||
export default Sidebar;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import DocumentScene from './DocumentScene';
|
||||
export default DocumentScene;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Error404 from './Error404';
|
||||
export default Error404;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import ErrorAuth from './ErrorAuth';
|
||||
export default ErrorAuth;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
@ -16,7 +17,11 @@ import { convertToMarkdown } from 'utils/markdown';
|
|||
const { title, content } = this.props.route;
|
||||
|
||||
return (
|
||||
<Layout title={<Title>{title}</Title>} titleText={title} search={false}>
|
||||
<Layout
|
||||
title={<Title content={title} />}
|
||||
titleText={title}
|
||||
search={false}
|
||||
>
|
||||
<CenteredContent>
|
||||
<DocumentHtml html={convertToMarkdown(content)} />
|
||||
</CenteredContent>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Flatpage from './Flatpage';
|
||||
export default Flatpage;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
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 { state } = this.props.location;
|
||||
|
||||
|
@ -54,7 +55,7 @@ export default class Home extends React.Component {
|
|||
</p>
|
||||
</div>}
|
||||
<div className={styles.action}>
|
||||
<SlackAuthLink redirectUri={`${URL}/auth/slack`}>
|
||||
<SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
|
||||
<img
|
||||
alt="Sign in with Slack"
|
||||
height="40"
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Home from './Home';
|
||||
export default Home;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import _ from 'lodash';
|
||||
import { Flex } from 'reflexbox';
|
||||
|
@ -11,13 +12,16 @@ import Layout, { Title } from 'components/Layout';
|
|||
import CenteredContent from 'components/CenteredContent';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
|
||||
@observer class Search extends React.Component {
|
||||
static propTypes = {
|
||||
route: PropTypes.object.isRequired,
|
||||
routeParams: PropTypes.object.isRequired,
|
||||
};
|
||||
type Props = {
|
||||
route: Object,
|
||||
routeParams: Object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@observer class Search extends React.Component {
|
||||
props: Props;
|
||||
store: SearchStore;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.store = new SearchStore();
|
||||
}
|
||||
|
@ -31,7 +35,7 @@ import DocumentPreview from 'components/DocumentPreview';
|
|||
}
|
||||
};
|
||||
|
||||
get viewNotFound() {
|
||||
get viewNotFound(): boolean {
|
||||
const { sceneType } = this.props.route;
|
||||
return sceneType === 'notFound';
|
||||
}
|
||||
|
@ -40,11 +44,7 @@ import DocumentPreview from 'components/DocumentPreview';
|
|||
const search = _.debounce(searchTerm => {
|
||||
this.store.search(searchTerm);
|
||||
}, 250);
|
||||
const title = (
|
||||
<Title>
|
||||
Search
|
||||
</Title>
|
||||
);
|
||||
const title = <Title content="Search" />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { Pagination, Document } from 'types';
|
||||
|
||||
class SearchStore {
|
||||
@observable documents;
|
||||
@observable pagination;
|
||||
@observable selectedDocument;
|
||||
@observable searchTerm;
|
||||
@observable documents: ?(Document[]);
|
||||
@observable pagination: Pagination;
|
||||
@observable searchTerm: ?string = null;
|
||||
|
||||
@observable isFetching = false;
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action search = async query => {
|
||||
@action search = async (query: string) => {
|
||||
this.searchTerm = query;
|
||||
this.isFetching = true;
|
||||
|
||||
if (query) {
|
||||
try {
|
||||
const res = await client.get('/documents.search', { query });
|
||||
invariant(res && res.data && res.pagination, 'API response');
|
||||
const { data, pagination } = res;
|
||||
runInAction('search document', () => {
|
||||
this.documents = data;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
@ -8,8 +9,8 @@ import styles from './SearchField.scss';
|
|||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
onChange = event => {
|
||||
this.props.onChange(event.currentTarget.value);
|
||||
onChange = (event: SyntheticEvent) => {
|
||||
event.currentTarget.value && this.props.onChange(event.currentTarget.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import SearchField from './SearchField';
|
||||
export default SearchField;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Search from './Search';
|
||||
export default Search;
|
||||
|
|
|
@ -13,7 +13,7 @@ import CenteredContent from 'components/CenteredContent';
|
|||
import SlackAuthLink from 'components/SlackAuthLink';
|
||||
|
||||
@observer class Settings extends React.Component {
|
||||
store = SettingsStore;
|
||||
store: SettingsStore;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -21,13 +21,7 @@ import SlackAuthLink from 'components/SlackAuthLink';
|
|||
}
|
||||
|
||||
render() {
|
||||
const title = (
|
||||
<Title>
|
||||
Settings
|
||||
</Title>
|
||||
);
|
||||
|
||||
// $FlowIssue global variable
|
||||
const title = <Title content="Settings" />;
|
||||
const showSlackSettings = DEPLOYMENT === 'hosted';
|
||||
|
||||
return (
|
||||
|
@ -48,8 +42,7 @@ import SlackAuthLink from 'components/SlackAuthLink';
|
|||
|
||||
<SlackAuthLink
|
||||
scopes={['commands']}
|
||||
redirectUri={// $FlowIssue URL is a global variable
|
||||
`${URL}/auth/slack/commands`}
|
||||
redirectUri={`${BASE_URL}/auth/slack/commands`}
|
||||
>
|
||||
<img
|
||||
alt="Add to Slack"
|
||||
|
@ -71,15 +64,16 @@ import SlackAuthLink from 'components/SlackAuthLink';
|
|||
{this.store.apiKeys &&
|
||||
<table className={styles.apiKeyTable}>
|
||||
<tbody>
|
||||
{this.store.apiKeys.map(key => (
|
||||
<ApiKeyRow
|
||||
id={key.id}
|
||||
key={key.id}
|
||||
name={key.name}
|
||||
secret={key.secret}
|
||||
onDelete={this.store.deleteApiKey}
|
||||
/>
|
||||
))}
|
||||
{this.store.apiKeys &&
|
||||
this.store.apiKeys.map(key => (
|
||||
<ApiKeyRow
|
||||
id={key.id}
|
||||
key={key.id}
|
||||
name={key.name}
|
||||
secret={key.secret}
|
||||
onDelete={this.store.deleteApiKey}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>}
|
||||
|
||||
|
@ -106,10 +100,10 @@ class InlineForm extends React.Component {
|
|||
placeholder: string,
|
||||
buttonLabel: string,
|
||||
name: string,
|
||||
value: string,
|
||||
value: ?string,
|
||||
onChange: Function,
|
||||
onSubmit: Function,
|
||||
disabled?: boolean,
|
||||
disabled?: ?boolean,
|
||||
};
|
||||
validationTimeout: number;
|
||||
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
// @flow
|
||||
import { observable, action, runInAction } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { ApiKey } from 'types';
|
||||
|
||||
class SearchStore {
|
||||
@observable apiKeys = [];
|
||||
@observable keyName;
|
||||
@observable apiKeys: ApiKey[] = [];
|
||||
@observable keyName: ?string;
|
||||
|
||||
@observable isFetching;
|
||||
@observable isFetching: boolean = false;
|
||||
|
||||
@action fetchApiKeys = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post('/apiKeys.list');
|
||||
invariant(res && res.data, 'Data shoule be available');
|
||||
const { data } = res;
|
||||
|
||||
runInAction('fetchApiKeys', () => {
|
||||
this.apiKeys = data;
|
||||
});
|
||||
|
@ -27,8 +32,9 @@ class SearchStore {
|
|||
|
||||
try {
|
||||
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;
|
||||
runInAction('createApiKey', () => {
|
||||
this.apiKeys.push(data);
|
||||
|
@ -40,7 +46,7 @@ class SearchStore {
|
|||
this.isFetching = false;
|
||||
};
|
||||
|
||||
@action deleteApiKey = async id => {
|
||||
@action deleteApiKey = async (id: string) => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
|
@ -56,7 +62,7 @@ class SearchStore {
|
|||
this.isFetching = false;
|
||||
};
|
||||
|
||||
@action setKeyName = value => {
|
||||
@action setKeyName = (value: SyntheticInputEvent) => {
|
||||
this.keyName = value.target.value;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import styles from './ApiKeyRow.scss';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import ApiKeyRow from './ApiKeyRow';
|
||||
export default ApiKeyRow;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import Settings from './Settings';
|
||||
export default Settings;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { browserHistory } from 'react-router';
|
||||
|
@ -12,6 +13,7 @@ class SlackAuth extends React.Component {
|
|||
route: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
// $FlowIssue wtf
|
||||
componentDidMount = async () => {
|
||||
const { error, code, state } = this.props.location.query;
|
||||
|
||||
|
@ -22,6 +24,7 @@ class SlackAuth extends React.Component {
|
|||
} else {
|
||||
browserHistory.push('/auth/error');
|
||||
}
|
||||
// $FlowIssue wtf
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// @flow
|
||||
import SlackAuth from './SlackAuth';
|
||||
export default SlackAuth;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import api from './api.markdown';
|
||||
import keyboard from './keyboard.markdown';
|
||||
// @flow
|
||||
import api from './api.md';
|
||||
import keyboard from './keyboard.md';
|
||||
|
||||
export default {
|
||||
api,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
// @flow
|
||||
import { observable, action, computed } from 'mobx';
|
||||
|
||||
const UI_STORE = 'UI_STORE';
|
||||
|
||||
class UiStore {
|
||||
@observable sidebar;
|
||||
@observable sidebar: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get asJson() {
|
||||
@computed get asJson(): string {
|
||||
return JSON.stringify({
|
||||
sidebar: this.sidebar,
|
||||
});
|
||||
|
@ -15,7 +16,7 @@ class UiStore {
|
|||
|
||||
/* Actions */
|
||||
|
||||
@action toggleSidebar = () => {
|
||||
@action toggleSidebar = (): void => {
|
||||
this.sidebar = !this.sidebar;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
// @flow
|
||||
import { observable, action, computed } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { browserHistory } from 'react-router';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { User, Team } from 'types';
|
||||
|
||||
const USER_STORE = 'USER_STORE';
|
||||
|
||||
class UserStore {
|
||||
@observable user;
|
||||
@observable team;
|
||||
@observable user: ?User;
|
||||
@observable team: ?Team;
|
||||
|
||||
@observable token;
|
||||
@observable oauthState;
|
||||
@observable token: ?string;
|
||||
@observable oauthState: string;
|
||||
|
||||
@observable isLoading;
|
||||
@observable isLoading: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get authenticated() {
|
||||
@computed get authenticated(): boolean {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
@computed get asJson() {
|
||||
@computed get asJson(): string {
|
||||
return JSON.stringify({
|
||||
user: this.user,
|
||||
team: this.team,
|
||||
|
@ -42,7 +45,11 @@ class UserStore {
|
|||
return this.oauthState;
|
||||
};
|
||||
|
||||
@action authWithSlack = async (code, state, redirectTo) => {
|
||||
@action authWithSlack = async (
|
||||
code: string,
|
||||
state: string,
|
||||
redirectTo: ?string
|
||||
) => {
|
||||
if (state !== this.oauthState) {
|
||||
browserHistory.push('/auth-error');
|
||||
return;
|
||||
|
@ -56,6 +63,10 @@ class UserStore {
|
|||
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.team = res.data.team;
|
||||
this.token = res.data.accessToken;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import { autorunAsync } from 'mobx';
|
||||
import UserStore, { USER_STORE } from './UserStore';
|
||||
import UiStore, { UI_STORE } from './UiStore';
|
||||
|
|
|
@ -6,12 +6,18 @@ export type User = {
|
|||
username: string,
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
id: string,
|
||||
name: string,
|
||||
};
|
||||
|
||||
export type Collection = {
|
||||
createdAt: string,
|
||||
description: ?string,
|
||||
id: string,
|
||||
name: string,
|
||||
type: 'atlas' | 'journal',
|
||||
navigationTree: Object, // TODO
|
||||
updatedAt: string,
|
||||
url: string,
|
||||
};
|
||||
|
@ -37,3 +43,9 @@ export type Pagination = {
|
|||
nextPath: string,
|
||||
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
Reference in New Issue