diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index ebb89a2c..550501a2 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -99,7 +99,8 @@ export default class MarkdownEditor extends Component { render = () => { return ( - + {!this.props.readOnly && + } (this.editor = ref)} @@ -114,7 +115,8 @@ export default class MarkdownEditor extends Component { onSave={this.props.onSave} readOnly={this.props.readOnly} /> - + {!this.props.readOnly && + } ); }; diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 2d6aec85..302dbbb6 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -6,23 +6,31 @@ import styled from 'styled-components'; import { observer, inject } from 'mobx-react'; import _ from 'lodash'; import keydown from 'react-keydown'; -import searchIcon from 'assets/icons/search.svg'; import { Flex } from 'reflexbox'; -import { textColor, headerHeight } from 'styles/constants.scss'; +import { textColor } from 'styles/constants.scss'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import LoadingIndicator from 'components/LoadingIndicator'; + +import SidebarCollection from './components/SidebarCollection'; +import SidebarCollectionList from './components/SidebarCollectionList'; +import SidebarLink from './components/SidebarLink'; + import UserStore from 'stores/UserStore'; import AuthStore from 'stores/AuthStore'; +import UiStore from 'stores/UiStore'; +import CollectionsStore from 'stores/CollectionsStore'; type Props = { history: Object, + collections: CollectionsStore, children?: ?React.Element, actions?: ?React.Element, title?: ?React.Element, loading?: boolean, user: UserStore, auth: AuthStore, + ui: UiStore, search: ?boolean, notifications?: React.Element, }; @@ -36,13 +44,13 @@ type Props = { @keydown(['/', 't']) search() { - if (this.props.auth.isAuthenticated) + if (this.props.auth.authenticated) _.defer(() => this.props.history.push('/search')); } @keydown(['d']) dashboard() { - if (this.props.auth.isAuthenticated) + if (this.props.auth.authenticated) _.defer(() => this.props.history.push('/')); } @@ -51,7 +59,7 @@ type Props = { }; render() { - const { auth, user } = this.props; + const { user, auth, ui, collections } = this.props; return ( @@ -69,78 +77,65 @@ type Props = { {this.props.notifications} -
- - Atlas - - {this.props.title} - - - - - - {this.props.actions} - - {auth.authenticated && - user && - - {this.props.search && - - - - - - - } - }> - - Settings - - - - Keyboard shortcuts - - - - API - - Logout - - } - - -
+ + {auth.authenticated && + user && + +
+ + Atlas + + }> + + Settings + + + + Keyboard shortcuts + + + + API + + Logout + +
- - {this.props.children} - + + + Search + + + Dashboard + Starred + + + {ui.activeCollection + ? + : } + + +
} + + + {this.props.children} + +
); } } const Container = styled(Flex)` + position: relative; width: 100%; height: 100%; `; -const Header = styled(Flex)` - display: flex; - justify-content: space-between; - align-items: center; - - padding: 0 20px; - - z-index: 1; - background: #fff; - height: ${headerHeight}; - border-bottom: 1px solid #eee; - - font-size: 14px; - line-height: 1; -`; - const LogoLink = styled(Link)` + margin-top: 5px; font-family: 'Atlas Grotesk'; font-weight: bold; color: ${textColor}; @@ -148,28 +143,6 @@ const LogoLink = styled(Link)` font-size: 16px; `; -const Title = styled.span` - color: #ccc; - - a { - color: #ccc; - } - - a:hover { - color: ${textColor}; - } -`; - -const Search = styled(Flex)` - margin: 0 5px; - padding: 15px 5px 0 5px; - cursor: pointer; -`; - -const SearchIcon = styled.img` - height: 20px; -`; - const Avatar = styled.img` width: 24px; height: 24px; @@ -181,8 +154,31 @@ const MenuLink = styled(Link)` `; const Content = styled(Flex)` - height: 100%; overflow: scroll; + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: ${props => (props.editMode ? 0 : '250px')}; + transition: left 200ms ease-in-out; `; -export default withRouter(inject('user', 'auth')(Layout)); +const Sidebar = styled(Flex)` + width: 250px; + margin-left: ${props => (props.editMode ? '-250px' : 0)}; + padding: 10px 20px; + 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; +`; + +const LinkSection = styled(Flex)` + margin-bottom: 20px; + flex-direction: column; +`; + +export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout)); diff --git a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js new file mode 100644 index 00000000..de6a1244 --- /dev/null +++ b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js @@ -0,0 +1,39 @@ +// @flow +import React from 'react'; +import { observer } from 'mobx-react'; +import { Flex } from 'reflexbox'; +import styled from 'styled-components'; + +import SidebarLink from '../SidebarLink'; + +import Collection from 'models/Collection'; + +type Props = { + collection: Collection, +}; + +const SidebarCollection = ({ collection }: Props) => { + if (collection) { + return ( + +
{collection.name}
+ {collection.documents.map(document => ( + + {document.title} + + ))} +
+ ); + } + return null; +}; + +const Header = styled(Flex)` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: #9FA6AB; + letter-spacing: 0.04em; +`; + +export default observer(SidebarCollection); diff --git a/frontend/components/Layout/components/SidebarCollection/index.js b/frontend/components/Layout/components/SidebarCollection/index.js new file mode 100644 index 00000000..80b2a046 --- /dev/null +++ b/frontend/components/Layout/components/SidebarCollection/index.js @@ -0,0 +1,3 @@ +// @flow +import SidebarCollection from './SidebarCollection'; +export default SidebarCollection; diff --git a/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js new file mode 100644 index 00000000..0115ec66 --- /dev/null +++ b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js @@ -0,0 +1,36 @@ +// @flow +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import { Flex } from 'reflexbox'; +import styled from 'styled-components'; + +import SidebarLink from '../SidebarLink'; + +import CollectionsStore from 'stores/CollectionsStore'; + +type Props = { + collections: CollectionsStore, +}; + +const SidebarCollectionList = observer(({ collections }: Props) => { + return ( + +
Collections
+ {collections.data.map(collection => ( + + {collection.name} + + ))} +
+ ); +}); + +const Header = styled(Flex)` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: #9FA6AB; + letter-spacing: 0.04em; +`; + +export default inject('collections')(SidebarCollectionList); diff --git a/frontend/components/Layout/components/SidebarCollectionList/index.js b/frontend/components/Layout/components/SidebarCollectionList/index.js new file mode 100644 index 00000000..ed4ba40c --- /dev/null +++ b/frontend/components/Layout/components/SidebarCollectionList/index.js @@ -0,0 +1,3 @@ +// @flow +import SidebarCollectionList from './SidebarCollectionList'; +export default SidebarCollectionList; diff --git a/frontend/components/Layout/components/SidebarLink/SidebarLink.js b/frontend/components/Layout/components/SidebarLink/SidebarLink.js new file mode 100644 index 00000000..73543635 --- /dev/null +++ b/frontend/components/Layout/components/SidebarLink/SidebarLink.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import { observer } from 'mobx-react'; +import { NavLink } from 'react-router-dom'; +import { Flex } from 'reflexbox'; +import styled from 'styled-components'; + +const activeStyle = { + color: '#000000', +}; + +const SidebarLink = observer(props => ( + + + +)); + +const LinkContainer = styled(Flex)` + padding: 5px 0; + + a { + color: #848484; + } +`; + +export default SidebarLink; diff --git a/frontend/components/Layout/components/SidebarLink/index.js b/frontend/components/Layout/components/SidebarLink/index.js new file mode 100644 index 00000000..eabfd77f --- /dev/null +++ b/frontend/components/Layout/components/SidebarLink/index.js @@ -0,0 +1,3 @@ +// @flow +import SidebarLink from './SidebarLink'; +export default SidebarLink; diff --git a/frontend/components/Sidebar/Sidebar.js b/frontend/components/Sidebar/Sidebar.js deleted file mode 100644 index d4a9f58b..00000000 --- a/frontend/components/Sidebar/Sidebar.js +++ /dev/null @@ -1,85 +0,0 @@ -// @flow -import React from 'react'; -import { observer } from 'mobx-react'; -import { withRouter } from 'react-router-dom'; - -import { Flex } from 'reflexbox'; -import Tree from 'components/Tree'; -import Separator from './components/Separator'; - -import styles from './Sidebar.scss'; -import classNames from 'classnames/bind'; -const cx = classNames.bind(styles); - -import SidebarStore from './SidebarStore'; - -type Props = { - open?: boolean, - onToggle: Function, - navigationTree: Object, - onNavigationUpdate: Function, - onNodeCollapse: Function, - history: Object, -}; - -@observer class Sidebar extends React.Component { - props: Props; - store: SidebarStore; - - constructor(props: Props) { - super(props); - - this.store = new SidebarStore(); - } - - toggleEdit = (e: MouseEvent) => { - e.preventDefault(); - this.store.toggleEdit(); - }; - - render() { - return ( - - {this.props.open && - - - - - - {this.store.isEditing && - - Drag & drop to organize   - } - - {!this.store.isEditing ? 'Organize documents' : 'Done'} - - - } -
- Menu -
-
- ); - } -} - -export default withRouter(Sidebar); diff --git a/frontend/components/Sidebar/Sidebar.scss b/frontend/components/Sidebar/Sidebar.scss deleted file mode 100644 index dc46e94e..00000000 --- a/frontend/components/Sidebar/Sidebar.scss +++ /dev/null @@ -1,56 +0,0 @@ -@import '~styles/constants.scss'; - -.sidebar { - position: relative; - width: 250px; - border-right: 1px solid #eee; - font-size: 13px; -} - -.sidebarToggle { - display: flex; - position: relative; - width: 60px; - cursor: pointer; - justify-content: center; - - &:hover { - background-color: #f5f5f5; - - .menuIcon { - opacity: 1; - } - } -} - -.menuIcon { - margin-top: 18px; - height: 28px; - opacity: 0.15; -} - -.content { - padding: 20px 20px 20px 5px; -} - -.tree { - -} - -.actions { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background-color: #fff; - padding: 10px 20px; - height: 40px; -} - -.action { - color: $gray; - - &.active { - color: $textColor; - } -} diff --git a/frontend/components/Sidebar/SidebarStore.js b/frontend/components/Sidebar/SidebarStore.js deleted file mode 100644 index b57621c5..00000000 --- a/frontend/components/Sidebar/SidebarStore.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import { observable, action } from 'mobx'; - -class SidebarStore { - @observable isEditing = false; - - /* Actions */ - - @action toggleEdit = () => { - this.isEditing = !this.isEditing; - }; -} - -export default SidebarStore; diff --git a/frontend/components/Sidebar/components/Separator/Separator.js b/frontend/components/Sidebar/components/Separator/Separator.js deleted file mode 100644 index 8cc898a7..00000000 --- a/frontend/components/Sidebar/components/Separator/Separator.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import React from 'react'; - -import styles from './Separator.scss'; - -class Separator extends React.Component { - render() { - return ( - - ยท - - ); - } -} - -export default Separator; diff --git a/frontend/components/Sidebar/components/Separator/Separator.scss b/frontend/components/Sidebar/components/Separator/Separator.scss deleted file mode 100644 index 7aa44d11..00000000 --- a/frontend/components/Sidebar/components/Separator/Separator.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '~styles/constants.scss'; - -.separator { - padding: 0 10px; - color: $lightGray; -} diff --git a/frontend/components/Sidebar/components/Separator/index.js b/frontend/components/Sidebar/components/Separator/index.js deleted file mode 100644 index 4b828564..00000000 --- a/frontend/components/Sidebar/components/Separator/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import Separator from './Separator'; -export default Separator; diff --git a/frontend/components/Sidebar/index.js b/frontend/components/Sidebar/index.js deleted file mode 100644 index c1b441c2..00000000 --- a/frontend/components/Sidebar/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import Sidebar from './Sidebar'; -export default Sidebar; diff --git a/frontend/components/Tree/Node.js b/frontend/components/Tree/Node.js deleted file mode 100644 index ba8157a3..00000000 --- a/frontend/components/Tree/Node.js +++ /dev/null @@ -1,139 +0,0 @@ -/* eslint-disable */ -import React from 'react'; - -import styles from './Tree.scss'; -import classNames from 'classnames/bind'; -const cx = classNames.bind(styles); - -class Node extends React.Component { - displayName: 'UITreeNode'; - - renderCollapse = () => { - const index = this.props.index; - - if (index.children && index.children.length) { - const collapsed = index.node.collapsed; - - return ( - - Expand - - ); - } - - return null; - }; - - renderChildren = () => { - const index = this.props.index; - const tree = this.props.tree; - const dragging = this.props.dragging; - - if (index.children && index.children.length) { - const childrenStyles = {}; - - if (!this.props.rootNode) { - if (index.node.collapsed) childrenStyles.display = 'none'; - childrenStyles.paddingLeft = `${this.props.paddingLeft}px`; - } - - return ( -
- {index.children.map(child => { - const childIndex = tree.getIndex(child); - return ( - - ); - })} -
- ); - } - - return null; - }; - - isModifying = () => { - return this.props.allowUpdates && !this.props.rootNode; - }; - - onClick = () => { - const index = this.props.index; - const node = index.node; - if (!this.isModifying()) this.props.history.push(node.url); - }; - - render() { - const index = this.props.index; - const dragging = this.props.dragging; - const node = index.node; - const style = {}; - - return ( -
-
e.stopPropagation() - : this.handleMouseDown - } - > - {!this.props.rootNode && this.renderCollapse()} - - {node.title} - -
- {this.renderChildren()} -
- ); - } - - handleCollapse = e => { - e.stopPropagation(); - const nodeId = this.props.index.id; - if (this.props.onCollapse) this.props.onCollapse(nodeId); - }; - - handleMouseDown = e => { - const nodeId = this.props.index.id; - const dom = this.refs.inner; - - if (this.props.onDragStart) { - this.props.onDragStart(nodeId, dom, e); - } - }; -} - -module.exports = Node; diff --git a/frontend/components/Tree/Tree.js b/frontend/components/Tree/Tree.js deleted file mode 100644 index 7296da9a..00000000 --- a/frontend/components/Tree/Tree.js +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-disable */ -const Tree = require('js-tree'); -const proto = Tree.prototype; - -proto.updateNodesPosition = function() { - let top = 1; - let left = 1; - const root = this.getIndex(1); - const self = this; - - root.top = top++; - root.left = left++; - - if (root.children && root.children.length) { - walk(root.children, root, left, root.node.collapsed); - } - - function walk(children, parent, left, collapsed) { - let height = 1; - children.forEach(id => { - const node = self.getIndex(id); - if (collapsed) { - node.top = null; - node.left = null; - } else { - node.top = top++; - node.left = left; - } - - if (node.children && node.children.length) { - height += walk( - node.children, - node, - left + 1, - collapsed || node.node.collapsed - ); - } else { - node.height = 1; - height += 1; - } - }); - - if (parent.node.collapsed) parent.height = 1; - else parent.height = height; - return parent.height; - } -}; - -proto.move = function(fromId, toId, placement) { - if (fromId === toId || toId === 1) return; - - const obj = this.remove(fromId); - let index = null; - - if (placement === 'before') index = this.insertBefore(obj, toId); - else if (placement === 'after') index = this.insertAfter(obj, toId); - else if (placement === 'prepend') index = this.prepend(obj, toId); - else if (placement === 'append') index = this.append(obj, toId); - - // todo: perf - this.updateNodesPosition(); - return index; -}; - -proto.getNodeByTop = function(top) { - const indexes = this.indexes; - for (const id in indexes) { - if (indexes.hasOwnProperty(id)) { - if (indexes[id].top === top) return indexes[id]; - } - } -}; - -module.exports = Tree; diff --git a/frontend/components/Tree/Tree.scss b/frontend/components/Tree/Tree.scss deleted file mode 100644 index 22c70218..00000000 --- a/frontend/components/Tree/Tree.scss +++ /dev/null @@ -1,87 +0,0 @@ -@mixin no-select { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.tree { - @include no-select; - padding: 20px 20px 20px 5px; - overflow: scroll; - position: absolute; - top: 0; - bottom: 40px; - right: 0; - left: 0; -} - -.draggable { - position: absolute; - opacity: 0.8; - @include no-select; -} - -.node { - &.placeholder > * { - visibility: hidden; - } - - &.placeholder { - border: 1px dashed #ccc; - } - - .inner { - position: relative; - cursor: pointer; - padding-left: 10px; - } - - .collapse { - position: absolute; - left: 0; - cursor: pointer; - - width: 20px; - height: 25px; - } - - .caretRight { - margin-top: 3px; - margin-left: -3px; - } - - .caretDown { - transform: rotate(90deg); - margin-left: -4px; - margin-top: 2px; - } -} - -.node { - &.placeholder { - border: 1px dashed #1385e5; - } - - .nodeLabel { - display: inline-block; - width: 100%; - padding: 4px 5px 0; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - &.isModifying { - cursor: move; - } - - &.isActive { - background-color: #31363F; - } - } - - .rootLabel { - color: #ccc; - } -} diff --git a/frontend/components/Tree/UiTree.js b/frontend/components/Tree/UiTree.js deleted file mode 100644 index 02d774d3..00000000 --- a/frontend/components/Tree/UiTree.js +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable */ -const React = require('react'); -const Tree = require('./Tree'); -const Node = require('./Node'); - -import styles from './Tree.scss'; - -export default React.createClass({ - displayName: 'UITree', - - propTypes: { - tree: React.PropTypes.object.isRequired, - paddingLeft: React.PropTypes.number, - onCollapse: React.PropTypes.func, - allowUpdates: React.PropTypes.bool, - history: React.PropTypes.object, - }, - - getDefaultProps() { - return { - paddingLeft: 20, - }; - }, - - getInitialState() { - return this.init(this.props); - }, - - componentWillReceiveProps(nextProps) { - if (!this._updated) this.setState(this.init(nextProps)); - else this._updated = false; - }, - - init(props) { - const tree = new Tree(props.tree); - tree.isNodeCollapsed = props.isNodeCollapsed; - tree.changeNodeCollapsed = props.changeNodeCollapsed; - tree.updateNodesPosition(); - - return { - tree, - dragging: { - id: null, - x: null, - y: null, - w: null, - h: null, - }, - }; - }, - - getDraggingDom() { - const tree = this.state.tree; - const dragging = this.state.dragging; - - if (dragging && dragging.id) { - const draggingIndex = tree.getIndex(dragging.id); - const draggingStyles = { - top: dragging.y, - left: dragging.x, - width: dragging.w, - }; - - return ( -
- -
- ); - } - - return null; - }, - - render() { - const tree = this.state.tree; - const dragging = this.state.dragging; - const draggingDom = this.getDraggingDom(); - - return ( -
- {draggingDom} - -
- ); - }, - - dragStart(id, dom, e) { - this.dragging = { - id, - w: dom.offsetWidth, - h: dom.offsetHeight, - x: dom.offsetLeft, - y: dom.offsetTop, - }; - - this._startX = dom.offsetLeft; - this._startY = dom.offsetTop; - this._offsetX = e.clientX; - this._offsetY = e.clientY; - this._start = true; - - window.addEventListener('mousemove', this.drag); - window.addEventListener('mouseup', this.dragEnd); - }, - - // oh - drag(e) { - if (this._start) { - this.setState({ - dragging: this.dragging, - }); - this._start = false; - } - - const tree = this.state.tree; - const dragging = this.state.dragging; - const paddingLeft = this.props.paddingLeft; - let newIndex = null; - let index = tree.getIndex(dragging.id); - const collapsed = index.node.collapsed; - - const _startX = this._startX; - const _startY = this._startY; - const _offsetX = this._offsetX; - const _offsetY = this._offsetY; - - const pos = { - x: _startX + e.clientX - _offsetX, - y: _startY + e.clientY - _offsetY, - }; - dragging.x = pos.x; - dragging.y = pos.y; - - const diffX = dragging.x - paddingLeft / 2 - (index.left - 2) * paddingLeft; - const diffY = dragging.y - dragging.h / 2 - (index.top - 2) * dragging.h; - - if (diffX < 0) { - // left - if (index.parent && !index.next) { - newIndex = tree.move(index.id, index.parent, 'after'); - } - } else if (diffX > paddingLeft) { - // right - if (index.prev) { - const prevNode = tree.getIndex(index.prev).node; - if (!prevNode.collapsed && !prevNode.leaf) { - newIndex = tree.move(index.id, index.prev, 'append'); - } - } - } - - if (newIndex) { - index = newIndex; - newIndex.node.collapsed = collapsed; - dragging.id = newIndex.id; - } - - if (diffY < 0) { - // up - const above = tree.getNodeByTop(index.top - 1); - newIndex = tree.move(index.id, above.id, 'before'); - } else if (diffY > dragging.h) { - // down - if (index.next) { - let below = tree.getIndex(index.next); - if (below.children && below.children.length && !below.node.collapsed) { - newIndex = tree.move(index.id, index.next, 'prepend'); - } else { - newIndex = tree.move(index.id, index.next, 'after'); - } - } else { - let below = tree.getNodeByTop(index.top + index.height); - if (below && below.parent !== index.id) { - if (below.children && below.children.length) { - newIndex = tree.move(index.id, below.id, 'prepend'); - } else { - newIndex = tree.move(index.id, below.id, 'after'); - } - } - } - } - - if (newIndex) { - newIndex.node.collapsed = collapsed; - dragging.id = newIndex.id; - } - - this.setState({ - tree, - dragging, - }); - }, - - dragEnd() { - this.setState({ - dragging: { - id: null, - x: null, - y: null, - w: null, - h: null, - }, - }); - - this.change(this.state.tree); - window.removeEventListener('mousemove', this.drag); - window.removeEventListener('mouseup', this.dragEnd); - }, - - change(tree) { - this._updated = true; - if (this.props.onChange) this.props.onChange(tree.obj); - }, - - toggleCollapse(nodeId) { - const tree = this.state.tree; - const index = tree.getIndex(nodeId); - const node = index.node; - node.collapsed = !node.collapsed; - tree.updateNodesPosition(); - - this.setState({ - tree, - }); - - if (this.props.onCollapse) this.props.onCollapse(node.id, node.collapsed); - }, - - // buildTreeNumbering(tree) { - // const numberBuilder = (index, node, parentNumbering) => { - // let numbering = parentNumbering ? `${parentNumbering}.${index}` : index; - // let children; - // if (node.children) { - // children = node.children.map((child, childIndex) => { - // return numberBuilder(childIndex+1, child, numbering); - // }); - // } - - // const data = { - // module: { - // ...node.module, - // index: numbering, - // } - // } - // if (children) { - // data.children = children; - // } - - // return data; - // }; - - // const newTree = {...tree}; - // newTree.children = []; - // tree.children.forEach((child, index) => { - // newTree.children.push(numberBuilder(index+1, child)); - // }) - // return newTree; - // } -}); diff --git a/frontend/components/Tree/assets/chevron.svg b/frontend/components/Tree/assets/chevron.svg deleted file mode 100644 index 4daab592..00000000 --- a/frontend/components/Tree/assets/chevron.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/components/Tree/index.js b/frontend/components/Tree/index.js deleted file mode 100644 index fe1ac2ee..00000000 --- a/frontend/components/Tree/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import UiTree from './UiTree'; -export default UiTree; diff --git a/frontend/index.js b/frontend/index.js index 901089c8..6a8224ab 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -31,6 +31,8 @@ import Flatpage from 'scenes/Flatpage'; import ErrorAuth from 'scenes/ErrorAuth'; import Error404 from 'scenes/Error404'; +import Layout from 'components/Layout'; + import flatpages from 'static/flatpages'; let DevTools; @@ -94,32 +96,35 @@ render( - - - - - - - + + + + + - - - + + + - - + + + - - - + + + + + + + diff --git a/frontend/scenes/Collection/Collection.js b/frontend/scenes/Collection/Collection.js index 96210a58..cd00815c 100644 --- a/frontend/scenes/Collection/Collection.js +++ b/frontend/scenes/Collection/Collection.js @@ -3,11 +3,10 @@ import React from 'react'; import { observer, inject } from 'mobx-react'; import { Redirect } from 'react-router'; import _ from 'lodash'; -import { notFoundUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import CollectionStore from './CollectionStore'; -import Layout from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import PreviewLoading from 'components/PreviewLoading'; @@ -16,50 +15,26 @@ type Props = { match: Object, }; -type State = { - redirectUrl: ?string, -}; - @observer class Collection extends React.Component { props: Props; - state: State; + store: CollectionStore; constructor(props) { super(props); - this.state = { - redirectUrl: null, - }; + this.store = new CollectionStore(); } componentDidMount = () => { const { id } = this.props.match.params; - this.props.collections - .getById(id) - .then(collection => { - if (collection.type !== 'atlas') - throw new Error('TODO code up non-atlas collections'); - - this.setState({ - redirectUrl: collection.documents[0].url, - }); - }) - .catch(() => { - this.setState({ - redirectUrl: notFoundUrl(), - }); - }); + this.store.fetchCollection(id); }; render() { - return ( - - {this.state.redirectUrl && } - - + return this.store.redirectUrl + ? + : - - - ); + ; } } export default inject('collections')(Collection); diff --git a/frontend/scenes/Collection/CollectionStore.js b/frontend/scenes/Collection/CollectionStore.js index 8aa446e8..16569b8e 100644 --- a/frontend/scenes/Collection/CollectionStore.js +++ b/frontend/scenes/Collection/CollectionStore.js @@ -1,37 +1,30 @@ // @flow -import { observable, action, computed } from 'mobx'; +import { observable, action } from 'mobx'; import invariant from 'invariant'; import { client } from 'utils/ApiClient'; -import Collection from 'models/Collection'; - -const store = new class AtlasStore { - @observable collection: ?(Collection & { recentDocuments?: Object[] }); +import { notFoundUrl } from 'utils/routeHelpers'; +class CollectionStore { + @observable redirectUrl: ?string; @observable isFetching = true; - /* Computed */ - - @computed get isLoaded(): boolean { - return !this.isFetching && !!this.collection; - } - /* Actions */ - @action fetchCollection = async (id: string, successCallback: Function) => { + @action fetchCollection = async (id: string) => { this.isFetching = true; - this.collection = null; try { const res = await client.get('/collections.info', { id }); invariant(res && res.data, 'Data should be available'); const { data } = res; - this.collection = new Collection(data); - successCallback(data); + + if (data.type === 'atlas') this.redirectUrl = data.documents[0].url; + else throw new Error('TODO code up non-atlas collections'); } catch (e) { - console.error('Something went wrong'); + this.redirectUrl = notFoundUrl(); } this.isFetching = false; }; -}(); +} -export default store; +export default CollectionStore; diff --git a/frontend/scenes/Dashboard/Dashboard.js b/frontend/scenes/Dashboard/Dashboard.js index 57ed95bb..af6ff217 100644 --- a/frontend/scenes/Dashboard/Dashboard.js +++ b/frontend/scenes/Dashboard/Dashboard.js @@ -5,7 +5,6 @@ import { Flex } from 'reflexbox'; import CollectionsStore from 'stores/CollectionsStore'; -import Layout from 'components/Layout'; import Collection from 'components/Collection'; import PreviewLoading from 'components/PreviewLoading'; import CenteredContent from 'components/CenteredContent'; @@ -21,17 +20,15 @@ type Props = { const { collections } = this.props; return ( - - - - {!collections.isLoaded - ? - : collections.data.map(collection => ( - - ))} - - - + + + {!collections.isLoaded + ? + : collections.data.map(collection => ( + + ))} + + ); } } diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index c8b0631d..d32e9448 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -2,15 +2,16 @@ import React, { Component } from 'react'; import get from 'lodash/get'; import styled from 'styled-components'; -import { observer } from 'mobx-react'; +import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; import { Flex } from 'reflexbox'; +import UiStore from 'stores/UiStore'; + import DocumentStore from './DocumentStore'; -import Breadcrumbs from './components/Breadcrumbs'; import Menu from './components/Menu'; import Editor from 'components/Editor'; -import Layout, { HeaderAction, SaveAction } from 'components/Layout'; +import { HeaderAction, SaveAction } from 'components/Layout'; import PublishingInfo from 'components/PublishingInfo'; import PreviewLoading from 'components/PreviewLoading'; import CenteredContent from 'components/CenteredContent'; @@ -21,25 +22,12 @@ You have unsaved changes. Are you sure you want to discard them? `; -const Container = styled.div` - position: relative; - font-weight: 400; - font-size: 1em; - line-height: 1.5em; - padding: 0 3em; - width: 50em; -`; - -const Meta = styled.div` - position: absolute; - top: 12px; -`; - type Props = { match: Object, history: Object, keydown: Object, newChildDocument?: boolean, + ui: UiStore, }; @observer class Document extends Component { @@ -48,10 +36,13 @@ type Props = { constructor(props: Props) { super(props); - this.store = new DocumentStore({ history: this.props.history }); + this.store = new DocumentStore({ + history: this.props.history, + ui: props.ui, + }); } - componentDidMount = () => { + componentDidMount() { if (this.props.newDocument) { this.store.collectionId = this.props.match.params.id; this.store.newDocument = true; @@ -67,19 +58,25 @@ type Props = { this.store.newDocument = false; this.store.fetchDocument(); } - }; + } + + componentWillUnmout() { + this.props.ui.clearActiveCollection(); + } onEdit = () => { const url = `${this.store.document.url}/edit`; this.props.history.push(url); + this.props.ui.enableEditMode(); }; - onSave = (options: { redirect?: boolean } = {}) => { + onSave = async (options: { redirect?: boolean } = {}) => { if (this.store.newDocument || this.store.newChildDocument) { - this.store.saveDocument(options); + await this.store.saveDocument(options); } else { - this.store.updateDocument(options); + await this.store.updateDocument(options); } + this.props.ui.disableEditMode(); }; onImageUploadStart = () => { @@ -97,12 +94,12 @@ type Props = { render() { const isNew = this.props.newDocument || this.props.newChildDocument; const isEditing = this.props.match.params.edit; - const title = ( + /*const title = ( - ); + );*/ const titleText = this.store.document && get(this.store, 'document.title'); @@ -117,49 +114,71 @@ type Props = { /> : Edit} - + {!isEditing && + } ); return ( - - - - {this.store.isFetching && - - - } - {this.store.document && - - {!isEditing && - + + {actions} + + + + {this.store.isFetching && + + + } + {this.store.document && + + {!isEditing && - } - - } - + />} + + } + + ); } } -export default withRouter(Document); +const Container = styled(Flex)` + position: relative; + width: 100%; +`; + +const PagePadding = styled(Flex)` + padding: 80px 20px; +`; + +const Actions = styled(Flex)` + position: absolute; + top: 0; + right: 20px; +`; + +const DocumentContainer = styled.div` + font-weight: 400; + font-size: 1em; + line-height: 1.5em; + padding: 0 3em; + width: 50em; +`; + +export default withRouter(inject('ui')(Document)); diff --git a/frontend/scenes/Error404/Error404.js b/frontend/scenes/Error404/Error404.js index d7865b16..6acd7564 100644 --- a/frontend/scenes/Error404/Error404.js +++ b/frontend/scenes/Error404/Error404.js @@ -2,23 +2,20 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import Layout from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; class Error404 extends React.Component { render() { return ( - + - -

