Merge pull request #86 from jorilallo/jori/layout-changes

Layout changes
This commit is contained in:
Jori Lallo 2017-06-25 00:31:16 -07:00 committed by GitHub
commit 1fa473b271
35 changed files with 480 additions and 1183 deletions

View File

@ -99,7 +99,8 @@ export default class MarkdownEditor extends Component {
render = () => {
return (
<span>
<ClickablePadding onClick={this.focusAtStart} />
{!this.props.readOnly &&
<ClickablePadding onClick={this.focusAtStart} />}
<Toolbar state={this.state.state} onChange={this.onChange} />
<Editor
ref={ref => (this.editor = ref)}
@ -114,7 +115,8 @@ export default class MarkdownEditor extends Component {
onSave={this.props.onSave}
readOnly={this.props.readOnly}
/>
<ClickablePadding onClick={this.focusAtEnd} grow />
{!this.props.readOnly &&
<ClickablePadding onClick={this.focusAtEnd} grow />}
</span>
);
};

View File

@ -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<any>,
actions?: ?React.Element<any>,
title?: ?React.Element<any>,
loading?: boolean,
user: UserStore,
auth: AuthStore,
ui: UiStore,
search: ?boolean,
notifications?: React.Element<any>,
};
@ -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 (
<Container column auto>
@ -69,78 +77,65 @@ type Props = {
{this.props.notifications}
<Header>
<Flex align="center">
<LogoLink to="/">Atlas</LogoLink>
<Title>
{this.props.title}
</Title>
</Flex>
<Flex>
<Flex>
<Flex align="center">
{this.props.actions}
</Flex>
{auth.authenticated &&
user &&
<Flex>
{this.props.search &&
<Flex>
<Link to="/search">
<Search title="Search (/)">
<SearchIcon src={searchIcon} alt="Search" />
</Search>
</Link>
</Flex>}
<DropdownMenu label={<Avatar src={user.user.avatarUrl} />}>
<MenuLink to="/settings">
<MenuItem>Settings</MenuItem>
</MenuLink>
<MenuLink to="/keyboard-shortcuts">
<MenuItem>
Keyboard shortcuts
</MenuItem>
</MenuLink>
<MenuLink to="/developers">
<MenuItem>API</MenuItem>
</MenuLink>
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
</DropdownMenu>
</Flex>}
</Flex>
</Flex>
</Header>
<Flex auto>
{auth.authenticated &&
user &&
<Sidebar column editMode={ui.editMode}>
<Header justify="space-between">
<Flex align="center">
<LogoLink to="/">Atlas</LogoLink>
</Flex>
<DropdownMenu label={<Avatar src={user.user.avatarUrl} />}>
<MenuLink to="/settings">
<MenuItem>Settings</MenuItem>
</MenuLink>
<MenuLink to="/keyboard-shortcuts">
<MenuItem>
Keyboard shortcuts
</MenuItem>
</MenuLink>
<MenuLink to="/developers">
<MenuItem>API</MenuItem>
</MenuLink>
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
</DropdownMenu>
</Header>
<Content auto justify="center">
{this.props.children}
</Content>
<Flex column>
<LinkSection>
<SidebarLink to="/search">Search</SidebarLink>
</LinkSection>
<LinkSection>
<SidebarLink to="/dashboard">Dashboard</SidebarLink>
<SidebarLink to="/starred">Starred</SidebarLink>
</LinkSection>
<LinkSection>
{ui.activeCollection
? <SidebarCollection
collection={collections.getById(ui.activeCollection)}
/>
: <SidebarCollectionList />}
</LinkSection>
</Flex>
</Sidebar>}
<Content auto justify="center" editMode={ui.editMode}>
{this.props.children}
</Content>
</Flex>
</Container>
);
}
}
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));

View File

@ -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 (
<Flex column>
<Header>{collection.name}</Header>
{collection.documents.map(document => (
<SidebarLink key={document.id} to={document.url}>
{document.title}
</SidebarLink>
))}
</Flex>
);
}
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);

View File

@ -0,0 +1,3 @@
// @flow
import SidebarCollection from './SidebarCollection';
export default SidebarCollection;

View File

