Settings Routes (#449)
* Building out settings area
* Flow and refactoring
* TeamLogo
* Add temporary profile screen
* 💚
* PR feedback
This commit is contained in:
parent
6aa0390e99
commit
505310c172
|
@ -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`
|
||||
|
|
|
@ -6,10 +6,7 @@ import type { Props } from './Icon';
|
|||
export default function BackIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M7.20710678,8.79289322 C6.81658249,8.40236893 6.18341751,8.40236893 5.79289322,8.79289322 C5.40236893,9.18341751 5.40236893,9.81658249 5.79289322,10.2071068 L10.7928932,15.2071068 C11.1834175,15.5976311 11.8165825,15.5976311 12.2071068,15.2071068 L17.2071068,10.2071068 C17.5976311,9.81658249 17.5976311,9.18341751 17.2071068,8.79289322 C16.8165825,8.40236893 16.1834175,8.40236893 15.7928932,8.79289322 L11.5,13.0857864 L7.20710678,8.79289322 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
<path d="M7.20710678,8.79289322 C6.81658249,8.40236893 6.18341751,8.40236893 5.79289322,8.79289322 C5.40236893,9.18341751 5.40236893,9.81658249 5.79289322,10.2071068 L10.7928932,15.2071068 C11.1834175,15.5976311 11.8165825,15.5976311 12.2071068,15.2071068 L17.2071068,10.2071068 C17.5976311,9.81658249 17.5976311,9.18341751 17.2071068,8.79289322 C16.8165825,8.40236893 16.1834175,8.40236893 15.7928932,8.79289322 L11.5,13.0857864 L7.20710678,8.79289322 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Icon {...props}>
|
||||
<path d="M6,6 L19,6 C20.1045695,6 21,6.8954305 21,8 L21,16 C21,17.1045695 20.1045695,18 19,18 L6,18 C4.8954305,18 4,17.1045695 4,16 L4,8 L4,8 C4,6.8954305 4.8954305,6 6,6 L6,6 Z M13,14 L13,16 L19,16 L19,14 L13,14 Z M13,11 L13,13 L17,13 L17,11 L13,11 Z M6,8 L6,16 L11,16 L11,8 L6,8 Z M13,8 L13,10 L19,10 L19,8 L13,8 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<Icon {...props}>
|
||||
<path d="M9.5005126,5.99794961 L9.81063391,4.75746437 L9.81063391,4.75746437 C9.92192566,4.31229737 10.3219088,4 10.7807764,4 L10.7807764,4 L13.2192236,4 L13.2192236,4 C13.6780912,4 14.0780743,4.31229737 14.1893661,4.75746437 L14.4994874,5.99794961 C15.0194857,6.21474677 15.5052885,6.49715027 15.9465564,6.83482093 L17.1775368,6.48268553 L17.1775368,6.48268553 C17.6187086,6.35648351 18.0891576,6.54673036 18.3185914,6.9441214 L18.3185914,6.9441214 L19.537815,9.0558786 C19.7672488,9.45326964 19.6967829,9.95581386 19.3669029,10.2747788 L18.4467943,11.1644429 C18.4819001,11.4380051 18.5,11.7168888 18.5,12 C18.5,12.2831112 18.4819001,12.5619949 18.4467943,12.8355571 L19.3669029,13.7252212 C19.6967829,14.0441861 19.7672488,14.5467304 19.537815,14.9441214 L18.3185914,17.0558786 C18.0891576,17.4532696 17.6187086,17.6435165 17.1775368,17.5173145 L15.9465564,17.1651791 C15.5052885,17.5028497 15.0194857,17.7852532 14.4994874,18.0020504 L14.1893661,19.2425356 C14.0780743,19.6877026 13.6780912,20 13.2192236,20 L10.7807764,20 C10.3219088,20 9.92192566,19.6877026 9.81063391,19.2425356 L9.5005126,18.0020504 C8.98051425,17.7852532 8.49471153,17.5028497 8.0534436,17.1651791 L6.82246321,17.5173145 C6.3812914,17.6435165 5.91084239,17.4532696 5.68140857,17.0558786 L4.46218497,14.9441214 C4.23275115,14.5467304 4.30321706,14.0441861 4.63309711,13.7252212 L5.5532057,12.8355571 C5.51809991,12.5619949 5.5,12.2831112 5.5,12 C5.5,11.7168888 5.51809991,11.4380051 5.5532057,11.1644429 L4.63309711,10.2747788 L4.63309711,10.2747788 C4.30321706,9.95581386 4.23275115,9.45326964 4.46218497,9.0558786 L4.46218497,9.0558786 L5.68140857,6.9441214 L5.68140857,6.9441214 C5.91084239,6.54673036 6.3812914,6.35648351 6.82246321,6.48268553 L8.0534436,6.83482093 C8.49471153,6.49715027 8.98051425,6.21474677 9.5005126,5.99794961 Z M12,15 C13.6568542,15 15,13.6568542 15,12 C15,10.3431458 13.6568542,9 12,9 C10.3431458,9 9,10.3431458 9,12 C9,13.6568542 10.3431458,15 12,15 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
|
||||
<Flex auto>
|
||||
{showSidebar && <Sidebar />}
|
||||
{showSidebar && (
|
||||
<Switch>
|
||||
<Route path="/settings" component={SettingsSidebar} />
|
||||
<Route component={Sidebar} />
|
||||
</Switch>
|
||||
)}
|
||||
|
||||
<Content auto justify="center" editMode={ui.editMode}>
|
||||
{this.props.children}
|
||||
|
|
|
@ -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 {
|
|||
<Modal name="keyboard-shortcuts" title="Keyboard shortcuts">
|
||||
<KeyboardShortcuts />
|
||||
</Modal>
|
||||
<Modal name="settings" title="Settings">
|
||||
<Settings />
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Container column>
|
||||
<HeaderBlock
|
||||
subheading="◄ Return to Dashboard"
|
||||
teamName={team.name}
|
||||
logoUrl={team.avatarUrl}
|
||||
onClick={this.returnToDashboard}
|
||||
/>
|
||||
|
||||
<Flex auto column>
|
||||
<Scrollable>
|
||||
<Section>
|
||||
<Header>Account</Header>
|
||||
<SidebarLink to="/settings" icon={<ProfileIcon />}>
|
||||
Profile
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/tokens" icon={<CodeIcon />}>
|
||||
API Tokens
|
||||
</SidebarLink>
|
||||
</Section>
|
||||
<Section>
|
||||
<Header>Team</Header>
|
||||
<SidebarLink
|
||||
to="/settings/integrations/slack"
|
||||
icon={<SettingsIcon />}
|
||||
>
|
||||
Integrations
|
||||
</SidebarLink>
|
||||
</Section>
|
||||
</Scrollable>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
|
@ -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 {
|
|||
<Container column editMode={ui.editMode}>
|
||||
<AccountMenu
|
||||
label={
|
||||
<HeaderBlock user={user} team={team}>
|
||||
<Avatar src={user.avatarUrl} />
|
||||
</HeaderBlock>
|
||||
<HeaderBlock
|
||||
subheading={user.name}
|
||||
teamName={team.name}
|
||||
logoUrl={team.avatarUrl}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
|
@ -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;
|
||||
`;
|
||||
|
|
|
@ -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;
|
|
@ -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<any>,
|
||||
teamName: string,
|
||||
subheading: string,
|
||||
logoUrl: string,
|
||||
};
|
||||
|
||||
function HeaderBlock({ user, team, children }: Props) {
|
||||
function HeaderBlock({ teamName, subheading, logoUrl, ...rest }: Props) {
|
||||
return (
|
||||
<Header justify="space-between" align="center">
|
||||
<Header justify="flex-start" align="center" {...rest}>
|
||||
<TeamLogo src={logoUrl} />
|
||||
<Flex align="flex-start" column>
|
||||
<TeamName>{team.name}</TeamName>
|
||||
<UserName>{user.name}</UserName>
|
||||
<TeamName>{teamName}</TeamName>
|
||||
<Subheading>{subheading}</Subheading>
|
||||
</Flex>
|
||||
{children}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||
<a href={slackAuth(auth.getOauthState(), scopes, redirectUri)}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject('auth')(SlackAuthLink);
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="148px" height="147px" viewBox="0 0 148 147" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs></defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M12.997,77.78 C7.503,77.822 2.849,74.548 1.133,69.438 C1.067,69.24 1.01,69.048 0.955,68.855 C-0.915,62.311 2.711,55.465 9.21,53.273 L113.45,18.35 C114.717,17.987 115.993,17.802 117.257,17.794 C122.897,17.75 127.679,21.096 129.437,26.314 L129.593,26.818 C131.543,33.634 126.698,39.718 120.893,41.668 C120.889,41.671 119.833,42.028 17.231,77.059 C15.844,77.53 14.421,77.768 12.997,77.78 L12.997,77.78 L12.997,77.78 Z" fill="#70CADB"></path>
|
||||
<path d="M30.372,129.045 C24.835,129.085 20.165,125.857 18.469,120.82 C18.405,120.628 18.344,120.435 18.289,120.241 C16.393,113.619 20.015,106.701 26.536,104.506 L130.78,69.263 C132.127,68.813 133.518,68.583 134.917,68.57 C140.469,68.528 145.347,71.92 147.068,77.014 L147.228,77.544 C148.235,81.065 147.64,85.022 145.638,88.145 C144.146,90.467 139.44,92.511 139.44,92.511 L34.8,128.29 C33.342,128.777 31.855,129.034 30.372,129.047 L30.372,129.045 L30.372,129.045 Z" fill="#E01765"></path>
|
||||
<path d="M117.148,129.268 C111.588,129.311 106.665,125.803 104.893,120.545 L70.103,17.205 L69.929,16.625 C68.044,10.035 71.669,3.161 78.166,0.971 C79.466,0.534 80.81,0.306 82.163,0.294 C84.173,0.279 86.118,0.732 87.95,1.637 C91.013,3.162 93.304,5.787 94.399,9.029 L129.186,112.36 L129.287,112.692 C131.241,119.534 127.624,126.412 121.127,128.602 C119.84,129.031 118.5,129.256 117.148,129.268 L117.148,129.268 L117.148,129.268 Z" fill="#E8A723"></path>
|
||||
<path d="M65.435,146.674 C59.875,146.717 54.948,143.209 53.175,137.944 L18.394,34.608 C18.334,34.418 18.274,34.228 18.216,34.033 C16.336,27.445 19.95,20.57 26.445,18.378 C27.74,17.948 29.079,17.721 30.43,17.71 C35.991,17.666 40.915,21.173 42.687,26.433 L77.469,129.773 C77.534,129.953 77.593,130.152 77.646,130.342 C79.53,136.935 75.914,143.814 69.409,146.006 C68.117,146.437 66.78,146.662 65.431,146.673 L65.435,146.673 L65.435,146.674 Z" fill="#3EB890"></path>
|
||||
<path d="M99.997,105.996 L124.255,97.702 L116.325,74.152 L92.039,82.359 L99.997,105.996 L99.997,105.996 Z" fill="#CC2027"></path>
|
||||
<path d="M48.364,123.65 L72.62,115.357 L64.63,91.627 L40.35,99.837 L48.364,123.65 L48.364,123.65 Z" fill="#361238"></path>
|
||||
<path d="M82.727,54.7 L106.987,46.417 L99.15,23.142 L74.845,31.285 L82.727,54.7 L82.727,54.7 Z" fill="#65863A"></path>
|
||||
<path d="M31.088,72.33 L55.348,64.047 L47.415,40.475 L23.11,48.617 L31.088,72.33 L31.088,72.33 Z" fill="#1A937D"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
import SlackAuthLink from './SlackAuthLink';
|
||||
export default SlackAuthLink;
|
|
@ -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;
|
13
app/index.js
13
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(
|
|||
<Switch>
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id"
|
||||
|
|
|
@ -20,7 +20,7 @@ class AccountMenu extends Component {
|
|||
};
|
||||
|
||||
handleOpenSettings = () => {
|
||||
this.props.ui.setActiveModal('settings');
|
||||
this.props.history.push('/settings');
|
||||
};
|
||||
|
||||
handleApi = () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<Flex column>
|
||||
{showSlackSettings && (
|
||||
<Section>
|
||||
<SectionLabel>Slack</SectionLabel>
|
||||
<HelpText>
|
||||
Connect Outline to your Slack to instantly search for your
|
||||
documents using <Code>/outline</Code> command.
|
||||
</HelpText>
|
||||
|
||||
<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>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<SectionLabel>API Access</SectionLabel>
|
||||
<HelpText>
|
||||
Create API tokens to hack on your Outline. Learn more in{' '}
|
||||
<Link to="/developers">API documentation</Link>.
|
||||
</HelpText>
|
||||
|
||||
{this.store.apiKeys && (
|
||||
<Table>
|
||||
<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>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
</Section>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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 (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Flex auto>
|
||||
<ApiKeyInput
|
||||
type="text"
|
||||
placeholder={
|
||||
this.validationError ? 'Please add a label' : placeholder
|
||||
}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
validationError={this.validationError}
|
||||
/>
|
||||
<Button type="submit" value={buttonLabel} />
|
||||
</Flex>
|
||||
</form>
|
||||
<CenteredContent>
|
||||
<PageTitle title="Profile" />
|
||||
<h1>Profile</h1>
|
||||
<HelpText>
|
||||
You’re signed in to Outline with Slack. To update your profile
|
||||
information here please{' '}
|
||||
<a href="https://slack.com/account/profile" target="_blank">
|
||||
update your profile on Slack
|
||||
</a>{' '}
|
||||
and re-login to refresh.
|
||||
</HelpText>
|
||||
|
||||
<form>
|
||||
<Input label="Name" value={user.name} disabled />
|
||||
<Input label="Email" value={user.email} disabled />
|
||||
</form>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Section = styled.div`
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const Table = styled.table`
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
margin-right: 20px;
|
||||
color: ${color.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionLabel = styled(Label)`
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eaebea;
|
||||
`;
|
||||
|
||||
const Code = styled.code`
|
||||
padding: 4px 6px;
|
||||
margin: 0 2px;
|
||||
background: #eaebea;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const ApiKeyInput = styled(Input)`
|
||||
width: 100%;
|
||||
margin-right: 12px;
|
||||
`;
|
||||
|
||||
export default Settings;
|
||||
export default inject('auth')(Settings);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import HelpText from 'components/HelpText';
|
||||
import SlackButton from './components/SlackButton';
|
||||
|
||||
@observer
|
||||
class Slack extends Component {
|
||||
render() {
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Slack" />
|
||||
<h1>Slack</h1>
|
||||
<HelpText>
|
||||
Connect Outline to your Slack team to instantly search for documents
|
||||
using the <Code>/outline</Code> command.
|
||||
</HelpText>
|
||||
|
||||
<SlackButton
|
||||
scopes={['commands']}
|
||||
redirectUri={`${BASE_URL}/auth/slack/commands`}
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Code = styled.code`
|
||||
padding: 4px 6px;
|
||||
margin: 0 2px;
|
||||
background: #eaebea;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
export default Slack;
|
|
@ -0,0 +1,99 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import ApiToken from './components/ApiToken';
|
||||
import SettingsStore from 'stores/SettingsStore';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
import Button from 'components/Button';
|
||||
import Input from 'components/Input';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Subheading from 'components/Subheading';
|
||||
|
||||
@observer
|
||||
class Settings extends Component {
|
||||
@observable name: string = '';
|
||||
props: {
|
||||
settings: SettingsStore,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.settings.fetchApiKeys();
|
||||
}
|
||||
|
||||
handleUpdate = (ev: SyntheticInputEvent) => {
|
||||
this.name = ev.target.value;
|
||||
};
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
await this.props.settings.createApiKey(this.name);
|
||||
this.name = '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings } = this.props;
|
||||
const hasApiKeys = settings.apiKeys.length > 0;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="API Tokens" />
|
||||
<h1>API Tokens</h1>
|
||||
|
||||
{hasApiKeys && [
|
||||
<Subheading>Your tokens</Subheading>,
|
||||
<Table>
|
||||
<tbody>
|
||||
{settings.apiKeys.map(key => (
|
||||
<ApiToken
|
||||
id={key.id}
|
||||
key={key.id}
|
||||
name={key.name}
|
||||
secret={key.secret}
|
||||
onDelete={settings.deleteApiKey}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>,
|
||||
<Subheading>Create a token</Subheading>,
|
||||
]}
|
||||
|
||||
<HelpText>
|
||||
You can create unlimited personal API tokens to hack on your wiki.
|
||||
Learn more in the <Link to="/developers">API documentation</Link>.
|
||||
</HelpText>
|
||||
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Input
|
||||
onChange={this.handleUpdate}
|
||||
placeholder="Token label (eg. development)"
|
||||
value={this.name}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
value="Create Token"
|
||||
disabled={settings.isSaving}
|
||||
/>
|
||||
</form>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Table = styled.table`
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
margin-right: 20px;
|
||||
color: ${color.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject('settings')(Settings);
|
|
@ -2,18 +2,17 @@
|
|||
import React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Button from 'components/Button';
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
name: ?string,
|
||||
secret: string,
|
||||
onDelete: Function,
|
||||
onDelete: (id: string) => *,
|
||||
};
|
||||
|
||||
@observer
|
||||
class ApiKeyRow extends React.Component {
|
||||
class ApiToken extends React.Component {
|
||||
props: Props;
|
||||
@observable disabled: boolean;
|
||||
|
||||
|
@ -32,21 +31,14 @@ class ApiKeyRow extends React.Component {
|
|||
<td>
|
||||
<code>{secret}</code>
|
||||
</td>
|
||||
<td>
|
||||
<Action role="button" onClick={this.onClick} disabled={disabled}>
|
||||
Action
|
||||
</Action>
|
||||
<td align="right">
|
||||
<Button onClick={this.onClick} disabled={disabled} neutral>
|
||||
Revoke
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Action = styled.span`
|
||||
font-size: 14px;
|
||||
color: ${color.text};
|
||||
|
||||
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
|
||||
`;
|
||||
|
||||
export default ApiKeyRow;
|
||||
export default ApiToken;
|
|
@ -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 (
|
||||
<Button onClick={handleClick} icon={<SpacedSlackLogo size={24} />} neutral>
|
||||
Add to <strong>Slack</strong>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const SpacedSlackLogo = styled(SlackLogo)`
|
||||
padding-right: 4px;
|
||||
`;
|
||||
|
||||
export default inject('auth')(SlackButton);
|
|
@ -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;
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<svg
|
||||
fill="#fff"
|
||||
width="34"
|
||||
height="34"
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 34 34"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g>
|
||||
<rect
|
||||
|
|
Reference in New Issue