Not Found

+

Not Found

-

We're unable to find the page you're accessing.

+

We're unable to find the page you're accessing.

-

Maybe you want to try search instead?

-
-
+

Maybe you want to try search instead?

+ ); } } diff --git a/frontend/scenes/ErrorAuth/ErrorAuth.js b/frontend/scenes/ErrorAuth/ErrorAuth.js index c9cf6f2b..edfbd009 100644 --- a/frontend/scenes/ErrorAuth/ErrorAuth.js +++ b/frontend/scenes/ErrorAuth/ErrorAuth.js @@ -2,23 +2,20 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import Layout from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; class ErrorAuth extends React.Component { render() { return ( - + - -

Authentication failed

+

Authentication failed

-

- We were unable to log you in. Please try again. -

-
-
+

+ We were unable to log you in. Please try again. +

+ ); } } diff --git a/frontend/scenes/Home/Home.js b/frontend/scenes/Home/Home.js index 4cf12891..022f1b94 100644 --- a/frontend/scenes/Home/Home.js +++ b/frontend/scenes/Home/Home.js @@ -7,7 +7,6 @@ import styled from 'styled-components'; import AuthStore from 'stores/AuthStore'; -import Layout from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import SlackAuthLink from 'components/SlackAuthLink'; import Alert from 'components/Alert'; @@ -40,30 +39,28 @@ type Props = { return ( - - {this.props.auth.authenticated && } + {this.props.auth.authenticated && } - - {showLandingPageCopy && -
- Simple, fast, markdown. - - We're building a modern wiki for engineering teams. - -
} + + {showLandingPageCopy &&
- - Sign in with Slack - -
-
-
+ Simple, fast, markdown. + + We're building a modern wiki for engineering teams. + + } +
+ + Sign in with Slack + +
+
); } diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 30dad695..543992e6 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -12,7 +12,6 @@ import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; import SearchField from './components/SearchField'; import SearchStore from './SearchStore'; -import Layout, { Title } from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import DocumentPreview from 'components/DocumentPreview'; import PageTitle from 'components/PageTitle'; @@ -83,41 +82,38 @@ const ResultsWrapper = styled(Flex)` render() { const query = this.props.match.params.query; - const title = ; const hasResults = this.store.documents.length > 0; return ( - <Layout title={title} search={false} loading={this.store.isFetching}> + <Container auto> <PageTitle title="Search" /> - <Container auto> - {this.props.notFound && - <div> - <h1>Not Found</h1> - <p>We're unable to find the page you're accessing.</p> - <hr /> - </div>} - <ResultsWrapper pinToTop={hasResults} column auto> - <SearchField - searchTerm={this.store.searchTerm} - onKeyDown={this.handleKeyDown} - onChange={this.updateQuery} - value={query || ''} - /> - <ArrowKeyNavigation - mode={ArrowKeyNavigation.mode.VERTICAL} - defaultActiveChildIndex={0} - > - {this.store.documents.map((document, index) => ( - <DocumentPreview - innerRef={ref => index === 0 && this.setFirstDocumentRef(ref)} - key={document.id} - document={document} - /> - ))} - </ArrowKeyNavigation> - </ResultsWrapper> - </Container> - </Layout> + {this.props.notFound && + <div> + <h1>Not Found</h1> + <p>We're unable to find the page you're accessing.</p> + <hr /> + </div>} + <ResultsWrapper pinToTop={hasResults} column auto> + <SearchField + searchTerm={this.store.searchTerm} + onKeyDown={this.handleKeyDown} + onChange={this.updateQuery} + value={query || ''} + /> + <ArrowKeyNavigation + mode={ArrowKeyNavigation.mode.VERTICAL} + defaultActiveChildIndex={0} + > + {this.store.documents.map((document, index) => ( + <DocumentPreview + innerRef={ref => index === 0 && this.setFirstDocumentRef(ref)} + key={document.id} + document={document} + /> + ))} + </ArrowKeyNavigation> + </ResultsWrapper> + </Container> ); } } diff --git a/frontend/scenes/Settings/Settings.js b/frontend/scenes/Settings/Settings.js index e16ac6e9..709a7dfd 100644 --- a/frontend/scenes/Settings/Settings.js +++ b/frontend/scenes/Settings/Settings.js @@ -8,7 +8,6 @@ import ApiKeyRow from './components/ApiKeyRow'; import styles from './Settings.scss'; import SettingsStore from './SettingsStore'; -import Layout, { Title } from 'components/Layout'; import CenteredContent from 'components/CenteredContent'; import SlackAuthLink from 'components/SlackAuthLink'; import PageTitle from 'components/PageTitle'; @@ -22,72 +21,69 @@ import PageTitle from 'components/PageTitle'; } render() { - const title = <Title content="Settings" />; const showSlackSettings = DEPLOYMENT === 'hosted'; return ( - <Layout title={title} search={false} loading={this.store.isFetching}> + <CenteredContent> <PageTitle title="Settings" /> - <CenteredContent> - {showSlackSettings && - <div className={styles.section}> - <h2 className={styles.sectionHeader}>Slack</h2> - <p> - Connect Atlas to your Slack to instantly search for your documents - using <code>/atlas</code> command. - </p> - - <SlackAuthLink - scopes={['commands']} - redirectUri={`${BASE_URL}/auth/slack/commands`} - > - <img - alt="Add to Slack" - height="40" - width="139" - src="https://platform.slack-edge.com/img/add_to_slack.png" - srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" - /> - </SlackAuthLink> - </div>} - + {showSlackSettings && <div className={styles.section}> - <h2 className={styles.sectionHeader}>API access</h2> + <h2 className={styles.sectionHeader}>Slack</h2> <p> - Create API tokens to hack on your Atlas. - Learn more in <a href>API documentation</a>. + Connect Atlas to your Slack to instantly search for your documents + using <code>/atlas</code> command. </p> - {this.store.apiKeys && - <table className={styles.apiKeyTable}> - <tbody> - {this.store.apiKeys && - this.store.apiKeys.map(key => ( - <ApiKeyRow - id={key.id} - key={key.id} - name={key.name} - secret={key.secret} - onDelete={this.store.deleteApiKey} - /> - ))} - </tbody> - </table>} - - <div> - <InlineForm - placeholder="Token name" - buttonLabel="Create token" - name="inline_form" - value={this.store.keyName} - onChange={this.store.setKeyName} - onSubmit={this.store.createApiKey} - disabled={this.store.isFetching} + <SlackAuthLink + scopes={['commands']} + redirectUri={`${BASE_URL}/auth/slack/commands`} + > + <img + alt="Add to Slack" + height="40" + width="139" + src="https://platform.slack-edge.com/img/add_to_slack.png" + srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /> - </div> + </SlackAuthLink> + </div>} + + <div className={styles.section}> + <h2 className={styles.sectionHeader}>API access</h2> + <p> + Create API tokens to hack on your Atlas. + Learn more in <a href>API documentation</a>. + </p> + + {this.store.apiKeys && + <table className={styles.apiKeyTable}> + <tbody> + {this.store.apiKeys && + this.store.apiKeys.map(key => ( + <ApiKeyRow + id={key.id} + key={key.id} + name={key.name} + secret={key.secret} + onDelete={this.store.deleteApiKey} + /> + ))} + </tbody> + </table>} + + <div> + <InlineForm + placeholder="Token name" + buttonLabel="Create token" + name="inline_form" + value={this.store.keyName} + onChange={this.store.setKeyName} + onSubmit={this.store.createApiKey} + disabled={this.store.isFetching} + /> </div> - </CenteredContent> - </Layout> + </div> + </CenteredContent> ); } } diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js index 745684a6..2f9d2daf 100644 --- a/frontend/stores/CollectionsStore.js +++ b/frontend/stores/CollectionsStore.js @@ -38,8 +38,7 @@ class CollectionsStore { } }; - @action getById = async (id: string): Promise<Collection> => { - if (!this.isLoaded) await this.fetch(); + getById = (id: string): Collection => { return _.find(this.data, { id }); }; diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js index 80c95352..4a3bb7aa 100644 --- a/frontend/stores/UiStore.js +++ b/frontend/stores/UiStore.js @@ -1,33 +1,26 @@ // @flow -import { observable, action, computed, autorunAsync } from 'mobx'; - -const UI_STORE = 'UI_STORE'; +import { observable, action } from 'mobx'; class UiStore { - @observable sidebar: boolean = false; - - /* Computed */ - - @computed get asJson(): string { - return JSON.stringify({ - sidebar: this.sidebar, - }); - } + @observable activeCollection: ?string; + @observable editMode: boolean = false; /* Actions */ - @action toggleSidebar = (): void => { - this.sidebar = !this.sidebar; + @action setActiveCollection = (id: string): void => { + this.activeCollection = id; }; - constructor() { - // Rehydrate - const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}'); - this.sidebar = data.sidebar; + @action clearActiveCollection = (): void => { + this.activeCollection = null; + }; - autorunAsync(() => { - localStorage.setItem(UI_STORE, this.asJson); - }); + @action enableEditMode() { + this.editMode = true; + } + + @action disableEditMode() { + this.editMode = false; } } diff --git a/frontend/styles/base.scss b/frontend/styles/base.scss index 10cd2225..c7bd8153 100644 --- a/frontend/styles/base.scss +++ b/frontend/styles/base.scss @@ -20,11 +20,11 @@ html, body, .viewport { } body { - font-family: 'Atlas Grotesk', -apple-system, 'Helvetica Neue', Helvetica, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-size: 15px; line-height: 1.5; margin: 0; - color: $textColor; + color: #617180; background-color: #fff; display: flex; position: absolute; @@ -32,6 +32,10 @@ body { right: 0; bottom: 0; left: 0; + + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } img { max-width: 100%; @@ -41,13 +45,13 @@ svg { max-height: 100%; } a { - color: $actionColor; + color: #005AA6; text-decoration: none; cursor: pointer; } h1, h2, h3, h4, h5, h6 { - font-weight: 600; + font-weight: 500; line-height: 1.25; margin-top: 1em; margin-bottom: .5em; diff --git a/server/static/index.html b/server/static/index.html index e732e8ce..21c5637e 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -1,36 +1,19 @@ <!doctype html> <html> - <head> - <title>Atlas - + - .container { - display: flex; - flex; - } + + - .header { - display: flex; - flex: 1; - height: 42px; - border-bottom: 1px solid #eee; - } - - - -
-
-
-
-
- - + \ No newline at end of file