@ -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 (
<Flex column>
<Header>Collections</Header>
{collections.data.map(collection => (
<SidebarLink key={collection.id} to={collection.url}>
{collection.name}
</SidebarLink>
))}
</Flex>
);
});
const Header = styled(Flex)`
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
color: #9FA6AB;
letter-spacing: 0.04em;
`;
export default inject('collections')(SidebarCollectionList);

View File

@ -0,0 +1,3 @@
// @flow
import SidebarCollectionList from './SidebarCollectionList';
export default SidebarCollectionList;

View File

@ -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 => (
<LinkContainer>
<NavLink {...props} activeStyle={activeStyle} />
</LinkContainer>
));
const LinkContainer = styled(Flex)`
padding: 5px 0;
a {
color: #848484;
}
`;
export default SidebarLink;

View File

@ -0,0 +1,3 @@
// @flow
import SidebarLink from './SidebarLink';
export default SidebarLink;

View File

@ -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 (
<Flex>
{this.props.open &&
<Flex column className={cx(styles.sidebar)}>
<Flex auto className={cx(styles.content)}>
<Tree
paddingLeft={10}
tree={this.props.navigationTree}
allowUpdates={this.store.isEditing}
onChange={this.props.onNavigationUpdate}
onCollapse={this.props.onNodeCollapse}
history={this.props.history}
/>
</Flex>
<Flex auto className={styles.actions}>
{this.store.isEditing &&
<span className={styles.action}>
Drag & drop to organize <Separator />&nbsp;
</span>}
<span
role="button"
onClick={this.toggleEdit}
className={cx(styles.action, { active: this.store.isEditing })}
>
{!this.store.isEditing ? 'Organize documents' : 'Done'}
</span>
</Flex>
</Flex>}
<div
onClick={this.props.onToggle}
className={cx(styles.sidebarToggle, { active: this.store.isEditing })}
title="Toggle sidebar (Cmd+/)"
>
<img
src={require('assets/icons/menu.svg')}
className={styles.menuIcon}
alt="Menu"
/>
</div>
</Flex>
);
}
}
export default withRouter(Sidebar);

View File

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

View File

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

View File

@ -1,16 +0,0 @@
// @flow
import React from 'react';
import styles from './Separator.scss';
class Separator extends React.Component {
render() {
return (
<span className={styles.separator}>
·
</span>
);
}
}
export default Separator;

View File

@ -1,6 +0,0 @@
@import '~styles/constants.scss';
.separator {
padding: 0 10px;
color: $lightGray;
}

View File

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

View File

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

View File

@ -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 (
<span
className={cx(
styles.collapse,
collapsed ? styles.caretRight : styles.caretDown
)}
onMouseDown={function(e) {
e.stopPropagation();
}}
onClick={this.handleCollapse}
>
<img alt="Expand" src={require('./assets/chevron.svg')} />
</span>
);
}
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 (
<div className={styles.children} style={childrenStyles}>
{index.children.map(child => {
const childIndex = tree.getIndex(child);
return (
<Node
tree={tree}
index={childIndex}
key={childIndex.id}
dragging={dragging}
allowUpdates={this.props.allowUpdates}
paddingLeft={this.props.paddingLeft}
onCollapse={this.props.onCollapse}
onDragStart={this.props.onDragStart}
history={this.props.history}
/>
);
})}
</div>
);
}
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 (
<div
className={cx(styles.node, {
placeholder: index.id === dragging,
rootNode: this.props.rootNode,
})}
style={style}
>
<div
className={styles.inner}
ref="inner"
onMouseDown={
this.props.rootNode || !this.props.allowUpdates
? e => e.stopPropagation()
: this.handleMouseDown
}
>
{!this.props.rootNode && this.renderCollapse()}
<span
className={cx(styles.nodeLabel, {
rootLabel: this.props.rootNode,
isModifying: this.isModifying(),
})}
onClick={this.onClick}
>
{node.title}
</span>
</div>
{this.renderChildren()}
</div>
);
}
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;

View File

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

View File

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

View File

