Merge master
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
import classNames from 'classnames/bind';
|
||||
import styles from './Alert.scss';
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
children?: React.Element<any>,
|
||||
style?: Object,
|
||||
maxWidth?: string,
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
@@ -13,20 +11,17 @@ const Container = styled.div`
|
||||
margin: 40px 20px;
|
||||
`;
|
||||
|
||||
const CenteredContent = ({
|
||||
children,
|
||||
maxWidth = '740px',
|
||||
style,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const styles = {
|
||||
maxWidth,
|
||||
...style,
|
||||
};
|
||||
const Content = styled.div`
|
||||
max-width: 740px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const CenteredContent = ({ children, ...rest }: Props) => {
|
||||
return (
|
||||
<Container style={styles} {...rest}>
|
||||
{children}
|
||||
<Container {...rest}>
|
||||
<Content>
|
||||
{children}
|
||||
</Content>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ class DocumentViewersStore {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.get(
|
||||
const res = await client.post(
|
||||
'/views.list',
|
||||
{
|
||||
id: this.documentId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import Popover from 'components/Popover';
|
||||
import styled from 'styled-components';
|
||||
import DocumentViewers from './components/DocumentViewers';
|
||||
import DocumentViewersStore from './DocumentViewersStore';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import map from 'lodash/map';
|
||||
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() {
|
||||
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
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
import { size } from 'styles/constants';
|
||||
|
||||
const RealTextarea = styled.textarea`
|
||||
|
||||
@@ -6,11 +6,13 @@ import styled from 'styled-components';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import _ from 'lodash';
|
||||
import keydown from 'react-keydown';
|
||||
import { Flex } from 'reflexbox';
|
||||
import { textColor } from 'styles/constants.scss';
|
||||
import Flex from 'components/Flex';
|
||||
import { color, layout } from 'styles/constants';
|
||||
|
||||
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
||||
import Scrollable from 'components/Scrollable';
|
||||
import Avatar from 'components/Avatar';
|
||||
|
||||
import SidebarCollection from './components/SidebarCollection';
|
||||
import SidebarCollectionList from './components/SidebarCollectionList';
|
||||
@@ -101,21 +103,24 @@ type Props = {
|
||||
</Header>
|
||||
|
||||
<Flex column>
|
||||
<LinkSection>
|
||||
<SidebarLink to="/search">Search</SidebarLink>
|
||||
</LinkSection>
|
||||
<LinkSection>
|
||||
<SidebarLink to="/dashboard">Home</SidebarLink>
|
||||
<SidebarLink to="/starred">Starred</SidebarLink>
|
||||
</LinkSection>
|
||||
<LinkSection>
|
||||
{ui.activeCollection
|
||||
? <SidebarCollection
|
||||
document={ui.activeDocument}
|
||||
collection={ui.activeCollection}
|
||||
/>
|
||||
: <SidebarCollectionList />}
|
||||
</LinkSection>
|
||||
<Scrollable>
|
||||
<LinkSection>
|
||||
<SidebarLink to="/search">Search</SidebarLink>
|
||||
</LinkSection>
|
||||
<LinkSection>
|
||||
<SidebarLink to="/dashboard">Home</SidebarLink>
|
||||
<SidebarLink to="/starred">Starred</SidebarLink>
|
||||
</LinkSection>
|
||||
<LinkSection>
|
||||
{ui.activeCollection
|
||||
? <SidebarCollection
|
||||
document={ui.activeDocument}
|
||||
collection={ui.activeCollection}
|
||||
history={this.props.history}
|
||||
/>
|
||||
: <SidebarCollectionList history={this.props.history} />}
|
||||
</LinkSection>
|
||||
</Scrollable>
|
||||
</Flex>
|
||||
</Sidebar>}
|
||||
|
||||
@@ -135,22 +140,16 @@ const Container = styled(Flex)`
|
||||
`;
|
||||
|
||||
const LogoLink = styled(Link)`
|
||||
margin-top: 5px;
|
||||
margin-top: 15px;
|
||||
font-family: 'Atlas Grotesk';
|
||||
font-weight: bold;
|
||||
color: ${textColor};
|
||||
color: ${color.text};
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const Avatar = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
const MenuLink = styled(Link)`
|
||||
color: ${textColor};
|
||||
color: ${color.text};
|
||||
`;
|
||||
|
||||
const Content = styled(Flex)`
|
||||
@@ -159,26 +158,27 @@ const Content = styled(Flex)`
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: ${props => (props.editMode ? 0 : '250px')};
|
||||
left: ${props => (props.editMode ? 0 : layout.sidebarWidth)};
|
||||
transition: left 200ms ease-in-out;
|
||||
`;
|
||||
|
||||
const Sidebar = styled(Flex)`
|
||||
width: 250px;
|
||||
margin-left: ${props => (props.editMode ? '-250px' : 0)};
|
||||
padding: 10px 20px;
|
||||
width: ${layout.sidebarWidth};
|
||||
margin-left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)};
|
||||
background: rgba(250, 251, 252, 0.71);
|
||||
border-right: 1px solid #eceff3;
|
||||
transition: margin-left 200ms ease-in-out;
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
padding: ${layout.padding};
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
|
||||
const LinkSection = styled(Flex)`
|
||||
margin-bottom: 20px;
|
||||
flex-direction: column;
|
||||
padding: 10px 0;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout));
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { layout } from 'styles/constants';
|
||||
import SidebarLink from '../SidebarLink';
|
||||
import DropToImport from 'components/DropToImport';
|
||||
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import type { NavigationNode } from 'types';
|
||||
|
||||
type Props = {
|
||||
collection: ?Collection,
|
||||
document: ?Document,
|
||||
history: Object,
|
||||
};
|
||||
|
||||
const activeStyle = {
|
||||
color: '#000',
|
||||
background: '#E1E1E1',
|
||||
};
|
||||
|
||||
class SidebarCollection extends React.Component {
|
||||
props: Props;
|
||||
|
||||
renderDocuments(documentList) {
|
||||
const { document } = this.props;
|
||||
renderDocuments(documentList: Array<NavigationNode>, depth: number = 0) {
|
||||
const { document, history } = this.props;
|
||||
const canDropToImport = depth === 0;
|
||||
|
||||
if (document) {
|
||||
return documentList.map(doc => (
|
||||
<Flex column key={doc.id}>
|
||||
<SidebarLink key={doc.id} to={doc.url}>
|
||||
{doc.title}
|
||||
</SidebarLink>
|
||||
{canDropToImport &&
|
||||
<DropToImport
|
||||
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.id === doc.id) &&
|
||||
<Children>
|
||||
{doc.children && this.renderDocuments(doc.children)}
|
||||
<Children column>
|
||||
{doc.children && this.renderDocuments(doc.children, depth + 1)}
|
||||
</Children>}
|
||||
</Flex>
|
||||
));
|
||||
@@ -57,10 +73,11 @@ const Header = styled(Flex)`
|
||||
text-transform: uppercase;
|
||||
color: #9FA6AB;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0 ${layout.hpadding};
|
||||
`;
|
||||
|
||||
const Children = styled(Flex)`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
export default observer(SidebarCollection);
|
||||
export default SidebarCollection;
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { layout } from 'styles/constants';
|
||||
|
||||
import SidebarLink from '../SidebarLink';
|
||||
import DropToImport from 'components/DropToImport';
|
||||
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
|
||||
type Props = {
|
||||
history: Object,
|
||||
collections: CollectionsStore,
|
||||
};
|
||||
|
||||
const SidebarCollectionList = observer(({ collections }: Props) => {
|
||||
const activeStyle = {
|
||||
color: '#000',
|
||||
background: '#E1E1E1',
|
||||
};
|
||||
|
||||
const SidebarCollectionList = observer(({ history, collections }: Props) => {
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>Collections</Header>
|
||||
{collections.data.map(collection => (
|
||||
<SidebarLink key={collection.id} to={collection.entryUrl}>
|
||||
{collection.name}
|
||||
</SidebarLink>
|
||||
<DropToImport
|
||||
history={history}
|
||||
collectionId={collection.id}
|
||||
activeStyle={activeStyle}
|
||||
>
|
||||
<SidebarLink key={collection.id} to={collection.entryUrl}>
|
||||
{collection.name}
|
||||
</SidebarLink>
|
||||
</DropToImport>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
@@ -31,6 +45,7 @@ const Header = styled(Flex)`
|
||||
text-transform: uppercase;
|
||||
color: #9FA6AB;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0 ${layout.hpadding};
|
||||
`;
|
||||
|
||||
export default inject('collections')(SidebarCollectionList);
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { Flex } from 'reflexbox';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { layout, color } from 'styles/constants';
|
||||
import { darken } from 'polished';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const activeStyle = {
|
||||
color: '#000000',
|
||||
};
|
||||
|
||||
@observer class SidebarLink extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
function SidebarLink(props: Object) {
|
||||
return <StyledNavLink exact {...props} activeStyle={activeStyle} />;
|
||||
}
|
||||
|
||||
const LinkContainer = styled(Flex)`
|
||||
padding: 5px 0;
|
||||
|
||||
a {
|
||||
color: #848484;
|
||||
const StyledNavLink = styled(NavLink)`
|
||||
display: block;
|
||||
padding: 5px ${layout.hpadding};
|
||||
color: ${color.slateDark};
|
||||
|
||||
&:hover {
|
||||
color: ${darken(0.1, color.slateDark)};
|
||||
}
|
||||
`;
|
||||
|
||||
export default withRouter(SidebarLink);
|
||||
export default SidebarLink;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
import { randomInteger } from 'utils/random';
|
||||
|
||||
@@ -11,7 +11,7 @@ const randomValues = Array.from(
|
||||
() => `${randomInteger(85, 100)}%`
|
||||
);
|
||||
|
||||
export default () => {
|
||||
export default (props: {}) => {
|
||||
return (
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="fadeIn"
|
||||
@@ -22,7 +22,7 @@ export default () => {
|
||||
transitionEnterTimeout={0}
|
||||
transitionLeaveTimeout={0}
|
||||
>
|
||||
<Flex column auto>
|
||||
<Flex column auto {...props}>
|
||||
<Mask style={{ width: randomValues[0] }} header />
|
||||
<Mask style={{ width: randomValues[1] }} />
|
||||
<Mask style={{ width: randomValues[2] }} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import type { User } from 'types';
|
||||
import { Flex } from 'reflexbox';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
justify-content: space-between;
|
||||
@@ -51,7 +51,6 @@ class PublishingInfo extends Component {
|
||||
<Avatar key={user.id} src={user.avatarUrl} title={user.name} />
|
||||
))}
|
||||
</Avatars>}
|
||||
|
||||
{createdAt === updatedAt
|
||||
? <span>
|
||||
{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;
|
||||
Reference in New Issue
Block a user