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": [
"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
}
}

View File

@ -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

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 { Flex } from 'reflexbox';
import classNames from 'classnames/bind';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
// @flow
import AtlasPreviewLoading from './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';
type Props = {
children: any,
style: Object,
maxWidth: string,
children?: React.Element<any>,
style?: Object,
maxWidth?: string,
};
const CenteredContent = (props: Props) => {

View File

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

View File

@ -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>;
};

View File

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

View File

@ -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
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -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;

View File

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

View File

@ -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();

View File

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

View File

@ -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>&nbsp;/&nbsp;</span>}
<TitleText title={this.props.children} untitled={usePlaceholder}>
<TitleText title={this.props.content} untitled={usePlaceholder}>
{title}
</TitleText>
</span>

View File

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

View File

@ -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} />

View File

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

View File

@ -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 => {

View File

@ -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}>&nbsp;</div>;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')
);

View File

@ -1,3 +1,4 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
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 { 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>

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 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);

View File

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

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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

View File

@ -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;

View File

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

View File

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

View File

@ -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' &&

View File

@ -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 || [];

View File

@ -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();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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"

View File

@ -1,2 +1,3 @@
// @flow
import Home from './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 _ 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

View File

@ -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;

View File

@ -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() {

View File

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

View File

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

View File

@ -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;

View File

@ -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;
};

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

@ -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,

View File

@ -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;
};

View File

@ -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;

View File

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

View File

@ -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