@ -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 (
<div className={styles.draggable} style={draggingStyles}>
<Node
tree={tree}
index={draggingIndex}
paddingLeft={this.props.paddingLeft}
allowUpdates={this.props.allowUpdates}
/>
</div>
);
}
return null;
},
render() {
const tree = this.state.tree;
const dragging = this.state.dragging;
const draggingDom = this.getDraggingDom();
return (
<div className={styles.tree}>
{draggingDom}
<Node
rootNode
tree={tree}
index={tree.getIndex(1)}
key={1}
paddingLeft={this.props.paddingLeft}
allowUpdates={this.props.allowUpdates}
onDragStart={this.dragStart}
onCollapse={this.toggleCollapse}
dragging={dragging && dragging.id}
history={this.props.history}
/>
</div>
);
},
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;
// }
});

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 30" x="0px" y="0px"><path d="M8.59,18.16L14.25,12.5L8.59,6.84L7.89,7.55L12.84,12.5L7.89,17.45L8.59,18.16Z"/></svg>

Before

Width:  |  Height:  |  Size: 227 B

View File

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

View File

@ -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(
<Route exact path="/auth/error" component={ErrorAuth} />
<Auth>
<Switch>
<Route exact path="/dashboard" component={Dashboard} />
<Route exact path="/collections/:id" component={Collection} />
<Route exact path="/d/:id" component={Document} />
<Route exact path="/d/:id/:edit" component={Document} />
<Route
exact
path="/collections/:id/new"
component={DocumentNew}
/>
<Route exact path="/d/:id/new" component={DocumentNewChild} />
<Layout>
<Switch>
<Route exact path="/dashboard" component={Dashboard} />
<Route exact path="/collections/:id" component={Collection} />
<Route exact path="/d/:id" component={Document} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:query" component={Search} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/d/:id/:edit" component={Document} />
<Route
exact
path="/collections/:id/new"
component={DocumentNew}
/>
<Route exact path="/d/:id/new" component={DocumentNewChild} />
<Route
exact
path="/keyboard-shortcuts"
component={KeyboardShortcuts}
/>
<Route exact path="/developers" component={Api} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:query" component={Search} />
<Route exact path="/settings" component={Settings} />
<Route path="/404" component={Error404} />
<Route component={notFoundSearch} />
</Switch>
<Route
exact
path="/keyboard-shortcuts"
component={KeyboardShortcuts}
/>
<Route exact path="/developers" component={Api} />
<Route path="/404" component={Error404} />
<Route component={notFoundSearch} />
</Switch>
</Layout>
</Auth>
</Switch>
</Router>

View File

@ -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 (
<Layout>
{this.state.redirectUrl && <Redirect to={this.state.redirectUrl} />}
<CenteredContent>
return this.store.redirectUrl
? <Redirect to={this.store.redirectUrl} />
: <CenteredContent>
<PreviewLoading />
</CenteredContent>
</Layout>
);
</CenteredContent>;
}
}
export default inject('collections')(Collection);

View File

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

View File

@ -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 (
<Layout>
<CenteredContent>
<Flex column auto>
{!collections.isLoaded
? <PreviewLoading />
: collections.data.map(collection => (
<Collection key={collection.id} data={collection} />
))}
</Flex>
</CenteredContent>
</Layout>
<CenteredContent>
<Flex column auto>
{!collections.isLoaded
? <PreviewLoading />
: collections.data.map(collection => (
<Collection key={collection.id} data={collection} />
))}
</Flex>
</CenteredContent>
);
}
}

View File

@ -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 = (
<Breadcrumbs
document={this.store.document}
pathToDocument={this.store.pathToDocument}
/>
);
);*/
const titleText = this.store.document && get(this.store, 'document.title');
@ -117,49 +114,71 @@ type Props = {
/>
: <a onClick={this.onEdit}>Edit</a>}
</HeaderAction>
<Menu store={this.store} document={this.store.document} />
{!isEditing &&
<Menu store={this.store} document={this.store.document} />}
</Flex>
);
return (
<Layout
actions={actions}
title={title}
loading={this.store.isSaving || this.store.isUploading}
search={false}
fixed
>
<PageTitle title={titleText} />
<Prompt when={this.store.hasPendingChanges} message={DISCARD_CHANGES} />
{this.store.isFetching &&
<CenteredContent>
<PreviewLoading />
</CenteredContent>}
{this.store.document &&
<Container>
{!isEditing &&
<Meta>
<Container>
<Actions>{actions}</Actions>
<PagePadding auto justify="center">
<PageTitle title={titleText} />
<Prompt
when={this.store.hasPendingChanges}
message={DISCARD_CHANGES}
/>
{this.store.isFetching &&
<CenteredContent>
<PreviewLoading />
</CenteredContent>}
{this.store.document &&
<DocumentContainer>
{!isEditing &&
<PublishingInfo
collaborators={this.store.document.collaborators}
createdAt={this.store.document.createdAt}
createdBy={this.store.document.createdBy}
updatedAt={this.store.document.updatedAt}
updatedBy={this.store.document.updatedBy}
/>
</Meta>}
<Editor
text={this.store.document.text}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onChange={this.store.updateText}
onSave={this.onSave}
onCancel={this.onCancel}
readOnly={!isEditing}
/>
</Container>}
</Layout>
/>}
<Editor
text={this.store.document.text}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onChange={this.store.updateText}
onSave={this.onSave}
onCancel={this.onCancel}
readOnly={!isEditing}
/>
</DocumentContainer>}
</PagePadding>
</Container>
);
}
}
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));

