mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cb82f9349 | |||
| 7b7c1c0bc2 |
@@ -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
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
sharedmemory.heap_size=3221225472
|
||||
|
||||
module.system.node.resolve_dirname=node_modules
|
||||
module.system.node.resolve_dirname=app
|
||||
|
||||
Vendored
+1
-2
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -141,12 +135,6 @@ function DocumentMeta({
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
{showNestedDocuments && nestedDocumentsCount > 0 && (
|
||||
<span>
|
||||
· {nestedDocumentsCount}{" "}
|
||||
{t("nested document", { count: nestedDocumentsCount })}
|
||||
</span>
|
||||
)}
|
||||
{timeSinceNow()}
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>{" "}
|
||||
·{" "}
|
||||
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
|
||||
</a>{" "}
|
||||
· <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;
|
||||
`;
|
||||
|
||||
@@ -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};
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
export const id = "skip-nav";
|
||||
|
||||
export default function SkipNavContent() {
|
||||
return <div id={id} />;
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,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,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,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
@@ -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: {|
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -27,6 +27,7 @@ function NewDocumentMenu() {
|
||||
as={Link}
|
||||
to={newDocumentUrl(collections.orderedData[0].id)}
|
||||
icon={<PlusIcon />}
|
||||
small
|
||||
>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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
@@ -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,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 (
|
||||
|
||||
@@ -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> doesn’t contain any
|
||||
documents yet."
|
||||
values={{ collectionName }}
|
||||
components={{ em: <strong /> }}
|
||||
/>
|
||||
<Trans>
|
||||
<strong>{{ collectionName }}</strong> doesn’t 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("Can’t find the group you’re 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 who’s 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>
|
||||
)}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
<a href={share.url} target="_blank" rel="noreferrer">
|
||||
<a href={share.url} target="_blank">
|
||||
Preview
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 who’s 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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user