New login screen (#1331)
* wip * feat: first draft of auth.config * chore: auth methodS * chore: styling * styling, styling, styling * feat: Auth notices * chore: Remove server-rendered pages, move shared/components -> components * lint * cleanup * cleanup * fix: Remove unused component * fix: Ensure env variables in prod too * style tweaks * fix: Entering SSO email into login form fails fix: Tweak language around guest signin
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export const Action = styled(Flex)`
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Flex from "shared/components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
type?: "info" | "success" | "warning" | "danger" | "offline",
|
||||
};
|
||||
|
||||
@observer
|
||||
class Alert extends React.Component<Props> {
|
||||
defaultProps = {
|
||||
type: "info",
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container align="center" justify="center" type={this.props.type}>
|
||||
{this.props.children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
height: $headerHeight;
|
||||
color: ${props => props.theme.white};
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ theme, type }) => theme.color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import { isCustomSubdomain } from "shared/utils/domains";
|
||||
@@ -35,7 +36,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
|
||||
}
|
||||
|
||||
auth.logout(true);
|
||||
return null;
|
||||
return <Redirect to="/" />;
|
||||
});
|
||||
|
||||
export default inject("auth")(Authenticated);
|
||||
|
||||
41
app/components/Branding.js
Normal file
41
app/components/Branding.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import OutlineLogo from "./OutlineLogo";
|
||||
|
||||
type Props = {
|
||||
href?: string,
|
||||
};
|
||||
|
||||
function Branding({ href = process.env.URL }: Props) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<OutlineLogo size={16} /> Outline
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const Link = styled.a`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
border-top-right-radius: 2px;
|
||||
color: ${props => props.theme.text};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
svg {
|
||||
fill: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.theme.sidebarBackground};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Branding;
|
||||
133
app/components/Breadcrumb.js
Normal file
133
app/components/Breadcrumb.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "react-router-dom";
|
||||
import { PadlockIcon, GoToIcon, MoreIcon } from "outline-icons";
|
||||
|
||||
import Document from "models/Document";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import { collectionUrl } from "utils/routeHelpers";
|
||||
import Flex from "components/Flex";
|
||||
import BreadcrumbMenu from "./BreadcrumbMenu";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
collections: CollectionsStore,
|
||||
onlyText: boolean,
|
||||
};
|
||||
|
||||
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
|
||||
const collection = collections.get(document.collectionId);
|
||||
if (!collection) return <div />;
|
||||
|
||||
const path = collection.pathToDocument(document).slice(0, -1);
|
||||
|
||||
if (onlyText === true) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{collection.private && (
|
||||
<React.Fragment>
|
||||
<SmallPadlockIcon color="currentColor" size={16} />{" "}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{collection.name}
|
||||
{path.map(n => (
|
||||
<React.Fragment key={n.id}>
|
||||
<SmallSlash />
|
||||
{n.title}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const isNestedDocument = path.length > 1;
|
||||
const lastPath = path.length ? path[path.length - 1] : undefined;
|
||||
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
|
||||
|
||||
return (
|
||||
<Wrapper justify="flex-start" align="center">
|
||||
<CollectionName to={collectionUrl(collection.id)}>
|
||||
<CollectionIcon collection={collection} expanded />{" "}
|
||||
<span>{collection.name}</span>
|
||||
</CollectionName>
|
||||
{isNestedDocument && (
|
||||
<React.Fragment>
|
||||
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{lastPath && (
|
||||
<React.Fragment>
|
||||
<Slash />{" "}
|
||||
<Crumb to={lastPath.url} title={lastPath.title}>
|
||||
{lastPath.title}
|
||||
</Crumb>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
display: none;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
|
||||
const SmallPadlockIcon = styled(PadlockIcon)`
|
||||
display: inline-block;
|
||||
vertical-align: sub;
|
||||
`;
|
||||
|
||||
const SmallSlash = styled(GoToIcon)`
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.25;
|
||||
`;
|
||||
|
||||
export const Slash = styled(GoToIcon)`
|
||||
flex-shrink: 0;
|
||||
fill: ${props => props.theme.divider};
|
||||
`;
|
||||
|
||||
const Overflow = styled(MoreIcon)`
|
||||
flex-shrink: 0;
|
||||
opacity: 0.25;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const Crumb = styled(Link)`
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 15px;
|
||||
height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const CollectionName = styled(Link)`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export default inject("collections")(Breadcrumb);
|
||||
25
app/components/BreadcrumbMenu.js
Normal file
25
app/components/BreadcrumbMenu.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
|
||||
type Props = {
|
||||
label: React.Node,
|
||||
path: Array<any>,
|
||||
};
|
||||
|
||||
export default class BreadcrumbMenu extends React.Component<Props> {
|
||||
render() {
|
||||
const { path } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownMenu label={this.props.label} position="center">
|
||||
{path.map(item => (
|
||||
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { darken, lighten } from "polished";
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
|
||||
const RealButton = styled.button`
|
||||
display: inline-block;
|
||||
display: ${props => (props.fullwidth ? "block" : "inline-block")};
|
||||
width: ${props => (props.fullwidth ? "100%" : "auto")};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
@@ -126,6 +127,7 @@ export type Props = {
|
||||
children?: React.Node,
|
||||
innerRef?: React.ElementRef<any>,
|
||||
disclosure?: boolean,
|
||||
fullwidth?: boolean,
|
||||
borderOnHover?: boolean,
|
||||
};
|
||||
|
||||
|
||||
13
app/components/ButtonLarge.js
Normal file
13
app/components/ButtonLarge.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Button, { Inner } from "./Button";
|
||||
|
||||
const ButtonLarge = styled(Button)`
|
||||
height: 40px;
|
||||
|
||||
${Inner} {
|
||||
padding: 4px 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default ButtonLarge;
|
||||
@@ -11,7 +11,7 @@ import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import RevisionsStore from "stores/RevisionsStore";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import { ListPlaceholder } from "components/LoadingPlaceholder";
|
||||
import Revision from "./components/Revision";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
@@ -5,8 +5,8 @@ import styled, { withTheme } from "styled-components";
|
||||
import format from "date-fns/format";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Time from "shared/components/Time";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import Avatar from "components/Avatar";
|
||||
import RevisionMenu from "menus/RevisionMenu";
|
||||
import Document from "models/Document";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { StarredIcon } from "outline-icons";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Badge from "components/Badge";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import Highlight from "components/Highlight";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PortalWithState } from "react-portal";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import { rgba } from "polished";
|
||||
import styled from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import { fadeAndScaleIn } from "shared/styles/animations";
|
||||
import NudeButton from "components/NudeButton";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Avatar from "components/Avatar";
|
||||
import User from "models/User";
|
||||
|
||||
|
||||
45
app/components/Flex.js
Normal file
45
app/components/Flex.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type JustifyValues =
|
||||
| "center"
|
||||
| "space-around"
|
||||
| "space-between"
|
||||
| "flex-start"
|
||||
| "flex-end";
|
||||
|
||||
type AlignValues =
|
||||
| "stretch"
|
||||
| "center"
|
||||
| "baseline"
|
||||
| "flex-start"
|
||||
| "flex-end";
|
||||
|
||||
type Props = {
|
||||
column?: ?boolean,
|
||||
shrink?: ?boolean,
|
||||
align?: AlignValues,
|
||||
justify?: JustifyValues,
|
||||
auto?: ?boolean,
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const Flex = (props: Props) => {
|
||||
const { children, ...restProps } = props;
|
||||
return <Container {...restProps}>{children}</Container>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
|
||||
flex-direction: ${({ column }) => (column ? "column" : "row")};
|
||||
align-items: ${({ align }) => align};
|
||||
justify-content: ${({ justify }) => justify};
|
||||
flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export default Flex;
|
||||
30
app/components/GithubLogo.js
Normal file
30
app/components/GithubLogo.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 36 36"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
fill="#191717"
|
||||
d="M18,1.4C9,1.4,1.7,8.7,1.7,17.7c0,7.2,4.7,13.3,11.1,15.5 c0.8,0.1,1.1-0.4,1.1-0.8c0-0.4,0-1.4,0-2.8c-4.5,1-5.5-2.2-5.5-2.2c-0.7-1.9-1.8-2.4-1.8-2.4c-1.5-1,0.1-1,0.1-1 c1.6,0.1,2.5,1.7,2.5,1.7c1.5,2.5,3.8,1.8,4.7,1.4c0.1-1.1,0.6-1.8,1-2.2c-3.6-0.4-7.4-1.8-7.4-8.1c0-1.8,0.6-3.2,1.7-4.4 c-0.2-0.4-0.7-2.1,0.2-4.3c0,0,1.4-0.4,4.5,1.7c1.3-0.4,2.7-0.5,4.1-0.5c1.4,0,2.8,0.2,4.1,0.5c3.1-2.1,4.5-1.7,4.5-1.7 c0.9,2.2,0.3,3.9,0.2,4.3c1,1.1,1.7,2.6,1.7,4.4c0,6.3-3.8,7.6-7.4,8c0.6,0.5,1.1,1.5,1.1,3c0,2.2,0,3.9,0,4.5 c0,0.4,0.3,0.9,1.1,0.8c6.5-2.2,11.1-8.3,11.1-15.5C34.3,8.7,27,1.4,18,1.4z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default GithubLogo;
|
||||
27
app/components/GoogleLogo.js
Normal file
27
app/components/GoogleLogo.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 34 34"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g>
|
||||
<path d="M32.6162791,13.9090909 L16.8837209,13.9090909 L16.8837209,20.4772727 L25.9395349,20.4772727 C25.0953488,24.65 21.5651163,27.0454545 16.8837209,27.0454545 C11.3581395,27.0454545 6.90697674,22.5636364 6.90697674,17 C6.90697674,11.4363636 11.3581395,6.95454545 16.8837209,6.95454545 C19.2627907,6.95454545 21.4116279,7.80454545 23.1,9.19545455 L28.0116279,4.25 C25.0186047,1.62272727 21.1813953,0 16.8837209,0 C7.52093023,0 0,7.57272727 0,17 C0,26.4272727 7.52093023,34 16.8837209,34 C25.3255814,34 33,27.8181818 33,17 C33,15.9954545 32.8465116,14.9136364 32.6162791,13.9090909 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default GoogleLogo;
|
||||
@@ -5,7 +5,7 @@ import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||
import Modal from "components/Modal";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Facepile from "components/Facepile";
|
||||
import GroupMembers from "scenes/GroupMembers";
|
||||
import ListItem from "components/List/Item";
|
||||
|
||||
@@ -27,7 +27,7 @@ import styled from "styled-components";
|
||||
import { LabelText } from "components/Input";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export const icons = {
|
||||
collection: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import styled from "styled-components";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
const RealTextarea = styled.textarea`
|
||||
border: 0;
|
||||
|
||||
13
app/components/InputLarge.js
Normal file
13
app/components/InputLarge.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Input from "./Input";
|
||||
|
||||
const InputLarge = styled(Input)`
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default InputLarge;
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import keydown from "react-keydown";
|
||||
import Analytics from "components/Analytics";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import {
|
||||
homeUrl,
|
||||
searchUrl,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
image?: React.Node,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { times } from "lodash";
|
||||
import styled from "styled-components";
|
||||
import Mask from "components/Mask";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
count?: number,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { times } from "lodash";
|
||||
import styled from "styled-components";
|
||||
import Mask from "components/Mask";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
count?: number,
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Mask from "components/Mask";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export default function LoadingPlaceholder(props: Object) {
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { pulsate } from "shared/styles/animations";
|
||||
import { randomInteger } from "shared/random";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
header?: boolean,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { transparentize } from "polished";
|
||||
import { CloseIcon, BackIcon } from "outline-icons";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import { fadeAndScaleIn } from "shared/styles/animations";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
ReactModal.setAppElement("#root");
|
||||
|
||||
|
||||
12
app/components/Notice.js
Normal file
12
app/components/Notice.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Notice = styled.p`
|
||||
background: ${props => props.theme.sidebarBackground};
|
||||
color: ${props => props.theme.sidebarText};
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default Notice;
|
||||
24
app/components/NoticeAlert.js
Normal file
24
app/components/NoticeAlert.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Notice from "components/Notice";
|
||||
|
||||
export default function AlertNotice({ children }: { children: React.Node }) {
|
||||
return (
|
||||
<Notice muted>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: "relative", top: "2px" }}
|
||||
>
|
||||
<path
|
||||
d="M15.6676 11.5372L10.0155 1.14735C9.10744 -0.381434 6.89378 -0.383465 5.98447 1.14735L0.332715 11.5372C-0.595598 13.0994 0.528309 15.0776 2.34778 15.0776H13.652C15.47 15.0776 16.5959 13.101 15.6676 11.5372ZM8 13.2026C7.48319 13.2026 7.0625 12.7819 7.0625 12.2651C7.0625 11.7483 7.48319 11.3276 8 11.3276C8.51681 11.3276 8.9375 11.7483 8.9375 12.2651C8.9375 12.7819 8.51681 13.2026 8 13.2026ZM8.9375 9.45257C8.9375 9.96938 8.51681 10.3901 8 10.3901C7.48319 10.3901 7.0625 9.96938 7.0625 9.45257V4.76507C7.0625 4.24826 7.48319 3.82757 8 3.82757C8.51681 3.82757 8.9375 4.24826 8.9375 4.76507V9.45257Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>{" "}
|
||||
{children}
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
25
app/components/OutlineLogo.js
Normal file
25
app/components/OutlineLogo.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function OutlineLogo({ size = 32, fill = "#333", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 64 64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="M32,57.6 L32,59.1606101 C32,61.3697491 30.209139,63.1606101 28,63.1606101 C27.3130526,63.1606101 26.6376816,62.9836959 26.038955,62.6469122 L2.03895504,49.1469122 C0.779447116,48.438439 -4.3614532e-15,47.1057033 -7.10542736e-15,45.6606101 L-7.10542736e-15,18.3393899 C-7.28240024e-15,16.8942967 0.779447116,15.561561 2.03895504,14.8530878 L26.038955,1.35308779 C27.9643866,0.270032565 30.4032469,0.952913469 31.4863021,2.87834498 C31.8230858,3.47707155 32,4.15244252 32,4.83938994 L32,6.4 L34.8506085,5.54481746 C36.9665799,4.91002604 39.1965137,6.11075966 39.8313051,8.22673106 C39.9431692,8.59961116 40,8.98682435 40,9.3761226 L40,11 L43.5038611,10.5620174 C45.6959408,10.2880074 47.6951015,11.8429102 47.9691115,14.0349899 C47.9896839,14.1995692 48,14.3652688 48,14.5311289 L48,49.4688711 C48,51.6780101 46.209139,53.4688711 44,53.4688711 C43.8341399,53.4688711 43.6684404,53.458555 43.5038611,53.4379826 L40,53 L40,54.6238774 C40,56.8330164 38.209139,58.6238774 36,58.6238774 C35.6107017,58.6238774 35.2234886,58.5670466 34.8506085,58.4551825 L32,57.6 Z M32,53.4238774 L36,54.6238774 L36,9.3761226 L32,10.5761226 L32,53.4238774 Z M40,15.0311289 L40,48.9688711 L44,49.4688711 L44,14.5311289 L40,15.0311289 Z M5.32907052e-15,44.4688711 L5.32907052e-15,19.5311289 L3.55271368e-15,44.4688711 Z M4,18.3393899 L4,45.6606101 L28,59.1606101 L28,4.83938994 L4,18.3393899 Z M8,21 L12,19 L12,45 L8,43 L8,21 Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default OutlineLogo;
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import styled from "styled-components";
|
||||
import { GoToIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import Document from "models/Document";
|
||||
import Collection from "models/Collection";
|
||||
|
||||
@@ -3,9 +3,9 @@ import * as React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Time from "shared/components/Time";
|
||||
import Breadcrumb from "shared/components/Breadcrumb";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import Breadcrumb from "components/Breadcrumb";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
PlusIcon,
|
||||
} from "outline-icons";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import Invite from "scenes/Invite";
|
||||
import AccountMenu from "menus/AccountMenu";
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import ZapierIcon from "./icons/Zapier";
|
||||
import SlackIcon from "./icons/Slack";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import Section from "./components/Section";
|
||||
@@ -54,7 +54,7 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
<HeaderBlock
|
||||
subheading={
|
||||
<ReturnToApp align="center">
|
||||
<BackIcon /> Return to App
|
||||
<BackIcon color="currentColor" /> Return to App
|
||||
</ReturnToApp>
|
||||
}
|
||||
teamName={team.name}
|
||||
|
||||
@@ -7,7 +7,7 @@ import breakpoint from "styled-components-breakpoint";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { CloseIcon, MenuIcon } from "outline-icons";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import UiStore from "stores/UiStore";
|
||||
|
||||
let firstRender = true;
|
||||
|
||||
@@ -11,7 +11,7 @@ import SidebarLink from "./SidebarLink";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import DropToImport from "components/DropToImport";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import keydown from "react-keydown";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import DropToImport from "components/DropToImport";
|
||||
import Fade from "components/Fade";
|
||||
import Collection from "models/Collection";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import { type NavigationNode } from "types";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Header = styled(Flex)`
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import * as React from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import TeamLogo from "shared/components/TeamLogo";
|
||||
import Flex from "components/Flex";
|
||||
import TeamLogo from "components/TeamLogo";
|
||||
|
||||
type Props = {
|
||||
teamName: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
const Section = styled(Flex)`
|
||||
position: relative;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { withRouter, NavLink } from "react-router-dom";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
to?: string | Object,
|
||||
|
||||
42
app/components/SlackLogo.js
Normal file
42
app/components/SlackLogo.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function SlackLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 34 34"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g stroke="none" strokeWidth="1" fillRule="evenodd">
|
||||
<g transform="translate(0.000000, 17.822581)">
|
||||
<path d="M7.23870968,3.61935484 C7.23870968,5.56612903 5.6483871,7.15645161 3.7016129,7.15645161 C1.75483871,7.15645161 0.164516129,5.56612903 0.164516129,3.61935484 C0.164516129,1.67258065 1.75483871,0.0822580645 3.7016129,0.0822580645 L7.23870968,0.0822580645 L7.23870968,3.61935484 Z" />
|
||||
<path d="M9.02096774,3.61935484 C9.02096774,1.67258065 10.6112903,0.0822580645 12.5580645,0.0822580645 C14.5048387,0.0822580645 16.0951613,1.67258065 16.0951613,3.61935484 L16.0951613,12.4758065 C16.0951613,14.4225806 14.5048387,16.0129032 12.5580645,16.0129032 C10.6112903,16.0129032 9.02096774,14.4225806 9.02096774,12.4758065 C9.02096774,12.4758065 9.02096774,3.61935484 9.02096774,3.61935484 Z" />
|
||||
</g>
|
||||
<g>
|
||||
<path d="M12.5580645,7.23870968 C10.6112903,7.23870968 9.02096774,5.6483871 9.02096774,3.7016129 C9.02096774,1.75483871 10.6112903,0.164516129 12.5580645,0.164516129 C14.5048387,0.164516129 16.0951613,1.75483871 16.0951613,3.7016129 L16.0951613,7.23870968 L12.5580645,7.23870968 Z" />
|
||||
<path d="M12.5580645,9.02096774 C14.5048387,9.02096774 16.0951613,10.6112903 16.0951613,12.5580645 C16.0951613,14.5048387 14.5048387,16.0951613 12.5580645,16.0951613 L3.7016129,16.0951613 C1.75483871,16.0951613 0.164516129,14.5048387 0.164516129,12.5580645 C0.164516129,10.6112903 1.75483871,9.02096774 3.7016129,9.02096774 C3.7016129,9.02096774 12.5580645,9.02096774 12.5580645,9.02096774 Z" />
|
||||
</g>
|
||||
<g transform="translate(17.822581, 0.000000)">
|
||||
<path d="M8.93870968,12.5580645 C8.93870968,10.6112903 10.5290323,9.02096774 12.4758065,9.02096774 C14.4225806,9.02096774 16.0129032,10.6112903 16.0129032,12.5580645 C16.0129032,14.5048387 14.4225806,16.0951613 12.4758065,16.0951613 L8.93870968,16.0951613 L8.93870968,12.5580645 Z" />
|
||||
<path d="M7.15645161,12.5580645 C7.15645161,14.5048387 5.56612903,16.0951613 3.61935484,16.0951613 C1.67258065,16.0951613 0.0822580645,14.5048387 0.0822580645,12.5580645 L0.0822580645,3.7016129 C0.0822580645,1.75483871 1.67258065,0.164516129 3.61935484,0.164516129 C5.56612903,0.164516129 7.15645161,1.75483871 7.15645161,3.7016129 L7.15645161,12.5580645 Z" />
|
||||
</g>
|
||||
<g transform="translate(17.822581, 17.822581)">
|
||||
<path d="M3.61935484,8.93870968 C5.56612903,8.93870968 7.15645161,10.5290323 7.15645161,12.4758065 C7.15645161,14.4225806 5.56612903,16.0129032 3.61935484,16.0129032 C1.67258065,16.0129032 0.0822580645,14.4225806 0.0822580645,12.4758065 L0.0822580645,8.93870968 L3.61935484,8.93870968 Z" />
|
||||
<path d="M3.61935484,7.15645161 C1.67258065,7.15645161 0.0822580645,5.56612903 0.0822580645,3.61935484 C0.0822580645,1.67258065 1.67258065,0.0822580645 3.61935484,0.0822580645 L12.4758065,0.0822580645 C14.4225806,0.0822580645 16.0129032,1.67258065 16.0129032,3.61935484 C16.0129032,5.56612903 14.4225806,7.15645161 12.4758065,7.15645161 L3.61935484,7.15645161 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SlackLogo;
|
||||
12
app/components/TeamLogo.js
Normal file
12
app/components/TeamLogo.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const TeamLogo = styled.img`
|
||||
width: auto;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
background: ${props => props.theme.background};
|
||||
border: 1px solid ${props => props.theme.divider};
|
||||
`;
|
||||
|
||||
export default TeamLogo;
|
||||
55
app/components/Time.js
Normal file
55
app/components/Time.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import format from "date-fns/format";
|
||||
|
||||
let callbacks = [];
|
||||
|
||||
// This is a shared timer that fires every minute, used for
|
||||
// updating all Time components across the page all at once.
|
||||
setInterval(() => {
|
||||
callbacks.forEach(cb => cb());
|
||||
}, 1000 * 60);
|
||||
|
||||
function eachMinute(fn) {
|
||||
callbacks.push(fn);
|
||||
|
||||
return () => {
|
||||
callbacks = callbacks.filter(cb => cb !== fn);
|
||||
};
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dateTime: string,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
class Time extends React.Component<Props> {
|
||||
removeEachMinuteCallback: () => void;
|
||||
|
||||
componentDidMount() {
|
||||
this.removeEachMinuteCallback = eachMinute(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.removeEachMinuteCallback();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tooltip
|
||||
tooltip={format(this.props.dateTime, "MMMM Do, YYYY h:mm a")}
|
||||
placement="bottom"
|
||||
>
|
||||
<time dateTime={this.props.dateTime}>
|
||||
{this.props.children || distanceInWordsToNow(this.props.dateTime)}
|
||||
</time>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Time;
|
||||
@@ -7,7 +7,7 @@ import { SunIcon, MoonIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
import UiStore from "stores/UiStore";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import Modal from "components/Modal";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route, Redirect } from "react-router-dom";
|
||||
import Home from "scenes/Home";
|
||||
import Login from "scenes/Login";
|
||||
import Dashboard from "scenes/Dashboard";
|
||||
import Starred from "scenes/Starred";
|
||||
import Drafts from "scenes/Drafts";
|
||||
@@ -38,7 +38,8 @@ const RedirectDocument = ({ match }: { match: Object }) => (
|
||||
export default function Routes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/" component={Login} />
|
||||
<Route exact path="/create" component={Login} />
|
||||
<Route exact path="/share/:shareId" component={KeyedDocument} />
|
||||
<Authenticated>
|
||||
<SocketProvider>
|
||||
|
||||
@@ -30,7 +30,7 @@ import HelpText from "components/HelpText";
|
||||
import DocumentList from "components/DocumentList";
|
||||
import Subheading from "components/Subheading";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import CollectionMembers from "scenes/CollectionMembers";
|
||||
import Tabs from "components/Tabs";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { homeUrl } from "utils/routeHelpers";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Collection from "models/Collection";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
|
||||
@@ -6,7 +6,7 @@ import Input from "components/Input";
|
||||
import InputRich from "components/InputRich";
|
||||
import Button from "components/Button";
|
||||
import Switch from "components/Switch";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import IconPicker from "components/IconPicker";
|
||||
import Collection from "models/Collection";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Collection from "models/Collection";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { inject, observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { debounce } from "lodash";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Input from "components/Input";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { debounce } from "lodash";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Input from "components/Input";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observable } from "mobx";
|
||||
import styled from "styled-components";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Subheading from "components/Subheading";
|
||||
import Button from "components/Button";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Time from "shared/components/Time";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import Badge from "components/Badge";
|
||||
import Button from "components/Button";
|
||||
import InputSelect from "components/InputSelect";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import Time from "shared/components/Time";
|
||||
import Time from "components/Time";
|
||||
import Avatar from "components/Avatar";
|
||||
import Button from "components/Button";
|
||||
import Badge from "components/Badge";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Input from "components/Input";
|
||||
import InputRich from "components/InputRich";
|
||||
import IconPicker, { icons } from "components/IconPicker";
|
||||
import HelpText from "components/HelpText";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import Collection from "models/Collection";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
const Container = styled(Flex)`
|
||||
position: relative;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { observer, inject } from "mobx-react";
|
||||
import { Prompt, Route, withRouter } from "react-router-dom";
|
||||
import type { Location, RouterHistory } from "react-router-dom";
|
||||
import keydown from "react-keydown";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import {
|
||||
collectionUrl,
|
||||
documentMoveUrl,
|
||||
@@ -29,9 +29,9 @@ import MarkAsViewed from "./MarkAsViewed";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import Branding from "shared/components/Branding";
|
||||
import Notice from "shared/components/Notice";
|
||||
import Time from "shared/components/Time";
|
||||
import Branding from "components/Branding";
|
||||
import Notice from "components/Notice";
|
||||
import Time from "components/Time";
|
||||
|
||||
import UiStore from "stores/UiStore";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
@@ -12,7 +12,7 @@ import Modal from "components/Modal";
|
||||
import Input from "components/Input";
|
||||
import Labeled from "components/Labeled";
|
||||
import PathToDocument from "components/PathToDocument";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import Document from "models/Document";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
|
||||
@@ -5,7 +5,7 @@ import Textarea from "react-autosize-textarea";
|
||||
import { observer } from "mobx-react";
|
||||
import Editor from "components/Editor";
|
||||
import ClickablePadding from "components/ClickablePadding";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import Document from "models/Document";
|
||||
import DocumentMeta from "./DocumentMeta";
|
||||
|
||||
@@ -13,8 +13,8 @@ import AuthStore from "stores/AuthStore";
|
||||
import { documentEditUrl } from "utils/routeHelpers";
|
||||
import { meta } from "utils/keyboard";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Breadcrumb, { Slash } from "shared/components/Breadcrumb";
|
||||
import Flex from "components/Flex";
|
||||
import Breadcrumb, { Slash } from "components/Breadcrumb";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
import NewChildDocumentMenu from "menus/NewChildDocumentMenu";
|
||||
import DocumentShare from "scenes/DocumentShare";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Document from "models/Document";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import { inject } from "mobx-react";
|
||||
import type { RouterHistory, Location } from "react-router-dom";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import LoadingPlaceholder from "components/LoadingPlaceholder";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { groupSettings } from "shared/utils/routeHelpers";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Group from "models/Group";
|
||||
import UiStore from "stores/UiStore";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { inject, observer } from "mobx-react";
|
||||
import Button from "components/Button";
|
||||
import Input from "components/Input";
|
||||
import HelpText from "components/HelpText";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import Group from "models/Group";
|
||||
import UiStore from "stores/UiStore";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { debounce } from "lodash";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Input from "components/Input";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Empty from "components/Empty";
|
||||
import HelpText from "components/HelpText";
|
||||
import Subheading from "components/Subheading";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Time from "shared/components/Time";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import Badge from "components/Badge";
|
||||
import Button from "components/Button";
|
||||
import ListItem from "components/List/Item";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import Time from "shared/components/Time";
|
||||
import Time from "components/Time";
|
||||
import Avatar from "components/Avatar";
|
||||
import Button from "components/Button";
|
||||
import Badge from "components/Badge";
|
||||
|
||||
@@ -8,7 +8,7 @@ import Input from "components/Input";
|
||||
import HelpText from "components/HelpText";
|
||||
import Modal from "components/Modal";
|
||||
import GroupMembers from "scenes/GroupMembers";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import Group from "models/Group";
|
||||
import GroupsStore from "stores/GroupsStore";
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
const Home = observer(({ auth }: Props) => {
|
||||
if (auth.authenticated) return <Redirect to="/home" />;
|
||||
auth.logout(true);
|
||||
return null;
|
||||
});
|
||||
|
||||
export default inject("auth")(Home);
|
||||
@@ -3,13 +3,12 @@ import * as React from "react";
|
||||
import { Link, withRouter, type RouterHistory } from "react-router-dom";
|
||||
import { observable, action } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { CloseIcon } from "outline-icons";
|
||||
import { LinkIcon, CloseIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Button from "components/Button";
|
||||
import Input from "components/Input";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import Checkbox from "components/Checkbox";
|
||||
import HelpText from "components/HelpText";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import NudeButton from "components/NudeButton";
|
||||
@@ -33,7 +32,6 @@ type Props = {
|
||||
type InviteRequest = {
|
||||
email: string,
|
||||
name: string,
|
||||
guest: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -42,9 +40,9 @@ class Invite extends React.Component<Props> {
|
||||
@observable linkCopied: boolean = false;
|
||||
@observable
|
||||
invites: InviteRequest[] = [
|
||||
{ email: "", name: "", guest: false },
|
||||
{ email: "", name: "", guest: false },
|
||||
{ email: "", name: "", guest: false },
|
||||
{ email: "", name: "" },
|
||||
{ email: "", name: "" },
|
||||
{ email: "", name: "" },
|
||||
];
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<>) => {
|
||||
@@ -80,7 +78,7 @@ class Invite extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
this.invites.push({ email: "", name: "", guest: false });
|
||||
this.invites.push({ email: "", name: "" });
|
||||
};
|
||||
|
||||
@action
|
||||
@@ -106,8 +104,8 @@ class Invite extends React.Component<Props> {
|
||||
{team.guestSignin ? (
|
||||
<HelpText>
|
||||
Invite team members or guests to join your knowledge base. Team
|
||||
members can sign in with {team.signinMethods} and guests can use
|
||||
their email address.
|
||||
members can sign in with {team.signinMethods} or use their email
|
||||
address.
|
||||
</HelpText>
|
||||
) : (
|
||||
<HelpText>
|
||||
@@ -116,22 +114,35 @@ class Invite extends React.Component<Props> {
|
||||
{can.update && (
|
||||
<React.Fragment>
|
||||
As an admin you can also{" "}
|
||||
<Link to="/settings/security">enable guest invites</Link>.
|
||||
<Link to="/settings/security">enable email sign-in</Link>.
|
||||
</React.Fragment>
|
||||
)}
|
||||
</HelpText>
|
||||
)}
|
||||
{team.subdomain && (
|
||||
<CopyBlock>
|
||||
Want a link to share directly with your team?
|
||||
<Flex>
|
||||
<Input type="text" value={team.url} readOnly flex />
|
||||
<Flex align="flex-end">
|
||||
<Input
|
||||
type="text"
|
||||
value={team.url}
|
||||
label="Want a link to share directly with your team?"
|
||||
readOnly
|
||||
flex
|
||||
/>
|
||||
<CopyToClipboard text={team.url} onCopy={this.handleCopy}>
|
||||
<Button type="button" neutral>
|
||||
<Button
|
||||
type="button"
|
||||
icon={<LinkIcon />}
|
||||
style={{ marginBottom: "16px" }}
|
||||
neutral
|
||||
>
|
||||
{this.linkCopied ? "Link copied" : "Copy link"}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Flex>
|
||||
<p>
|
||||
<hr />
|
||||
</p>
|
||||
</CopyBlock>
|
||||
)}
|
||||
{this.invites.map((invite, index) => (
|
||||
@@ -159,29 +170,6 @@ class Invite extends React.Component<Props> {
|
||||
required={!!invite.email}
|
||||
flex
|
||||
/>
|
||||
{team.guestSignin && (
|
||||
<React.Fragment>
|
||||
|
||||
<Tooltip
|
||||
tooltip={
|
||||
<span>
|
||||
Guests can sign in with email and <br />do not require{" "}
|
||||
{team.signinMethods} accounts
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Guest>
|
||||
<Checkbox
|
||||
name="guest"
|
||||
label="Guest"
|
||||
onChange={ev => this.handleGuestChange(ev, index)}
|
||||
checked={invite.guest}
|
||||
/>
|
||||
</Guest>
|
||||
</Tooltip>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<Remove>
|
||||
<Tooltip tooltip="Remove invite" placement="top">
|
||||
@@ -220,22 +208,8 @@ class Invite extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const CopyBlock = styled("div")`
|
||||
margin: 2em 0;
|
||||
font-size: 14px;
|
||||
background: ${props => props.theme.secondaryBackground};
|
||||
padding: 8px 16px 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
input {
|
||||
background: ${props => props.theme.background};
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Guest = styled("div")`
|
||||
padding-top: 4px;
|
||||
margin: 0 4px 16px;
|
||||
align-self: flex-end;
|
||||
`;
|
||||
|
||||
const Remove = styled("div")`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Key from "components/Key";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import { meta } from "utils/keyboard";
|
||||
|
||||
|
||||
56
app/scenes/Login/Notices.js
Normal file
56
app/scenes/Login/Notices.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import NoticeAlert from "components/NoticeAlert";
|
||||
|
||||
type Props = {
|
||||
notice?: string,
|
||||
};
|
||||
|
||||
export default function Notices({ notice }: Props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{notice === "google-hd" && (
|
||||
<NoticeAlert>
|
||||
Sorry, Google sign in cannot be used with a personal email. Please try
|
||||
signing in with your company GSuite account.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "hd-not-allowed" && (
|
||||
<NoticeAlert>
|
||||
Sorry, your Google apps domain is not allowed. Please try again with
|
||||
an allowed company domain.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "email-auth-required" && (
|
||||
<NoticeAlert>
|
||||
Your account uses email sign-in, please sign-in with email to
|
||||
continue.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "email-auth-ratelimit" && (
|
||||
<NoticeAlert>
|
||||
An email sign-in link was recently sent, please check your inbox or
|
||||
try again in a few minutes.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "auth-error" && (
|
||||
<NoticeAlert>
|
||||
Authentication failed - we were unable to sign you in at this time.
|
||||
Please try again.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "expired-token" && (
|
||||
<NoticeAlert>
|
||||
Sorry, it looks like that sign-in link is no longer valid, please try
|
||||
requesting another.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "suspended" && (
|
||||
<NoticeAlert>
|
||||
Your Outline account has been suspended. To re-activate your account,
|
||||
please contact a team admin.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
147
app/scenes/Login/Service.js
Normal file
147
app/scenes/Login/Service.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { EmailIcon } from "outline-icons";
|
||||
import { client } from "utils/ApiClient";
|
||||
import ButtonLarge from "components/ButtonLarge";
|
||||
import SlackLogo from "components/SlackLogo";
|
||||
import GoogleLogo from "components/GoogleLogo";
|
||||
import InputLarge from "components/InputLarge";
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
name: string,
|
||||
authUrl: string,
|
||||
isCreate: boolean,
|
||||
onEmailSuccess: (email: string) => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
showEmailSignin: boolean,
|
||||
isSubmitting: boolean,
|
||||
email: string,
|
||||
};
|
||||
|
||||
class Service extends React.Component<Props, State> {
|
||||
state = {
|
||||
showEmailSignin: false,
|
||||
isSubmitting: false,
|
||||
email: "",
|
||||
};
|
||||
|
||||
handleChangeEmail = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
this.setState({ email: event.target.value });
|
||||
};
|
||||
|
||||
handleSubmitEmail = async (event: SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.showEmailSignin && this.state.email) {
|
||||
this.setState({ isSubmitting: true });
|
||||
|
||||
try {
|
||||
const response = await client.post(event.currentTarget.action, {
|
||||
email: this.state.email,
|
||||
});
|
||||
if (response.redirect) {
|
||||
window.location.href = response.redirect;
|
||||
} else {
|
||||
this.props.onEmailSuccess(this.state.email);
|
||||
}
|
||||
} finally {
|
||||
this.setState({ isSubmitting: false });
|
||||
}
|
||||
} else {
|
||||
this.setState({ showEmailSignin: true });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isCreate, id, name, authUrl } = this.props;
|
||||
|
||||
if (id === "email") {
|
||||
if (isCreate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper key="email">
|
||||
<Form
|
||||
method="POST"
|
||||
action="/auth/email"
|
||||
onSubmit={this.handleSubmitEmail}
|
||||
>
|
||||
{this.state.showEmailSignin ? (
|
||||
<React.Fragment>
|
||||
<InputLarge
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="me@domain.com"
|
||||
value={this.state.email}
|
||||
onChange={this.handleChangeEmail}
|
||||
disabled={this.state.isSubmitting}
|
||||
autoFocus
|
||||
required
|
||||
short
|
||||
/>
|
||||
<ButtonLarge type="submit" disabled={this.state.isSubmitting}>
|
||||
Sign In →
|
||||
</ButtonLarge>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<ButtonLarge type="submit" icon={<EmailIcon />} fullwidth>
|
||||
Continue with Email
|
||||
</ButtonLarge>
|
||||
)}
|
||||
</Form>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const icon =
|
||||
id === "slack" ? (
|
||||
<Logo>
|
||||
<SlackLogo size={16} />
|
||||
</Logo>
|
||||
) : id === "google" ? (
|
||||
<Logo>
|
||||
<GoogleLogo size={16} />
|
||||
</Logo>
|
||||
) : (
|
||||
undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper key={id}>
|
||||
<ButtonLarge
|
||||
onClick={() => (window.location.href = authUrl)}
|
||||
icon={icon}
|
||||
fullwidth
|
||||
>
|
||||
{isCreate ? "Sign up" : "Continue"} with {name}
|
||||
</ButtonLarge>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Logo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Form = styled.form`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export default Service;
|
||||
230
app/scenes/Login/index.js
Normal file
230
app/scenes/Login/index.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { BackIcon, EmailIcon } from "outline-icons";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { find } from "lodash";
|
||||
import Flex from "components/Flex";
|
||||
import TeamLogo from "components/TeamLogo";
|
||||
import OutlineLogo from "components/OutlineLogo";
|
||||
import Heading from "components/Heading";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import ButtonLarge from "components/ButtonLarge";
|
||||
import HelpText from "components/HelpText";
|
||||
import Fade from "components/Fade";
|
||||
import Service from "./Service";
|
||||
import Notices from "./Notices";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import getQueryVariable from "shared/utils/getQueryVariable";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
location: Object,
|
||||
};
|
||||
|
||||
type State = {
|
||||
emailLinkSentTo: string,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Login extends React.Component<Props, State> {
|
||||
state = {
|
||||
emailLinkSentTo: "",
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ emailLinkSentTo: "" });
|
||||
};
|
||||
|
||||
handleEmailSuccess = email => {
|
||||
this.setState({ emailLinkSentTo: email });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { auth, location } = this.props;
|
||||
const { config } = auth;
|
||||
const isCreate = location.pathname === "/create";
|
||||
|
||||
if (auth.authenticated) {
|
||||
return <Redirect to="/home" />;
|
||||
}
|
||||
|
||||
// we're counting on the config request being fast
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasMultipleServices = config.services.length > 1;
|
||||
const defaultService = find(
|
||||
config.services,
|
||||
service => service.id === auth.lastSignedIn
|
||||
);
|
||||
|
||||
const header =
|
||||
process.env.DEPLOYMENT === "hosted" &&
|
||||
(config.hostname ? (
|
||||
<Back href={process.env.URL}>
|
||||
<BackIcon color="currentColor" /> Back to home
|
||||
</Back>
|
||||
) : (
|
||||
<Back href="https://www.getoutline.com">
|
||||
<BackIcon color="currentColor" /> Back to website
|
||||
</Back>
|
||||
));
|
||||
|
||||
if (this.state.emailLinkSentTo) {
|
||||
return (
|
||||
<Background>
|
||||
{header}
|
||||
<Centered align="center" justify="center" column auto>
|
||||
<PageTitle title="Check your email" />
|
||||
<CheckEmailIcon size={38} color="currentColor" />
|
||||
|
||||
<Heading>Check your email</Heading>
|
||||
<Note>
|
||||
A magic sign-in link has been sent to the email{" "}
|
||||
<em>{this.state.emailLinkSentTo}</em>, no password needed.
|
||||
</Note>
|
||||
<br />
|
||||
<ButtonLarge onClick={this.handleReset} fullwidth neutral>
|
||||
Back to login
|
||||
</ButtonLarge>
|
||||
</Centered>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Background>
|
||||
{header}
|
||||
<Centered align="center" justify="center" column auto>
|
||||
<PageTitle title="Login" />
|
||||
<Logo>
|
||||
{process.env.TEAM_LOGO && process.env.DEPLOYMENT !== "hosted" ? (
|
||||
<TeamLogo src={process.env.TEAM_LOGO} />
|
||||
) : (
|
||||
<OutlineLogo size={38} fill="currentColor" />
|
||||
)}
|
||||
</Logo>
|
||||
|
||||
{isCreate ? (
|
||||
<Heading>Create an account</Heading>
|
||||
) : (
|
||||
<Heading>Login to {config.name || "Outline"}</Heading>
|
||||
)}
|
||||
|
||||
<Notices notice={getQueryVariable("notice")} />
|
||||
|
||||
{defaultService && (
|
||||
<React.Fragment key={defaultService.id}>
|
||||
<Service
|
||||
isCreate={isCreate}
|
||||
onEmailSuccess={this.handleEmailSuccess}
|
||||
{...defaultService}
|
||||
/>
|
||||
{hasMultipleServices && (
|
||||
<React.Fragment>
|
||||
<Note>
|
||||
You signed in with {defaultService.name} last time.
|
||||
</Note>
|
||||
<Or />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{config.services.map(service => {
|
||||
if (service.id === auth.lastSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Service
|
||||
key={service.id}
|
||||
isCreate={isCreate}
|
||||
onEmailSuccess={this.handleEmailSuccess}
|
||||
{...service}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Centered>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CheckEmailIcon = styled(EmailIcon)`
|
||||
margin-bottom: -1.5em;
|
||||
`;
|
||||
|
||||
const Background = styled(Fade)`
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: ${props => props.theme.background};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Logo = styled.div`
|
||||
margin-bottom: -1.5em;
|
||||
height: 38px;
|
||||
`;
|
||||
|
||||
const Note = styled(HelpText)`
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
const Back = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
padding: 32px;
|
||||
font-weight: 500;
|
||||
position: absolute;
|
||||
|
||||
svg {
|
||||
transition: transform 100ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Or = styled.hr`
|
||||
margin: 1em 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&:after {
|
||||
content: "Or";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
color: ${props => props.theme.textSecondary};
|
||||
background: ${props => props.theme.background};
|
||||
border-radius: 2px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Centered = styled(Flex)`
|
||||
user-select: none;
|
||||
width: 90vw;
|
||||
height: 100%;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
export default inject("auth")(Login);
|
||||
@@ -19,7 +19,7 @@ import UsersStore from "stores/UsersStore";
|
||||
import { newDocumentUrl, searchUrl } from "utils/routeHelpers";
|
||||
import { meta } from "utils/keyboard";
|
||||
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Button from "components/Button";
|
||||
import Empty from "components/Empty";
|
||||
import Fade from "components/Fade";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { CheckmarkIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
import HelpText from "components/HelpText";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
onChange: string => void,
|
||||
|
||||
@@ -12,7 +12,7 @@ import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import HelpText from "components/HelpText";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
|
||||
@@ -9,7 +9,7 @@ import HelpText from "components/HelpText";
|
||||
import Input from "components/Input";
|
||||
import Subheading from "components/Subheading";
|
||||
import NotificationListItem from "./components/NotificationListItem";
|
||||
import Notice from "shared/components/Notice";
|
||||
import Notice from "components/Notice";
|
||||
|
||||
import UiStore from "stores/UiStore";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
@@ -12,7 +12,7 @@ import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import UserDelete from "scenes/UserDelete";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
|
||||
@@ -60,8 +60,6 @@ class Security extends React.Component<Props> {
|
||||
}, 500);
|
||||
|
||||
render() {
|
||||
const { team } = this.props.auth;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Security" />
|
||||
@@ -72,27 +70,25 @@ class Security extends React.Component<Props> {
|
||||
</HelpText>
|
||||
|
||||
<Checkbox
|
||||
label="Allow guest invites"
|
||||
label="Allow email authentication"
|
||||
name="guestSignin"
|
||||
checked={this.guestSignin}
|
||||
onChange={this.handleChange}
|
||||
note={`When enabled guests can be invited by email address and are able to signin without ${
|
||||
team ? team.signinMethods : "SSO"
|
||||
}`}
|
||||
note="When enabled, users can sign-in using their email address"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Public document sharing"
|
||||
name="sharing"
|
||||
checked={this.sharing}
|
||||
onChange={this.handleChange}
|
||||
note="When enabled documents can be shared publicly by any team member"
|
||||
note="When enabled, documents can be shared publicly on the internet by any team member"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Rich service embeds"
|
||||
name="documentEmbeds"
|
||||
checked={this.documentEmbeds}
|
||||
onChange={this.handleChange}
|
||||
note="Convert links to supported services into rich embeds within your documents"
|
||||
note="Links to supported services are shown as rich embeds within your documents"
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import SlackButton from "./components/SlackButton";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import IntegrationsStore from "stores/IntegrationsStore";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import Notice from "shared/components/Notice";
|
||||
import Notice from "components/Notice";
|
||||
import getQueryVariable from "shared/utils/getQueryVariable";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { capitalize } from "lodash";
|
||||
import styled from "styled-components";
|
||||
import Time from "shared/components/Time";
|
||||
import Time from "components/Time";
|
||||
import ListItem from "components/List/Item";
|
||||
import Avatar from "components/Avatar";
|
||||
import Event from "models/Event";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer, inject } from "mobx-react";
|
||||
import styled from "styled-components";
|
||||
import Dropzone from "react-dropzone";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/Button";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import ShareMenu from "menus/ShareMenu";
|
||||
import ListItem from "components/List/Item";
|
||||
import Time from "shared/components/Time";
|
||||
import Time from "components/Time";
|
||||
import Share from "models/Share";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { slackAuth } from "shared/utils/routeHelpers";
|
||||
import SlackLogo from "shared/components/SlackLogo";
|
||||
import SlackLogo from "components/SlackLogo";
|
||||
import Button from "components/Button";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import Avatar from "components/Avatar";
|
||||
import Badge from "components/Badge";
|
||||
import UserProfile from "scenes/UserProfile";
|
||||
import ListItem from "components/List/Item";
|
||||
import Time from "shared/components/Time";
|
||||
import Time from "components/Time";
|
||||
import User from "models/User";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Button from "components/Button";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Modal from "components/Modal";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
@@ -5,7 +5,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import { EditIcon } from "outline-icons";
|
||||
import Flex from "shared/components/Flex";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/Button";
|
||||
|
||||
@@ -10,13 +10,27 @@ import Team from "models/Team";
|
||||
|
||||
const AUTH_STORE = "AUTH_STORE";
|
||||
|
||||
type Service = {
|
||||
id: string,
|
||||
name: string,
|
||||
authUrl: string,
|
||||
};
|
||||
|
||||
type Config = {
|
||||
name?: string,
|
||||
hostname?: string,
|
||||
services: Service[],
|
||||
};
|
||||
|
||||
export default class AuthStore {
|
||||
@observable user: ?User;
|
||||
@observable team: ?Team;
|
||||
@observable token: ?string;
|
||||
@observable lastSignedIn: ?string;
|
||||
@observable isSaving: boolean = false;
|
||||
@observable isSuspended: boolean = false;
|
||||
@observable suspendedContactEmail: ?string;
|
||||
@observable config: ?Config;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
@@ -32,8 +46,12 @@ export default class AuthStore {
|
||||
this.user = new User(data.user);
|
||||
this.team = new Team(data.team);
|
||||
this.token = getCookie("accessToken");
|
||||
this.lastSignedIn = getCookie("lastSignedIn");
|
||||
setImmediate(() => this.fetchConfig());
|
||||
|
||||
if (this.token) setImmediate(() => this.fetch());
|
||||
if (this.token) {
|
||||
setImmediate(() => this.fetch());
|
||||
}
|
||||
|
||||
autorun(() => {
|
||||
try {
|
||||
@@ -63,6 +81,13 @@ export default class AuthStore {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
fetchConfig = async () => {
|
||||
const res = await client.post("/auth.config");
|
||||
invariant(res && res.data, "Config not available");
|
||||
this.config = res.data;
|
||||
};
|
||||
|
||||
@action
|
||||
fetch = async () => {
|
||||
try {
|
||||
@@ -158,10 +183,16 @@ export default class AuthStore {
|
||||
})
|
||||
);
|
||||
|
||||
this.token = null;
|
||||
|
||||
// if this logout was forced from an authenticated route then
|
||||
// save the current path so we can go back there once signed in
|
||||
if (savePath) {
|
||||
setCookie("postLoginRedirectPath", window.location.pathname);
|
||||
const pathName = window.location.pathname;
|
||||
|
||||
if (pathName !== "/" && pathName !== "/create") {
|
||||
setCookie("postLoginRedirectPath", pathName);
|
||||
}
|
||||
}
|
||||
|
||||
// remove authentication token itself
|
||||
@@ -178,8 +209,5 @@ export default class AuthStore {
|
||||
});
|
||||
this.team = null;
|
||||
}
|
||||
|
||||
// add a timestamp to force reload from server
|
||||
window.location.href = `${BASE_URL}?done=${new Date().getTime()}`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class ApiClient {
|
||||
) => {
|
||||
let body;
|
||||
let modifiedPath;
|
||||
let urlToFetch;
|
||||
|
||||
if (method === "GET") {
|
||||
if (data) {
|
||||
@@ -45,6 +46,12 @@ class ApiClient {
|
||||
body = data ? JSON.stringify(data) : undefined;
|
||||
}
|
||||
|
||||
if (path.match(/^http/)) {
|
||||
urlToFetch = modifiedPath || path;
|
||||
} else {
|
||||
urlToFetch = this.baseUrl + (modifiedPath || path);
|
||||
}
|
||||
|
||||
// Construct headers
|
||||
const headers = new Headers({
|
||||
Accept: "application/json",
|
||||
@@ -60,7 +67,7 @@ class ApiClient {
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(this.baseUrl + (modifiedPath || path), {
|
||||
response = await fetch(urlToFetch, {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
|
||||
Reference in New Issue
Block a user