Merge master
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
require('localenv');
|
require('dotenv').config({ silent: true });
|
||||||
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Atlas
|
# Atlas
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install dependencies with `yarn`
|
1. Install dependencies with `yarn`
|
||||||
|
24
circle.yml
Normal file
24
circle.yml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version: 7.6
|
||||||
|
services:
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
ENVIRONMENT: test
|
||||||
|
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||||
|
SEQUELIZE_SECRET: F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B
|
||||||
|
DATABASE_URL_TEST: postgres://ubuntu@localhost:5432/circle_test
|
||||||
|
DATABASE_URL: postgres://ubuntu@localhost:5432/circle_test
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
override:
|
||||||
|
- yarn
|
||||||
|
cache_directories:
|
||||||
|
- ~/.cache/yarn
|
||||||
|
|
||||||
|
test:
|
||||||
|
pre:
|
||||||
|
- sequelize db:migrate --url postgres://ubuntu@localhost:5432/circle_test
|
||||||
|
override:
|
||||||
|
- yarn test
|
||||||
|
- yarn lint
|
@ -1,6 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import classNames from 'classnames/bind';
|
import classNames from 'classnames/bind';
|
||||||
import styles from './Alert.scss';
|
import styles from './Alert.scss';
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.Element<any>,
|
children?: React.Element<any>,
|
||||||
style?: Object,
|
|
||||||
maxWidth?: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
@ -13,20 +11,17 @@ const Container = styled.div`
|
|||||||
margin: 40px 20px;
|
margin: 40px 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CenteredContent = ({
|
const Content = styled.div`
|
||||||
children,
|
max-width: 740px;
|
||||||
maxWidth = '740px',
|
margin: 0 auto;
|
||||||
style,
|
`;
|
||||||
...rest
|
|
||||||
}: Props) => {
|
|
||||||
const styles = {
|
|
||||||
maxWidth,
|
|
||||||
...style,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const CenteredContent = ({ children, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Container style={styles} {...rest}>
|
<Container {...rest}>
|
||||||
{children}
|
<Content>
|
||||||
|
{children}
|
||||||
|
</Content>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ class DocumentViewersStore {
|
|||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await client.get(
|
const res = await client.post(
|
||||||
'/views.list',
|
'/views.list',
|
||||||
{
|
{
|
||||||
id: this.documentId,
|
id: this.documentId,
|
||||||
|
@ -5,7 +5,7 @@ import Popover from 'components/Popover';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import DocumentViewers from './components/DocumentViewers';
|
import DocumentViewers from './components/DocumentViewers';
|
||||||
import DocumentViewersStore from './DocumentViewersStore';
|
import DocumentViewersStore from './DocumentViewersStore';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
|
||||||
const Container = styled(Flex)`
|
const Container = styled(Flex)`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import map from 'lodash/map';
|
import map from 'lodash/map';
|
||||||
import Avatar from 'components/Avatar';
|
import Avatar from 'components/Avatar';
|
||||||
|
108
frontend/components/DropToImport/DropToImport.js
Normal file
108
frontend/components/DropToImport/DropToImport.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inject } from 'mobx-react';
|
||||||
|
import invariant from 'invariant';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import Dropzone from 'react-dropzone';
|
||||||
|
import Document from 'models/Document';
|
||||||
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
|
|
||||||
|
class DropToImport extends Component {
|
||||||
|
state: {
|
||||||
|
isImporting: boolean,
|
||||||
|
};
|
||||||
|
props: {
|
||||||
|
children?: React$Element<any>,
|
||||||
|
collectionId: string,
|
||||||
|
documentId?: string,
|
||||||
|
activeClassName?: string,
|
||||||
|
rejectClassName?: string,
|
||||||
|
documents: DocumentsStore,
|
||||||
|
history: Object,
|
||||||
|
};
|
||||||
|
state = {
|
||||||
|
isImporting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
importFile = async ({ file, documentId, collectionId, redirect }) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async ev => {
|
||||||
|
const text = ev.target.result;
|
||||||
|
let data = {
|
||||||
|
parentDocument: undefined,
|
||||||
|
collection: { id: collectionId },
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (documentId) {
|
||||||
|
data.parentDocument = {
|
||||||
|
id: documentId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = new Document(data);
|
||||||
|
document = await document.save();
|
||||||
|
this.props.documents.add(document);
|
||||||
|
|
||||||
|
if (redirect && this.props.history) {
|
||||||
|
this.props.history.push(document.url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
onDropAccepted = async (files = []) => {
|
||||||
|
this.setState({ isImporting: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let collectionId = this.props.collectionId;
|
||||||
|
const documentId = this.props.documentId;
|
||||||
|
const redirect = files.length === 1;
|
||||||
|
|
||||||
|
if (documentId && !collectionId) {
|
||||||
|
const document = await this.props.documents.fetch(documentId);
|
||||||
|
invariant(document, 'Document not available');
|
||||||
|
collectionId = document.collection.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
await this.importFile({ file, documentId, collectionId, redirect });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: show error alert.
|
||||||
|
} finally {
|
||||||
|
this.setState({ isImporting: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = _.omit(
|
||||||
|
this.props,
|
||||||
|
'history',
|
||||||
|
'documentId',
|
||||||
|
'collectionId',
|
||||||
|
'documents'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropzone
|
||||||
|
accept="text/markdown, text/plain"
|
||||||
|
onDropAccepted={this.onDropAccepted}
|
||||||
|
style={{}}
|
||||||
|
disableClick
|
||||||
|
disablePreview
|
||||||
|
multiple
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{this.state.isImporting && <LoadingIndicator />}
|
||||||
|
{this.props.children}
|
||||||
|
</span>
|
||||||
|
</Dropzone>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject('documents')(DropToImport);
|
3
frontend/components/DropToImport/index.js
Normal file
3
frontend/components/DropToImport/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
import DropToImport from './DropToImport';
|
||||||
|
export default DropToImport;
|
@ -61,6 +61,12 @@ type KeyData = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.readOnly && !this.props.readOnly) {
|
||||||
|
this.focusAtEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
return { starred: this.props.starred };
|
return { starred: this.props.starred };
|
||||||
}
|
}
|
||||||
|
41
frontend/components/Flex/Flex.js
Normal file
41
frontend/components/Flex/Flex.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
type JustifyValues =
|
||||||
|
| 'center'
|
||||||
|
| 'space-around'
|
||||||
|
| 'space-between'
|
||||||
|
| 'flex-start'
|
||||||
|
| 'flex-end';
|
||||||
|
|
||||||
|
type AlignValues =
|
||||||
|
| 'stretch'
|
||||||
|
| 'center'
|
||||||
|
| 'baseline'
|
||||||
|
| 'flex-start'
|
||||||
|
| 'flex-end';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
column?: ?boolean,
|
||||||
|
align?: AlignValues,
|
||||||
|
justify?: JustifyValues,
|
||||||
|
auto?: ?boolean,
|
||||||
|
className?: string,
|
||||||
|
children?: React.Element<any>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Flex = (props: Props) => {
|
||||||
|
const { children, ...restProps } = props;
|
||||||
|
return <Container {...restProps}>{children}</Container>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')};
|
||||||
|
flex-direction: ${({ column }) => (column ? 'column' : 'row')};
|
||||||
|
align-items: ${({ align }) => align};
|
||||||
|
justify-content: ${({ justify }) => justify};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Flex;
|
3
frontend/components/Flex/index.js
Normal file
3
frontend/components/Flex/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
import Flex from './Flex';
|
||||||
|
export default Flex;
|
@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import { size } from 'styles/constants';
|
import { size } from 'styles/constants';
|
||||||
|
|
||||||
const RealTextarea = styled.textarea`
|
const RealTextarea = styled.textarea`
|
||||||
|
@ -6,11 +6,13 @@ import styled from 'styled-components';
|
|||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import keydown from 'react-keydown';
|
import keydown from 'react-keydown';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import { textColor } from 'styles/constants.scss';
|
import { color, layout } from 'styles/constants';
|
||||||
|
|
||||||
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||||
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
||||||
|
import Scrollable from 'components/Scrollable';
|
||||||
|
import Avatar from 'components/Avatar';
|
||||||
|
|
||||||
import SidebarCollection from './components/SidebarCollection';
|
import SidebarCollection from './components/SidebarCollection';
|
||||||
import SidebarCollectionList from './components/SidebarCollectionList';
|
import SidebarCollectionList from './components/SidebarCollectionList';
|
||||||
@ -101,21 +103,24 @@ type Props = {
|
|||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<LinkSection>
|
<Scrollable>
|
||||||
<SidebarLink to="/search">Search</SidebarLink>
|
<LinkSection>
|
||||||
</LinkSection>
|
<SidebarLink to="/search">Search</SidebarLink>
|
||||||
<LinkSection>
|
</LinkSection>
|
||||||
<SidebarLink to="/dashboard">Home</SidebarLink>
|
<LinkSection>
|
||||||
<SidebarLink to="/starred">Starred</SidebarLink>
|
<SidebarLink to="/dashboard">Home</SidebarLink>
|
||||||
</LinkSection>
|
<SidebarLink to="/starred">Starred</SidebarLink>
|
||||||
<LinkSection>
|
</LinkSection>
|
||||||
{ui.activeCollection
|
<LinkSection>
|
||||||
? <SidebarCollection
|
{ui.activeCollection
|
||||||
document={ui.activeDocument}
|
? <SidebarCollection
|
||||||
collection={ui.activeCollection}
|
document={ui.activeDocument}
|
||||||
/>
|
collection={ui.activeCollection}
|
||||||
: <SidebarCollectionList />}
|
history={this.props.history}
|
||||||
</LinkSection>
|
/>
|
||||||
|
: <SidebarCollectionList history={this.props.history} />}
|
||||||
|
</LinkSection>
|
||||||
|
</Scrollable>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Sidebar>}
|
</Sidebar>}
|
||||||
|
|
||||||
@ -135,22 +140,16 @@ const Container = styled(Flex)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const LogoLink = styled(Link)`
|
const LogoLink = styled(Link)`
|
||||||
margin-top: 5px;
|
margin-top: 15px;
|
||||||
font-family: 'Atlas Grotesk';
|
font-family: 'Atlas Grotesk';
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: ${textColor};
|
color: ${color.text};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Avatar = styled.img`
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuLink = styled(Link)`
|
const MenuLink = styled(Link)`
|
||||||
color: ${textColor};
|
color: ${color.text};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Content = styled(Flex)`
|
const Content = styled(Flex)`
|
||||||
@ -159,26 +158,27 @@ const Content = styled(Flex)`
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: ${props => (props.editMode ? 0 : '250px')};
|
left: ${props => (props.editMode ? 0 : layout.sidebarWidth)};
|
||||||
transition: left 200ms ease-in-out;
|
transition: left 200ms ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Sidebar = styled(Flex)`
|
const Sidebar = styled(Flex)`
|
||||||
width: 250px;
|
width: ${layout.sidebarWidth};
|
||||||
margin-left: ${props => (props.editMode ? '-250px' : 0)};
|
margin-left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)};
|
||||||
padding: 10px 20px;
|
|
||||||
background: rgba(250, 251, 252, 0.71);
|
background: rgba(250, 251, 252, 0.71);
|
||||||
border-right: 1px solid #eceff3;
|
border-right: 1px solid #eceff3;
|
||||||
transition: margin-left 200ms ease-in-out;
|
transition: margin-left 200ms ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Header = styled(Flex)`
|
const Header = styled(Flex)`
|
||||||
margin-bottom: 20px;
|
flex-shrink: 0;
|
||||||
|
padding: ${layout.padding};
|
||||||
|
padding-bottom: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LinkSection = styled(Flex)`
|
const LinkSection = styled(Flex)`
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 10px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout));
|
export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout));
|
||||||
|
@ -1,35 +1,51 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import Flex from 'components/Flex';
|
||||||
import { Flex } from 'reflexbox';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { layout } from 'styles/constants';
|
||||||
import SidebarLink from '../SidebarLink';
|
import SidebarLink from '../SidebarLink';
|
||||||
|
import DropToImport from 'components/DropToImport';
|
||||||
|
|
||||||
import Collection from 'models/Collection';
|
import Collection from 'models/Collection';
|
||||||
import Document from 'models/Document';
|
import Document from 'models/Document';
|
||||||
|
import type { NavigationNode } from 'types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collection: ?Collection,
|
collection: ?Collection,
|
||||||
document: ?Document,
|
document: ?Document,
|
||||||
|
history: Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeStyle = {
|
||||||
|
color: '#000',
|
||||||
|
background: '#E1E1E1',
|
||||||
};
|
};
|
||||||
|
|
||||||
class SidebarCollection extends React.Component {
|
class SidebarCollection extends React.Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
renderDocuments(documentList) {
|
renderDocuments(documentList: Array<NavigationNode>, depth: number = 0) {
|
||||||
const { document } = this.props;
|
const { document, history } = this.props;
|
||||||
|
const canDropToImport = depth === 0;
|
||||||
|
|
||||||
if (document) {
|
if (document) {
|
||||||
return documentList.map(doc => (
|
return documentList.map(doc => (
|
||||||
<Flex column key={doc.id}>
|
<Flex column key={doc.id}>
|
||||||
<SidebarLink key={doc.id} to={doc.url}>
|
{canDropToImport &&
|
||||||
{doc.title}
|
<DropToImport
|
||||||
</SidebarLink>
|
history={history}
|
||||||
|
documentId={doc.id}
|
||||||
|
activeStyle={activeStyle}
|
||||||
|
>
|
||||||
|
<SidebarLink to={doc.url}>{doc.title}</SidebarLink>
|
||||||
|
</DropToImport>}
|
||||||
|
{!canDropToImport &&
|
||||||
|
<SidebarLink to={doc.url}>{doc.title}</SidebarLink>}
|
||||||
|
|
||||||
{(document.pathToDocument.includes(doc.id) ||
|
{(document.pathToDocument.includes(doc.id) ||
|
||||||
document.id === doc.id) &&
|
document.id === doc.id) &&
|
||||||
<Children>
|
<Children column>
|
||||||
{doc.children && this.renderDocuments(doc.children)}
|
{doc.children && this.renderDocuments(doc.children, depth + 1)}
|
||||||
</Children>}
|
</Children>}
|
||||||
</Flex>
|
</Flex>
|
||||||
));
|
));
|
||||||
@ -57,10 +73,11 @@ const Header = styled(Flex)`
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #9FA6AB;
|
color: #9FA6AB;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0 ${layout.hpadding};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Children = styled(Flex)`
|
const Children = styled(Flex)`
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(SidebarCollection);
|
export default SidebarCollection;
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { layout } from 'styles/constants';
|
||||||
|
|
||||||
import SidebarLink from '../SidebarLink';
|
import SidebarLink from '../SidebarLink';
|
||||||
|
import DropToImport from 'components/DropToImport';
|
||||||
|
|
||||||
import CollectionsStore from 'stores/CollectionsStore';
|
import CollectionsStore from 'stores/CollectionsStore';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
history: Object,
|
||||||
collections: CollectionsStore,
|
collections: CollectionsStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SidebarCollectionList = observer(({ collections }: Props) => {
|
const activeStyle = {
|
||||||
|
color: '#000',
|
||||||
|
background: '#E1E1E1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarCollectionList = observer(({ history, collections }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header>Collections</Header>
|
<Header>Collections</Header>
|
||||||
{collections.data.map(collection => (
|
{collections.data.map(collection => (
|
||||||
<SidebarLink key={collection.id} to={collection.entryUrl}>
|
<DropToImport
|
||||||
{collection.name}
|
history={history}
|
||||||
</SidebarLink>
|
collectionId={collection.id}
|
||||||
|
activeStyle={activeStyle}
|
||||||
|
>
|
||||||
|
<SidebarLink key={collection.id} to={collection.entryUrl}>
|
||||||
|
{collection.name}
|
||||||
|
</SidebarLink>
|
||||||
|
</DropToImport>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@ -31,6 +45,7 @@ const Header = styled(Flex)`
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #9FA6AB;
|
color: #9FA6AB;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0 ${layout.hpadding};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default inject('collections')(SidebarCollectionList);
|
export default inject('collections')(SidebarCollectionList);
|
||||||
|
@ -1,35 +1,26 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { layout, color } from 'styles/constants';
|
||||||
import { Flex } from 'reflexbox';
|
import { darken } from 'polished';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const activeStyle = {
|
const activeStyle = {
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer class SidebarLink extends React.Component {
|
function SidebarLink(props: Object) {
|
||||||
shouldComponentUpdate(nextProps) {
|
return <StyledNavLink exact {...props} activeStyle={activeStyle} />;
|
||||||
// Navlink is having issues updating, forcing update on URL changes
|
|
||||||
return this.props.match !== nextProps.match;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<LinkContainer>
|
|
||||||
<NavLink exact {...this.props} activeStyle={activeStyle} />
|
|
||||||
</LinkContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkContainer = styled(Flex)`
|
const StyledNavLink = styled(NavLink)`
|
||||||
padding: 5px 0;
|
display: block;
|
||||||
|
padding: 5px ${layout.hpadding};
|
||||||
a {
|
color: ${color.slateDark};
|
||||||
color: #848484;
|
|
||||||
|
&:hover {
|
||||||
|
color: ${darken(0.1, color.slateDark)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withRouter(SidebarLink);
|
export default SidebarLink;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||||
import styled, { keyframes } from 'styled-components';
|
import styled, { keyframes } from 'styled-components';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
|
||||||
import { randomInteger } from 'utils/random';
|
import { randomInteger } from 'utils/random';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ const randomValues = Array.from(
|
|||||||
() => `${randomInteger(85, 100)}%`
|
() => `${randomInteger(85, 100)}%`
|
||||||
);
|
);
|
||||||
|
|
||||||
export default () => {
|
export default (props: {}) => {
|
||||||
return (
|
return (
|
||||||
<ReactCSSTransitionGroup
|
<ReactCSSTransitionGroup
|
||||||
transitionName="fadeIn"
|
transitionName="fadeIn"
|
||||||
@ -22,7 +22,7 @@ export default () => {
|
|||||||
transitionEnterTimeout={0}
|
transitionEnterTimeout={0}
|
||||||
transitionLeaveTimeout={0}
|
transitionLeaveTimeout={0}
|
||||||
>
|
>
|
||||||
<Flex column auto>
|
<Flex column auto {...props}>
|
||||||
<Mask style={{ width: randomValues[0] }} header />
|
<Mask style={{ width: randomValues[0] }} header />
|
||||||
<Mask style={{ width: randomValues[1] }} />
|
<Mask style={{ width: randomValues[1] }} />
|
||||||
<Mask style={{ width: randomValues[2] }} />
|
<Mask style={{ width: randomValues[2] }} />
|
||||||
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import type { User } from 'types';
|
import type { User } from 'types';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
|
||||||
const Container = styled(Flex)`
|
const Container = styled(Flex)`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -51,7 +51,6 @@ class PublishingInfo extends Component {
|
|||||||
<Avatar key={user.id} src={user.avatarUrl} title={user.name} />
|
<Avatar key={user.id} src={user.avatarUrl} title={user.name} />
|
||||||
))}
|
))}
|
||||||
</Avatars>}
|
</Avatars>}
|
||||||
|
|
||||||
{createdAt === updatedAt
|
{createdAt === updatedAt
|
||||||
? <span>
|
? <span>
|
||||||
{createdBy.name}
|
{createdBy.name}
|
||||||
|
18
frontend/components/ScrollToTop/ScrollToTop.js
Normal file
18
frontend/components/ScrollToTop/ScrollToTop.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// @flow
|
||||||
|
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
|
||||||
|
import { Component } from 'react';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
|
||||||
|
class ScrollToTop extends Component {
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.location !== prevProps.location) {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(ScrollToTop);
|
3
frontend/components/ScrollToTop/index.js
Normal file
3
frontend/components/ScrollToTop/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
import ScrollToTop from './ScrollToTop';
|
||||||
|
export default ScrollToTop;
|
25
frontend/components/SidebarHidden/SidebarHidden.js
Normal file
25
frontend/components/SidebarHidden/SidebarHidden.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// @flow
|
||||||
|
import { Component } from 'react';
|
||||||
|
import { inject } from 'mobx-react';
|
||||||
|
import UiStore from 'stores/UiStore';
|
||||||
|
|
||||||
|
class SidebarHidden extends Component {
|
||||||
|
props: {
|
||||||
|
ui: UiStore,
|
||||||
|
children: React$Element<any>,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.ui.enableEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.ui.disableEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject('ui')(SidebarHidden);
|
3
frontend/components/SidebarHidden/index.js
Normal file
3
frontend/components/SidebarHidden/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
import SidebarHidden from './SidebarHidden';
|
||||||
|
export default SidebarHidden;
|
@ -8,7 +8,7 @@ import {
|
|||||||
Route,
|
Route,
|
||||||
Redirect,
|
Redirect,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
|
||||||
import stores from 'stores';
|
import stores from 'stores';
|
||||||
import DocumentsStore from 'stores/DocumentsStore';
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
@ -33,7 +33,9 @@ import Flatpage from 'scenes/Flatpage';
|
|||||||
import ErrorAuth from 'scenes/ErrorAuth';
|
import ErrorAuth from 'scenes/ErrorAuth';
|
||||||
import Error404 from 'scenes/Error404';
|
import Error404 from 'scenes/Error404';
|
||||||
|
|
||||||
|
import ScrollToTop from 'components/ScrollToTop';
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
|
import SidebarHidden from 'components/SidebarHidden';
|
||||||
|
|
||||||
import flatpages from 'static/flatpages';
|
import flatpages from 'static/flatpages';
|
||||||
|
|
||||||
@ -85,52 +87,73 @@ const KeyboardShortcuts = () => (
|
|||||||
);
|
);
|
||||||
const Api = () => <Flatpage title="API" content={flatpages.api} />;
|
const Api = () => <Flatpage title="API" content={flatpages.api} />;
|
||||||
const DocumentNew = () => <Document newDocument />;
|
const DocumentNew = () => <Document newDocument />;
|
||||||
const DocumentNewChild = () => <Document newChildDocument />;
|
const RedirectDocument = ({ match }: { match: Object }) => (
|
||||||
|
<Redirect to={`/doc/${match.params.documentSlug}`} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchDocumentSlug = ':documentSlug([0-9a-zA-Z-]*-[a-zA-z0-9]{10,15})';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<div style={{ display: 'flex', flex: 1, height: '100%' }}>
|
<div style={{ display: 'flex', flex: 1, height: '100%' }}>
|
||||||
<Provider {...stores}>
|
<Provider {...stores}>
|
||||||
<Router>
|
<Router>
|
||||||
<Switch>
|
<ScrollToTop>
|
||||||
<Route exact path="/" component={Home} />
|
<Switch>
|
||||||
|
<Route exact path="/" component={Home} />
|
||||||
|
|
||||||
<Route exact path="/auth/slack" component={SlackAuth} />
|
<Route exact path="/auth/slack" component={SlackAuth} />
|
||||||
<Route exact path="/auth/slack/commands" component={SlackAuth} />
|
<Route exact path="/auth/slack/commands" component={SlackAuth} />
|
||||||
<Route exact path="/auth/error" component={ErrorAuth} />
|
<Route exact path="/auth/error" component={ErrorAuth} />
|
||||||
|
|
||||||
<Auth>
|
<Auth>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/dashboard" component={Dashboard} />
|
<Route exact path="/dashboard" component={Dashboard} />
|
||||||
<Route exact path="/starred" component={Starred} />
|
<Route exact path="/starred" component={Starred} />
|
||||||
<Route exact path="/collections/:id" component={Collection} />
|
<Route exact path="/collections/:id" component={Collection} />
|
||||||
<Route exact path="/d/:id" component={Document} />
|
<Route
|
||||||
|
exact
|
||||||
|
path={`/d/${matchDocumentSlug}`}
|
||||||
|
component={RedirectDocument}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`/doc/${matchDocumentSlug}`}
|
||||||
|
component={Document}
|
||||||
|
/>
|
||||||
|
<SidebarHidden>
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`/doc/${matchDocumentSlug}/:edit`}
|
||||||
|
component={Document}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/collections/:id/new"
|
||||||
|
component={DocumentNew}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</SidebarHidden>
|
||||||
|
|
||||||
<Route exact path="/d/:id/:edit" component={Document} />
|
<Route exact path="/search" component={Search} />
|
||||||
<Route
|
<Route exact path="/search/:query" component={Search} />
|
||||||
exact
|
<Route exact path="/settings" component={Settings} />
|
||||||
path="/collections/:id/new"
|
|
||||||
component={DocumentNew}
|
|
||||||
/>
|
|
||||||
<Route exact path="/d/:id/new" component={DocumentNewChild} />
|
|
||||||
|
|
||||||
<Route exact path="/search" component={Search} />
|
<Route
|
||||||
<Route exact path="/search/:query" component={Search} />
|
exact
|
||||||
<Route exact path="/settings" component={Settings} />
|
path="/keyboard-shortcuts"
|
||||||
|
component={KeyboardShortcuts}
|
||||||
|
/>
|
||||||
|
<Route exact path="/developers" component={Api} />
|
||||||
|
|
||||||
<Route
|
<Route path="/404" component={Error404} />
|
||||||
exact
|
<Route component={notFoundSearch} />
|
||||||
path="/keyboard-shortcuts"
|
</Switch>
|
||||||
component={KeyboardShortcuts}
|
</Layout>
|
||||||
/>
|
</Auth>
|
||||||
<Route exact path="/developers" component={Api} />
|
</Switch>
|
||||||
|
</ScrollToTop>
|
||||||
<Route path="/404" component={Error404} />
|
|
||||||
<Route component={notFoundSearch} />
|
|
||||||
</Switch>
|
|
||||||
</Layout>
|
|
||||||
</Auth>
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
|
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
|
||||||
|
@ -10,25 +10,25 @@ import type { User } from 'types';
|
|||||||
import Collection from './Collection';
|
import Collection from './Collection';
|
||||||
|
|
||||||
const parseHeader = text => {
|
const parseHeader = text => {
|
||||||
const firstLine = text.split(/\r?\n/)[0];
|
const firstLine = text.trim().split(/\r?\n/)[0];
|
||||||
return firstLine.replace(/^#/, '').trim();
|
return firstLine.replace(/^#/, '').trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
class Document {
|
class Document {
|
||||||
isSaving: boolean;
|
isSaving: boolean = false;
|
||||||
hasPendingChanges: boolean = false;
|
hasPendingChanges: boolean = false;
|
||||||
errors: ErrorsStore;
|
errors: ErrorsStore;
|
||||||
|
|
||||||
collaborators: Array<User>;
|
collaborators: Array<User>;
|
||||||
collection: Collection;
|
collection: $Shape<Collection>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdBy: User;
|
createdBy: User;
|
||||||
html: string;
|
html: string;
|
||||||
id: string;
|
id: string;
|
||||||
private: boolean;
|
|
||||||
starred: boolean;
|
|
||||||
team: string;
|
team: string;
|
||||||
text: string;
|
private: boolean = false;
|
||||||
|
starred: boolean = false;
|
||||||
|
text: string = '';
|
||||||
title: string = 'Untitled document';
|
title: string = 'Untitled document';
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
updatedBy: User;
|
updatedBy: User;
|
||||||
@ -83,9 +83,9 @@ class Document {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action view = async () => {
|
@action view = async () => {
|
||||||
|
this.views++;
|
||||||
try {
|
try {
|
||||||
await client.post('/views.create', { id: this.id });
|
await client.post('/views.create', { id: this.id });
|
||||||
this.views++;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors.add('Document failed to record view');
|
this.errors.add('Document failed to record view');
|
||||||
}
|
}
|
||||||
@ -113,7 +113,7 @@ class Document {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action save = async () => {
|
@action save = async () => {
|
||||||
if (this.isSaving) return;
|
if (this.isSaving) return this;
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -125,28 +125,38 @@ class Document {
|
|||||||
text: this.text,
|
text: this.text,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res = await client.post('/documents.create', {
|
const data = {
|
||||||
|
parentDocument: undefined,
|
||||||
collection: this.collection.id,
|
collection: this.collection.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
});
|
};
|
||||||
|
if (this.parentDocument) {
|
||||||
|
data.parentDocument = this.parentDocument.id;
|
||||||
|
}
|
||||||
|
res = await client.post('/documents.create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant(res && res.data, 'Data should be available');
|
invariant(res && res.data, 'Data should be available');
|
||||||
this.hasPendingChanges = false;
|
this.updateData({
|
||||||
|
...res.data,
|
||||||
|
hasPendingChanges: false,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors.add('Document failed saving');
|
this.errors.add('Document failed saving');
|
||||||
} finally {
|
} finally {
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateData(data: Object | Document) {
|
updateData(data: Object | Document) {
|
||||||
data.title = parseHeader(data.text);
|
if (data.text) data.title = parseHeader(data.text);
|
||||||
extendObservable(this, data);
|
extendObservable(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(document: Document) {
|
constructor(document?: Object = {}) {
|
||||||
this.updateData(document);
|
this.updateData(document);
|
||||||
this.errors = stores.errors;
|
this.errors = stores.errors;
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ describe('Document model', () => {
|
|||||||
test('should initialize with data', () => {
|
test('should initialize with data', () => {
|
||||||
const document = new Document({
|
const document = new Document({
|
||||||
id: 123,
|
id: 123,
|
||||||
title: 'Onboarding',
|
text: '# Onboarding\nSome body text',
|
||||||
text: 'Some body text'
|
|
||||||
});
|
});
|
||||||
expect(document.title).toBe('Onboarding');
|
expect(document.title).toBe('Onboarding');
|
||||||
});
|
});
|
||||||
|
@ -4,16 +4,18 @@ import get from 'lodash/get';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import { withRouter, Prompt } from 'react-router';
|
import { withRouter, Prompt } from 'react-router';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
import { layout } from 'styles/constants';
|
||||||
|
|
||||||
|
import Document from 'models/Document';
|
||||||
import UiStore from 'stores/UiStore';
|
import UiStore from 'stores/UiStore';
|
||||||
import DocumentsStore from 'stores/DocumentsStore';
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
import Menu from './components/Menu';
|
import Menu from './components/Menu';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
|
import DropToImport from 'components/DropToImport';
|
||||||
import { HeaderAction, SaveAction } from 'components/Layout';
|
import { HeaderAction, SaveAction } from 'components/Layout';
|
||||||
import LoadingIndicator from 'components/LoadingIndicator';
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
import PublishingInfo from 'components/PublishingInfo';
|
import PublishingInfo from 'components/PublishingInfo';
|
||||||
import AuthorInfo from 'components/AuthorInfo';
|
|
||||||
import PreviewLoading from 'components/PreviewLoading';
|
import PreviewLoading from 'components/PreviewLoading';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
@ -28,15 +30,19 @@ type Props = {
|
|||||||
history: Object,
|
history: Object,
|
||||||
keydown: Object,
|
keydown: Object,
|
||||||
documents: DocumentsStore,
|
documents: DocumentsStore,
|
||||||
newChildDocument?: boolean,
|
newDocument?: boolean,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer class Document extends Component {
|
@observer class DocumentScene extends Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
|
state: {
|
||||||
|
newDocument?: Document,
|
||||||
|
};
|
||||||
state = {
|
state = {
|
||||||
|
isDragging: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
newDocument: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -44,7 +50,10 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.match.params.id !== this.props.match.params.id) {
|
if (
|
||||||
|
nextProps.match.params.documentSlug !==
|
||||||
|
this.props.match.params.documentSlug
|
||||||
|
) {
|
||||||
this.loadDocument(nextProps);
|
this.loadDocument(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,42 +63,49 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadDocument = async props => {
|
loadDocument = async props => {
|
||||||
await this.props.documents.fetch(props.match.params.id);
|
if (props.newDocument) {
|
||||||
const document = this.document;
|
const newDocument = new Document({
|
||||||
|
collection: { id: props.match.params.id },
|
||||||
if (document) {
|
});
|
||||||
this.props.ui.setActiveDocument(document);
|
this.setState({ newDocument });
|
||||||
document.view();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.match.params.edit) {
|
|
||||||
this.props.ui.enableEditMode();
|
|
||||||
} else {
|
} else {
|
||||||
this.props.ui.disableEditMode();
|
let document = this.document;
|
||||||
|
if (document) {
|
||||||
|
this.props.ui.setActiveDocument(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.props.documents.fetch(props.match.params.documentSlug);
|
||||||
|
document = this.document;
|
||||||
|
|
||||||
|
if (document) {
|
||||||
|
this.props.ui.setActiveDocument(document);
|
||||||
|
document.view();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get document() {
|
get document() {
|
||||||
return this.props.documents.getByUrl(`/d/${this.props.match.params.id}`);
|
if (this.state.newDocument) return this.state.newDocument;
|
||||||
|
return this.props.documents.getByUrl(
|
||||||
|
`/doc/${this.props.match.params.documentSlug}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickEdit = () => {
|
onClickEdit = () => {
|
||||||
if (!this.document) return;
|
if (!this.document) return;
|
||||||
const url = `${this.document.url}/edit`;
|
const url = `${this.document.url}/edit`;
|
||||||
this.props.history.push(url);
|
this.props.history.push(url);
|
||||||
this.props.ui.enableEditMode();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSave = async (redirect: boolean = false) => {
|
onSave = async (redirect: boolean = false) => {
|
||||||
const document = this.document;
|
let document = this.document;
|
||||||
|
|
||||||
if (!document) return;
|
if (!document) return;
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
await document.save();
|
document = await document.save();
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
this.props.ui.disableEditMode();
|
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect || this.props.newDocument) {
|
||||||
this.props.history.push(document.url);
|
this.props.history.push(document.url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -111,86 +127,111 @@ type Props = {
|
|||||||
this.props.history.goBack();
|
this.props.history.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onStartDragging = () => {
|
||||||
|
this.setState({ isDragging: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onStopDragging = () => {
|
||||||
|
this.setState({ isDragging: false });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isNew = this.props.newDocument || this.props.newChildDocument;
|
const isNew = this.props.newDocument;
|
||||||
const isEditing = this.props.match.params.edit;
|
const isEditing = this.props.match.params.edit || isNew;
|
||||||
const isFetching = !this.document && get(this.document, 'isFetching');
|
const isFetching = !this.document;
|
||||||
const titleText = get(this.document, 'title', 'Loading');
|
const titleText = get(this.document, 'title', 'Loading');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container column auto>
|
<Container column auto>
|
||||||
|
{this.state.isDragging &&
|
||||||
|
<DropHere align="center" justify="center">
|
||||||
|
Drop files here to import into Atlas.
|
||||||
|
</DropHere>}
|
||||||
{titleText && <PageTitle title={titleText} />}
|
{titleText && <PageTitle title={titleText} />}
|
||||||
{this.state.isLoading && <LoadingIndicator />}
|
{this.state.isLoading && <LoadingIndicator />}
|
||||||
{isFetching &&
|
{isFetching &&
|
||||||
<CenteredContent>
|
<CenteredContent>
|
||||||
<PreviewLoading />
|
<LoadingState />
|
||||||
</CenteredContent>}
|
</CenteredContent>}
|
||||||
{!isFetching &&
|
{!isFetching &&
|
||||||
this.document &&
|
this.document &&
|
||||||
<PagePadding justify="center" auto>
|
<DropToImport
|
||||||
<Prompt
|
documentId={this.document.id}
|
||||||
when={this.document.hasPendingChanges}
|
history={this.props.history}
|
||||||
message={DISCARD_CHANGES}
|
onDragEnter={this.onStartDragging}
|
||||||
/>
|
onDragLeave={this.onStopDragging}
|
||||||
<DocumentContainer>
|
onDrop={this.onStopDragging}
|
||||||
<InfoWrapper visible={!isEditing}>
|
>
|
||||||
<PublishingInfo
|
<PagePadding justify="center" auto>
|
||||||
collaborators={this.document.collaborators}
|
<Prompt
|
||||||
createdAt={this.document.createdAt}
|
when={this.document.hasPendingChanges}
|
||||||
createdBy={this.document.createdBy}
|
message={DISCARD_CHANGES}
|
||||||
updatedAt={this.document.updatedAt}
|
/>
|
||||||
updatedBy={this.document.updatedBy}
|
<DocumentContainer>
|
||||||
/>
|
<InfoWrapper visible={!isEditing}>
|
||||||
</InfoWrapper>
|
<PublishingInfo
|
||||||
<Content>
|
collaborators={this.document.collaborators}
|
||||||
<Editor
|
createdAt={this.document.createdAt}
|
||||||
key={this.document.id}
|
createdBy={this.document.createdBy}
|
||||||
text={this.document.text}
|
updatedAt={this.document.updatedAt}
|
||||||
onImageUploadStart={this.onImageUploadStart}
|
updatedBy={this.document.updatedBy}
|
||||||
onImageUploadStop={this.onImageUploadStop}
|
/>
|
||||||
onChange={this.onChange}
|
</InfoWrapper>
|
||||||
onSave={this.onSave}
|
<Content>
|
||||||
onCancel={this.onCancel}
|
<Editor
|
||||||
onStar={this.document.star}
|
key={this.document.id}
|
||||||
onUnstar={this.document.unstar}
|
text={this.document.text}
|
||||||
starred={this.document.starred}
|
onImageUploadStart={this.onImageUploadStart}
|
||||||
readOnly={!isEditing}
|
onImageUploadStop={this.onImageUploadStop}
|
||||||
/>
|
onChange={this.onChange}
|
||||||
</Content>
|
onSave={this.onSave}
|
||||||
<InfoWrapper visible={!isEditing} bottom>
|
onCancel={this.onCancel}
|
||||||
<AuthorInfo
|
onStar={this.document.star}
|
||||||
collaborators={this.document.collaborators}
|
onUnstar={this.document.unstar}
|
||||||
views={this.document.views}
|
starred={this.document.starred}
|
||||||
/>
|
readOnly={!isEditing}
|
||||||
</InfoWrapper>
|
/>
|
||||||
</DocumentContainer>
|
</Content>
|
||||||
<Meta align="center" justify="flex-end" readOnly={!isEditing}>
|
</DocumentContainer>
|
||||||
<Flex align="center">
|
<Meta align="center" justify="flex-end" readOnly={!isEditing}>
|
||||||
<HeaderAction>
|
<Flex align="center">
|
||||||
{isEditing
|
<HeaderAction>
|
||||||
? <SaveAction
|
{isEditing
|
||||||
onClick={this.onSave.bind(this, true)}
|
? <SaveAction
|
||||||
disabled={get(this.document, 'isSaving')}
|
onClick={this.onSave.bind(this, true)}
|
||||||
isNew={!!isNew}
|
disabled={get(this.document, 'isSaving')}
|
||||||
/>
|
isNew={!!isNew}
|
||||||
: <a onClick={this.onClickEdit}>Edit</a>}
|
/>
|
||||||
</HeaderAction>
|
: <a onClick={this.onClickEdit}>Edit</a>}
|
||||||
{!isEditing && <Menu document={this.document} />}
|
</HeaderAction>
|
||||||
</Flex>
|
{!isEditing && <Menu document={this.document} />}
|
||||||
</Meta>
|
</Flex>
|
||||||
</PagePadding>}
|
</Meta>
|
||||||
|
</PagePadding>
|
||||||
|
</DropToImport>}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DropHere = styled(Flex)`
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: ${layout.sidebarWidth};
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255,255,255,.9);
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
const Meta = styled(Flex)`
|
const Meta = styled(Flex)`
|
||||||
justify-content: ${props => (props.readOnly ? 'space-between' : 'flex-end')};
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding: 10px 20px;
|
padding: ${layout.padding};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Content = styled(Flex)`
|
const Content = styled(Flex)`
|
||||||
@ -207,6 +248,10 @@ const Container = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LoadingState = styled(PreviewLoading)`
|
||||||
|
margin: 80px 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
const PagePadding = styled(Flex)`
|
const PagePadding = styled(Flex)`
|
||||||
padding: 80px 20px;
|
padding: 80px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -220,4 +265,4 @@ const DocumentContainer = styled.div`
|
|||||||
width: 50em;
|
width: 50em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withRouter(inject('ui', 'documents')(Document));
|
export default withRouter(inject('ui', 'user', 'documents')(DocumentScene));
|
||||||
|
@ -16,9 +16,7 @@ type Props = {
|
|||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
onCreateDocument = () => {
|
onCreateDocument = () => {
|
||||||
// Disabled until created a better API
|
this.props.history.push(`${this.props.document.collection.url}/new`);
|
||||||
// invariant(this.props.collectionTree, 'collectionTree is not available');
|
|
||||||
// this.props.history.push(`${this.props.collectionTree.url}/new`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onCreateChild = () => {
|
onCreateChild = () => {
|
||||||
@ -68,7 +66,6 @@ type Props = {
|
|||||||
<MenuItem onClick={this.onCreateDocument}>
|
<MenuItem onClick={this.onCreateDocument}>
|
||||||
New document
|
New document
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={this.onCreateChild}>New child</MenuItem>
|
|
||||||
</div>}
|
</div>}
|
||||||
<MenuItem onClick={this.onExport}>Export</MenuItem>
|
<MenuItem onClick={this.onExport}>Export</MenuItem>
|
||||||
{allowDelete && <MenuItem onClick={this.onDelete}>Delete</MenuItem>}
|
{allowDelete && <MenuItem onClick={this.onDelete}>Delete</MenuItem>}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import { Redirect } from 'react-router';
|
import { Redirect } from 'react-router';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import AuthStore from 'stores/AuthStore';
|
import AuthStore from 'stores/AuthStore';
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { searchUrl } from 'utils/routeHelpers';
|
import { searchUrl } from 'utils/routeHelpers';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import searchImg from 'assets/icons/search.svg';
|
import searchImg from 'assets/icons/search.svg';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Flex } from 'reflexbox';
|
import Flex from 'components/Flex';
|
||||||
|
|
||||||
import ApiKeyRow from './components/ApiKeyRow';
|
import ApiKeyRow from './components/ApiKeyRow';
|
||||||
import styles from './Settings.scss';
|
import styles from './Settings.scss';
|
||||||
|
@ -50,10 +50,14 @@ class DocumentsStore {
|
|||||||
const res = await client.post('/documents.info', { id });
|
const res = await client.post('/documents.info', { id });
|
||||||
invariant(res && res.data, 'Document not available');
|
invariant(res && res.data, 'Document not available');
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
|
const document = new Document(data);
|
||||||
|
|
||||||
runInAction('DocumentsStore#fetch', () => {
|
runInAction('DocumentsStore#fetch', () => {
|
||||||
this.data.set(data.id, new Document(data));
|
this.data.set(data.id, document);
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return document;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors.add('Failed to load documents');
|
this.errors.add('Failed to load documents');
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
export const layout = {
|
||||||
|
padding: '1.5vw 1.875vw',
|
||||||
|
vpadding: '1.5vw',
|
||||||
|
hpadding: '1.875vw',
|
||||||
|
sidebarWidth: '22%',
|
||||||
|
sidebarMinWidth: '250px',
|
||||||
|
sidebarMaxWidth: '350px',
|
||||||
|
};
|
||||||
|
|
||||||
export const size = {
|
export const size = {
|
||||||
tiny: '2px',
|
tiny: '2px',
|
||||||
small: '4px',
|
small: '4px',
|
||||||
@ -28,6 +37,8 @@ export const fontWeight = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const color = {
|
export const color = {
|
||||||
|
text: '#171B35',
|
||||||
|
|
||||||
/* Brand */
|
/* Brand */
|
||||||
primary: '#73DF7B',
|
primary: '#73DF7B',
|
||||||
|
|
||||||
|
14
index.js
14
index.js
@ -1,13 +1,13 @@
|
|||||||
require('./init');
|
require('./init');
|
||||||
var app = require('./server').default;
|
const app = require('./server').default;
|
||||||
var http = require('http');
|
const http = require('http');
|
||||||
|
|
||||||
var server = http.createServer(app.callback());
|
const server = http.createServer(app.callback());
|
||||||
server.listen(process.env.PORT || '3000');
|
server.listen(process.env.PORT || '3000');
|
||||||
server.on('error', (err) => {
|
server.on('error', err => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
server.on('listening', () => {
|
server.on('listening', () => {
|
||||||
var address = server.address();
|
const address = server.address();
|
||||||
console.log('Listening on %s%s', address.address, address.port);
|
console.log(`Listening on http://localhost:${address.port}`);
|
||||||
});
|
});
|
||||||
|
2
init.js
2
init.js
@ -3,4 +3,4 @@ require('safestart')(__dirname, {
|
|||||||
});
|
});
|
||||||
require('babel-core/register');
|
require('babel-core/register');
|
||||||
require('babel-polyfill');
|
require('babel-polyfill');
|
||||||
require('localenv');
|
require('dotenv').config({ silent: true });
|
||||||
|
10
package.json
10
package.json
@ -4,11 +4,11 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
|
"build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js",
|
||||||
"build:analyze": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer",
|
"build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer",
|
||||||
"build": "npm run clean && npm run build:webpack",
|
"build": "npm run clean && npm run build:webpack",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"dev": "cross-env NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js",
|
"dev": "NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js",
|
||||||
"lint": "npm run lint:js && npm run lint:flow",
|
"lint": "npm run lint:js && npm run lint:flow",
|
||||||
"lint:js": "eslint frontend",
|
"lint:js": "eslint frontend",
|
||||||
"lint:flow": "flow check",
|
"lint:flow": "flow check",
|
||||||
@ -80,7 +80,6 @@
|
|||||||
"boundless-popover": "^1.0.4",
|
"boundless-popover": "^1.0.4",
|
||||||
"bugsnag": "^1.7.0",
|
"bugsnag": "^1.7.0",
|
||||||
"classnames": "2.2.3",
|
"classnames": "2.2.3",
|
||||||
"cross-env": "1.0.7",
|
|
||||||
"css-loader": "0.23.1",
|
"css-loader": "0.23.1",
|
||||||
"debug": "2.2.0",
|
"debug": "2.2.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
@ -119,7 +118,6 @@
|
|||||||
"koa-mount": "^3.0.0",
|
"koa-mount": "^3.0.0",
|
||||||
"koa-router": "7.0.1",
|
"koa-router": "7.0.1",
|
||||||
"koa-sendfile": "2.0.0",
|
"koa-sendfile": "2.0.0",
|
||||||
"localenv": "0.2.2",
|
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"lodash.orderby": "4.4.0",
|
"lodash.orderby": "4.4.0",
|
||||||
"marked": "0.3.6",
|
"marked": "0.3.6",
|
||||||
@ -148,7 +146,6 @@
|
|||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"redis": "^2.6.2",
|
"redis": "^2.6.2",
|
||||||
"redis-lock": "^0.1.0",
|
"redis-lock": "^0.1.0",
|
||||||
"reflexbox": "^2.2.3",
|
|
||||||
"rimraf": "^2.5.4",
|
"rimraf": "^2.5.4",
|
||||||
"safestart": "1.1.0",
|
"safestart": "1.1.0",
|
||||||
"sass-loader": "4.0.0",
|
"sass-loader": "4.0.0",
|
||||||
@ -181,7 +178,6 @@
|
|||||||
"fetch-test-server": "^1.1.0",
|
"fetch-test-server": "^1.1.0",
|
||||||
"flow-bin": "^0.45.0",
|
"flow-bin": "^0.45.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"ignore-loader": "0.1.1",
|
|
||||||
"jest-cli": "^20.0.0",
|
"jest-cli": "^20.0.0",
|
||||||
"koa-webpack-dev-middleware": "1.4.5",
|
"koa-webpack-dev-middleware": "1.4.5",
|
||||||
"koa-webpack-hot-middleware": "1.0.3",
|
"koa-webpack-hot-middleware": "1.0.3",
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
"<rootDir>/server"
|
"<rootDir>/server"
|
||||||
],
|
],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"<rootDir>/__mocks__/console.js",
|
"<rootDir>/__mocks__/console.js"
|
||||||
"./server/test/helper.js"
|
|
||||||
],
|
],
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
@ -24,7 +24,7 @@ router.post('collections.create', auth(), async ctx => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentCollection(ctx, atlas, true),
|
data: await presentCollection(ctx, atlas),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ router.post('collections.info', auth(), async ctx => {
|
|||||||
ctx.assertPresent(id, 'id is required');
|
ctx.assertPresent(id, 'id is required');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const atlas = await Collection.findOne({
|
const atlas = await Collection.scope('withRecentDocuments').findOne({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@ -43,7 +43,7 @@ router.post('collections.info', auth(), async ctx => {
|
|||||||
if (!atlas) throw httpErrors.NotFound();
|
if (!atlas) throw httpErrors.NotFound();
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentCollection(ctx, atlas, true),
|
data: await presentCollection(ctx, atlas),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,16 +58,10 @@ router.post('collections.list', auth(), pagination(), async ctx => {
|
|||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collectiones
|
const data = await Promise.all(
|
||||||
let data = [];
|
collections.map(async atlas => await presentCollection(ctx, atlas))
|
||||||
await Promise.all(
|
|
||||||
collections.map(async atlas => {
|
|
||||||
return data.push(await presentCollection(ctx, atlas, true));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
data = _.orderBy(data, ['updatedAt'], ['desc']);
|
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
data,
|
data,
|
||||||
|
@ -8,7 +8,6 @@ import { presentDocument } from '../presenters';
|
|||||||
import { Document, Collection, Star, View } from '../models';
|
import { Document, Collection, Star, View } from '../models';
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||||
let { sort = 'updatedAt', direction } = ctx.body;
|
let { sort = 'updatedAt', direction } = ctx.body;
|
||||||
if (direction !== 'ASC') direction = 'DESC';
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
@ -19,9 +18,12 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
|||||||
order: [[sort, direction]],
|
order: [[sort, direction]],
|
||||||
offset: ctx.state.pagination.offset,
|
offset: ctx.state.pagination.offset,
|
||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
|
include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
|
||||||
});
|
});
|
||||||
|
|
||||||
let data = await Promise.all(documents.map(doc => presentDocument(ctx, doc)));
|
const data = await Promise.all(
|
||||||
|
documents.map(document => presentDocument(ctx, document))
|
||||||
|
);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
@ -42,7 +44,7 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
|
|||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
let data = await Promise.all(
|
const data = await Promise.all(
|
||||||
views.map(view => presentDocument(ctx, view.document))
|
views.map(view => presentDocument(ctx, view.document))
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -60,12 +62,17 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
|
|||||||
const views = await Star.findAll({
|
const views = await Star.findAll({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
order: [[sort, direction]],
|
order: [[sort, direction]],
|
||||||
include: [{ model: Document }],
|
include: [
|
||||||
|
{
|
||||||
|
model: Document,
|
||||||
|
include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
offset: ctx.state.pagination.offset,
|
offset: ctx.state.pagination.offset,
|
||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
let data = await Promise.all(
|
const data = await Promise.all(
|
||||||
views.map(view => presentDocument(ctx, view.document))
|
views.map(view => presentDocument(ctx, view.document))
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -94,8 +101,7 @@ router.post('documents.info', auth(), async ctx => {
|
|||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(ctx, document, {
|
data: await presentDocument(ctx, document, {
|
||||||
includeCollection: document.private,
|
includeViews: true,
|
||||||
includeCollaborators: true,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -108,16 +114,8 @@ router.post('documents.search', auth(), async ctx => {
|
|||||||
|
|
||||||
const documents = await Document.searchForUser(user, query);
|
const documents = await Document.searchForUser(user, query);
|
||||||
|
|
||||||
const data = [];
|
const data = await Promise.all(
|
||||||
await Promise.all(
|
documents.map(async document => await presentDocument(ctx, document))
|
||||||
documents.map(async document => {
|
|
||||||
data.push(
|
|
||||||
await presentDocument(ctx, document, {
|
|
||||||
includeCollection: true,
|
|
||||||
includeCollaborators: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
@ -200,11 +198,7 @@ router.post('documents.create', auth(), async ctx => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(ctx, newDocument, {
|
data: await presentDocument(ctx, newDocument),
|
||||||
includeCollection: true,
|
|
||||||
includeCollaborators: true,
|
|
||||||
collection: ownerCollection,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,11 +224,7 @@ router.post('documents.update', auth(), async ctx => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(ctx, document, {
|
data: await presentDocument(ctx, document),
|
||||||
includeCollection: true,
|
|
||||||
includeCollaborators: true,
|
|
||||||
collection: collection,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -273,11 +263,7 @@ router.post('documents.move', auth(), async ctx => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(ctx, document, {
|
data: await presentDocument(ctx, document),
|
||||||
includeCollection: true,
|
|
||||||
includeCollaborators: true,
|
|
||||||
collection: collection,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,4 +11,4 @@
|
|||||||
"use_env_variable": "DATABASE_URL",
|
"use_env_variable": "DATABASE_URL",
|
||||||
"dialect": "postgres"
|
"dialect": "postgres"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,10 @@
|
|||||||
|
// @flow
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
const debugCache = debug('cache');
|
const debugCache = debug('cache');
|
||||||
|
|
||||||
export default function cache() {
|
export default function cache() {
|
||||||
return async function cacheMiddleware(ctx, next) {
|
return async function cacheMiddleware(ctx: Object, next: Function) {
|
||||||
ctx.cache = {};
|
ctx.cache = {};
|
||||||
|
|
||||||
ctx.cache.set = async (id, value) => {
|
ctx.cache.set = async (id, value) => {
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
up: (queryInterface, Sequelize) => {
|
up: (queryInterface, Sequelize) => {
|
||||||
queryInterface.renameTable('atlases', 'collections');
|
queryInterface.renameTable('atlases', 'collections').then(() => {
|
||||||
queryInterface.addColumn('collections', 'documentStructure', {
|
queryInterface.addColumn('collections', 'documentStructure', {
|
||||||
type: Sequelize.JSONB,
|
type: Sequelize.JSONB,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
down: (queryInterface, _Sequelize) => {
|
down: (queryInterface, _Sequelize) => {
|
||||||
queryInterface.renameTable('collections', 'atlases');
|
queryInterface.renameTable('collections', 'atlases').then(() => {
|
||||||
queryInterface.removeColumn('atlases', 'documentStructure');
|
queryInterface.removeColumn('atlases', 'documentStructure');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,40 +1,44 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
up: function(queryInterface, Sequelize) {
|
up: function(queryInterface, Sequelize) {
|
||||||
queryInterface.createTable('views', {
|
queryInterface
|
||||||
id: {
|
.createTable('views', {
|
||||||
type: Sequelize.UUID,
|
id: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
primaryKey: true,
|
allowNull: false,
|
||||||
},
|
primaryKey: true,
|
||||||
documentId: {
|
},
|
||||||
type: Sequelize.UUID,
|
documentId: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
},
|
allowNull: false,
|
||||||
userId: {
|
},
|
||||||
type: Sequelize.UUID,
|
userId: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
},
|
allowNull: false,
|
||||||
count: {
|
},
|
||||||
type: Sequelize.INTEGER,
|
count: {
|
||||||
allowNull: false,
|
type: Sequelize.INTEGER,
|
||||||
defaultValue: 1,
|
allowNull: false,
|
||||||
},
|
defaultValue: 1,
|
||||||
createdAt: {
|
},
|
||||||
type: Sequelize.DATE,
|
createdAt: {
|
||||||
allowNull: false,
|
type: Sequelize.DATE,
|
||||||
},
|
allowNull: false,
|
||||||
updatedAt: {
|
},
|
||||||
type: Sequelize.DATE,
|
updatedAt: {
|
||||||
allowNull: false,
|
type: Sequelize.DATE,
|
||||||
},
|
allowNull: false,
|
||||||
});
|
},
|
||||||
queryInterface.addIndex('views', ['documentId', 'userId'], {
|
})
|
||||||
indicesType: 'UNIQUE',
|
.then(() => {
|
||||||
});
|
queryInterface.addIndex('views', ['documentId', 'userId'], {
|
||||||
|
indicesType: 'UNIQUE',
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
down: function(queryInterface, Sequelize) {
|
down: function(queryInterface, Sequelize) {
|
||||||
queryInterface.removeIndex('views', ['documentId', 'userId']);
|
queryInterface.removeIndex('views', ['documentId', 'userId']).then(() => {
|
||||||
queryInterface.dropTable('views');
|
queryInterface.dropTable('views');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,35 +1,39 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
up: function(queryInterface, Sequelize) {
|
up: function(queryInterface, Sequelize) {
|
||||||
queryInterface.createTable('stars', {
|
queryInterface
|
||||||
id: {
|
.createTable('stars', {
|
||||||
type: Sequelize.UUID,
|
id: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
primaryKey: true,
|
allowNull: false,
|
||||||
},
|
primaryKey: true,
|
||||||
documentId: {
|
},
|
||||||
type: Sequelize.UUID,
|
documentId: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
},
|
allowNull: false,
|
||||||
userId: {
|
},
|
||||||
type: Sequelize.UUID,
|
userId: {
|
||||||
allowNull: false,
|
type: Sequelize.UUID,
|
||||||
},
|
allowNull: false,
|
||||||
createdAt: {
|
},
|
||||||
type: Sequelize.DATE,
|
createdAt: {
|
||||||
allowNull: false,
|
type: Sequelize.DATE,
|
||||||
},
|
allowNull: false,
|
||||||
updatedAt: {
|
},
|
||||||
type: Sequelize.DATE,
|
updatedAt: {
|
||||||
allowNull: false,
|
type: Sequelize.DATE,
|
||||||
},
|
allowNull: false,
|
||||||
});
|
},
|
||||||
queryInterface.addIndex('stars', ['documentId', 'userId'], {
|
})
|
||||||
indicesType: 'UNIQUE',
|
.then(() => {
|
||||||
});
|
queryInterface.addIndex('stars', ['documentId', 'userId'], {
|
||||||
|
indicesType: 'UNIQUE',
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
down: function(queryInterface, Sequelize) {
|
down: function(queryInterface, Sequelize) {
|
||||||
queryInterface.removeIndex('stars', ['documentId', 'userId']);
|
queryInterface.removeIndex('stars', ['documentId', 'userId']).then(() => {
|
||||||
queryInterface.dropTable('stars');
|
queryInterface.dropTable('stars');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -60,6 +60,16 @@ const Collection = sequelize.define(
|
|||||||
as: 'documents',
|
as: 'documents',
|
||||||
foreignKey: 'atlasId',
|
foreignKey: 'atlasId',
|
||||||
});
|
});
|
||||||
|
Collection.addScope('withRecentDocuments', {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
as: 'documents',
|
||||||
|
limit: 10,
|
||||||
|
model: models.Document,
|
||||||
|
order: [['updatedAt', 'DESC']],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instanceMethods: {
|
instanceMethods: {
|
||||||
|
@ -100,7 +100,7 @@ const Document = sequelize.define(
|
|||||||
instanceMethods: {
|
instanceMethods: {
|
||||||
getUrl() {
|
getUrl() {
|
||||||
const slugifiedTitle = slugify(this.title);
|
const slugifiedTitle = slugify(this.title);
|
||||||
return `/d/${slugifiedTitle}-${this.urlId}`;
|
return `/doc/${slugifiedTitle}-${this.urlId}`;
|
||||||
},
|
},
|
||||||
toJSON() {
|
toJSON() {
|
||||||
// Warning: only use for new documents as order of children is
|
// Warning: only use for new documents as order of children is
|
||||||
@ -115,7 +115,32 @@ const Document = sequelize.define(
|
|||||||
},
|
},
|
||||||
classMethods: {
|
classMethods: {
|
||||||
associate: models => {
|
associate: models => {
|
||||||
Document.belongsTo(models.User);
|
Document.belongsTo(models.Collection, {
|
||||||
|
as: 'collection',
|
||||||
|
foreignKey: 'atlasId',
|
||||||
|
});
|
||||||
|
Document.belongsTo(models.User, {
|
||||||
|
as: 'createdBy',
|
||||||
|
foreignKey: 'createdById',
|
||||||
|
});
|
||||||
|
Document.belongsTo(models.User, {
|
||||||
|
as: 'updatedBy',
|
||||||
|
foreignKey: 'lastModifiedById',
|
||||||
|
});
|
||||||
|
Document.hasMany(models.Star, {
|
||||||
|
as: 'starred',
|
||||||
|
});
|
||||||
|
Document.addScope(
|
||||||
|
'defaultScope',
|
||||||
|
{
|
||||||
|
include: [
|
||||||
|
{ model: models.Collection, as: 'collection' },
|
||||||
|
{ model: models.User, as: 'createdBy' },
|
||||||
|
{ model: models.User, as: 'updatedBy' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ override: true }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
findById: async id => {
|
findById: async id => {
|
||||||
if (isUUID(id)) {
|
if (isUUID(id)) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
// @flow
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Document } from '../models';
|
import { Collection } from '../models';
|
||||||
import presentDocument from './document';
|
import presentDocument from './document';
|
||||||
|
|
||||||
async function present(ctx, collection, includeRecentDocuments = false) {
|
async function present(ctx: Object, collection: Collection) {
|
||||||
ctx.cache.set(collection.id, collection);
|
ctx.cache.set(collection.id, collection);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -13,31 +14,21 @@ async function present(ctx, collection, includeRecentDocuments = false) {
|
|||||||
type: collection.type,
|
type: collection.type,
|
||||||
createdAt: collection.createdAt,
|
createdAt: collection.createdAt,
|
||||||
updatedAt: collection.updatedAt,
|
updatedAt: collection.updatedAt,
|
||||||
|
recentDocuments: undefined,
|
||||||
|
documents: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (collection.type === 'atlas')
|
if (collection.type === 'atlas') {
|
||||||
data.documents = await collection.getDocumentsStructure();
|
data.documents = await collection.getDocumentsStructure();
|
||||||
|
}
|
||||||
|
|
||||||
if (includeRecentDocuments) {
|
if (collection.documents) {
|
||||||
const documents = await Document.findAll({
|
data.recentDocuments = await Promise.all(
|
||||||
where: {
|
collection.documents.map(
|
||||||
atlasId: collection.id,
|
async document =>
|
||||||
},
|
await presentDocument(ctx, document, { includeCollaborators: true })
|
||||||
limit: 10,
|
)
|
||||||
order: [['updatedAt', 'DESC']],
|
|
||||||
});
|
|
||||||
|
|
||||||
const recentDocuments = [];
|
|
||||||
await Promise.all(
|
|
||||||
documents.map(async document => {
|
|
||||||
recentDocuments.push(
|
|
||||||
await presentDocument(ctx, document, {
|
|
||||||
includeCollaborators: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
data.recentDocuments = _.orderBy(recentDocuments, ['updatedAt'], ['desc']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -1,26 +1,22 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { Collection, Star, User, View, Document } from '../models';
|
import _ from 'lodash';
|
||||||
|
import { User, Document, View } from '../models';
|
||||||
import presentUser from './user';
|
import presentUser from './user';
|
||||||
import presentCollection from './collection';
|
import presentCollection from './collection';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
includeCollection?: boolean,
|
|
||||||
includeCollaborators?: boolean,
|
includeCollaborators?: boolean,
|
||||||
includeViews?: boolean,
|
includeViews?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function present(ctx: Object, document: Document, options: Options) {
|
async function present(ctx: Object, document: Document, options: ?Options) {
|
||||||
options = {
|
options = {
|
||||||
includeCollection: true,
|
|
||||||
includeCollaborators: true,
|
includeCollaborators: true,
|
||||||
includeViews: true,
|
includeViews: false,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
ctx.cache.set(document.id, document);
|
ctx.cache.set(document.id, document);
|
||||||
|
const data = {
|
||||||
const userId = ctx.state.user.id;
|
|
||||||
let data = {
|
|
||||||
id: document.id,
|
id: document.id,
|
||||||
url: document.getUrl(),
|
url: document.getUrl(),
|
||||||
private: document.private,
|
private: document.private,
|
||||||
@ -29,36 +25,23 @@ async function present(ctx: Object, document: Document, options: Options) {
|
|||||||
html: document.html,
|
html: document.html,
|
||||||
preview: document.preview,
|
preview: document.preview,
|
||||||
createdAt: document.createdAt,
|
createdAt: document.createdAt,
|
||||||
createdBy: undefined,
|
createdBy: presentUser(ctx, document.createdBy),
|
||||||
starred: false,
|
|
||||||
updatedAt: document.updatedAt,
|
updatedAt: document.updatedAt,
|
||||||
updatedBy: undefined,
|
updatedBy: presentUser(ctx, document.updatedBy),
|
||||||
team: document.teamId,
|
team: document.teamId,
|
||||||
collaborators: [],
|
collaborators: [],
|
||||||
|
starred: !!document.starred,
|
||||||
|
collection: undefined,
|
||||||
|
views: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
data.starred = !!await Star.findOne({
|
if (document.private) {
|
||||||
where: { documentId: document.id, userId },
|
data.collection = await presentCollection(ctx, document.collection);
|
||||||
});
|
|
||||||
|
|
||||||
if (options.includeViews) {
|
|
||||||
// $FlowIssue not found in object literal?
|
|
||||||
data.views = await View.sum('count', {
|
|
||||||
where: { documentId: document.id },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.includeCollection) {
|
if (options.includeViews) {
|
||||||
// $FlowIssue not found in object literal?
|
data.views = await View.sum('count', {
|
||||||
data.collection = await ctx.cache.get(document.atlasId, async () => {
|
where: { documentId: document.id },
|
||||||
const collection =
|
|
||||||
options.collection ||
|
|
||||||
(await Collection.findOne({
|
|
||||||
where: {
|
|
||||||
id: document.atlasId,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
return presentCollection(ctx, collection);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,27 +49,13 @@ async function present(ctx: Object, document: Document, options: Options) {
|
|||||||
// This could be further optimized by using ctx.cache
|
// This could be further optimized by using ctx.cache
|
||||||
data['collaborators'] = await User.findAll({
|
data['collaborators'] = await User.findAll({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: { $in: _.takeRight(document.collaboratorIds, 10) || [] },
|
||||||
$in: _.takeRight(document.collaboratorIds, 10) || [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).map(user => presentUser(ctx, user));
|
}).map(user => presentUser(ctx, user));
|
||||||
// $FlowIssue not found in object literal?
|
// $FlowIssue not found in object literal?
|
||||||
data.collaboratorCount = document.collaboratorIds.length;
|
data.collaboratorCount = document.collaboratorIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdBy = await ctx.cache.get(
|
|
||||||
document.createdById,
|
|
||||||
async () => await User.findById(document.createdById)
|
|
||||||
);
|
|
||||||
data.createdBy = await presentUser(ctx, createdBy);
|
|
||||||
|
|
||||||
const updatedBy = await ctx.cache.get(
|
|
||||||
document.lastModifiedById,
|
|
||||||
async () => await User.findById(document.lastModifiedById)
|
|
||||||
);
|
|
||||||
data.updatedBy = await presentUser(ctx, updatedBy);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
function present(ctx, team) {
|
// @flow
|
||||||
|
import { Team } from '../models';
|
||||||
|
|
||||||
|
function present(ctx: Object, team: Team) {
|
||||||
ctx.cache.set(team.id, team);
|
ctx.cache.set(team.id, team);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,28 +1,33 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<title>Atlas</title>
|
|
||||||
<link href="/static/styles.css" rel="stylesheet"></head>
|
|
||||||
<style>
|
|
||||||
body, html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
<head>
|
||||||
display: flex;
|
<title>Atlas</title>
|
||||||
width: 100%;
|
<link href="/static/styles.css" rel="stylesheet">
|
||||||
height: 100%;
|
</head>
|
||||||
}
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
body {
|
||||||
flex: 1;
|
display: flex;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
</style>
|
}
|
||||||
</head>
|
|
||||||
<body>
|
#root {
|
||||||
<div id="root"></div>
|
flex: 1;
|
||||||
<script src="/static/bundle.js"></script>
|
height: 100%;
|
||||||
</body>
|
}
|
||||||
</html>
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,16 +1,30 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<title>Atlas</title>
|
<head>
|
||||||
<style>
|
<title>Atlas</title>
|
||||||
body,
|
<style>
|
||||||
html {
|
body,
|
||||||
display: flex;
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
</head>
|
body {
|
||||||
<body>
|
display: flex;
|
||||||
</body>
|
width: 100%;
|
||||||
</html>
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,4 +1,5 @@
|
|||||||
require('localenv');
|
// @flow
|
||||||
|
require('../../init');
|
||||||
|
|
||||||
// test environment variables
|
// test environment variables
|
||||||
process.env.DATABASE_URL = process.env.DATABASE_URL_TEST;
|
process.env.DATABASE_URL = process.env.DATABASE_URL_TEST;
|
||||||
|
@ -36,13 +36,6 @@ module.exports = {
|
|||||||
loader: 'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]',
|
loader: 'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]',
|
||||||
},
|
},
|
||||||
{ test: /\.md/, loader: 'raw-loader' },
|
{ test: /\.md/, loader: 'raw-loader' },
|
||||||
|
|
||||||
// Excludes
|
|
||||||
{
|
|
||||||
// slug
|
|
||||||
test: /unicode/,
|
|
||||||
loader: 'ignore-loader',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -41,12 +41,5 @@ productionWebpackConfig.plugins.push(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
productionWebpackConfig.plugins.push(
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: JSON.stringify('production'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = productionWebpackConfig;
|
module.exports = productionWebpackConfig;
|
||||||
|
34
yarn.lock
34
yarn.lock
@ -1639,10 +1639,6 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
commander@2.5.0:
|
|
||||||
version "2.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.0.tgz#d777b6a4d847d423e5d475da864294ac1ff5aa9d"
|
|
||||||
|
|
||||||
commander@2.8.x:
|
commander@2.8.x:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
|
||||||
@ -1899,20 +1895,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
|
|||||||
create-hash "^1.1.0"
|
create-hash "^1.1.0"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
|
|
||||||
cross-env@1.0.7:
|
|
||||||
version "1.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-1.0.7.tgz#dd6cea13b31df4ffab4591343e605e370182647e"
|
|
||||||
dependencies:
|
|
||||||
cross-spawn-async "2.0.0"
|
|
||||||
lodash.assign "^3.2.0"
|
|
||||||
|
|
||||||
cross-spawn-async@2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.0.0.tgz#4af143df4156900d012be41cabf4da3abfc797c0"
|
|
||||||
dependencies:
|
|
||||||
lru-cache "^2.6.5"
|
|
||||||
which "^1.1.1"
|
|
||||||
|
|
||||||
cross-spawn@^3.0.0:
|
cross-spawn@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
|
||||||
@ -3970,10 +3952,6 @@ ignore-by-default@^1.0.0:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||||
|
|
||||||
ignore-loader@0.1.1:
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.1.tgz#187c846c661afcdee269ef4c3f42c73888903334"
|
|
||||||
|
|
||||||
ignore@^3.2.0:
|
ignore@^3.2.0:
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd"
|
||||||
@ -5165,12 +5143,6 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.14, loader-utils@^0.
|
|||||||
json5 "^0.5.0"
|
json5 "^0.5.0"
|
||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
|
|
||||||
localenv@0.2.2:
|
|
||||||
version "0.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/localenv/-/localenv-0.2.2.tgz#c508f29d3485bdc9341d3ead17f61c5abd1b0bab"
|
|
||||||
dependencies:
|
|
||||||
commander "2.5.0"
|
|
||||||
|
|
||||||
locate-path@^2.0.0:
|
locate-path@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
|
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
|
||||||
@ -5293,7 +5265,7 @@ lodash._topath@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash.isarray "^3.0.0"
|
lodash.isarray "^3.0.0"
|
||||||
|
|
||||||
lodash.assign@^3.0.0, lodash.assign@^3.2.0:
|
lodash.assign@^3.0.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
|
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5564,7 +5536,7 @@ lowercase-keys@^1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||||
|
|
||||||
lru-cache@2, lru-cache@^2.6.5:
|
lru-cache@2:
|
||||||
version "2.7.3"
|
version "2.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
||||||
|
|
||||||
@ -8969,7 +8941,7 @@ which-module@^1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
|
||||||
|
|
||||||
which@1, which@^1.0.5, which@^1.1.1, which@^1.2.10, which@^1.2.12, which@^1.2.9:
|
which@1, which@^1.0.5, which@^1.2.10, which@^1.2.12, which@^1.2.9:
|
||||||
version "1.2.14"
|
version "1.2.14"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
|
resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Reference in New Issue
Block a user