diff --git a/app/components/Button/Button.js b/app/components/Button/Button.js index 46700009..4a30c745 100644 --- a/app/components/Button/Button.js +++ b/app/components/Button/Button.js @@ -32,6 +32,11 @@ const RealButton = styled.button` top: 0.05em; } + &:disabled { + opacity: 0.8; + cursor: default; + } + ${props => props.light && ` @@ -57,10 +62,7 @@ const RealButton = styled.button` &:hover { background: ${darken(0.05, color.danger)}; } - `} &:disabled { - background: ${color.slateLight}; - cursor: default; - } + `}; `; const Label = styled.span` diff --git a/app/components/Icon/BackIcon.js b/app/components/Icon/BackIcon.js index 4586fd1a..42f2c6a9 100644 --- a/app/components/Icon/BackIcon.js +++ b/app/components/Icon/BackIcon.js @@ -6,10 +6,7 @@ import type { Props } from './Icon'; export default function BackIcon(props: Props) { return ( - + ); } diff --git a/app/components/Icon/ProfileIcon.js b/app/components/Icon/ProfileIcon.js new file mode 100644 index 00000000..7fc415b0 --- /dev/null +++ b/app/components/Icon/ProfileIcon.js @@ -0,0 +1,12 @@ +// @flow +import React from 'react'; +import Icon from './Icon'; +import type { Props } from './Icon'; + +export default function ProfileIcon(props: Props) { + return ( + + + + ); +} diff --git a/app/components/Icon/SettingsIcon.js b/app/components/Icon/SettingsIcon.js new file mode 100644 index 00000000..e00dca74 --- /dev/null +++ b/app/components/Icon/SettingsIcon.js @@ -0,0 +1,12 @@ +// @flow +import React from 'react'; +import Icon from './Icon'; +import type { Props } from './Icon'; + +export default function SettingsIcon(props: Props) { + return ( + + + + ); +} diff --git a/app/components/Input/Input.js b/app/components/Input/Input.js index 94c2d8ec..71608f1e 100644 --- a/app/components/Input/Input.js +++ b/app/components/Input/Input.js @@ -11,6 +11,7 @@ const RealTextarea = styled.textarea` outline: none; background: none; + &:disabled, &::placeholder { color: ${color.slate}; } @@ -23,6 +24,7 @@ const RealInput = styled.input` outline: none; background: none; + &:disabled, &::placeholder { color: ${color.slate}; } @@ -52,13 +54,18 @@ export const LabelText = styled.div` `; export type Props = { - type: string, + type?: string, value?: string, label?: string, className?: string, }; -export default function Input({ type, label, className, ...rest }: Props) { +export default function Input({ + type = 'text', + label, + className, + ...rest +}: Props) { const InputComponent = type === 'textarea' ? RealTextarea : RealInput; return ( diff --git a/app/components/Layout/Layout.js b/app/components/Layout/Layout.js index 34e7a94b..a87ae84f 100644 --- a/app/components/Layout/Layout.js +++ b/app/components/Layout/Layout.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { Switch, Route, withRouter } from 'react-router-dom'; import type { Location } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import styled from 'styled-components'; @@ -13,6 +13,7 @@ import { documentEditUrl, homeUrl, searchUrl } from 'utils/routeHelpers'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; import Sidebar from 'components/Sidebar'; +import SettingsSidebar from 'components/Sidebar/Settings'; import Modals from 'components/Modals'; import Toasts from 'components/Toasts'; @@ -84,7 +85,12 @@ class Layout extends React.Component { {this.props.notifications} - {showSidebar && } + {showSidebar && ( + + + + + )} {this.props.children} diff --git a/app/components/Modals/Modals.js b/app/components/Modals/Modals.js index 00b4bb7c..b08ebf25 100644 --- a/app/components/Modals/Modals.js +++ b/app/components/Modals/Modals.js @@ -8,7 +8,6 @@ import CollectionEdit from 'scenes/CollectionEdit'; import CollectionDelete from 'scenes/CollectionDelete'; import DocumentDelete from 'scenes/DocumentDelete'; import KeyboardShortcuts from 'scenes/KeyboardShortcuts'; -import Settings from 'scenes/Settings'; @observer class Modals extends Component { @@ -52,9 +51,6 @@ class Modals extends Component { - - - ); } diff --git a/app/components/Sidebar/Settings.js b/app/components/Sidebar/Settings.js new file mode 100644 index 00000000..1d0767a6 --- /dev/null +++ b/app/components/Sidebar/Settings.js @@ -0,0 +1,90 @@ +// @flow +import React, { Component } from 'react'; +import { withRouter } from 'react-router-dom'; +import type { Location } from 'react-router-dom'; +import styled from 'styled-components'; +import { observer, inject } from 'mobx-react'; +import Flex from 'shared/components/Flex'; +import { color, layout } from 'shared/styles/constants'; + +import Scrollable from 'components/Scrollable'; +import ProfileIcon from 'components/Icon/ProfileIcon'; +import SettingsIcon from 'components/Icon/SettingsIcon'; +import CodeIcon from 'components/Icon/CodeIcon'; +import Header from './components/Header'; +import SidebarLink from './components/SidebarLink'; +import HeaderBlock from './components/HeaderBlock'; +import AuthStore from 'stores/AuthStore'; + +type Props = { + history: Object, + location: Location, + auth: AuthStore, +}; + +@observer +class Sidebar extends Component { + props: Props; + + returnToDashboard = () => { + this.props.history.push('/'); + }; + + render() { + const { team } = this.props.auth; + if (!team) return; + + return ( + + + + + +
+
Account
+ }> + Profile + + }> + API Tokens + +
+
+
Team
+ } + > + Integrations + +
+
+
+
+ ); + } +} + +const Container = styled(Flex)` + position: fixed; + top: 0; + bottom: 0; + left: 0; + width: ${layout.sidebarWidth}; + background: ${color.smoke}; + transition: left 200ms ease-in-out; +`; + +const Section = styled(Flex)` + flex-direction: column; + margin: 24px 0; + padding: 0 24px; + position: relative; +`; + +export default withRouter(inject('auth')(Sidebar)); diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index 1242e663..92fbf89a 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -8,7 +8,6 @@ import Flex from 'shared/components/Flex'; import { color, layout } from 'shared/styles/constants'; import AccountMenu from 'menus/AccountMenu'; -import Avatar from 'components/Avatar'; import Scrollable from 'components/Scrollable'; import HomeIcon from 'components/Icon/HomeIcon'; import SearchIcon from 'components/Icon/SearchIcon'; @@ -63,9 +62,11 @@ class Sidebar extends Component { - - + } /> diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index 157c410f..3fa4066f 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -5,8 +5,9 @@ import { observer, inject } from 'mobx-react'; import type { Location } from 'react-router-dom'; import Flex from 'shared/components/Flex'; import styled from 'styled-components'; -import { color, fontWeight } from 'shared/styles/constants'; +import { color } from 'shared/styles/constants'; +import Header from './Header'; import SidebarLink from './SidebarLink'; import DropToImport from 'components/DropToImport'; import PlusIcon from 'components/Icon/PlusIcon'; @@ -261,15 +262,6 @@ const StyledDropToImport = styled(DropToImport)` } `; -const Header = styled(Flex)` - font-size: 12px; - font-weight: ${fontWeight.semiBold}; - text-transform: uppercase; - color: ${color.slate}; - letter-spacing: 0.04em; - margin-bottom: 4px; -`; - const Children = styled(Flex)` margin-left: 12px; `; diff --git a/app/components/Sidebar/components/Header.js b/app/components/Sidebar/components/Header.js new file mode 100644 index 00000000..63cff780 --- /dev/null +++ b/app/components/Sidebar/components/Header.js @@ -0,0 +1,15 @@ +// @flow +import Flex from 'shared/components/Flex'; +import styled from 'styled-components'; +import { color, fontWeight } from 'shared/styles/constants'; + +const Header = styled(Flex)` + font-size: 11px; + font-weight: ${fontWeight.semiBold}; + text-transform: uppercase; + color: ${color.slateDark}; + letter-spacing: 0.04em; + margin-bottom: 4px; +`; + +export default Header; diff --git a/app/components/Sidebar/components/HeaderBlock.js b/app/components/Sidebar/components/HeaderBlock.js index 242a4442..be93d154 100644 --- a/app/components/Sidebar/components/HeaderBlock.js +++ b/app/components/Sidebar/components/HeaderBlock.js @@ -2,33 +2,38 @@ import React from 'react'; import styled from 'styled-components'; import { color } from 'shared/styles/constants'; -import type { User, Team } from 'types'; import Flex from 'shared/components/Flex'; +import TeamLogo from './TeamLogo'; type Props = { - user: User, - team: Team, - children?: React$Element, + teamName: string, + subheading: string, + logoUrl: string, }; -function HeaderBlock({ user, team, children }: Props) { +function HeaderBlock({ teamName, subheading, logoUrl, ...rest }: Props) { return ( -
+
+ - {team.name} - {user.name} + {teamName} + {subheading} - {children}
); } -const UserName = styled.div` - font-size: 13px; +const Subheading = styled.div` + padding-left: 10px; + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + color: ${color.slateDark}; `; const TeamName = styled.div` - font-weight: bold; + padding-left: 10px; + font-weight: 600; color: ${color.text}; text-decoration: none; font-size: 16px; @@ -43,18 +48,9 @@ const Header = styled(Flex)` &:active, &:hover { + transition: background 100ms ease-in-out; background: rgba(0, 0, 0, 0.05); } - - &::after { - content: ''; - left: 24px; - right: 24px; - background: rgba(0, 0, 0, 0.075); - height: 1px; - position: absolute; - bottom: 0; - } `; export default HeaderBlock; diff --git a/app/components/Sidebar/components/TeamLogo.js b/app/components/Sidebar/components/TeamLogo.js new file mode 100644 index 00000000..f699b32a --- /dev/null +++ b/app/components/Sidebar/components/TeamLogo.js @@ -0,0 +1,13 @@ +// @flow +import styled from 'styled-components'; +import { color } from 'shared/styles/constants'; + +const TeamLogo = styled.img` + width: 38px; + height: 38px; + border-radius: 4px; + background: ${color.white}; + border: 1px solid ${color.slateLight}; +`; + +export default TeamLogo; diff --git a/app/components/SlackAuthLink/SlackAuthLink.js b/app/components/SlackAuthLink/SlackAuthLink.js deleted file mode 100644 index d4f8c164..00000000 --- a/app/components/SlackAuthLink/SlackAuthLink.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import React from 'react'; -import { inject } from 'mobx-react'; -import { slackAuth } from 'shared/utils/routeHelpers'; -import AuthStore from 'stores/AuthStore'; - -type Props = { - children: React$Element<*>, - auth: AuthStore, - scopes?: string[], - redirectUri?: string, -}; - -function SlackAuthLink({ auth, children, scopes, redirectUri }: Props) { - return ( - - {children} - - ); -} - -export default inject('auth')(SlackAuthLink); diff --git a/app/components/SlackAuthLink/assets/slack_icon.svg b/app/components/SlackAuthLink/assets/slack_icon.svg deleted file mode 100644 index fc10280f..00000000 --- a/app/components/SlackAuthLink/assets/slack_icon.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/components/SlackAuthLink/index.js b/app/components/SlackAuthLink/index.js deleted file mode 100644 index 5984174e..00000000 --- a/app/components/SlackAuthLink/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import SlackAuthLink from './SlackAuthLink'; -export default SlackAuthLink; diff --git a/app/components/Subheading.js b/app/components/Subheading.js new file mode 100644 index 00000000..32cbe5be --- /dev/null +++ b/app/components/Subheading.js @@ -0,0 +1,17 @@ +// @flow +import styled from 'styled-components'; +import { color } from 'shared/styles/constants'; + +const Subheading = styled.h3` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: ${color.slate}; + letter-spacing: 0.04em; + border-bottom: 1px solid ${color.slateLight}; + padding-bottom: 8px; + margin-top: 30px; + margin-bottom: 10px; +`; + +export default Subheading; diff --git a/app/index.js b/app/index.js index 8341b0c4..4b1a1084 100644 --- a/app/index.js +++ b/app/index.js @@ -10,6 +10,7 @@ import { } from 'react-router-dom'; import stores from 'stores'; +import SettingsStore from 'stores/SettingsStore'; import DocumentsStore from 'stores/DocumentsStore'; import CollectionsStore from 'stores/CollectionsStore'; import CacheStore from 'stores/CacheStore'; @@ -22,6 +23,9 @@ import Starred from 'scenes/Starred'; import Collection from 'scenes/Collection'; import Document from 'scenes/Document'; import Search from 'scenes/Search'; +import Settings from 'scenes/Settings'; +import Slack from 'scenes/Settings/Slack'; +import Tokens from 'scenes/Settings/Tokens'; import SlackAuth from 'scenes/SlackAuth'; import ErrorAuth from 'scenes/ErrorAuth'; import Error404 from 'scenes/Error404'; @@ -54,6 +58,7 @@ const Auth = ({ children }: AuthProps) => { const { user, team } = stores.auth; const cache = new CacheStore(user.id); authenticatedStores = { + settings: new SettingsStore(), documents: new DocumentsStore({ ui: stores.ui, cache, @@ -110,6 +115,14 @@ render( + + + + { - this.props.ui.setActiveModal('settings'); + this.props.history.push('/settings'); }; handleApi = () => { diff --git a/app/scenes/Dashboard/Dashboard.js b/app/scenes/Dashboard/Dashboard.js index 19a195c9..b33042fc 100644 --- a/app/scenes/Dashboard/Dashboard.js +++ b/app/scenes/Dashboard/Dashboard.js @@ -2,26 +2,15 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; import DocumentsStore from 'stores/DocumentsStore'; import Flex from 'shared/components/Flex'; import DocumentList from 'components/DocumentList'; import PageTitle from 'components/PageTitle'; +import Subheading from 'components/Subheading'; import CenteredContent from 'components/CenteredContent'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; -const Subheading = styled.h3` - font-size: 11px; - font-weight: 500; - text-transform: uppercase; - color: #9fa6ab; - letter-spacing: 0.04em; - border-bottom: 1px solid #ddd; - padding-bottom: 10px; - margin-top: 30px; -`; - type Props = { documents: DocumentsStore, }; diff --git a/app/scenes/Settings/Settings.js b/app/scenes/Settings/Settings.js index 6650bc65..6172b1b4 100644 --- a/app/scenes/Settings/Settings.js +++ b/app/scenes/Settings/Settings.js @@ -1,179 +1,43 @@ // @flow import React, { Component } from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import ApiKeyRow from './components/ApiKeyRow'; -import SettingsStore from './SettingsStore'; -import { color } from 'shared/styles/constants'; +import { observer, inject } from 'mobx-react'; -import Flex from 'shared/components/Flex'; -import Button from 'components/Button'; +import AuthStore from 'stores/AuthStore'; import Input from 'components/Input'; +import CenteredContent from 'components/CenteredContent'; +import PageTitle from 'components/PageTitle'; import HelpText from 'components/HelpText'; -import { Label } from 'components/Labeled'; -import SlackAuthLink from 'components/SlackAuthLink'; @observer class Settings extends Component { - store: SettingsStore; - - constructor() { - super(); - this.store = new SettingsStore(); - } - - render() { - const showSlackSettings = DEPLOYMENT === 'hosted'; - - return ( - - {showSlackSettings && ( -
- Slack - - Connect Outline to your Slack to instantly search for your - documents using /outline command. - - - - Add to Slack - -
- )} - -
- API Access - - Create API tokens to hack on your Outline. Learn more in{' '} - API documentation. - - - {this.store.apiKeys && ( - - - {this.store.apiKeys && - this.store.apiKeys.map(key => ( - - ))} - -
- )} - -
-
- ); - } -} - -@observer -class InlineForm extends Component { props: { - placeholder: string, - buttonLabel: string, - name: string, - value: ?string, - onChange: Function, - onSubmit: Function, - disabled?: ?boolean, - }; - - @observable validationError: boolean = false; - validationTimeout: number; - - componentWillUnmount() { - clearTimeout(this.validationTimeout); - } - - handleSubmit = event => { - event.preventDefault(); - if (this.props.value) { - this.props.onSubmit(); - } else { - this.validationError = true; - this.validationTimeout = setTimeout( - () => (this.validationError = false), - 2500 - ); - } + auth: AuthStore, }; render() { - const { placeholder, value, onChange, buttonLabel } = this.props; + const { user } = this.props.auth; + if (!user) return null; return ( -
- - - ); } } -const Action = styled.span` - font-size: 14px; - color: ${color.text}; - - opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; -`; - -export default ApiKeyRow; +export default ApiToken; diff --git a/app/scenes/Settings/components/SlackButton.js b/app/scenes/Settings/components/SlackButton.js new file mode 100644 index 00000000..9dcb63c5 --- /dev/null +++ b/app/scenes/Settings/components/SlackButton.js @@ -0,0 +1,35 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; +import { inject } from 'mobx-react'; +import { slackAuth } from 'shared/utils/routeHelpers'; +import Button from 'components/Button'; +import SlackLogo from 'shared/components/SlackLogo'; +import AuthStore from 'stores/AuthStore'; + +type Props = { + auth: AuthStore, + scopes?: string[], + redirectUri?: string, +}; + +function SlackButton({ auth, scopes, redirectUri }: Props) { + const handleClick = () => + (window.location.href = slackAuth( + auth.getOauthState(), + scopes, + redirectUri + )); + + return ( + + ); +} + +const SpacedSlackLogo = styled(SlackLogo)` + padding-right: 4px; +`; + +export default inject('auth')(SlackButton); diff --git a/app/scenes/Settings/SettingsStore.js b/app/stores/SettingsStore.js similarity index 65% rename from app/scenes/Settings/SettingsStore.js rename to app/stores/SettingsStore.js index 3e5a3c36..a59105e0 100644 --- a/app/scenes/Settings/SettingsStore.js +++ b/app/stores/SettingsStore.js @@ -4,11 +4,10 @@ import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import type { ApiKey } from 'types'; -class SearchStore { +class SettingsStore { @observable apiKeys: ApiKey[] = []; - @observable keyName: ?string; - @observable isFetching: boolean = false; + @observable isSaving: boolean = false; @action fetchApiKeys = async () => { @@ -29,50 +28,33 @@ class SearchStore { }; @action - createApiKey = async () => { - this.isFetching = true; + createApiKey = async (name: string) => { + this.isSaving = true; try { - const res = await client.post('/apiKeys.create', { - name: this.keyName ? this.keyName : 'Untitled key', - }); + const res = await client.post('/apiKeys.create', { name }); invariant(res && res.data, 'Data should be available'); const { data } = res; runInAction('createApiKey', () => { this.apiKeys.push(data); - this.keyName = ''; }); } catch (e) { console.error('Something went wrong'); } - this.isFetching = false; + this.isSaving = false; }; @action deleteApiKey = async (id: string) => { - this.isFetching = true; - try { - await client.post('/apiKeys.delete', { - id, - }); + await client.post('/apiKeys.delete', { id }); runInAction('deleteApiKey', () => { this.fetchApiKeys(); }); } catch (e) { console.error('Something went wrong'); } - this.isFetching = false; }; - - @action - setKeyName = (value: SyntheticInputEvent) => { - this.keyName = value.target.value; - }; - - constructor() { - this.fetchApiKeys(); - } } -export default SearchStore; +export default SettingsStore; diff --git a/app/types/index.js b/app/types/index.js index a0d1688b..63d86139 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -3,12 +3,14 @@ export type User = { avatarUrl: string, id: string, name: string, + email: string, username: string, }; export type Team = { id: string, name: string, + avatarUrl: string, }; export type NavigationNode = { diff --git a/server/api/__snapshots__/user.test.js.snap b/server/api/__snapshots__/user.test.js.snap index 2f6503ac..888d2362 100644 --- a/server/api/__snapshots__/user.test.js.snap +++ b/server/api/__snapshots__/user.test.js.snap @@ -13,6 +13,7 @@ exports[`#user.info should return known user 1`] = ` Object { "data": Object { "avatarUrl": "http://example.com/avatar.png", + "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", "name": "User 1", "username": "user1", diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap index a4e6bd70..8a5c7ad1 100644 --- a/server/presenters/__snapshots__/user.test.js.snap +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -3,6 +3,7 @@ exports[`presents a user 1`] = ` Object { "avatarUrl": "http://example.com/avatar.png", + "email": undefined, "id": "123", "name": "Test User", "username": "testuser", @@ -12,6 +13,7 @@ Object { exports[`presents a user without slack data 1`] = ` Object { "avatarUrl": null, + "email": undefined, "id": "123", "name": "Test User", "username": "testuser", diff --git a/server/presenters/team.js b/server/presenters/team.js index 6587798a..d4121b68 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -7,6 +7,8 @@ function present(ctx: Object, team: Team) { return { id: team.id, name: team.name, + avatarUrl: + team.avatarUrl || (team.slackData ? team.slackData.image_88 : null), }; } diff --git a/server/presenters/user.js b/server/presenters/user.js index 4e9a7e3d..1281c45d 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -8,6 +8,7 @@ function present(ctx: Object, user: User) { id: user.id, username: user.username, name: user.name, + email: user.email, avatarUrl: user.avatarUrl || (user.slackData ? user.slackData.image_192 : null), }; diff --git a/shared/components/SlackLogo.js b/shared/components/SlackLogo.js index a4512f2e..ea644941 100644 --- a/shared/components/SlackLogo.js +++ b/shared/components/SlackLogo.js @@ -1,14 +1,21 @@ // @flow import React from 'react'; -function SlackLogo() { +type Props = { + size?: number, + fill?: string, + className?: string, +}; + +function SlackLogo({ size = 34, fill = '#FFF', className }: Props) { return (