Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Moor 0cb82f9349 attempt 2 – set explicit charset tag 2021-01-15 18:36:24 -08:00
Tom Moor 7b7c1c0bc2 possible fix 2021-01-15 18:24:15 -08:00
143 changed files with 1261 additions and 3003 deletions
-7
View File
@@ -10,14 +10,9 @@ DATABASE_URL=postgres://user:pass@localhost:5532/outline
DATABASE_URL_TEST=postgres://user:pass@localhost:5532/outline-test
REDIS_URL=redis://localhost:6479
# Must point to the publicly accessible URL for the installation
URL=http://localhost:3000
PORT=3000
# Optional. If using a Cloudfront distribution or similar the origin server
# should be set to the same as URL.
CDN_URL=
# enforce (auto redirect to) https in production, (optional) default is true.
# set to false if your SSL is terminated at a loadbalancer, for example
FORCE_HTTPS=true
@@ -71,6 +66,4 @@ SMTP_REPLY_EMAIL=
# Custom logo that displays on the authentication screen, scaled to height: 60px
# TEAM_LOGO=https://example.com/images/logo.png
# See translate.getoutline.com for a list of available language codes and their
# percentage translated.
DEFAULT_LANGUAGE=en_US
-1
View File
@@ -18,7 +18,6 @@
[options]
emoji=true
sharedmemory.heap_size=3221225472
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=app
+1 -2
View File
@@ -1,7 +1,6 @@
{
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.validate.enable": false,
"typescript.format.enable": false,
"editor.formatOnSave": true,
"typescript.format.enable": false
}
+1 -1
View File
@@ -87,7 +87,7 @@ docker run --rm outlinewiki/outline:latest yarn sequelize:migrate
If you're running Outline by cloning this repository, run the following command to upgrade:
```
yarn run upgrade
yarn upgrade
```
## Development
+2 -4
View File
@@ -20,10 +20,8 @@ const Authenticated = ({ children }: Props) => {
// Watching for language changes here as this is the earliest point we have
// the user available and means we can start loading translations faster
React.useEffect(() => {
if (language && i18n.language !== language) {
// Languages are stored in en_US format in the database, however the
// frontend translation framework (i18next) expects en-US
i18n.changeLanguage(language.replace("_", "-"));
if (i18n.language !== language) {
i18n.changeLanguage(language);
}
}, [i18n, language]);
+2 -6
View File
@@ -3,17 +3,13 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import User from "models/User";
import placeholder from "./placeholder.png";
type Props = {|
type Props = {
src: string,
size: number,
icon?: React.Node,
user?: User,
onClick?: () => void,
className?: string,
|};
};
@observer
class Avatar extends React.Component<Props> {
+3 -15
View File
@@ -108,8 +108,8 @@ export const Inner = styled.span`
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`;
export type Props = {|
type?: "button" | "submit",
export type Props = {
type?: string,
value?: string,
icon?: React.Node,
iconColor?: string,
@@ -118,21 +118,9 @@ export type Props = {|
innerRef?: React.ElementRef<any>,
disclosure?: boolean,
neutral?: boolean,
danger?: boolean,
primary?: boolean,
disabled?: boolean,
fullwidth?: boolean,
autoFocus?: boolean,
style?: Object,
as?: React.ComponentType<any>,
to?: string,
onClick?: (event: SyntheticEvent<>) => mixed,
borderOnHover?: boolean,
"data-on"?: string,
"data-event-category"?: string,
"data-event-action"?: string,
|};
};
function Button({
type = "text",
-23
View File
@@ -1,23 +0,0 @@
// @flow
import * as React from "react";
import styled from "styled-components";
type Props = {
onClick: (ev: SyntheticEvent<>) => void,
children: React.Node,
};
export default function ButtonLink(props: Props) {
return <Button {...props} />;
}
const Button = styled.button`
margin: 0;
padding: 0;
border: 0;
color: ${(props) => props.theme.link};
line-height: inherit;
background: none;
text-decoration: none;
cursor: pointer;
`;
+3 -6
View File
@@ -1,21 +1,18 @@
// @flow
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import HelpText from "components/HelpText";
import VisuallyHidden from "components/VisuallyHidden";
export type Props = {|
export type Props = {
checked?: boolean,
label?: string,
labelHidden?: boolean,
className?: string,
name?: string,
disabled?: boolean,
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
note?: string,
short?: boolean,
small?: boolean,
|};
};
const LabelText = styled.span`
font-weight: 500;
+2 -5
View File
@@ -4,16 +4,13 @@ import * as React from "react";
import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled from "styled-components";
type Props = {|
type Props = {
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
to?: string,
href?: string,
target?: "_blank",
as?: string | React.ComponentType<*>,
|};
};
const MenuItem = ({
onClick,
@@ -156,7 +156,7 @@ const Wrapper = styled(Flex)`
top: 0;
right: 0;
z-index: 1;
min-width: ${(props) => props.theme.sidebarWidth}px;
min-width: ${(props) => props.theme.sidebarWidth};
height: 100%;
overflow-y: auto;
overscroll-behavior: none;
@@ -165,7 +165,7 @@ const Wrapper = styled(Flex)`
const Sidebar = styled(Flex)`
display: none;
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth}px;
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
+2 -7
View File
@@ -4,15 +4,10 @@ import * as React from "react";
import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem";
type Props = {|
type Props = {
documents: Document[],
limit?: number,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
};
export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents;
-3
View File
@@ -23,7 +23,6 @@ type Props = {|
document: Document,
highlight?: ?string,
context?: ?string,
showNestedDocuments?: boolean,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
@@ -45,7 +44,6 @@ function DocumentListItem(props: Props) {
const [menuOpen, setMenuOpen] = React.useState(false);
const {
document,
showNestedDocuments,
showCollection,
showPublished,
showPin,
@@ -106,7 +104,6 @@ function DocumentListItem(props: Props) {
document={document}
showCollection={showCollection}
showPublished={showPublished}
showNestedDocuments={showNestedDocuments}
showLastViewed
/>
</Content>
+2 -14
View File
@@ -23,21 +23,19 @@ const Modified = styled.span`
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
type Props = {|
type Props = {
showCollection?: boolean,
showPublished?: boolean,
showLastViewed?: boolean,
showNestedDocuments?: boolean,
document: Document,
children: React.Node,
to?: string,
|};
};
function DocumentMeta({
showPublished,
showCollection,
showLastViewed,
showNestedDocuments,
document,
children,
to,
@@ -125,10 +123,6 @@ function DocumentMeta({
);
};
const nestedDocumentsCount = collection
? collection.getDocumentChildren(document.id).length
: 0;
return (
<Container align="center" {...rest}>
{updatedByMe ? t("You") : updatedBy.name}&nbsp;
@@ -141,12 +135,6 @@ function DocumentMeta({
</strong>
</span>
)}
{showNestedDocuments && nestedDocumentsCount > 0 && (
<span>
&nbsp;&middot; {nestedDocumentsCount}{" "}
{t("nested document", { count: nestedDocumentsCount })}
</span>
)}
&nbsp;{timeSinceNow()}
{children}
</Container>
+5 -22
View File
@@ -8,39 +8,22 @@ import UiStore from "stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import { isModKey } from "utils/keyboard";
import isInternalUrl from "utils/isInternalUrl";
import { isMetaKey } from "utils/keyboard";
import { uploadFile } from "utils/uploadFile";
import { isInternalUrl } from "utils/urls";
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
const EMPTY_ARRAY = [];
export type Props = {|
type Props = {
id?: string,
value?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
ui?: UiStore,
autoFocus?: boolean,
template?: boolean,
placeholder?: string,
scrollTo?: string,
readOnlyWriteCheckboxes?: boolean,
onBlur?: (event: SyntheticEvent<>) => any,
onFocus?: (event: SyntheticEvent<>) => any,
onPublish?: (event: SyntheticEvent<>) => any,
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
onCancel?: () => any,
onChange?: (getValue: () => string) => any,
onSearchLink?: (title: string) => any,
onHoverLink?: (event: MouseEvent) => any,
onCreateLink?: (title: string) => Promise<string>,
onImageUploadStart?: () => any,
onImageUploadStop?: () => any,
|};
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<any>,
@@ -67,7 +50,7 @@ function Editor(props: PropsWithRef) {
return;
}
if (isInternalUrl(href) && !isModKey(event) && !event.shiftKey) {
if (isInternalUrl(href) && !isMetaKey(event) && !event.shiftKey) {
// relative
let navigateTo = href;
+3 -4
View File
@@ -1,5 +1,4 @@
// @flow
import * as Sentry from "@sentry/react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
@@ -37,8 +36,8 @@ class ErrorBoundary extends React.Component<Props> {
return;
}
if (env.SENTRY_DSN) {
Sentry.captureException(error);
if (window.Sentry) {
window.Sentry.captureException(error);
}
}
@@ -57,7 +56,7 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const error = this.error;
const isReported = !!env.SENTRY_DSN && env.DEPLOYMENT === "hosted";
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
const isChunkError = this.error.message.match(/chunk/);
if (isChunkError) {
+1 -1
View File
@@ -8,7 +8,7 @@ import { fadeAndSlideIn } from "shared/styles/animations";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import { isInternalUrl } from "utils/urls";
import isInternalUrl from "utils/isInternalUrl";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
+2 -2
View File
@@ -145,8 +145,8 @@ function IconPicker({ onOpen, icon, color, onChange }: Props) {
</Label>
<MenuButton {...menu}>
{(props) => (
<Button aria-label={t("Show menu")} {...props}>
<Component color={color} size={30} />
<Button {...props}>
<Component role="button" color={color} size={30} />
</Button>
)}
</MenuButton>
-15
View File
@@ -1,15 +0,0 @@
// @flow
import * as React from "react";
import { cdnPath } from "utils/urls";
type Props = {|
alt: string,
src: string,
title?: string,
width?: number,
height?: number,
|};
export default function Image({ src, alt, ...rest }: Props) {
return <img src={cdnPath(src)} alt={alt} {...rest} />;
}
+4 -13
View File
@@ -2,9 +2,9 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import Flex from "components/Flex";
import VisuallyHidden from "components/VisuallyHidden";
const RealTextarea = styled.textarea`
border: 0;
@@ -75,8 +75,8 @@ export const LabelText = styled.div`
display: inline-block;
`;
export type Props = {|
type?: "text" | "email" | "checkbox" | "search",
export type Props = {
type?: string,
value?: string,
label?: string,
className?: string,
@@ -85,18 +85,9 @@ export type Props = {|
short?: boolean,
margin?: string | number,
icon?: React.Node,
name?: string,
minLength?: number,
maxLength?: number,
autoFocus?: boolean,
autoComplete?: boolean | string,
readOnly?: boolean,
required?: boolean,
placeholder?: string,
onChange?: (ev: SyntheticInputEvent<HTMLInputElement>) => mixed,
onFocus?: (ev: SyntheticEvent<>) => void,
onBlur?: (ev: SyntheticEvent<>) => void,
|};
};
@observer
class Input extends React.Component<Props> {
+2 -2
View File
@@ -8,13 +8,13 @@ import Editor from "components/Editor";
import HelpText from "components/HelpText";
import { LabelText, Outline } from "components/Input";
type Props = {|
type Props = {
label: string,
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
|};
};
@observer
class InputRich extends React.Component<Props> {
-4
View File
@@ -17,8 +17,6 @@ type Props = {
theme: Theme,
source: string,
placeholder?: string,
label?: string,
labelHidden?: boolean,
collectionId?: string,
t: TFunction,
};
@@ -70,8 +68,6 @@ class InputSearch extends React.Component<Props> {
color={this.focused ? theme.inputBorderFocused : theme.inputBorder}
/>
}
label={this.props.label}
labelHidden={this.props.labelHidden}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
margin={0}
+2 -5
View File
@@ -2,19 +2,18 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import VisuallyHidden from "components/VisuallyHidden";
import { Outline, LabelText } from "./Input";
const Select = styled.select`
border: 0;
flex: 1;
padding: 4px 0;
padding: 8px 0;
margin: 0 12px;
outline: none;
background: none;
color: ${(props) => props.theme.text};
height: 30px;
&:disabled,
&::placeholder {
@@ -36,8 +35,6 @@ export type Props = {
className?: string,
labelHidden?: boolean,
options: Option[],
onBlur?: () => void,
onFocus?: () => void,
};
@observer
+2 -2
View File
@@ -4,10 +4,10 @@ import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {|
type Props = {
label: React.Node | string,
children: React.Node,
|};
};
const Labeled = ({ label, children, ...props }: Props) => (
<Flex column {...props}>
+3 -14
View File
@@ -4,7 +4,6 @@ import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import { languages, languageOptions } from "shared/i18n";
import ButtonLink from "components/ButtonLink";
import Flex from "components/Flex";
import NoticeTip from "components/NoticeTip";
import useCurrentUser from "hooks/useCurrentUser";
@@ -69,7 +68,7 @@ export default function LanguagePrompt() {
like to change?
</Trans>
<br />
<Link
<a
onClick={() => {
auth.updateUser({
language,
@@ -78,24 +77,14 @@ export default function LanguagePrompt() {
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
</a>{" "}
&middot; <a onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</a>
</span>
</Flex>
</NoticeTip>
);
}
const Link = styled(ButtonLink)`
color: ${(props) => props.theme.almostBlack};
font-weight: 500;
&:hover {
text-decoration: underline;
}
`;
const LanguageIcon = styled(Icon)`
margin-right: 12px;
`;
+7 -45
View File
@@ -1,7 +1,6 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { MenuIcon } from "outline-icons";
import * as React from "react";
import { Helmet } from "react-helmet";
import { withTranslation, type TFunction } from "react-i18next";
@@ -15,15 +14,13 @@ import UiStore from "stores/UiStore";
import ErrorSuspended from "scenes/ErrorSuspended";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Analytics from "components/Analytics";
import Button from "components/Button";
import DocumentHistory from "components/DocumentHistory";
import Flex from "components/Flex";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import SkipNavContent from "components/SkipNavContent";
import SkipNavLink from "components/SkipNavLink";
import { type Theme } from "types";
import { meta } from "utils/keyboard";
import {
@@ -102,7 +99,6 @@ class Layout extends React.Component<Props> {
const { auth, t, ui } = this.props;
const { user, team } = auth;
const showSidebar = auth.authenticated && user && team;
const sidebarCollapsed = ui.editMode || ui.sidebarCollapsed;
if (auth.isSuspended) return <ErrorSuspended />;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
@@ -116,19 +112,11 @@ class Layout extends React.Component<Props> {
content="width=device-width, initial-scale=1.0"
/>
</Helmet>
<SkipNavLink />
<Analytics />
{this.props.ui.progressBarVisible && <LoadingIndicatorBar />}
{this.props.notifications}
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
iconColor="currentColor"
neutral
/>
<Container auto>
{showSidebar && (
<Switch>
@@ -137,17 +125,10 @@ class Layout extends React.Component<Props> {
</Switch>
)}
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
style={
sidebarCollapsed
? undefined
: { marginLeft: `${ui.sidebarWidth}px` }
}
sidebarCollapsed={ui.editMode || ui.sidebarCollapsed}
>
{this.props.children}
</Content>
@@ -179,38 +160,19 @@ const Container = styled(Flex)`
min-height: 100%;
`;
const MobileMenuButton = styled(Button)`
position: fixed;
top: 12px;
left: 12px;
z-index: ${(props) => props.theme.depths.sidebar - 1};
${breakpoint("tablet")`
display: none;
`};
@media print {
display: none;
}
`;
const Content = styled(Flex)`
margin: 0;
transition: ${(props) =>
props.$isResizing ? "none" : `margin-left 100ms ease-out`};
transition: margin-left 100ms ease-out;
@media print {
margin: 0;
}
${breakpoint("mobile", "tablet")`
margin-left: 0 !important;
`}
${breakpoint("tablet")`
${(props) =>
props.$sidebarCollapsed &&
`margin-left: ${props.theme.sidebarCollapsedWidth}px;`}
margin-left: ${(props) =>
props.sidebarCollapsed
? props.theme.sidebarCollapsedWidth
: props.theme.sidebarWidth};
`};
`;
-2
View File
@@ -10,10 +10,8 @@ const locales = {
de: require(`date-fns/locale/de`),
es: require(`date-fns/locale/es`),
fr: require(`date-fns/locale/fr`),
it: require(`date-fns/locale/it`),
ko: require(`date-fns/locale/ko`),
pt: require(`date-fns/locale/pt`),
zh: require(`date-fns/locale/zh_cn`),
};
let callbacks = [];
+3 -3
View File
@@ -5,10 +5,10 @@ import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex";
type Props = {|
type Props = {
header?: boolean,
height?: number,
|};
};
class Mask extends React.Component<Props> {
width: number;
@@ -23,7 +23,7 @@ class Mask extends React.Component<Props> {
}
render() {
return <Redacted width={this.width} />;
return <Redacted width={this.width} {...this.props} />;
}
}
+2 -2
View File
@@ -13,12 +13,12 @@ import Scrollable from "components/Scrollable";
ReactModal.setAppElement("#root");
type Props = {|
type Props = {
children?: React.Node,
isOpen: boolean,
title?: string,
onRequestClose: () => void,
|};
};
const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {
+9 -10
View File
@@ -1,17 +1,16 @@
// @flow
import { observer } from "mobx-react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import useStores from "hooks/useStores";
import { cdnPath } from "utils/urls";
import AuthStore from "stores/AuthStore";
type Props = {|
type Props = {
title: string,
favicon?: string,
|};
auth: AuthStore,
};
const PageTitle = ({ title, favicon }: Props) => {
const { auth } = useStores();
const PageTitle = observer(({ auth, title, favicon }: Props) => {
const { team } = auth;
return (
@@ -22,12 +21,12 @@ const PageTitle = ({ title, favicon }: Props) => {
<link
rel="shortcut icon"
type="image/png"
href={favicon || cdnPath("/favicon-32.png")}
href={favicon || "/favicon-32.png"}
sizes="32x32"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
);
};
});
export default observer(PageTitle);
export default inject("auth")(PageTitle);
+2 -8
View File
@@ -5,19 +5,13 @@ import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem";
import PaginatedList from "components/PaginatedList";
type Props = {|
type Props = {
documents: Document[],
fetch: (options: ?Object) => Promise<void>,
options?: Object,
heading?: React.Node,
empty?: React.Node,
showNestedDocuments?: boolean,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
};
@observer
class PaginatedDocumentList extends React.Component<Props> {
-12
View File
@@ -1,12 +0,0 @@
// @flow
import * as Sentry from "@sentry/react";
import { Route } from "react-router-dom";
import env from "env";
let Component = Route;
if (env.SENTRY_DSN) {
Component = Sentry.withSentryRouting(Route);
}
export default Component;
+19 -54
View File
@@ -1,52 +1,28 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import useWindowSize from "hooks/useWindowSize";
type Props = {|
type Props = {
shadow?: boolean,
topShadow?: boolean,
bottomShadow?: boolean,
|};
};
function Scrollable({ shadow, topShadow, bottomShadow, ...rest }: Props) {
const ref = React.useRef<?HTMLDivElement>();
const [topShadowVisible, setTopShadow] = React.useState(false);
const [bottomShadowVisible, setBottomShadow] = React.useState(false);
const { height } = useWindowSize();
@observer
class Scrollable extends React.Component<Props> {
@observable shadow: boolean = false;
const updateShadows = React.useCallback(() => {
const c = ref.current;
if (!c) return;
handleScroll = (ev: SyntheticMouseEvent<HTMLDivElement>) => {
this.shadow = !!(this.props.shadow && ev.currentTarget.scrollTop > 0);
};
const scrollTop = c.scrollTop;
const tsv = !!((shadow || topShadow) && scrollTop > 0);
if (tsv !== topShadowVisible) {
setTopShadow(tsv);
}
render() {
const { shadow, ...rest } = this.props;
const wrapperHeight = c.scrollHeight - c.clientHeight;
const bsv = !!((shadow || bottomShadow) && wrapperHeight - scrollTop !== 0);
if (bsv !== bottomShadowVisible) {
setBottomShadow(bsv);
}
}, [shadow, topShadow, bottomShadow, topShadowVisible, bottomShadowVisible]);
React.useEffect(() => {
updateShadows();
}, [height, updateShadows]);
return (
<Wrapper
ref={ref}
onScroll={updateShadows}
$topShadowVisible={topShadowVisible}
$bottomShadowVisible={bottomShadowVisible}
{...rest}
/>
);
return (
<Wrapper onScroll={this.handleScroll} shadow={this.shadow} {...rest} />
);
}
}
const Wrapper = styled.div`
@@ -55,20 +31,9 @@ const Wrapper = styled.div`
overflow-x: hidden;
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
box-shadow: ${(props) => {
if (props.$topShadowVisible && props.$bottomShadowVisible) {
return "0 1px inset rgba(0,0,0,.1), 0 -1px inset rgba(0,0,0,.1)";
}
if (props.$topShadowVisible) {
return "0 1px inset rgba(0,0,0,.1)";
}
if (props.$bottomShadowVisible) {
return "0 -1px inset rgba(0,0,0,.1)";
}
return "none";
}};
transition: all 100ms ease-in-out;
box-shadow: ${(props) =>
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
transition: all 250ms ease-in-out;
`;
export default observer(Scrollable);
export default Scrollable;
+161 -160
View File
@@ -1,5 +1,6 @@
// @flow
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import {
ArchiveIcon,
HomeIcon,
@@ -9,11 +10,14 @@ import {
ShapesIcon,
TrashIcon,
PlusIcon,
SettingsIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
import Flex from "components/Flex";
@@ -25,179 +29,176 @@ import Collections from "./components/Collections";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import useStores from "hooks/useStores";
import AccountMenu from "menus/AccountMenu";
function MainSidebar() {
const { t } = useTranslation();
const { policies, auth, documents } = useStores();
const [inviteModalOpen, setInviteModalOpen] = React.useState(false);
const [
createCollectionModalOpen,
setCreateCollectionModalOpen,
] = React.useState(false);
type Props = {
auth: AuthStore,
documents: DocumentsStore,
policies: PoliciesStore,
t: TFunction,
};
React.useEffect(() => {
documents.fetchDrafts();
documents.fetchTemplates();
}, [documents]);
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen = false;
@observable createCollectionModalOpen = false;
const handleCreateCollectionModalOpen = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
setCreateCollectionModalOpen(true);
},
[]
);
componentDidMount() {
this.props.documents.fetchDrafts();
this.props.documents.fetchTemplates();
}
const handleCreateCollectionModalClose = React.useCallback(() => {
setCreateCollectionModalOpen(false);
}, []);
const handleInviteModalOpen = React.useCallback((ev: SyntheticEvent<>) => {
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
setInviteModalOpen(true);
}, []);
this.createCollectionModalOpen = true;
};
const handleInviteModalClose = React.useCallback(() => {
setInviteModalOpen(false);
}, []);
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
this.createCollectionModalOpen = false;
};
const { user, team } = auth;
if (!user || !team) return null;
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.inviteModalOpen = true;
};
const can = policies.abilities(team.id);
handleInviteModalClose = () => {
this.inviteModalOpen = false;
};
return (
<Sidebar>
<AccountMenu>
{(props) => (
<HeaderBlock
{...props}
subheading={user.name}
teamName={team.name}
logoUrl={team.avatarUrl}
showDisclosure
/>
)}
</AccountMenu>
<Flex auto column>
<Scrollable shadow>
<Section>
<SidebarLink
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label={t("Home")}
render() {
const { auth, documents, policies, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
const can = policies.abilities(team.id);
return (
<Sidebar>
<AccountMenu>
{(props) => (
<HeaderBlock
{...props}
subheading={user.name}
teamName={team.name}
logoUrl={team.avatarUrl}
showDisclosure
/>
<SidebarLink
to={{
pathname: "/search",
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label={t("Templates")}
active={documents.active ? documents.active.template : undefined}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
</Drafts>
}
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>
</Section>
<Section>
<Collections onCreateCollection={handleCreateCollectionModalOpen} />
</Section>
</Scrollable>
<Secondary>
<Section>
<SidebarLink
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
: undefined
}
/>
<SidebarLink
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label={t("Trash")}
active={documents.active ? documents.active.isDeleted : undefined}
/>
<SidebarLink
to="/settings"
icon={<SettingsIcon color="currentColor" />}
exact={false}
label={t("Settings")}
/>
{can.invite && (
)}
</AccountMenu>
<Flex auto column>
<Scrollable shadow>
<Section>
<SidebarLink
to="/settings/people"
onClick={handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label={`${t("Invite people")}`}
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label={t("Home")}
/>
)}
</Section>
</Secondary>
</Flex>
<Modal
title={t("Invite people")}
onRequestClose={handleInviteModalClose}
isOpen={inviteModalOpen}
>
<Invite onSubmit={handleInviteModalClose} />
</Modal>
<Modal
title={t("Create a collection")}
onRequestClose={handleCreateCollectionModalClose}
isOpen={createCollectionModalOpen}
>
<CollectionNew onSubmit={handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
<SidebarLink
to={{
pathname: "/search",
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label={t("Templates")}
active={
documents.active ? documents.active.template : undefined
}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
</Drafts>
}
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>
</Section>
<Section>
<Collections
onCreateCollection={this.handleCreateCollectionModalOpen}
/>
</Section>
<Section>
<SidebarLink
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
: undefined
}
/>
<SidebarLink
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label={t("Trash")}
active={
documents.active ? documents.active.isDeleted : undefined
}
/>
{can.invite && (
<SidebarLink
to="/settings/people"
onClick={this.handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label={t("Invite people…")}
/>
)}
</Section>
</Scrollable>
</Flex>
<Modal
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title={t("Create a collection")}
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
}
}
const Secondary = styled.div`
overflow-x: hidden;
flex-shrink: 0;
`;
const Drafts = styled(Flex)`
height: 24px;
`;
export default observer(MainSidebar);
export default withTranslation()<MainSidebar>(
inject("documents", "policies", "auth")(MainSidebar)
);
+120 -108
View File
@@ -1,5 +1,5 @@
// @flow
import { observer } from "mobx-react";
import { observer, inject } from "mobx-react";
import {
DocumentIcon,
EmailIcon,
@@ -13,9 +13,11 @@ import {
ExpandedIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { withTranslation, type TFunction } from "react-i18next";
import type { RouterHistory } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import PoliciesStore from "stores/PoliciesStore";
import Flex from "components/Flex";
import Scrollable from "components/Scrollable";
@@ -28,123 +30,131 @@ import Version from "./components/Version";
import SlackIcon from "./icons/Slack";
import ZapierIcon from "./icons/Zapier";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
const isHosted = env.DEPLOYMENT === "hosted";
function SettingsSidebar() {
const { t } = useTranslation();
const history = useHistory();
const team = useCurrentTeam();
const { policies } = useStores();
const can = policies.abilities(team.id);
type Props = {
history: RouterHistory,
policies: PoliciesStore,
auth: AuthStore,
t: TFunction,
};
const returnToDashboard = React.useCallback(() => {
history.push("/home");
}, [history]);
@observer
class SettingsSidebar extends React.Component<Props> {
returnToDashboard = () => {
this.props.history.push("/home");
};
return (
<Sidebar>
<HeaderBlock
subheading={
<ReturnToApp align="center">
<BackIcon color="currentColor" /> {t("Return to App")}
</ReturnToApp>
}
teamName={team.name}
logoUrl={team.avatarUrl}
onClick={returnToDashboard}
/>
render() {
const { policies, t, auth } = this.props;
const { team } = auth;
if (!team) return null;
<Flex auto column>
<Scrollable topShadow>
<Section>
<Header>{t("Account")}</Header>
<SidebarLink
to="/settings"
icon={<ProfileIcon color="currentColor" />}
label={t("Profile")}
/>
<SidebarLink
to="/settings/notifications"
icon={<EmailIcon color="currentColor" />}
label={t("Notifications")}
/>
<SidebarLink
to="/settings/tokens"
icon={<CodeIcon color="currentColor" />}
label={t("API Tokens")}
/>
</Section>
<Section>
<Header>{t("Team")}</Header>
{can.update && (
<SidebarLink
to="/settings/details"
icon={<TeamIcon color="currentColor" />}
label={t("Details")}
/>
)}
{can.update && (
<SidebarLink
to="/settings/security"
icon={<PadlockIcon color="currentColor" />}
label={t("Security")}
/>
)}
<SidebarLink
to="/settings/people"
icon={<UserIcon color="currentColor" />}
exact={false}
label={t("People")}
/>
<SidebarLink
to="/settings/groups"
icon={<GroupIcon color="currentColor" />}
exact={false}
label={t("Groups")}
/>
<SidebarLink
to="/settings/shares"
icon={<LinkIcon color="currentColor" />}
label={t("Share Links")}
/>
{can.export && (
<SidebarLink
to="/settings/export"
icon={<DocumentIcon color="currentColor" />}
label={t("Export Data")}
/>
)}
</Section>
{can.update && (
const can = policies.abilities(team.id);
return (
<Sidebar>
<HeaderBlock
subheading={
<ReturnToApp align="center">
<BackIcon color="currentColor" /> {t("Return to App")}
</ReturnToApp>
}
teamName={team.name}
logoUrl={team.avatarUrl}
onClick={this.returnToDashboard}
/>
<Flex auto column>
<Scrollable shadow>
<Section>
<Header>{t("Integrations")}</Header>
<Header>Account</Header>
<SidebarLink
to="/settings/integrations/slack"
icon={<SlackIcon color="currentColor" />}
label="Slack"
to="/settings"
icon={<ProfileIcon color="currentColor" />}
label={t("Profile")}
/>
{isHosted && (
<SidebarLink
to="/settings/notifications"
icon={<EmailIcon color="currentColor" />}
label={t("Notifications")}
/>
<SidebarLink
to="/settings/tokens"
icon={<CodeIcon color="currentColor" />}
label={t("API Tokens")}
/>
</Section>
<Section>
<Header>Team</Header>
{can.update && (
<SidebarLink
to="/settings/integrations/zapier"
icon={<ZapierIcon color="currentColor" />}
label="Zapier"
to="/settings/details"
icon={<TeamIcon color="currentColor" />}
label={t("Details")}
/>
)}
{can.update && (
<SidebarLink
to="/settings/security"
icon={<PadlockIcon color="currentColor" />}
label={t("Security")}
/>
)}
<SidebarLink
to="/settings/people"
icon={<UserIcon color="currentColor" />}
exact={false}
label={t("People")}
/>
<SidebarLink
to="/settings/groups"
icon={<GroupIcon color="currentColor" />}
exact={false}
label={t("Groups")}
/>
<SidebarLink
to="/settings/shares"
icon={<LinkIcon color="currentColor" />}
label={t("Share Links")}
/>
{can.export && (
<SidebarLink
to="/settings/export"
icon={<DocumentIcon color="currentColor" />}
label={t("Export Data")}
/>
)}
</Section>
)}
{can.update && !isHosted && (
<Section>
<Header>{t("Installation")}</Header>
<Version />
</Section>
)}
</Scrollable>
</Flex>
</Sidebar>
);
{can.update && (
<Section>
<Header>{t("Integrations")}</Header>
<SidebarLink
to="/settings/integrations/slack"
icon={<SlackIcon color="currentColor" />}
label="Slack"
/>
{isHosted && (
<SidebarLink
to="/settings/integrations/zapier"
icon={<ZapierIcon color="currentColor" />}
label="Zapier"
/>
)}
</Section>
)}
{can.update && !isHosted && (
<Section>
<Header>{t("Installation")}</Header>
<Version />
</Section>
)}
</Scrollable>
</Flex>
</Sidebar>
);
}
}
const BackIcon = styled(ExpandedIcon)`
@@ -156,4 +166,6 @@ const ReturnToApp = styled(Flex)`
height: 16px;
`;
export default observer(SettingsSidebar);
export default withTranslation()<SettingsSidebar>(
inject("auth", "policies")(SettingsSidebar)
);
+63 -164
View File
@@ -1,171 +1,55 @@
// @flow
import { observer } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Portal } from "react-portal";
import { withRouter } from "react-router-dom";
import type { Location } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade";
import Flex from "components/Flex";
import CollapseToggle, {
Button as CollapseButton,
} from "./components/CollapseToggle";
import ResizeBorder from "./components/ResizeBorder";
import ResizeHandle from "./components/ResizeHandle";
import CollapseToggle, { Button } from "./components/CollapseToggle";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
let firstRender = true;
let BOUNCE_ANIMATION_MS = 250;
type Props = {
children: React.Node,
location: Location,
};
const useResize = ({ width, minWidth, maxWidth, setWidth }) => {
const [offset, setOffset] = React.useState(0);
const [isAnimating, setAnimating] = React.useState(false);
const [isResizing, setResizing] = React.useState(false);
const isSmallerThanMinimum = width < minWidth;
const handleDrag = React.useCallback(
(event: MouseEvent) => {
// suppresses text selection
event.preventDefault();
// this is simple because the sidebar is always against the left edge
const width = Math.min(event.pageX - offset, maxWidth);
setWidth(width);
},
[offset, maxWidth, setWidth]
);
const handleStopDrag = React.useCallback(() => {
setResizing(false);
if (isSmallerThanMinimum) {
setWidth(minWidth);
setAnimating(true);
} else {
setWidth(width);
}
}, [isSmallerThanMinimum, minWidth, width, setWidth]);
const handleStartDrag = React.useCallback(
(event) => {
setOffset(event.pageX - width);
setResizing(true);
setAnimating(false);
},
[width]
);
React.useEffect(() => {
if (isAnimating) {
setTimeout(() => setAnimating(false), BOUNCE_ANIMATION_MS);
}
}, [isAnimating]);
React.useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", handleStopDrag);
}
return () => {
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleStopDrag);
};
}, [isResizing, handleDrag, handleStopDrag]);
return { isAnimating, isSmallerThanMinimum, isResizing, handleStartDrag };
};
function Sidebar({ location, children }: Props) {
const theme = useTheme();
const { t } = useTranslation();
const { ui } = useStores();
const previousLocation = usePrevious(location);
const width = ui.sidebarWidth;
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const collapsed = ui.editMode || ui.sidebarCollapsed;
const {
isAnimating,
isSmallerThanMinimum,
isResizing,
handleStartDrag,
} = useResize({
width,
minWidth,
maxWidth,
setWidth: ui.setSidebarWidth,
});
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
ui.setSidebarResizing(isResizing);
}, [ui, isResizing]);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
}, [ui, location, previousLocation]);
const style = React.useMemo(
() => ({
width: `${width}px`,
left:
collapsed && !ui.mobileSidebarVisible
? `${-width + theme.sidebarCollapsedWidth}px`
: 0,
}),
[width, collapsed, theme.sidebarCollapsedWidth, ui.mobileSidebarVisible]
);
const content = (
<Container
style={style}
$sidebarWidth={ui.sidebarWidth}
$isAnimating={isAnimating}
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
mobileSidebarVisible={ui.mobileSidebarVisible}
collapsed={ui.editMode || ui.sidebarCollapsed}
column
>
{!isResizing && (
<CollapseToggle
collapsed={ui.sidebarCollapsed}
onClick={ui.toggleCollapsedSidebar}
/>
)}
{ui.mobileSidebarVisible && (
<Portal>
<Fade>
<Background onClick={ui.toggleMobileSidebar} />
</Fade>
</Portal>
)}
<CollapseToggle
collapsed={ui.sidebarCollapsed}
onClick={ui.toggleCollapsedSidebar}
/>
<Toggle
onClick={ui.toggleMobileSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
>
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
{!ui.sidebarCollapsed && (
<ResizeBorder
onMouseDown={handleStartDrag}
onDoubleClick={handleReset}
$isResizing={isResizing}
>
<ResizeHandle aria-label={t("Resize sidebar")} />
</ResizeBorder>
)}
</Container>
);
@@ -178,67 +62,82 @@ function Sidebar({ location, children }: Props) {
return content;
}
const Background = styled.a`
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
cursor: default;
z-index: ${(props) => props.theme.depths.sidebar - 1};
background: rgba(0, 0, 0, 0.5);
`;
const Container = styled(Flex)`
position: fixed;
top: 0;
bottom: 0;
width: 100%;
background: ${(props) => props.theme.sidebarBackground};
transition: box-shadow, 100ms, ease-in-out, margin-left 100ms ease-out,
left 100ms ease-out,
${(props) => props.theme.backgroundTransition}
${(props) =>
props.$isAnimating ? `,width ${BOUNCE_ANIMATION_MS}ms ease-out` : ""};
margin-left: ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")};
transition: box-shadow, 100ms, ease-in-out, left 100ms ease-out,
${(props) => props.theme.backgroundTransition};
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
z-index: ${(props) => props.theme.depths.sidebar};
max-width: 70%;
min-width: 280px;
@media print {
display: none;
left: 0;
}
&:before,
&:after {
content: "";
background: ${(props) => props.theme.sidebarBackground};
position: absolute;
top: -50vh;
left: 0;
width: 100%;
height: 50vh;
}
&:after {
top: auto;
bottom: -50vh;
}
${breakpoint("tablet")`
left: ${(props) =>
props.collapsed
? `calc(-${props.theme.sidebarWidth} + ${props.theme.sidebarCollapsedWidth})`
: 0};
width: ${(props) => props.theme.sidebarWidth};
margin: 0;
z-index: 3;
min-width: 0;
&:hover,
&:focus-within {
left: 0 !important;
left: 0;
box-shadow: ${(props) =>
props.$collapsed
? "rgba(0, 0, 0, 0.2) 1px 0 4px"
: props.$isSmallerThanMinimum
? "rgba(0, 0, 0, 0.1) inset -1px 0 2px"
: "none"};
props.collapsed ? "rgba(0, 0, 0, 0.2) 1px 0 4px" : "none"};
& ${CollapseButton} {
& ${Button} {
opacity: .75;
}
& ${CollapseButton}:hover {
& ${Button}:hover {
opacity: 1;
}
}
&:not(:hover):not(:focus-within) > div {
opacity: ${(props) => (props.$collapsed ? "0" : "1")};
opacity: ${(props) => (props.collapsed ? "0" : "1")};
transition: opacity 100ms ease-in-out;
}
`};
`;
const Toggle = styled.a`
display: flex;
align-items: center;
position: fixed;
top: 0;
left: ${(props) => (props.mobileSidebarVisible ? "auto" : 0)};
right: ${(props) => (props.mobileSidebarVisible ? 0 : "auto")};
z-index: 1;
margin: 12px;
${breakpoint("tablet")`
display: none;
`};
`;
export default withRouter(observer(Sidebar));
@@ -8,7 +8,7 @@ import { meta } from "utils/keyboard";
type Props = {|
collapsed: boolean,
onClick?: (event: SyntheticEvent<>) => void,
onClick?: () => void,
|};
function CollapseToggle({ collapsed, ...rest }: Props) {
@@ -21,7 +21,7 @@ function CollapseToggle({ collapsed, ...rest }: Props) {
delay={500}
placement="bottom"
>
<Button {...rest} tabIndex="-1" aria-hidden>
<Button {...rest} aria-hidden>
{collapsed ? (
<NextIcon color="currentColor" />
) : (
@@ -43,7 +43,7 @@ export const Button = styled.button`
z-index: 1;
font-weight: 600;
color: ${(props) => props.theme.sidebarText};
background: transparent;
background: ${(props) => props.theme.sidebarItemBackground};
transition: opacity 100ms ease-in-out;
border-radius: 4px;
opacity: 0;
@@ -21,6 +21,7 @@ type Props = {|
canUpdate: boolean,
collection?: Collection,
activeDocument: ?Document,
activeDocumentRef?: (?HTMLElement) => void,
prefetchDocument: (documentId: string) => Promise<void>,
depth: number,
index: number,
@@ -32,6 +33,7 @@ function DocumentLink({
canUpdate,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
index,
@@ -211,6 +213,7 @@ function DocumentLink({
<div ref={dropToReparent}>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
innerRef={isActiveDocument ? activeDocumentRef : undefined}
onMouseEnter={handleMouseEnter}
to={{
pathname: node.url,
@@ -5,24 +5,18 @@ import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
type Props = {|
type Props = {
teamName: string,
subheading: React.Node,
showDisclosure?: boolean,
onClick: (event: SyntheticEvent<>) => void,
logoUrl: string,
|};
};
const HeaderBlock = React.forwardRef<Props, any>(
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
<Wrapper>
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => {
return (
<Header justify="flex-start" align="center" ref={ref} {...rest}>
<TeamLogo
alt={`${teamName} logo`}
src={logoUrl}
width={38}
height={38}
/>
<TeamLogo alt={`${teamName} logo`} src={logoUrl} size="38px" />
<Flex align="flex-start" column>
<TeamName showDisclosure>
{teamName}{" "}
@@ -31,8 +25,8 @@ const HeaderBlock = React.forwardRef<Props, any>(
<Subheading>{subheading}</Subheading>
</Flex>
</Header>
</Wrapper>
)
);
}
);
const StyledExpandedIcon = styled(ExpandedIcon)`
@@ -46,7 +40,6 @@ const Subheading = styled.div`
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
white-space: nowrap;
color: ${(props) => props.theme.sidebarText};
`;
@@ -56,20 +49,16 @@ const TeamName = styled.div`
padding-right: 24px;
font-weight: 600;
color: ${(props) => props.theme.text};
white-space: nowrap;
text-decoration: none;
font-size: 16px;
`;
const Wrapper = styled.div`
flex-shrink: 0;
overflow: hidden;
`;
const Header = styled.button`
display: flex;
align-items: center;
flex-shrink: 0;
padding: 20px 24px;
position: relative;
background: none;
line-height: inherit;
border: 0;
@@ -1,105 +0,0 @@
// @flow
// ref: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/NavLink.js
// This file is pulled almost 100% from react-router with the addition of one
// thing, automatic scroll to the active link. It's worth the copy paste because
// it avoids recalculating the link match again.
import { createLocation } from "history";
import * as React from "react";
import {
__RouterContext as RouterContext,
matchPath,
type Location,
} from "react-router";
import { Link } from "react-router-dom";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
const resolveToLocation = (to, currentLocation) =>
typeof to === "function" ? to(currentLocation) : to;
const normalizeToLocation = (to, currentLocation) => {
return typeof to === "string"
? createLocation(to, null, null, currentLocation)
: to;
};
const joinClassnames = (...classnames) => {
return classnames.filter((i) => i).join(" ");
};
type Props = {|
activeClassName?: String,
activeStyle?: Object,
className?: string,
exact?: boolean,
isActive?: any,
location?: Location,
strict?: boolean,
style?: Object,
to: string,
|};
/**
* A <Link> wrapper that knows if it's "active" or not.
*/
const NavLink = ({
"aria-current": ariaCurrent = "page",
activeClassName = "active",
activeStyle,
className: classNameProp,
exact,
isActive: isActiveProp,
location: locationProp,
strict,
style: styleProp,
to,
...rest
}: Props) => {
const linkRef = React.useRef();
const context = React.useContext(RouterContext);
const currentLocation = locationProp || context.location;
const toLocation = normalizeToLocation(
resolveToLocation(to, currentLocation),
currentLocation
);
const { pathname: path } = toLocation;
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
const match = escapedPath
? matchPath(currentLocation.pathname, {
path: escapedPath,
exact,
strict,
})
: null;
const isActive = !!(isActiveProp
? isActiveProp(match, currentLocation)
: match);
const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
React.useEffect(() => {
if (isActive && linkRef.current) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "instant",
});
}
}, [linkRef, isActive]);
const props = {
"aria-current": (isActive && ariaCurrent) || null,
className,
style,
to: toLocation,
...rest,
};
return <Link ref={linkRef} {...props} />;
};
export default NavLink;
@@ -1,28 +0,0 @@
// @flow
import styled from "styled-components";
import ResizeHandle from "./ResizeHandle";
const ResizeBorder = styled.div`
position: absolute;
top: 0;
bottom: 0;
right: -6px;
width: 12px;
cursor: ew-resize;
${(props) =>
props.$isResizing &&
`
${ResizeHandle} {
opacity: 1;
}
`}
&:hover {
${ResizeHandle} {
opacity: 1;
}
}
`;
export default ResizeBorder;
@@ -1,39 +0,0 @@
// @flow
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
const ResizeHandle = styled.button`
opacity: 0;
transition: opacity 100ms ease-in-out;
transform: translateY(-50%);
position: absolute;
top: 50%;
height: 40px;
right: -10px;
width: 8px;
padding: 0;
border: 0;
background: ${(props) => props.theme.sidebarBackground};
border-radius: 8px;
pointer-events: none;
&:after {
content: "";
position: absolute;
top: -24px;
bottom: -24px;
left: -12px;
right: -12px;
}
&:active {
background: ${(props) => props.theme.sidebarText};
}
${breakpoint("tablet")`
pointer-events: all;
cursor: ew-resize;
`}
`;
export default ResizeHandle;
+1 -3
View File
@@ -5,9 +5,7 @@ import Flex from "components/Flex";
const Section = styled(Flex)`
position: relative;
flex-direction: column;
margin: 20px 8px;
min-width: ${(props) => props.theme.sidebarMinWidth}px;
flex-shrink: 0;
margin: 24px 8px;
`;
export default Section;
@@ -1,10 +1,13 @@
// @flow
import * as React from "react";
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
import {
withRouter,
NavLink,
type RouterHistory,
type Match,
} from "react-router-dom";
import styled, { withTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "components/EventBoundary";
import NavLink from "./NavLink";
import { type Theme } from "types";
type Props = {
@@ -43,6 +46,7 @@ function SidebarLink({
theme,
exact,
href,
innerRef,
depth,
history,
match,
@@ -61,14 +65,14 @@ function SidebarLink({
...style,
};
const activeDropStyle = {
const activeFontWeightOnly = {
fontWeight: 600,
};
return (
<Link
<StyledNavLink
$isActiveDrop={isActiveDrop}
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
activeStyle={isActiveDrop ? activeFontWeightOnly : activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
onMouseEnter={onMouseEnter}
@@ -76,12 +80,13 @@ function SidebarLink({
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
ref={innerRef}
className={className}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
{menu && <Actions showActions={showActions}>{menu}</Actions>}
</Link>
</StyledNavLink>
);
}
@@ -91,7 +96,6 @@ const IconWrapper = styled.span`
margin-right: 4px;
height: 24px;
overflow: hidden;
flex-shrink: 0;
`;
const Actions = styled(EventBoundary)`
@@ -115,11 +119,11 @@ const Actions = styled(EventBoundary)`
}
`;
const Link = styled(NavLink)`
const StyledNavLink = styled(NavLink)`
display: flex;
position: relative;
text-overflow: ellipsis;
padding: 6px 16px;
padding: 4px 16px;
border-radius: 4px;
transition: background 50ms, color 50ms;
background: ${(props) =>
@@ -132,7 +136,7 @@ const Link = styled(NavLink)`
svg {
${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")}
transition: fill 50ms;
transition: fill 50ms
}
&:hover {
@@ -155,10 +159,6 @@ const Link = styled(NavLink)`
}
}
}
${breakpoint("tablet")`
padding: 4px 16px;
`}
`;
const Label = styled.div`
-8
View File
@@ -1,8 +0,0 @@
// @flow
import * as React from "react";
export const id = "skip-nav";
export default function SkipNavContent() {
return <div id={id} />;
}
-34
View File
@@ -1,34 +0,0 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { id } from "components/SkipNavContent";
export default function SkipNavLink() {
return <Anchor href={`#${id}`}>Skip navigation</Anchor>;
}
const Anchor = styled.a`
border: 0;
clip: rect(0 0 0 0);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
z-index: 1;
&:focus {
padding: 1rem;
position: fixed;
top: 12px;
left: 12px;
background: ${(props) => props.theme.background};
color: ${(props) => props.theme.text};
outline-color: ${(props) => props.theme.primary};
z-index: ${(props) => props.theme.depths.popover};
width: auto;
height: auto;
clip: auto;
}
`;
+7 -14
View File
@@ -1,7 +1,6 @@
// @flow
import { StarredIcon, UnstarredIcon } from "outline-icons";
import { StarredIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Document from "models/Document";
import NudeButton from "./NudeButton";
@@ -12,7 +11,6 @@ type Props = {|
|};
function Star({ size, document, ...rest }: Props) {
const { t } = useTranslation();
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
@@ -32,17 +30,12 @@ function Star({ size, document, ...rest }: Props) {
}
return (
<Button
onClick={handleClick}
size={size}
aria-label={document.isStarred ? t("Unstar") : t("Star")}
{...rest}
>
{document.isStarred ? (
<AnimatedStar size={size} color="currentColor" />
) : (
<AnimatedStar size={size} color="currentColor" as={UnstarredIcon} />
)}
<Button onClick={handleClick} size={size} {...rest}>
<AnimatedStar
solid={document.isStarred}
size={size}
color="currentColor"
/>
</Button>
);
}
+2 -5
View File
@@ -3,15 +3,12 @@ import * as React from "react";
import styled from "styled-components";
import { LabelText } from "components/Input";
type Props = {|
type Props = {
width?: number,
height?: number,
label?: string,
checked?: boolean,
disabled?: boolean,
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
id?: string,
|};
};
function Switch({ width = 38, height = 20, label, ...props }: Props) {
const component = (
+2 -5
View File
@@ -2,15 +2,12 @@
import styled from "styled-components";
const TeamLogo = styled.img`
width: ${(props) =>
props.width ? `${props.width}px` : props.size || "auto"};
height: ${(props) =>
props.height ? `${props.height}px` : props.size || "38px"};
width: ${(props) => props.size || "auto"};
height: ${(props) => props.size || "38px"};
border-radius: 4px;
background: ${(props) => props.theme.background};
border: 1px solid ${(props) => props.theme.divider};
overflow: hidden;
flex-shrink: 0;
`;
export default TeamLogo;
+2 -2
View File
@@ -3,14 +3,14 @@ import Tippy from "@tippy.js/react";
import * as React from "react";
import styled from "styled-components";
type Props = {|
type Props = {
tooltip: React.Node,
shortcut?: React.Node,
placement?: "top" | "bottom" | "left" | "right",
children: React.Node,
delay?: number,
className?: string,
|};
};
class Tooltip extends React.Component<Props> {
render() {
+13
View File
@@ -0,0 +1,13 @@
// @flow
import styled from "styled-components";
const VisuallyHidden = styled("span")`
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
`;
export default VisuallyHidden;
+1 -2
View File
@@ -1,6 +1,5 @@
// @flow
import * as React from "react";
import Image from "components/Image";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https?://docs.google.com/document/(.*)$");
@@ -21,7 +20,7 @@ export default class GoogleDocs extends React.Component<Props> {
{...this.props}
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
<img
src="/images/google-docs.png"
alt="Google Docs Icon"
width={16}
-39
View File
@@ -1,39 +0,0 @@
// @flow
import * as React from "react";
import Image from "components/Image";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https://docs.google.com/drawings/d/(.*)/(edit|preview)(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
export default class GoogleDrawings extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return (
<Frame
{...this.props}
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
src="/images/google-drawings.png"
alt="Google Drawings"
width={16}
height={16}
/>
}
canonicalUrl={this.props.attrs.href.replace("/preview", "/edit")}
title="Google Drawings"
border
/>
);
}
}
-29
View File
@@ -1,29 +0,0 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleDrawings from "./GoogleDrawings";
describe("GoogleDrawings", () => {
const match = GoogleDrawings.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/drawings/d/1zDLtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/drawings/d/1zDLtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit?usp=sharing".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://docs.google.com/drawings/d/e/2PACX-1vRtzIzEWN6svSrIYZq-kq2XZEN6WaOFXHbPKRLXNOFRlxLIdJg0Vo6RfretGqs9SzD-fUazLeS594Kw/pub?w=960&h=720".match(
match
)
).toBe(null);
expect("https://docs.google.com/drawings".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
});
});
+1 -2
View File
@@ -1,6 +1,5 @@
// @flow
import * as React from "react";
import Image from "components/Image";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
@@ -22,7 +21,7 @@ export default class GoogleDrive extends React.Component<Props> {
<Frame
src={this.props.attrs.href.replace("/view", "/preview")}
icon={
<Image
<img
src="/images/google-drive.png"
alt="Google Drive Icon"
width={16}
+1 -2
View File
@@ -1,6 +1,5 @@
// @flow
import * as React from "react";
import Image from "components/Image";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https?://docs.google.com/spreadsheets/d/(.*)$");
@@ -21,7 +20,7 @@ export default class GoogleSlides extends React.Component<Props> {
{...this.props}
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
<img
src="/images/google-sheets.png"
alt="Google Sheets Icon"
width={16}
+1 -2
View File
@@ -1,6 +1,5 @@
// @flow
import * as React from "react";
import Image from "components/Image";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https?://docs.google.com/presentation/d/(.*)$");
@@ -23,7 +22,7 @@ export default class GoogleSlides extends React.Component<Props> {
.replace("/edit", "/preview")
.replace("/pub", "/embed")}
icon={
<Image
<img
src="/images/google-slides.png"
alt="Google Slides Icon"
width={16}
+1 -1
View File
@@ -2,7 +2,7 @@
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|)(\d+)(?:|\/\?)/;
const URL_REGEX = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/;
type Props = {|
attrs: {|
+11 -11
View File
@@ -6,17 +6,6 @@ import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
// https://www.styled-components.com/docs/basics#passed-props
const Iframe = (props) => <iframe title="Embed" {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${(props) => props.theme.embedBorder};
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
display: block;
`;
type Props = {
src?: string,
border?: boolean,
@@ -140,6 +129,17 @@ const Bar = styled(Flex)`
user-select: none;
`;
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
// https://www.styled-components.com/docs/basics#passed-props
const Iframe = (props) => <iframe {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${(props) => props.theme.embedBorder};
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
display: block;
`;
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
<Frame {...props} forwardedRef={ref} />
));
+1 -10
View File
@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Image from "components/Image";
import Abstract from "./Abstract";
import Airtable from "./Airtable";
import ClickUp from "./ClickUp";
@@ -10,7 +9,6 @@ import Figma from "./Figma";
import Framer from "./Framer";
import Gist from "./Gist";
import GoogleDocs from "./GoogleDocs";
import GoogleDrawings from "./GoogleDrawings";
import GoogleDrive from "./GoogleDrive";
import GoogleSheets from "./GoogleSheets";
import GoogleSlides from "./GoogleSlides";
@@ -40,7 +38,7 @@ function matcher(Component) {
};
}
const Img = styled(Image)`
const Img = styled.img`
margin: 4px;
width: 18px;
height: 18px;
@@ -96,13 +94,6 @@ export default [
component: Gist,
matcher: matcher(Gist),
},
{
title: "Google Drawings",
keywords: "drawings",
icon: () => <Img src="/images/google-drawings.png" />,
component: GoogleDrawings,
matcher: matcher(GoogleDrawings),
},
{
title: "Google Drive",
keywords: "drive",
-9
View File
@@ -1,9 +0,0 @@
// @flow
import invariant from "invariant";
import useStores from "./useStores";
export default function useCurrentTeam() {
const { auth } = useStores();
invariant(auth.team, "team required");
return auth.team;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import useStores from "./useStores";
export default function useUserLocale() {
const { auth } = useStores();
if (!auth.user || !auth.user.language) {
if (!auth.user) {
return undefined;
}
-31
View File
@@ -1,31 +0,0 @@
// @flow
import { debounce } from "lodash";
import * as React from "react";
export default function useWindowSize() {
const [windowSize, setWindowSize] = React.useState({
width: undefined,
height: undefined,
});
React.useEffect(() => {
// Handler to call on window resize
const handleResize = debounce(() => {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 100);
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}
+2 -9
View File
@@ -1,12 +1,11 @@
// @flow
import "focus-visible";
import { createBrowserHistory } from "history";
import { Provider } from "mobx-react";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { render } from "react-dom";
import { Router } from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { initI18n } from "shared/i18n";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
@@ -15,16 +14,10 @@ import Theme from "components/Theme";
import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
import { initSentry } from "utils/sentry";
initI18n();
const element = document.getElementById("root");
const history = createBrowserHistory();
if (env.SENTRY_DSN) {
initSentry(history);
}
if (element) {
render(
@@ -32,7 +25,7 @@ if (element) {
<Theme>
<ErrorBoundary>
<DndProvider backend={HTML5Backend}>
<Router history={history}>
<Router>
<>
<ScrollToTop>
<Routes />
+1 -1
View File
@@ -19,7 +19,7 @@ export default function BreadcrumbMenu({ path }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show path to document")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Path to document")}>
<Template
{...menu}
+1 -1
View File
@@ -18,7 +18,7 @@ function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Group member options")}>
<Template
{...menu}
+2 -2
View File
@@ -4,7 +4,6 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useMenuState, MenuButton } from "reakit/Menu";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import Collection from "models/Collection";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
@@ -14,6 +13,7 @@ import ContextMenu from "components/ContextMenu";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Template from "components/ContextMenu/Template";
import Modal from "components/Modal";
import VisuallyHidden from "components/VisuallyHidden";
import useStores from "hooks/useStores";
import getDataTransferFiles from "utils/getDataTransferFiles";
import { newDocumentUrl } from "utils/routeHelpers";
@@ -116,7 +116,7 @@ function CollectionMenu({
{label ? (
<MenuButton {...menu}>{label}</MenuButton>
) : (
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
)}
<ContextMenu
{...menu}
+1 -1
View File
@@ -38,7 +38,7 @@ function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
<>
<MenuButton {...menu}>
{(props) => (
<NudeButton aria-label={t("Show sort menu")} {...props}>
<NudeButton {...props}>
{alphabeticalSort ? <AlphabeticalSortIcon /> : <ManualSortIcon />}
</NudeButton>
)}
+1 -7
View File
@@ -106,7 +106,6 @@ function DocumentMenu({
const handleStar = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
document.star();
},
@@ -115,7 +114,6 @@ function DocumentMenu({
const handleUnstar = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
document.unstar();
},
@@ -140,11 +138,7 @@ function DocumentMenu({
{label ? (
<MenuButton {...menu}>{label}</MenuButton>
) : (
<OverflowMenuButton
className={className}
aria-label={t("Show menu")}
{...menu}
/>
<OverflowMenuButton className={className} {...menu} />
)}
<ContextMenu
{...menu}
+1 -1
View File
@@ -17,7 +17,7 @@ function GroupMemberMenu({ onRemove }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Group member options")}>
<Template
{...menu}
+1 -1
View File
@@ -41,7 +41,7 @@ function GroupMenu({ group, onMembers }: Props) {
>
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
</Modal>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Group options")}>
<Template
{...menu}
+1 -1
View File
@@ -17,7 +17,7 @@ function MemberMenu({ onRemove }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Member options")}>
<Template
{...menu}
+3 -5
View File
@@ -31,11 +31,9 @@ function NewChildDocumentMenu({ document, label }: Props) {
{
title: (
<span>
<Trans
defaults="New document in <em>{{ collectionName }}</em>"
values={{ collectionName }}
components={{ em: <strong /> }}
/>
<Trans>
New document in <strong>{{ collectionName }}</strong>
</Trans>
</span>
),
to: newDocumentUrl(document.collectionId),
+1
View File
@@ -27,6 +27,7 @@ function NewDocumentMenu() {
as={Link}
to={newDocumentUrl(collections.orderedData[0].id)}
icon={<PlusIcon />}
small
>
{t("New doc")}
</Button>
-1
View File
@@ -51,7 +51,6 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) {
<OverflowMenuButton
className={className}
iconColor={iconColor}
aria-label={t("Show menu")}
{...menu}
/>
<ContextMenu {...menu} aria-label={t("Revision options")}>
+1 -1
View File
@@ -49,7 +49,7 @@ function ShareMenu({ share }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("Share options")}>
<CopyToClipboard text={share.url} onCopy={handleCopy}>
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
+1 -1
View File
@@ -88,7 +88,7 @@ function UserMenu({ user }: Props) {
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<OverflowMenuButton {...menu} />
<ContextMenu {...menu} aria-label={t("User options")}>
<Template
{...menu}
+5 -9
View File
@@ -142,7 +142,7 @@ export default class Document extends BaseModel {
};
@action
updateFromJson = (data: Object) => {
updateFromJson = (data) => {
set(this, data);
};
@@ -150,7 +150,7 @@ export default class Document extends BaseModel {
return this.store.archive(this);
};
restore = (options: { revisionId?: string, collectionId?: string }) => {
restore = (options) => {
return this.store.restore(this, options);
};
@@ -233,7 +233,7 @@ export default class Document extends BaseModel {
};
@action
save = async (options: SaveOptions = {}) => {
save = async (options: SaveOptions) => {
if (this.isSaving) return this;
const isCreating = !this.id;
@@ -246,9 +246,7 @@ export default class Document extends BaseModel {
collectionId: this.collectionId,
title: this.title,
text: this.text,
publish: options.publish,
done: options.done,
autosave: options.autosave,
...options,
});
}
@@ -259,9 +257,7 @@ export default class Document extends BaseModel {
text: this.text,
templateId: this.templateId,
lastRevision: options.lastRevision,
publish: options.publish,
done: options.done,
autosave: options.autosave,
...options,
});
}
+1 -2
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { Switch, Redirect, type Match } from "react-router-dom";
import { Switch, Route, Redirect, type Match } from "react-router-dom";
import Archive from "scenes/Archive";
import Collection from "scenes/Collection";
import Dashboard from "scenes/Dashboard";
@@ -16,7 +16,6 @@ import Trash from "scenes/Trash";
import CenteredContent from "components/CenteredContent";
import Layout from "components/Layout";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import Route from "components/ProfiledRoute";
import SocketProvider from "components/SocketProvider";
import { matchDocumentSlug as slug } from "utils/routeHelpers";
+1 -2
View File
@@ -1,9 +1,8 @@
// @flow
import * as React from "react";
import { Switch } from "react-router-dom";
import { Switch, Route } from "react-router-dom";
import DelayedMount from "components/DelayedMount";
import FullscreenLoading from "components/FullscreenLoading";
import Route from "components/ProfiledRoute";
const Authenticated = React.lazy(() => import("components/Authenticated"));
const AuthenticatedRoutes = React.lazy(() => import("./authenticated"));
+1 -2
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { Switch } from "react-router-dom";
import { Switch, Route } from "react-router-dom";
import Settings from "scenes/Settings";
import Details from "scenes/Settings/Details";
import Export from "scenes/Settings/Export";
@@ -12,7 +12,6 @@ import Shares from "scenes/Settings/Shares";
import Slack from "scenes/Settings/Slack";
import Tokens from "scenes/Settings/Tokens";
import Zapier from "scenes/Settings/Zapier";
import Route from "components/ProfiledRoute";
export default function SettingsRoutes() {
return (
+7 -32
View File
@@ -145,8 +145,6 @@ class CollectionScene extends React.Component<Props> {
<InputSearch
source="collection"
placeholder={`${t("Search in collection")}`}
label={`${t("Search in collection")}`}
labelHidden
collectionId={match.params.id}
/>
</Action>
@@ -207,12 +205,10 @@ class CollectionScene extends React.Component<Props> {
{collection.isEmpty ? (
<Centered column>
<HelpText>
<Trans
defaults="<em>{{ collectionName }}</em> doesnt contain any
documents yet."
values={{ collectionName }}
components={{ em: <strong /> }}
/>
<Trans>
<strong>{{ collectionName }}</strong> doesnt contain any
documents yet.
</Trans>
<br />
<Trans>Get started by creating a new one!</Trans>
</HelpText>
@@ -280,12 +276,9 @@ class CollectionScene extends React.Component<Props> {
<Tabs>
<Tab to={collectionUrl(collection.id)} exact>
{t("Documents")}
</Tab>
<Tab to={collectionUrl(collection.id, "updated")} exact>
{t("Recently updated")}
</Tab>
<Tab to={collectionUrl(collection.id, "published")} exact>
<Tab to={collectionUrl(collection.id, "recent")} exact>
{t("Recently published")}
</Tab>
<Tab to={collectionUrl(collection.id, "old")} exact>
@@ -319,11 +312,8 @@ class CollectionScene extends React.Component<Props> {
/>
</Route>
<Route path={collectionUrl(collection.id, "recent")}>
<Redirect to={collectionUrl(collection.id, "published")} />
</Route>
<Route path={collectionUrl(collection.id, "published")}>
<PaginatedDocumentList
key="published"
key="recent"
documents={documents.recentlyPublishedInCollection(
collection.id
)}
@@ -333,9 +323,8 @@ class CollectionScene extends React.Component<Props> {
showPin
/>
</Route>
<Route path={collectionUrl(collection.id, "updated")}>
<Route path={collectionUrl(collection.id)}>
<PaginatedDocumentList
key="updated"
documents={documents.recentlyUpdatedInCollection(
collection.id
)}
@@ -344,20 +333,6 @@ class CollectionScene extends React.Component<Props> {
showPin
/>
</Route>
<Route path={collectionUrl(collection.id)} exact>
<PaginatedDocumentList
documents={documents.rootInCollection(collection.id)}
fetch={documents.fetchPage}
options={{
collectionId: collection.id,
parentDocumentId: null,
sort: collection.sort.field,
direction: "ASC",
}}
showNestedDocuments
showPin
/>
</Route>
</Switch>
</>
)}
@@ -13,7 +13,6 @@ import Collection from "models/Collection";
import Group from "models/Group";
import GroupNew from "scenes/GroupNew";
import Button from "components/Button";
import ButtonLink from "components/ButtonLink";
import Empty from "components/Empty";
import Flex from "components/Flex";
import GroupListItem from "components/GroupListItem";
@@ -86,9 +85,9 @@ class AddGroupsToCollection extends React.Component<Props> {
<Flex column>
<HelpText>
{t("Cant find the group youre looking for?")}{" "}
<ButtonLink onClick={this.handleNewGroupModalOpen}>
<a role="button" onClick={this.handleNewGroupModalOpen}>
{t("Create a group")}
</ButtonLink>
</a>
.
</HelpText>
@@ -11,7 +11,6 @@ import UsersStore from "stores/UsersStore";
import Collection from "models/Collection";
import User from "models/User";
import Invite from "scenes/Invite";
import ButtonLink from "components/ButtonLink";
import Empty from "components/Empty";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
@@ -82,9 +81,9 @@ class AddPeopleToCollection extends React.Component<Props> {
<Flex column>
<HelpText>
{t("Need to add someone whos not yet on the team yet?")}{" "}
<ButtonLink onClick={this.handleInviteModalOpen}>
<a role="button" onClick={this.handleInviteModalOpen}>
{t("Invite people to {{ teamName }}", { teamName: team.name })}
</ButtonLink>
</a>
.
</HelpText>
@@ -12,7 +12,6 @@ import UiStore from "stores/UiStore";
import UsersStore from "stores/UsersStore";
import Collection from "models/Collection";
import Button from "components/Button";
import ButtonLink from "components/ButtonLink";
import Empty from "components/Empty";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
@@ -140,9 +139,9 @@ class CollectionMembers extends React.Component<Props> {
documents in the private <strong>{collection.name}</strong>{" "}
collection. You can make this collection visible to the entire
team by{" "}
<ButtonLink onClick={this.props.onEdit}>
<a role="button" onClick={this.props.onEdit}>
changing the visibility
</ButtonLink>
</a>
.
</HelpText>
<span>
@@ -161,7 +160,9 @@ class CollectionMembers extends React.Component<Props> {
The <strong>{collection.name}</strong> collection is accessible by
everyone on the team. If you want to limit who can view the
collection,{" "}
<ButtonLink onClick={this.props.onEdit}>make it private</ButtonLink>
<a role="button" onClick={this.props.onEdit}>
make it private
</a>
.
</HelpText>
)}
+1 -5
View File
@@ -64,11 +64,7 @@ function Dashboard() {
</Switch>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch
source="dashboard"
label={t("Search documents")}
labelHidden
/>
<InputSearch source="dashboard" />
</Action>
<Action>
<NewDocumentMenu />
+1 -1
View File
@@ -24,8 +24,8 @@ import Loading from "./Loading";
import SocketPresence from "./SocketPresence";
import { type LocationWithState, type Theme } from "types";
import { NotFoundError, OfflineError } from "utils/errors";
import isInternalUrl from "utils/isInternalUrl";
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
import { isInternalUrl } from "utils/urls";
type Props = {|
match: Match,
+1 -1
View File
@@ -61,7 +61,7 @@ type Props = {
document: Document,
revision: Revision,
readOnly: boolean,
onCreateLink: (title: string) => Promise<string>,
onCreateLink: (title: string) => string,
onSearchLink: (term: string) => any,
theme: Theme,
auth: AuthStore,
+12 -13
View File
@@ -9,23 +9,24 @@ import parseTitle from "shared/utils/parseTitle";
import Document from "models/Document";
import ClickablePadding from "components/ClickablePadding";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor, { type Props as EditorProps } from "components/Editor";
import Editor from "components/Editor";
import Flex from "components/Flex";
import HoverPreview from "components/HoverPreview";
import Star, { AnimatedStar } from "components/Star";
import { isModKey } from "utils/keyboard";
import { isMetaKey } from "utils/keyboard";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {|
...EditorProps,
type Props = {
onChangeTitle: (event: SyntheticInputEvent<>) => void,
title: string,
defaultValue: string,
document: Document,
isDraft: boolean,
isShare: boolean,
onSave: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
readOnly?: boolean,
onSave: ({ publish?: boolean, done?: boolean, autosave?: boolean }) => mixed,
innerRef: { current: any },
|};
};
@observer
class DocumentEditor extends React.Component<Props> {
@@ -54,7 +55,7 @@ class DocumentEditor extends React.Component<Props> {
handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => {
if (event.key === "Enter") {
event.preventDefault();
if (isModKey(event)) {
if (isMetaKey(event)) {
this.props.onSave({ done: true });
return;
}
@@ -68,12 +69,12 @@ class DocumentEditor extends React.Component<Props> {
this.focusAtStart();
return;
}
if (event.key === "p" && isModKey(event) && event.shiftKey) {
if (event.key === "p" && isMetaKey(event) && event.shiftKey) {
event.preventDefault();
this.props.onSave({ publish: true, done: true });
return;
}
if (event.key === "s" && isModKey(event)) {
if (event.key === "s" && isMetaKey(event)) {
event.preventDefault();
this.props.onSave({});
return;
@@ -97,7 +98,6 @@ class DocumentEditor extends React.Component<Props> {
isShare,
readOnly,
innerRef,
...rest
} = this.props;
const { emoji } = parseTitle(title);
@@ -135,13 +135,12 @@ class DocumentEditor extends React.Component<Props> {
/>
<Editor
ref={innerRef}
autoFocus={!!title && !this.props.defaultValue}
autoFocus={title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
readOnly={readOnly}
grow
{...rest}
{...this.props}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (
+9
View File
@@ -171,6 +171,7 @@ class Header extends React.Component<Props> {
iconColor="currentColor"
borderOnHover
neutral
small
/>
</Tooltip>
</>
@@ -222,6 +223,7 @@ class Header extends React.Component<Props> {
icon={isPubliclyShared ? <GlobeIcon /> : undefined}
onClick={this.handleShareLink}
neutral
small
>
{t("Share")}
</Button>
@@ -240,7 +242,9 @@ class Header extends React.Component<Props> {
<Button
onClick={this.handleSave}
disabled={savingIsDisabled}
isSaving={isSaving}
neutral={isDraft}
small
>
{isDraft ? t("Save Draft") : t("Done Editing")}
</Button>
@@ -261,6 +265,7 @@ class Header extends React.Component<Props> {
icon={<EditIcon />}
to={editDocumentUrl(this.props.document)}
neutral
small
>
{t("Edit")}
</Button>
@@ -295,6 +300,7 @@ class Header extends React.Component<Props> {
templateId: document.id,
})}
primary
small
>
{t("New from template")}
</Button>
@@ -310,7 +316,9 @@ class Header extends React.Component<Props> {
>
<Button
onClick={this.handlePublish}
title={t("Publish document")}
disabled={publishingIsDisabled}
small
>
{isPublishing ? `${t("Publishing")}` : t("Publish")}
</Button>
@@ -331,6 +339,7 @@ class Header extends React.Component<Props> {
{...props}
borderOnHover
neutral
small
/>
)}
showToggleEmbeds={canToggleEmbeds}
@@ -7,11 +7,11 @@ import Document from "models/Document";
import DocumentMeta from "components/DocumentMeta";
import type { NavigationNode } from "types";
type Props = {|
type Props = {
document: Document | NavigationNode,
anchor?: string,
showCollection?: boolean,
|};
};
const DocumentLink = styled(Link)`
display: block;
+4 -4
View File
@@ -32,15 +32,15 @@ class References extends React.Component<Props> {
: [];
const showBacklinks = !!backlinks.length;
const showNestedDocuments = !!children.length;
const showChildren = !!children.length;
const isBacklinksTab =
this.props.location.hash === "#backlinks" || !showNestedDocuments;
this.props.location.hash === "#backlinks" || !showChildren;
return (
(showBacklinks || showNestedDocuments) && (
(showBacklinks || showChildren) && (
<Fade>
<Tabs>
{showNestedDocuments && (
{showChildren && (
<Tab to="#children" isActive={() => !isBacklinksTab}>
Nested documents
</Tab>
+10 -10
View File
@@ -79,17 +79,17 @@ function DocumentDelete({ document, onSubmit }: Props) {
<form onSubmit={handleSubmit}>
<HelpText>
{document.isTemplate ? (
<Trans
defaults="Are you sure you want to delete the <em>{{ documentTitle }}</em> template?"
values={{ documentTitle: document.titleWithDefault }}
components={{ em: <strong /> }}
/>
<Trans>
Are you sure you want to delete the{" "}
<strong>{{ documentTitle: document.titleWithDefault }}</strong>{" "}
template?
</Trans>
) : (
<Trans
defaults="Are you sure about that? Deleting the <em>{{ documentTitle }}</em> document will delete all of its history and any nested documents."
values={{ documentTitle: document.titleWithDefault }}
components={{ em: <strong /> }}
/>
<Trans>
Are you sure about that? Deleting the{" "}
<strong>{{ documentTitle: document.titleWithDefault }}</strong>{" "}
document will delete all of its history and any nested documents.
</Trans>
)}
</HelpText>
{canArchive && (
+2 -2
View File
@@ -43,7 +43,7 @@ class DocumentShare extends React.Component<Props> {
this.isSaving = true;
try {
await share.save({ published: event.currentTarget.checked });
await share.save({ published: event.target.checked });
} catch (err) {
this.props.ui.showToast(err.message, { type: "error" });
} finally {
@@ -115,7 +115,7 @@ class DocumentShare extends React.Component<Props> {
</Button>
</CopyToClipboard>
&nbsp;&nbsp;&nbsp;
<a href={share.url} target="_blank" rel="noreferrer">
<a href={share.url} target="_blank">
Preview
</a>
</div>
+1 -5
View File
@@ -113,11 +113,7 @@ class Drafts extends React.Component<Props> {
<Actions align="center" justify="flex-end">
<Action>
<InputSearch
source="drafts"
label={t("Search documents")}
labelHidden
/>
<InputSearch source="drafts" />
</Action>
<Action>
<NewDocumentMenu />
+8 -5
View File
@@ -21,11 +21,14 @@ const ErrorSuspended = ({ auth }: { auth: AuthStore }) => {
</h1>
<p>
<Trans
defaults="A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly."
values={{ suspendedContactEmail: auth.suspendedContactEmail }}
components={{ em: <strong /> }}
/>
<Trans>
A team admin (
<strong>
{{ suspendedContactEmail: auth.suspendedContactEmail }}
</strong>
) has suspended your account. To re-activate your account, please
reach out to them directly.
</Trans>
</p>
</CenteredContent>
);
+3 -3
View File
@@ -11,7 +11,6 @@ import UsersStore from "stores/UsersStore";
import Group from "models/Group";
import User from "models/User";
import Invite from "scenes/Invite";
import ButtonLink from "components/ButtonLink";
import Empty from "components/Empty";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
@@ -82,11 +81,12 @@ class AddPeopleToGroup extends React.Component<Props> {
{t(
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?"
)}{" "}
<ButtonLink onClick={this.handleInviteModalOpen}>
<a role="button" onClick={this.handleInviteModalOpen}>
{t("Invite them to {{teamName}}", { teamName: team.name })}
</ButtonLink>
</a>
.
</HelpText>
<Input
type="search"
placeholder={`${t("Search by name")}`}
+125 -118
View File
@@ -1,11 +1,12 @@
// @flow
import { find } from "lodash";
import { observer } from "mobx-react";
import { observer, inject } from "mobx-react";
import { BackIcon, EmailIcon } from "outline-icons";
import * as React from "react";
import { Redirect, Link, type Location } from "react-router-dom";
import { Redirect, Link } from "react-router-dom";
import styled from "styled-components";
import getQueryVariable from "shared/utils/getQueryVariable";
import AuthStore from "stores/AuthStore";
import ButtonLarge from "components/ButtonLarge";
import Fade from "components/Fade";
import Flex from "components/Flex";
@@ -17,141 +18,147 @@ import TeamLogo from "components/TeamLogo";
import Notices from "./Notices";
import Service from "./Service";
import env from "env";
import useStores from "hooks/useStores";
type Props = {|
location: Location,
|};
type Props = {
auth: AuthStore,
location: Object,
};
function Login({ location }: Props) {
const { auth } = useStores();
const { config } = auth;
const [emailLinkSentTo, setEmailLinkSentTo] = React.useState("");
const isCreate = location.pathname === "/create";
type State = {
emailLinkSentTo: string,
};
const handleReset = React.useCallback(() => {
setEmailLinkSentTo("");
}, []);
@observer
class Login extends React.Component<Props, State> {
state = {
emailLinkSentTo: "",
};
const handleEmailSuccess = React.useCallback((email) => {
setEmailLinkSentTo(email);
}, []);
handleReset = () => {
this.setState({ emailLinkSentTo: "" });
};
React.useEffect(() => {
auth.fetchConfig();
}, [auth]);
handleEmailSuccess = (email) => {
this.setState({ emailLinkSentTo: email });
};
console.log(config);
render() {
const { auth, location } = this.props;
const { config } = auth;
const isCreate = location.pathname === "/create";
if (auth.authenticated) {
return <Redirect to="/home" />;
}
if (auth.authenticated) {
return <Redirect to="/home" />;
}
// we're counting on the config request being fast
if (!config) {
return null;
}
// 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 && !isCreate
);
const hasMultipleServices = config.services.length > 1;
const defaultService = find(
config.services,
(service) => service.id === auth.lastSignedIn && !isCreate
);
const header =
env.DEPLOYMENT === "hosted" &&
(config.hostname ? (
<Back href={env.URL}>
<BackIcon color="currentColor" /> Back to home
</Back>
) : (
<Back href="https://www.getoutline.com">
<BackIcon color="currentColor" /> Back to website
</Back>
));
const header =
env.DEPLOYMENT === "hosted" &&
(config.hostname ? (
<Back href={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 centered>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>
);
}
if (emailLinkSentTo) {
return (
<Background>
{header}
<Centered align="center" justify="center" column auto>
<PageTitle title="Check your email" />
<CheckEmailIcon size={38} color="currentColor" />
<PageTitle title="Login" />
<Logo>
{env.TEAM_LOGO && env.DEPLOYMENT !== "hosted" ? (
<TeamLogo src={env.TEAM_LOGO} />
) : (
<OutlineLogo size={38} fill="currentColor" />
)}
</Logo>
<Heading centered>Check your email</Heading>
<Note>
A magic sign-in link has been sent to the email{" "}
<em>{emailLinkSentTo}</em>, no password needed.
</Note>
<br />
<ButtonLarge onClick={handleReset} fullwidth neutral>
Back to login
</ButtonLarge>
{isCreate ? (
<Heading centered>Create an account</Heading>
) : (
<Heading centered>Login to {config.name || "Outline"}</Heading>
)}
<Notices notice={getQueryVariable("notice")} />
{defaultService && (
<React.Fragment key={defaultService.id}>
<Service
isCreate={isCreate}
onEmailSuccess={this.handleEmailSuccess}
{...defaultService}
/>
{hasMultipleServices && (
<>
<Note>
You signed in with {defaultService.name} last time.
</Note>
<Or />
</>
)}
</React.Fragment>
)}
{config.services.map((service) => {
if (defaultService && service.id === defaultService.id) {
return null;
}
return (
<Service
key={service.id}
isCreate={isCreate}
onEmailSuccess={this.handleEmailSuccess}
{...service}
/>
);
})}
{isCreate && (
<Note>
Already have an account? Go to <Link to="/">login</Link>.
</Note>
)}
</Centered>
</Background>
);
}
return (
<Background>
{header}
<Centered align="center" justify="center" column auto>
<PageTitle title="Login" />
<Logo>
{env.TEAM_LOGO && env.DEPLOYMENT !== "hosted" ? (
<TeamLogo src={env.TEAM_LOGO} />
) : (
<OutlineLogo size={38} fill="currentColor" />
)}
</Logo>
{isCreate ? (
<Heading centered>Create an account</Heading>
) : (
<Heading centered>Login to {config.name || "Outline"}</Heading>
)}
<Notices notice={getQueryVariable("notice")} />
{defaultService && (
<React.Fragment key={defaultService.id}>
<Service
isCreate={isCreate}
onEmailSuccess={handleEmailSuccess}
{...defaultService}
/>
{hasMultipleServices && (
<>
<Note>You signed in with {defaultService.name} last time.</Note>
<Or />
</>
)}
</React.Fragment>
)}
{config.services.map((service) => {
if (defaultService && service.id === defaultService.id) {
return null;
}
return (
<Service
key={service.id}
isCreate={isCreate}
onEmailSuccess={handleEmailSuccess}
{...service}
/>
);
})}
{isCreate && (
<Note>
Already have an account? Go to <Link to="/">login</Link>.
</Note>
)}
</Centered>
</Background>
);
}
const CheckEmailIcon = styled(EmailIcon)`
@@ -227,4 +234,4 @@ const Centered = styled(Flex)`
margin: 0 auto;
`;
export default observer(Login);
export default inject("auth")(Login);
+4 -5
View File
@@ -282,11 +282,10 @@ class Search extends React.Component<Props> {
{showShortcutTip && (
<Fade>
<HelpText small>
<Trans
defaults="Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base"
values={{ meta: metaDisplay }}
components={{ em: <strong /> }}
/>
<Trans>
Use the <strong>{{ meta: metaDisplay }}+K</strong> shortcut to
search from anywhere in your knowledge base
</Trans>
</HelpText>
</Fade>
)}
+11 -11
View File
@@ -154,16 +154,13 @@ class Profile extends React.Component<Props> {
</form>
<DangerZone>
<h2>{t("Delete Account")}</h2>
<HelpText small>
<Trans>
You may delete your account at any time, note that this is
unrecoverable
</Trans>
</HelpText>
<Button onClick={this.toggleDeleteAccount} neutral>
{t("Delete account")}
</Button>
<LabelText>{t("Delete Account")}</LabelText>
<p>
{t(
"You may delete your account at any time, note that this is unrecoverable"
)}
. <a onClick={this.toggleDeleteAccount}>{t("Delete account")}</a>.
</p>
</DangerZone>
{this.showDeleteModal && (
<UserDelete onRequestClose={this.toggleDeleteAccount} />
@@ -174,7 +171,10 @@ class Profile extends React.Component<Props> {
}
const DangerZone = styled.div`
margin-top: 60px;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
position: absolute;
bottom: 16px;
`;
const ProfilePicture = styled(Flex)`

Some files were not shown because too many files have changed in this diff Show More