View File

@ -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 (
<Layout>
<CenteredContent>
<PageTitle title="Not found" />
<CenteredContent>
<h1>Not Found</h1>
<h1>Not Found</h1>
<p>We're unable to find the page you're accessing.</p>
<p>We're unable to find the page you're accessing.</p>
<p>Maybe you want to try <Link to="/search">search</Link> instead?</p>
</CenteredContent>
</Layout>
<p>Maybe you want to try <Link to="/search">search</Link> instead?</p>
</CenteredContent>
);
}
}

View File

@ -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 (
<Layout>
<CenteredContent>
<PageTitle title="Authentication error" />
<CenteredContent>
<h1>Authentication failed</h1>
<h1>Authentication failed</h1>
<p>
We were unable to log you in. <Link to="/">Please try again.</Link>
</p>
</CenteredContent>
</Layout>
<p>
We were unable to log you in. <Link to="/">Please try again.</Link>
</p>
</CenteredContent>
);
}
}

View File

@ -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 (
<Flex auto>
<Layout notifications={this.notifications}>
{this.props.auth.authenticated && <Redirect to="/dashboard" />}
{this.props.auth.authenticated && <Redirect to="/dashboard" />}
<CenteredContent>
{showLandingPageCopy &&
<div>
<Title>Simple, fast, markdown.</Title>
<Copy>
We're building a modern wiki for engineering teams.
</Copy>
</div>}
<CenteredContent>
{showLandingPageCopy &&
<div>
<SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
<img
alt="Sign in with Slack"
height="40"
width="172"
src="https://platform.slack-edge.com/img/sign_in_with_slack.png"
srcSet="https://platform.slack-edge.com/img/sign_in_with_slack.png 1x, https://platform.slack-edge.com/img/sign_in_with_slack@2x.png 2x"
/>
</SlackAuthLink>
</div>
</CenteredContent>
</Layout>
<Title>Simple, fast, markdown.</Title>
<Copy>
We're building a modern wiki for engineering teams.
</Copy>
</div>}
<div>
<SlackAuthLink redirectUri={`${BASE_URL}/auth/slack`}>
<img
alt="Sign in with Slack"
height="40"
width="172"
src="https://platform.slack-edge.com/img/sign_in_with_slack.png"
srcSet="https://platform.slack-edge.com/img/sign_in_with_slack.png 1x, https://platform.slack-edge.com/img/sign_in_with_slack@2x.png 2x"
/>
</SlackAuthLink>
</div>
</CenteredContent>
</Flex>
);
}

View File

@ -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 = <Title content="Search" />;
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>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,19 @@
<!doctype html>
<html>
<head>
<title>Atlas</title>
<style>
body, html {
margin: 0;
padding: 0;
}
#root {
flex: 1;
height: 100%;
}
<head>
<title>Atlas</title>
<style>
body,
html {
display: flex;
margin: 0;
padding: 0;
}
</style>
</head>
.container {
display: flex;
flex;
}
<body>
</body>
.header {
display: flex;
flex: 1;
height: 42px;
border-bottom: 1px solid #eee;
}
</style>
</head>
<body style='display: flex; width: 100%; height: 100%;'>
<div id="root">
<div class="container">
<div class="header"></div>
</div>
</div>
</body>
</html>
</html>