diff --git a/frontend/components/CenteredContent/CenteredContent.js b/frontend/components/CenteredContent/CenteredContent.js index 6f135e1e..b9f15975 100644 --- a/frontend/components/CenteredContent/CenteredContent.js +++ b/frontend/components/CenteredContent/CenteredContent.js @@ -8,11 +8,11 @@ type Props = { const Container = styled.div` width: 100%; - margin: 40px 20px; + margin: 60px 20px; `; const Content = styled.div` - max-width: 740px; + max-width: 50em; margin: 0 auto; `; diff --git a/frontend/components/Collaborators/Collaborators.js b/frontend/components/Collaborators/Collaborators.js new file mode 100644 index 00000000..487921fd --- /dev/null +++ b/frontend/components/Collaborators/Collaborators.js @@ -0,0 +1,56 @@ +// @flow +import React from 'react'; +import moment from 'moment'; +import styled from 'styled-components'; +import { color } from 'styles/constants'; +import Flex from 'components/Flex'; +import Tooltip from 'components/Tooltip'; +import Document from 'models/Document'; + +const Collaborators = function({ document }: { document: Document }) { + const { + createdAt, + updatedAt, + createdBy, + updatedBy, + collaborators, + } = document; + let tooltip; + + if (createdAt === updatedAt) { + tooltip = `${createdBy.name} published ${moment(createdAt).fromNow()}`; + } else { + tooltip = `${updatedBy.name} modified ${moment(updatedAt).fromNow()}`; + } + + return ( + + + {collaborators.map(user => ( + + ))} + + + ); +}; + +const Avatars = styled(Flex)` + flex-direction: row-reverse; + margin-right: 10px; + height: 26px; +`; + +const Avatar = styled.img` + width: 26px; + height: 26px; + flex-shrink: 0; + border-radius: 50%; + border: 2px solid ${color.white}; + margin-right: -13px; + + &:first-child { + margin-right: 0; + } +`; + +export default Collaborators; diff --git a/frontend/components/Collaborators/index.js b/frontend/components/Collaborators/index.js new file mode 100644 index 00000000..68cac4de --- /dev/null +++ b/frontend/components/Collaborators/index.js @@ -0,0 +1,3 @@ +// @flow +import Collaborators from './Collaborators'; +export default Collaborators; diff --git a/frontend/components/DocumentPreview/DocumentPreview.js b/frontend/components/DocumentPreview/DocumentPreview.js index 6703e896..91d8bff1 100644 --- a/frontend/components/DocumentPreview/DocumentPreview.js +++ b/frontend/components/DocumentPreview/DocumentPreview.js @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import Document from 'models/Document'; import styled from 'styled-components'; import { color } from 'styles/constants'; -import PublishingInfo from 'components/PublishingInfo'; +import PublishingInfo from './components/PublishingInfo'; import StarIcon from 'components/Icon/StarIcon'; type Props = { diff --git a/frontend/components/PublishingInfo/PublishingInfo.js b/frontend/components/DocumentPreview/components/PublishingInfo.js similarity index 70% rename from frontend/components/PublishingInfo/PublishingInfo.js rename to frontend/components/DocumentPreview/components/PublishingInfo.js index 90b00c6d..878bfdda 100644 --- a/frontend/components/PublishingInfo/PublishingInfo.js +++ b/frontend/components/DocumentPreview/components/PublishingInfo.js @@ -5,7 +5,6 @@ import styled from 'styled-components'; import { color } from 'styles/constants'; import Collection from 'models/Collection'; import Document from 'models/Document'; -import type { User } from 'types'; import Flex from 'components/Flex'; const Container = styled(Flex)` @@ -13,24 +12,6 @@ const Container = styled(Flex)` font-size: 13px; `; -const Avatars = styled(Flex)` - flex-direction: row-reverse; - margin-right: 10px; -`; - -const Avatar = styled.img` - width: 26px; - height: 26px; - flex-shrink: 0; - border-radius: 50%; - border: 2px solid ${color.white}; - margin-right: -13px; - - &:first-child { - margin-right: 0; - } -`; - const Modified = styled.span` color: ${props => (props.highlight ? color.slateDark : color.slate)}; font-weight: ${props => (props.highlight ? '600' : '400')}; @@ -38,14 +19,13 @@ const Modified = styled.span` class PublishingInfo extends Component { props: { - collaborators?: Array, collection?: Collection, document: Document, views?: number, }; render() { - const { collaborators, collection, document } = this.props; + const { collection, document } = this.props; const { modifiedSinceViewed, createdAt, @@ -56,12 +36,6 @@ class PublishingInfo extends Component { return ( - {collaborators && - - {collaborators.map(user => ( - - ))} - } {createdAt === updatedAt ? {createdBy.name} diff --git a/frontend/components/DropdownMenu/DropdownMenu.js b/frontend/components/DropdownMenu/DropdownMenu.js index 36c56004..69874c0a 100644 --- a/frontend/components/DropdownMenu/DropdownMenu.js +++ b/frontend/components/DropdownMenu/DropdownMenu.js @@ -22,6 +22,7 @@ const DropdownMenuItem = ({ onClick, children }: MenuItemProps) => { type DropdownMenuProps = { label: React.Element, children?: React.Element, + style?: Object, }; @observer class DropdownMenu extends React.Component { @@ -42,7 +43,7 @@ type DropdownMenuProps = { {this.menuOpen && - + {this.props.children} } @@ -65,9 +66,7 @@ const Label = styled(Flex).attrs({ })` cursor: pointer; z-index: 1000; - min-height: 43px; - margin: 0 5px; `; const MenuContainer = styled.div` diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 25803128..f496faf2 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -27,7 +27,6 @@ type Props = { onImageUploadStop: Function, emoji: string, readOnly: boolean, - heading?: ?React.Element<*>, }; type KeyData = { @@ -187,12 +186,7 @@ type KeyData = { auto > - - {this.props.heading} - +
(this.editor = ref)} @@ -224,11 +218,10 @@ const MaxWidth = styled(Flex)` height: 100%; `; -const HeaderContainer = styled(Flex).attrs({ - align: 'flex-end', -})` - height: 100px; +const Header = styled(Flex)` + height: 60px; flex-shrink: 0; + align-items: flex-end; ${({ readOnly }) => !readOnly && 'cursor: text;'} `; diff --git a/frontend/components/Editor/Editor.scss b/frontend/components/Editor/Editor.scss index a7694603..7508c602 100644 --- a/frontend/components/Editor/Editor.scss +++ b/frontend/components/Editor/Editor.scss @@ -1,9 +1,9 @@ .editor { font-weight: 400; font-size: 1em; - line-height: 1.5em; + line-height: 1.7em; width: 100%; - color: #1b2631; + color: #1b2830; h1, h2, diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 9679eb83..c0a6121e 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -10,11 +10,11 @@ import keydown from 'react-keydown'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; import { documentEditUrl, homeUrl, searchUrl } from 'utils/routeHelpers'; - import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; + +import Avatar from 'components/Avatar'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; import Scrollable from 'components/Scrollable'; -import Avatar from 'components/Avatar'; import Modal from 'components/Modal'; import AddIcon from 'components/Icon/AddIcon'; import MoreIcon from 'components/Icon/MoreIcon'; @@ -26,8 +26,8 @@ import Settings from 'scenes/Settings'; import SidebarCollection from './components/SidebarCollection'; import SidebarCollectionList from './components/SidebarCollectionList'; import SidebarLink from './components/SidebarLink'; +import HeaderBlock from './components/HeaderBlock'; -import UserStore from 'stores/UserStore'; import AuthStore from 'stores/AuthStore'; import UiStore from 'stores/UiStore'; import CollectionsStore from 'stores/CollectionsStore'; @@ -40,7 +40,6 @@ type Props = { children?: ?React.Element, actions?: ?React.Element, title?: ?React.Element, - user: UserStore, auth: AuthStore, ui: UiStore, search: ?boolean, @@ -108,7 +107,8 @@ type Props = { }; render() { - const { user, auth, documents, collections, history, ui } = this.props; + const { auth, documents, collections, history, ui } = this.props; + const { user, team } = auth; return ( @@ -129,34 +129,35 @@ type Props = { {auth.authenticated && user && + team && -
- - Atlas - - }> - - Settings - - - Keyboard shortcuts - - - API - - - Logout - - -
+ + + + } + > + + Settings + + + Keyboard shortcuts + + + API + + + Logout + + - - Search - Home + Search Starred @@ -228,7 +229,7 @@ type Props = { const CollectionAction = styled.a` position: absolute; - top: 8px; + top: -2px; right: ${layout.hpadding}; svg { @@ -250,46 +251,28 @@ const Container = styled(Flex)` height: 100%; `; -const LogoLink = styled(Link)` - margin-top: 15px; - font-family: 'Atlas Grotesk'; - font-weight: bold; - color: ${color.text}; - text-decoration: none; - font-size: 16px; +const Content = styled(Flex)` + margin-left: ${props => (props.editMode ? 0 : layout.sidebarWidth)}; + transition: margin-left 200ms ease-in-out; `; const MenuLink = styled(Link)` color: ${color.text}; `; -const Content = styled(Flex)` - overflow: scroll; - position: absolute; +const Sidebar = styled(Flex)` + position: fixed; top: 0; bottom: 0; - right: 0; - left: ${props => (props.editMode ? 0 : layout.sidebarWidth)}; - transition: left 200ms ease-in-out; -`; - -const Sidebar = styled(Flex)` + left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)}; 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)` - flex-shrink: 0; - padding: ${layout.padding}; - padding-bottom: 10px; + background: ${color.smoke}; + transition: left 200ms ease-in-out; `; const LinkSection = styled(Flex)` flex-direction: column; - padding: 10px 0; + margin: 24px 0; position: relative; `; diff --git a/frontend/components/Layout/components/HeaderBlock.js b/frontend/components/Layout/components/HeaderBlock.js new file mode 100644 index 00000000..1038738e --- /dev/null +++ b/frontend/components/Layout/components/HeaderBlock.js @@ -0,0 +1,61 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; +import { color, layout } from 'styles/constants'; +import type { User, Team } from 'types'; +import Flex from 'components/Flex'; + +type Props = { + user: User, + team: Team, + children?: React$Element, +}; + +function HeaderBlock({ user, team, children }: Props) { + return ( +
+ + {team.name} + {user.name} + + {children} +
+ ); +} + +const UserName = styled.div` + font-size: 13px; +`; + +const TeamName = styled.div` + font-family: 'Atlas Grotesk'; + font-weight: bold; + color: ${color.text}; + text-decoration: none; + font-size: 16px; +`; + +const Header = styled(Flex)` + flex-shrink: 0; + padding: ${layout.padding}; + position: relative; + cursor: pointer; + width: 100%; + + &:active, + &:hover { + background: rgba(0,0,0,.05); + } + + &::after { + content: ""; + left: ${layout.hpadding}; + right: ${layout.hpadding}; + background: rgba(0,0,0,.075); + height: 1px; + position: absolute; + bottom: 0; + } +`; + +export default HeaderBlock; diff --git a/frontend/components/Layout/components/SidebarLink/SidebarLink.js b/frontend/components/Layout/components/SidebarLink/SidebarLink.js index 7df81e86..d09f85c3 100644 --- a/frontend/components/Layout/components/SidebarLink/SidebarLink.js +++ b/frontend/components/Layout/components/SidebarLink/SidebarLink.js @@ -17,6 +17,7 @@ const StyledNavLink = styled(NavLink)` display: block; padding: 5px ${layout.hpadding}; color: ${color.slateDark}; + font-size: 15px; &:hover { color: ${darken(0.1, color.slateDark)}; diff --git a/frontend/components/Layout/index.js b/frontend/components/Layout/index.js index 7956f3e9..38c7a426 100644 --- a/frontend/components/Layout/index.js +++ b/frontend/components/Layout/index.js @@ -2,8 +2,7 @@ import Layout from './Layout'; import Title from './components/Title'; import HeaderAction from './components/HeaderAction'; -import SaveAction from './components/SaveAction'; export default Layout; -export { Title, HeaderAction, SaveAction }; +export { Title, HeaderAction }; diff --git a/frontend/components/PublishingInfo/index.js b/frontend/components/PublishingInfo/index.js deleted file mode 100644 index fb57b45f..00000000 --- a/frontend/components/PublishingInfo/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import PublishingInfo from './PublishingInfo'; -export default PublishingInfo; diff --git a/frontend/components/Tooltip/index.js b/frontend/components/Tooltip/index.js new file mode 100644 index 00000000..7416605c --- /dev/null +++ b/frontend/components/Tooltip/index.js @@ -0,0 +1,11 @@ +// @flow +import { TooltipTrigger } from 'pui-react-tooltip'; +import { injectGlobal } from 'styled-components'; + +injectGlobal([ + ` + .tooltip:hover .tooltip-container:not(.tooltip-container-hidden){visibility:visible;opacity:1}.tooltip-container{visibility:hidden;-webkit-transition:opacity ease-out 0.2s;transition:opacity ease-out 0.2s;z-index:10;position:absolute;bottom:100%;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0 0 8px 0;text-align:left}.tooltip-container.tooltip-container-visible{visibility:visible}.tooltip-container.tooltip-hoverable:after{content:"";position:absolute;width:calc(100% + 16px);height:calc(100% + 16px);top:50%;left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.tooltip-container .tooltip-content{white-space:nowrap;padding:4px 8px;font-size:12px;line-height:16px;font-weight:400;letter-spacing:0;text-transform:none;background-color:#243641;color:#fff;border-radius:2px;border:1px solid #243641;box-shadow:0px 2px 2px 0px rgba(36, 54, 65, .1),0px 0px 2px 0px rgba(36, 54, 65, .1)}.tooltip-container .tooltip-content:before{content:"";z-index:1;position:absolute;bottom:-4px;left:50%;-webkit-transform:translateX(-50%) rotateZ(45deg);transform:translateX(-50%) rotateZ(45deg);background-color:#243641;border-bottom:1px solid #243641;border-right:1px solid #243641;width:8px;height:8px}.tooltip-container .tooltip-content:after{content:"";box-sizing:content-box;z-index:-1;position:absolute;bottom:-4px;left:50%;-webkit-transform:translateX(-50%) rotateZ(45deg);transform:translateX(-50%) rotateZ(45deg);background-color:#243641;box-shadow:0px 2px 2px 0px rgba(36, 54, 65, .1),0px 0px 2px 0px rgba(36, 54, 65, .1);width:8px;height:8px}.tooltip{position:relative;display:inline-block}.tooltip.tooltip-light .tooltip-content{background-color:#fff;color:#243641;border:1px solid #DFE5E8}.tooltip.tooltip-light .tooltip-content:before{background-color:#fff;border-bottom:1px solid #DFE5E8;border-right:1px solid #DFE5E8}.tooltip.tooltip-light .tooltip-content:after{background-color:#fff}.tooltip.tooltip-bottom .tooltip-container{top:100%;bottom:auto;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:8px 0 0 0}.tooltip.tooltip-bottom .tooltip-container .tooltip-content:before{bottom:auto;top:-4px;border-top:1px solid #243641;border-right:none;border-bottom:none;border-left:1px solid #243641}.tooltip.tooltip-bottom .tooltip-container .tooltip-content:after{bottom:auto;top:-4px}.tooltip.tooltip-bottom.tooltip-light .tooltip-content:before{border-top:1px solid #DFE5E8;border-left:1px solid #DFE5E8}.tooltip.tooltip-right .tooltip-container{top:50%;bottom:auto;left:100%;-webkit-transform:translatey(-50%);transform:translatey(-50%);margin:0 0 0 8px}.tooltip.tooltip-right .tooltip-container .tooltip-content:before{bottom:auto;left:-4px;top:50%;-webkit-transform:translatey(-50%) rotateZ(45deg);transform:translatey(-50%) rotateZ(45deg);border-top:none;border-right:none;border-bottom:1px solid #243641;border-left:1px solid #243641}.tooltip.tooltip-right .tooltip-container .tooltip-content:after{bottom:auto;left:-4px;top:50%;-webkit-transform:translatey(-50%) rotateZ(45deg);transform:translatey(-50%) rotateZ(45deg)}.tooltip.tooltip-right.tooltip-light .tooltip-content:before{border-bottom:1px solid #DFE5E8;border-left:1px solid #DFE5E8}.tooltip.tooltip-left .tooltip-container{top:50%;bottom:auto;right:100%;left:auto;-webkit-transform:translatey(-50%);transform:translatey(-50%);margin:0 8px 0 0}.tooltip.tooltip-left .tooltip-container .tooltip-content:before{bottom:auto;right:-4px;left:auto;top:50%;-webkit-transform:translatey(-50%) rotateZ(45deg);transform:translatey(-50%) rotateZ(45deg);border-top:1px solid #243641;border-right:1px solid #243641;border-bottom:none;border-left:none}.tooltip.tooltip-left .tooltip-container .tooltip-content:after{bottom:auto;right:-4px;left:auto;top:50%;-webkit-transform:translatey(-50%) rotateZ(45deg);transform:translatey(-50%) rotateZ(45deg)}.tooltip.tooltip-left.tooltip-light .tooltip-content:before{border-top:1px solid #DFE5E8;border-right:1px solid #DFE5E8}.tooltip-sm.tooltip-container{width:120px}.tooltip-sm.tooltip-container .tooltip-content{white-space:normal}.tooltip-md.tooltip-container{width:240px}.tooltip-md.tooltip-container .tooltip-content{white-space:normal}.tooltip-lg.tooltip-container{width:360px}.tooltip-lg.tooltip-container .tooltip-content{white-space:normal}.tether-element{z-index:99}.overlay-trigger{color:#1B78B3;-webkit-transition:all 300ms ease-out;transition:all 300ms ease-out;-webkit-transition-property:background-color, color, opacity;transition-property:background-color, color, opacity}.overlay-trigger:hover,.overlay-trigger:focus{color:#1f8ace;cursor:pointer;outline:none;text-decoration:none}.overlay-trigger:active,.overlay-trigger.active{color:#176698} +`, +]); + +export default TooltipTrigger; diff --git a/frontend/index.js b/frontend/index.js index d47f3774..b783b4e7 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -51,23 +51,22 @@ type AuthProps = { }; const Auth = ({ children }: AuthProps) => { - if (stores.auth.authenticated && stores.auth.team) { + if (stores.auth.authenticated && stores.auth.team && stores.auth.user) { // Only initialize stores once. Kept in global scope // because otherwise they will get overriden on route // change if (!authenticatedStores) { // Stores for authenticated user - const user = stores.auth.getUserStore(); - const cache = new CacheStore(user.user.id); + const { user, team } = stores.auth; + const cache = new CacheStore(user.id); authenticatedStores = { - user, documents: new DocumentsStore({ ui: stores.ui, cache, }), collections: new CollectionsStore({ ui: stores.ui, - teamId: user.team.id, + teamId: team.id, cache, }), }; diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 28994dc3..531d289f 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -6,18 +6,18 @@ import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; import Flex from 'components/Flex'; import { layout } from 'styles/constants'; -import invariant from 'invariant'; import Document from 'models/Document'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; +import SaveAction from './components/SaveAction'; import LoadingPlaceholder from 'components/LoadingPlaceholder'; import Editor from 'components/Editor'; import DropToImport from 'components/DropToImport'; -import { HeaderAction, SaveAction } from 'components/Layout'; +import { HeaderAction } from 'components/Layout'; import LoadingIndicator from 'components/LoadingIndicator'; -import PublishingInfo from 'components/PublishingInfo'; +import Collaborators from 'components/Collaborators'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; @@ -44,6 +44,7 @@ type Props = { state = { isDragging: false, isLoading: false, + isSaving: false, newDocument: undefined, showAsSaved: false, }; @@ -108,7 +109,7 @@ type Props = { let document = this.document; if (!document) return; - this.setState({ isLoading: true }); + this.setState({ isLoading: true, isSaving: true }); document = await document.save(); this.setState({ isLoading: false }); @@ -120,7 +121,7 @@ type Props = { }; showAsSaved() { - this.setState({ showAsSaved: true }); + this.setState({ showAsSaved: true, isSaving: false }); this.savedTimeout = setTimeout( () => this.setState({ showAsSaved: false }), 2000 @@ -152,20 +153,6 @@ type Props = { this.setState({ isDragging: false }); }; - renderHeading(isEditing: boolean) { - invariant(this.document, 'document not available'); - if (this.props.newDocument) return; - - return ( - - - - ); - } - render() { const isNew = this.props.newDocument; const isEditing = !!this.props.match.params.edit || isNew; @@ -209,21 +196,31 @@ type Props = { onChange={this.onChange} onSave={this.onSave} onCancel={this.onCancel} - heading={this.renderHeading(!!isEditing)} readOnly={!isEditing} /> + {document && + !isNew && + !isEditing && + } {isEditing ? : Edit} + {isEditing && + + Cancel + } {!isEditing && } @@ -254,11 +251,6 @@ const Meta = styled(Flex)` padding: ${layout.padding}; `; -const InfoWrapper = styled(Flex)` - opacity: ${({ visible }) => (visible ? '1' : '0')}; - transition: all 100ms ease-in-out; -`; - const Container = styled(Flex)` position: relative; width: 100%; diff --git a/frontend/scenes/Document/components/Breadcrumbs/Breadcrumbs.js b/frontend/scenes/Document/components/Breadcrumbs/Breadcrumbs.js deleted file mode 100644 index 14e59899..00000000 --- a/frontend/scenes/Document/components/Breadcrumbs/Breadcrumbs.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import React from 'react'; -import { Link } from 'react-router-dom'; -import type { Document, NavigationNode } from 'types'; - -type Props = { - document: Document, - pathToDocument: Array, -}; - -const Breadcrumbs = ({ document, pathToDocument }: Props) => { - if (document && document.collection) { - const titleSections = pathToDocument - ? pathToDocument.map(node => ( - {node.title} - )) - : []; - titleSections.unshift( - - {document.collection.name} - - ); - - return ( - -  /  - {titleSections.reduce((prev, curr) => [prev, ' / ', curr])} - {` / ${document.title}`} - - ); - } - return null; -}; - -export default Breadcrumbs; diff --git a/frontend/scenes/Document/components/Breadcrumbs/index.js b/frontend/scenes/Document/components/Breadcrumbs/index.js deleted file mode 100644 index 8fe21291..00000000 --- a/frontend/scenes/Document/components/Breadcrumbs/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import Breadcrumbs from './Breadcrumbs'; -export default Breadcrumbs; diff --git a/frontend/components/Layout/components/SaveAction/SaveAction.js b/frontend/scenes/Document/components/SaveAction/SaveAction.js similarity index 57% rename from frontend/components/Layout/components/SaveAction/SaveAction.js rename to frontend/scenes/Document/components/SaveAction/SaveAction.js index f33cfec4..48ec1d00 100644 --- a/frontend/components/Layout/components/SaveAction/SaveAction.js +++ b/frontend/scenes/Document/components/SaveAction/SaveAction.js @@ -1,28 +1,26 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import CheckIcon from 'components/Icon/CheckIcon'; -import { fadeAndScaleIn } from 'styles/animations'; type Props = { onClick: Function, - showCheckmark: boolean, disabled?: boolean, isNew?: boolean, + isSaving?: boolean, }; class SaveAction extends React.Component { props: Props; - onClick = (event: MouseEvent) => { + onClick = (ev: MouseEvent) => { if (this.props.disabled) return; - event.preventDefault(); + ev.preventDefault(); this.props.onClick(); }; render() { - const { showCheckmark, disabled, isNew } = this.props; + const { isSaving, isNew, disabled } = this.props; return ( - {showCheckmark && } - {isNew ? 'Publish' : 'Save'} + {isNew + ? isSaving ? 'Publishing…' : 'Publish' + : isSaving ? 'Saving…' : 'Save'} ); } @@ -45,17 +44,4 @@ const Link = styled.a` cursor: ${props => (props.disabled ? 'default' : 'pointer')}; `; -const SavedIcon = styled(CheckIcon)` - animation: ${fadeAndScaleIn} 250ms ease; - display: inline-block; - margin-right: 4px; - width: 18px; - height: 18px; - - svg { - width: 18px; - height: 18px; - } -`; - export default SaveAction; diff --git a/frontend/components/Layout/components/SaveAction/index.js b/frontend/scenes/Document/components/SaveAction/index.js similarity index 100% rename from frontend/components/Layout/components/SaveAction/index.js rename to frontend/scenes/Document/components/SaveAction/index.js diff --git a/frontend/stores/AuthStore.js b/frontend/stores/AuthStore.js index dcc879c4..f5b77ecb 100644 --- a/frontend/stores/AuthStore.js +++ b/frontend/stores/AuthStore.js @@ -2,7 +2,6 @@ import { observable, action, computed, autorunAsync } from 'mobx'; import invariant from 'invariant'; import { client } from 'utils/ApiClient'; -import UserStore from 'stores/UserStore'; import type { User, Team } from 'types'; const AUTH_STORE = 'AUTH_STORE'; @@ -72,17 +71,6 @@ class AuthStore { }; }; - getUserStore(): UserStore { - invariant( - this.user && this.team, - 'Tried to create a user store without data' - ); - return new UserStore({ - user: this.user, - team: this.team, - }); - } - constructor() { // Rehydrate const data = JSON.parse(localStorage.getItem(AUTH_STORE) || '{}'); diff --git a/frontend/stores/UserStore.js b/frontend/stores/UserStore.js deleted file mode 100644 index 7d368c93..00000000 --- a/frontend/stores/UserStore.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow -import { observable, computed } from 'mobx'; -import type { User, Team } from 'types'; - -type Options = { - user: User, - team: Team, -}; - -class UserStore { - @observable user: User; - @observable team: Team; - - @observable isLoading: boolean = false; - - /* Computed */ - - @computed get asJson(): string { - return JSON.stringify({ - user: this.user, - team: this.team, - }); - } - - constructor(options: Options) { - // Rehydrate - this.user = options.user; - this.team = options.team; - } -} - -export default UserStore; diff --git a/frontend/styles/base.scss b/frontend/styles/base.scss index 3fcc4acd..1e745df7 100644 --- a/frontend/styles/base.scss +++ b/frontend/styles/base.scss @@ -16,22 +16,18 @@ html, body, .viewport { width: 100%; + min-height: 100vh; margin: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; - font-size: 15px; + font-size: 16px; line-height: 1.5; margin: 0; color: #617180; background-color: #fff; display: flex; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; diff --git a/package.json b/package.json index 12755300..64b20ba4 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "pg": "^6.1.5", "pg-hstore": "2.3.2", "polished": "1.2.1", + "pui-react-tooltip": "^8.3.3", "query-string": "^4.3.4", "randomstring": "1.1.5", "raw-loader": "^0.5.1", diff --git a/server/api/auth.js b/server/api/auth.js index 811055cf..35ae88c1 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -1,11 +1,9 @@ // @flow import Router from 'koa-router'; -import apiError, { httpErrors } from '../errors'; -import fetch from 'isomorphic-fetch'; -import querystring from 'querystring'; - +import apiError from '../errors'; import { presentUser, presentTeam } from '../presenters'; import { User, Team } from '../models'; +import * as Slack from '../slack'; const router = new Router(); @@ -88,24 +86,7 @@ router.post('auth.slack', async ctx => { const { code } = ctx.body; ctx.assertPresent(code, 'code is required'); - const body = { - client_id: process.env.SLACK_KEY, - client_secret: process.env.SLACK_SECRET, - redirect_uri: `${process.env.URL || ''}/auth/slack`, - code, - }; - - let data; - try { - const response = await fetch( - `https://slack.com/api/oauth.access?${querystring.stringify(body)}` - ); - data = await response.json(); - } catch (e) { - throw httpErrors.BadRequest(); - } - - if (!data.ok) throw httpErrors.BadRequest(data.error); + const data = await Slack.oauthAccess(code); // Temp to block const allowedSlackDomains = (process.env.ALLOWED_SLACK_DOMAINS || '') @@ -118,22 +99,20 @@ router.post('auth.slack', async ctx => { ); } - // User let user = await User.findOne({ where: { slackId: data.user.id } }); - - // Team let team = await Team.findOne({ where: { slackId: data.team.id } }); const teamExisted = !!team; - if (!team) { + + if (team) { + team.name = data.team.name; + team.slackData = data.team; + await team.save(); + } else { team = await Team.create({ name: data.team.name, slackId: data.team.id, slackData: data.team, }); - } else { - team.name = data.team.name; - team.slackData = data.team; - team = await team.save(); } if (user) { @@ -143,7 +122,6 @@ router.post('auth.slack', async ctx => { } else { user = await User.create({ slackId: data.user.id, - username: data.user.name, name: data.user.name, email: data.user.email, teamId: team.id, @@ -169,24 +147,7 @@ router.post('auth.slackCommands', async ctx => { const { code } = ctx.body; ctx.assertPresent(code, 'code is required'); - const body = { - client_id: process.env.SLACK_KEY, - client_secret: process.env.SLACK_SECRET, - redirect_uri: `${process.env.URL || ''}/auth/slack/commands`, - code, - }; - - let data; - try { - const response = await fetch( - `https://slack.com/api/oauth.access?${querystring.stringify(body)}` - ); - data = await response.json(); - } catch (e) { - throw httpErrors.BadRequest(); - } - - if (!data.ok) throw httpErrors.BadRequest(data.error); + await Slack.oauthAccess(code, `${process.env.URL || ''}/auth/slack/commands`); }); export default router; diff --git a/server/migrations/20170904202454-allow-null-username.js b/server/migrations/20170904202454-allow-null-username.js new file mode 100644 index 00000000..399dc4e8 --- /dev/null +++ b/server/migrations/20170904202454-allow-null-username.js @@ -0,0 +1,15 @@ +module.exports = { + up: async function(queryInterface, Sequelize) { + await queryInterface.changeColumn('users', 'username', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: async function(queryInterface, Sequelize) { + await queryInterface.changeColumn('users', 'username', { + type: Sequelize.STRING, + allowNull: false, + }); + }, +}; diff --git a/server/slack.js b/server/slack.js new file mode 100644 index 00000000..62bb75eb --- /dev/null +++ b/server/slack.js @@ -0,0 +1,34 @@ +// @flow +import fetch from 'isomorphic-fetch'; +import querystring from 'querystring'; +import { httpErrors } from './errors'; + +const SLACK_API_URL = 'https://slack.com/api'; + +export async function request(endpoint: string, body: Object) { + let data; + try { + const response = await fetch( + `${SLACK_API_URL}/${endpoint}?${querystring.stringify(body)}` + ); + data = await response.json(); + } catch (e) { + throw httpErrors.BadRequest(); + } + console.log('DATA', data); + if (!data.ok) throw httpErrors.BadRequest(data.error); + + return data; +} + +export async function oauthAccess( + code: string, + redirect_uri: string = `${process.env.URL || ''}/auth/slack` +) { + return request('oauth.access', { + client_id: process.env.SLACK_KEY, + client_secret: process.env.SLACK_SECRET, + redirect_uri: `${process.env.URL || ''}/auth/slack`, + code, + }); +} diff --git a/server/static/dev.html b/server/static/dev.html index 1933c80b..8eea6eee 100644 --- a/server/static/dev.html +++ b/server/static/dev.html @@ -20,7 +20,7 @@ #root { flex: 1; - height: 100%; + min-height: 100vh; } @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/server/static/index.html b/server/static/index.html index 90df71b6..e79463d5 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -18,7 +18,7 @@ #root { flex: 1; - height: 100%; + min-height: 100vh; } @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/yarn.lock b/yarn.lock index b04a33ea..104fa6ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -946,7 +946,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.1.18, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1506,7 +1506,7 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" -classnames@2.2.3, classnames@^2.1.5: +classnames@2.2.3, classnames@^2.1.5, classnames@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.3.tgz#551b774b6762a0c0a997187f7ba4f1d603961ac5" @@ -7207,7 +7207,7 @@ promise@7.x, promise@^7.0.3, promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8: +prop-types@>=15.5.6, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8: version "15.5.10" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" dependencies: @@ -7242,6 +7242,19 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" +pui-css-tooltips@=8.3.3: + version "8.3.3" + resolved "https://registry.yarnpkg.com/pui-css-tooltips/-/pui-css-tooltips-8.3.3.tgz#3e4dd13e4883991dcdb27b065377b3e866edfd7e" + +pui-react-tooltip@^8.3.3: + version "8.3.3" + resolved "https://registry.yarnpkg.com/pui-react-tooltip/-/pui-react-tooltip-8.3.3.tgz#bb7e67c38a06e88501f245e6c2891e6252a7f546" + dependencies: + babel-runtime "^6.1.18" + classnames "^2.2.0" + prop-types ">=15.5.6" + pui-css-tooltips "=8.3.3" + "pullstream@>= 0.4.1 < 1": version "0.4.1" resolved "https://registry.yarnpkg.com/pullstream/-/pullstream-0.4.1.tgz#d6fb3bf5aed697e831150eb1002c25a3f8ae1314"