Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Moor 8e9beac59f test 2023-08-08 23:12:41 -04:00
Tom Moor a0f7c76405 Add support for SSL in development 2023-08-08 22:46:31 -04:00
361 changed files with 2838 additions and 4864 deletions
+1 -2
View File
@@ -21,7 +21,7 @@
"eslint-plugin-import",
"eslint-plugin-node",
"eslint-plugin-react",
"eslint-plugin-lodash"
"import"
],
"rules": {
"eqeqeq": 2,
@@ -55,7 +55,6 @@
],
"padding-line-between-statements": ["error", { "blankLine": "always", "prev": "*", "next": "export" }],
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
"lodash/import-scope": ["warn", "method"],
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"import/newline-after-import": 2,
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.71.0
Licensed Work: Outline 0.64.0
The Licensed Work is (c) 2020 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2027-08-18
Change Date: 2026-05-23
Change License: Apache License, Version 2.0
+1 -5
View File
@@ -96,10 +96,6 @@ Or to run migrations on test database:
yarn sequelize db:migrate --env test
```
# Activity
![Alt](https://repobeats.axiom.co/api/embed/ff2e4e6918afff1acf9deb72d1ba6b071d586178.svg "Repobeats analytics image")
# License
## License
Outline is [BSL 1.1 licensed](LICENSE).
+1 -1
View File
@@ -14,7 +14,6 @@ import {
BrowserIcon,
} from "outline-icons";
import * as React from "react";
import { isMac } from "@shared/utils/browser";
import {
developersUrl,
changelogUrl,
@@ -27,6 +26,7 @@ import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import Desktop from "~/utils/Desktop";
import { isMac } from "~/utils/browser";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import flattenDeep from "lodash/flattenDeep";
import { flattenDeep } from "lodash";
import * as React from "react";
import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
+1 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable prefer-rest-params */
/* global ga */
import escape from "lodash/escape";
import { escape } from "lodash";
import * as React from "react";
import { IntegrationService } from "@shared/types";
import env from "~/env";
+1 -4
View File
@@ -1,7 +1,4 @@
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import sortBy from "lodash/sortBy";
import uniq from "lodash/uniq";
import { sortBy, filter, uniq, isEqual } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+2 -7
View File
@@ -7,7 +7,6 @@ import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { homePath } from "~/utils/routeHelpers";
type Props = {
@@ -18,20 +17,16 @@ type Props = {
function CollectionDeleteDialog({ collection, onSubmit }: Props) {
const team = useCurrentTeam();
const { ui } = useStores();
const { showToast } = useToasts();
const history = useHistory();
const { t } = useTranslation();
const handleSubmit = async () => {
const redirect = collection.id === ui.activeCollectionId;
await collection.delete();
onSubmit();
if (redirect) {
history.push(homePath());
}
await collection.delete();
onSubmit();
showToast(t("Collection deleted"), { type: "success" });
};
return (
+5 -38
View File
@@ -14,48 +14,15 @@ function ConnectionStatus() {
const theme = useTheme();
const { t } = useTranslation();
const codeToMessage = {
1009: {
title: t("Document is too large"),
body: t(
"This document has reached the maximum size and can no longer be edited"
),
},
4401: {
title: t("Authentication failed"),
body: t("Please try logging out and back in again"),
},
4403: {
title: t("Authorization failed"),
body: t("You may have lost access to this document, try reloading"),
},
4503: {
title: t("Too many users connected to document"),
body: t("Your edits will sync once other users leave the document"),
},
};
const message = ui.multiplayerErrorCode
? codeToMessage[ui.multiplayerErrorCode]
: undefined;
return ui.multiplayerStatus === "connecting" ||
ui.multiplayerStatus === "disconnected" ? (
<Tooltip
tooltip={
message ? (
<Centered>
<strong>{message.title}</strong>
<br />
{message.body}
</Centered>
) : (
<Centered>
<strong>{t("Server connection lost")}</strong>
<br />
{t("Edits you make will sync once youre online")}
</Centered>
)
<Centered>
<strong>{t("Server connection lost")}</strong>
<br />
{t("Edits you make will sync once youre online")}
</Centered>
}
placement="bottom"
>
+1 -1
View File
@@ -151,7 +151,7 @@ const ContextMenu: React.FC<Props> = ({
ref={backgroundRef}
hiddenScrollbars
style={
topAnchor && !isMobile
topAnchor
? {
maxHeight,
}
+2 -2
View File
@@ -279,8 +279,8 @@ const Heading = styled.h3`
overflow: hidden;
color: ${s("text")};
font-family: ${s("fontFamily")};
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
export default observer(DocumentCard);
+1 -6
View File
@@ -1,10 +1,5 @@
import FuzzySearch from "fuzzy-search";
import concat from "lodash/concat";
import difference from "lodash/difference";
import fill from "lodash/fill";
import filter from "lodash/filter";
import includes from "lodash/includes";
import map from "lodash/map";
import { includes, difference, concat, filter, map, fill } from "lodash";
import { observer } from "mobx-react";
import { StarredIcon, DocumentIcon } from "outline-icons";
import * as React from "react";
+2 -2
View File
@@ -262,8 +262,8 @@ const Heading = styled.h3<{ rtl?: boolean }>`
margin-bottom: 0.25em;
white-space: nowrap;
color: ${s("text")};
font-family: ${s("fontFamily")};
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
const StarPositioner = styled(Flex)`
+1 -1
View File
@@ -1,4 +1,4 @@
import sortBy from "lodash/sortBy";
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+2 -6
View File
@@ -1,6 +1,4 @@
import deburr from "lodash/deburr";
import difference from "lodash/difference";
import sortBy from "lodash/sortBy";
import { deburr, difference, sortBy } from "lodash";
import { observer } from "mobx-react";
import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
@@ -48,7 +46,6 @@ export type Props = Optional<
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
previewsDisabled?: boolean;
onHeadingsChange?: (headings: Heading[]) => void;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => any;
@@ -63,7 +60,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
onHeadingsChange,
onCreateCommentMark,
onDeleteCommentMark,
previewsDisabled,
} = props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
@@ -341,7 +337,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
userPreferences={preferences}
dictionary={dictionary}
{...props}
onHoverLink={previewsDisabled ? undefined : handleLinkActive}
onHoverLink={handleLinkActive}
onClickLink={handleClickLink}
onSearchLink={handleSearchLink}
onChange={handleChange}
+1 -1
View File
@@ -80,7 +80,7 @@ const Note = styled(Text)`
margin-bottom: 0;
line-height: 1.2em;
font-size: 14px;
font-weight: 500;
font-weight: 400;
color: ${s("textTertiary")};
`;
+2 -2
View File
@@ -1,4 +1,4 @@
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import { transparentize } from "polished";
@@ -6,7 +6,6 @@ import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import { supportsPassiveListener } from "@shared/utils/browser";
import Button from "~/components/Button";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
@@ -15,6 +14,7 @@ import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import Desktop from "~/utils/Desktop";
import { supportsPassiveListener } from "~/utils/browser";
type Props = {
left?: React.ReactNode;
+1 -1
View File
@@ -1,4 +1,4 @@
import escapeRegExp from "lodash/escapeRegExp";
import { escapeRegExp } from "lodash";
import * as React from "react";
import replace from "string-replace-to-array";
import styled from "styled-components";
+1 -1
View File
@@ -4,7 +4,7 @@ import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
export const CARD_MARGIN = 10;
export const CARD_MARGIN = 16;
const NUMBER_OF_LINES = 10;
+27 -59
View File
@@ -2,7 +2,7 @@ import { m } from "framer-motion";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { depths } from "@shared/styles";
import { depths, s } from "@shared/styles";
import { UnfurlType } from "@shared/types";
import LoadingIndicator from "~/components/LoadingIndicator";
import useEventListener from "~/hooks/useEventListener";
@@ -27,14 +27,6 @@ type Props = {
onClose: () => void;
};
enum Direction {
UP,
DOWN,
}
const POINTER_HEIGHT = 22;
const POINTER_WIDTH = 22;
function HoverPreviewInternal({ element, onClose }: Props) {
const url = element.href || element.dataset.url;
const [isVisible, setVisible] = React.useState(false);
@@ -44,46 +36,31 @@ function HoverPreviewInternal({ element, onClose }: Props) {
const stores = useStores();
const [cardLeft, setCardLeft] = React.useState(0);
const [cardTop, setCardTop] = React.useState(0);
const [pointerLeft, setPointerLeft] = React.useState(0);
const [pointerTop, setPointerTop] = React.useState(0);
const [pointerDir, setPointerDir] = React.useState(Direction.UP);
const [pointerOffset, setPointerOffset] = React.useState(0);
React.useLayoutEffect(() => {
if (isVisible && cardRef.current) {
const elem = element.getBoundingClientRect();
const card = cardRef.current.getBoundingClientRect();
let cTop = elem.bottom + window.scrollY + CARD_MARGIN;
let pTop = -POINTER_HEIGHT;
let pDir = Direction.UP;
if (cTop + card.height > window.innerHeight + window.scrollY) {
// shift card upwards if it goes out of screen
const bottom = elem.top + window.scrollY;
cTop = bottom - card.height;
// shift a little further to leave some margin between card and element boundary
cTop -= CARD_MARGIN;
// pointer should be shifted downwards to align with card's bottom
pTop = card.height;
pDir = Direction.DOWN;
}
setCardTop(cTop);
setPointerTop(pTop);
setPointerDir(pDir);
const top = elem.bottom + window.scrollY;
setCardTop(top);
let cLeft = elem.left;
let pLeft = elem.width / 2;
if (cLeft + card.width > window.innerWidth) {
let left = elem.left;
let pointerOffset = elem.width / 2;
if (left + card.width > window.innerWidth) {
// shift card leftwards by the amount it went out of screen
let shiftBy = cLeft + card.width - window.innerWidth;
// shift a little further to leave some margin between card and window boundary
let shiftBy = left + card.width - window.innerWidth;
// shift a littler further to leave some margin between card and window boundary
shiftBy += CARD_MARGIN;
cLeft -= shiftBy;
left -= shiftBy;
// shift pointer rightwards by same amount so as to position it back correctly
pLeft += shiftBy;
pointerOffset += shiftBy;
}
setCardLeft(cLeft);
setPointerLeft(pLeft);
setCardLeft(left);
setPointerOffset(pointerOffset);
}
}, [isVisible, element]);
@@ -216,11 +193,7 @@ function HoverPreviewInternal({ element, onClose }: Props) {
description={data.description}
/>
)}
<Pointer
top={pointerTop}
left={pointerLeft}
direction={pointerDir}
/>
<Pointer offset={pointerOffset} />
</Animate>
) : null}
</Position>
@@ -244,6 +217,7 @@ const Animate = styled(m.div)`
`;
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
z-index: ${depths.hoverPreview};
display: flex;
@@ -253,11 +227,11 @@ const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`;
const Pointer = styled.div<{ top: number; left: number; direction: Direction }>`
top: ${(props) => props.top}px;
left: ${(props) => props.left}px;
width: ${POINTER_WIDTH}px;
height: ${POINTER_HEIGHT}px;
const Pointer = styled.div<{ offset: number }>`
top: -22px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
@@ -267,26 +241,20 @@ const Pointer = styled.div<{ top: number; left: number; direction: Direction }>`
content: "";
display: inline-block;
position: absolute;
${({ direction }) => (direction === Direction.UP ? "bottom: 0" : "top: 0")};
${({ direction }) => (direction === Direction.UP ? "right: 0" : "left: 0")};
bottom: 0;
right: 0;
}
&:before {
border: 8px solid transparent;
${({ direction, theme }) =>
direction === Direction.UP
? `border-bottom-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`
: `border-top-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`};
${({ direction }) =>
direction === Direction.UP ? "right: -1px" : "left: -1px"};
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
}
&:after {
border: 7px solid transparent;
${({ direction, theme }) =>
direction === Direction.UP
? `border-bottom-color: ${theme.menuBackground}`
: `border-top-color: ${theme.menuBackground}`};
border-bottom-color: ${s("menuBackground")};
}
`;
@@ -26,9 +26,9 @@ const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
) {
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column ref={ref}>
<Flex column>
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
<Card>
<Card ref={ref}>
<CardContent>
<Flex column>
<Title>{title}</Title>
+1 -1
View File
@@ -18,7 +18,7 @@ export default function InputSelectPermission(
const handleChange = React.useCallback(
(value) => {
if (value === "no_access") {
value = null;
value = "";
}
onChange?.(value);
+1 -1
View File
@@ -1,4 +1,4 @@
import find from "lodash/find";
import { find } from "lodash";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
+1 -1
View File
@@ -1,4 +1,4 @@
import times from "lodash/times";
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Fade from "~/components/Fade";
+5 -3
View File
@@ -94,9 +94,11 @@ const Modal: React.FC<Props> = ({
{title}
</Text>
)}
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
<Text as="span" size="large">
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
</Text>
</Header>
</Centered>
</Small>
@@ -1,18 +1,19 @@
import { observer } from "mobx-react";
import { SubscribeIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import styled, { useTheme } from "styled-components";
import { s } from "@shared/styles";
import useStores from "~/hooks/useStores";
import Relative from "../Sidebar/components/Relative";
const NotificationIcon = () => {
const { notifications } = useStores();
const theme = useTheme();
const count = notifications.approximateUnreadCount;
return (
<Relative style={{ height: 24 }}>
<SubscribeIcon />
<SubscribeIcon color={theme.textTertiary} />
{count > 0 && <Badge />}
</Relative>
);
@@ -64,7 +64,6 @@ function NotificationListItem({ notification, onNavigate }: Props) {
{notification.comment && (
<StyledCommentEditor
defaultValue={toJS(notification.comment.data)}
previewsDisabled
/>
)}
</Flex>
+1 -1
View File
@@ -1,4 +1,4 @@
import isEqual from "lodash/isEqual";
import { isEqual } from "lodash";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
+1 -1
View File
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+13 -29
View File
@@ -1,11 +1,5 @@
import { observer } from "mobx-react";
import {
EditIcon,
SearchIcon,
ShapesIcon,
HomeIcon,
SidebarIcon,
} from "outline-icons";
import { EditIcon, SearchIcon, ShapesIcon, HomeIcon } from "outline-icons";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
@@ -20,7 +14,7 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import OrganizationMenu from "~/menus/OrganizationMenu";
import { metaDisplay } from "~/utils/keyboard";
import Desktop from "~/utils/Desktop";
import {
homePath,
draftsPath,
@@ -28,23 +22,21 @@ import {
searchPath,
} from "~/utils/routeHelpers";
import TeamLogo from "../TeamLogo";
import Tooltip from "../Tooltip";
import Sidebar from "./Sidebar";
import ArchiveLink from "./components/ArchiveLink";
import Collections from "./components/Collections";
import DragPlaceholder from "./components/DragPlaceholder";
import HeaderButton, { HeaderButtonProps } from "./components/HeaderButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarAction from "./components/SidebarAction";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import SidebarLink from "./components/SidebarLink";
import Starred from "./components/Starred";
import ToggleButton from "./components/ToggleButton";
import TrashLink from "./components/TrashLink";
function AppSidebar() {
const { t } = useTranslation();
const { documents, ui } = useStores();
const { documents } = useStores();
const team = useCurrentTeam();
const user = useCurrentUser();
const can = usePolicy(team);
@@ -73,31 +65,23 @@ function AppSidebar() {
<DragPlaceholder />
<OrganizationMenu>
{(props: SidebarButtonProps) => (
<SidebarButton
{(props: HeaderButtonProps) => (
<HeaderButton
{...props}
title={team.name}
image={
<TeamLogo
model={team}
size={24}
size={Desktop.hasInsetTitlebar() ? 24 : 32}
alt={t("Logo")}
style={{ marginLeft: 4 }}
/>
}
>
<Tooltip
tooltip={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
onClick={ui.toggleCollapsedSidebar}
/>
</Tooltip>
</SidebarButton>
style={
// Move the logo over to align with smaller size
Desktop.hasInsetTitlebar() ? { paddingLeft: 8 } : undefined
}
showDisclosure
/>
)}
</OrganizationMenu>
<Scrollable flex shadow>
+7 -22
View File
@@ -1,6 +1,6 @@
import groupBy from "lodash/groupBy";
import { groupBy } from "lodash";
import { observer } from "mobx-react";
import { BackIcon, SidebarIcon } from "outline-icons";
import { BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
@@ -8,21 +8,17 @@ import styled from "styled-components";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import useStores from "~/hooks/useStores";
import Desktop from "~/utils/Desktop";
import isCloudHosted from "~/utils/isCloudHosted";
import { metaDisplay } from "~/utils/keyboard";
import Tooltip from "../Tooltip";
import Sidebar from "./Sidebar";
import Header from "./components/Header";
import HeaderButton from "./components/HeaderButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarButton from "./components/SidebarButton";
import SidebarLink from "./components/SidebarLink";
import ToggleButton from "./components/ToggleButton";
import Version from "./components/Version";
function SettingsSidebar() {
const { ui } = useStores();
const { t } = useTranslation();
const history = useHistory();
const configs = useSettingsConfig();
@@ -35,23 +31,12 @@ function SettingsSidebar() {
return (
<Sidebar>
<HistoryNavigation />
<SidebarButton
<HeaderButton
title={t("Return to App")}
image={<StyledBackIcon />}
onClick={returnToApp}
>
<Tooltip
tooltip={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
onClick={ui.toggleCollapsedSidebar}
/>
</Tooltip>
</SidebarButton>
minHeight={Desktop.hasInsetTitlebar() ? undefined : 48}
/>
<Flex auto column>
<Scrollable shadow>
+2 -2
View File
@@ -11,9 +11,9 @@ import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import HeaderButton from "./components/HeaderButton";
import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
import SidebarButton from "./components/SidebarButton";
type Props = {
rootNode: NavigationNode;
@@ -28,7 +28,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
return (
<Sidebar>
{team && (
<SidebarButton
<HeaderButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
onClick={() =>
+64 -66
View File
@@ -1,8 +1,9 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Portal } from "react-portal";
import { useLocation } from "react-router-dom";
import styled, { css, useTheme } from "styled-components";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import Flex from "~/components/Flex";
@@ -10,29 +11,29 @@ import useMenuContext from "~/hooks/useMenuContext";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import AccountMenu from "~/menus/AccountMenu";
import { fadeOnDesktopBackgrounded } from "~/styles";
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import { fadeIn } from "~/styles/animations";
import Desktop from "~/utils/Desktop";
import Avatar from "../Avatar";
import NotificationIcon from "../Notifications/NotificationIcon";
import NotificationsPopover from "../Notifications/NotificationsPopover";
import HeaderButton, { HeaderButtonProps } from "./components/HeaderButton";
import ResizeBorder from "./components/ResizeBorder";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import ToggleButton from "./components/ToggleButton";
import Toggle, { ToggleButton, Positioner } from "./components/Toggle";
const ANIMATION_MS = 250;
type Props = {
children: React.ReactNode;
className?: string;
};
const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{ children, className }: Props,
{ children }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const [isCollapsing, setCollapsing] = React.useState(false);
const theme = useTheme();
const { t } = useTranslation();
const { ui, auth } = useStores();
const location = useLocation();
const previousLocation = usePrevious(location);
@@ -45,7 +46,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isHovering, setHovering] = React.useState(false);
const [isAnimating, setAnimating] = React.useState(false);
const [isResizing, setResizing] = React.useState(false);
const isSmallerThanMinimum = width < minWidth;
@@ -99,22 +99,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
[width]
);
const handlePointerMove = React.useCallback(() => {
setHovering(true);
}, []);
const handlePointerLeave = React.useCallback(
(ev) => {
setHovering(
ev.pageX < width &&
ev.pageX > 0 &&
ev.pageY < window.innerHeight &&
ev.pageY > 0
);
},
[width]
);
React.useEffect(() => {
if (isAnimating) {
setTimeout(() => setAnimating(false), ANIMATION_MS);
@@ -163,19 +147,23 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
[width]
);
const toggleStyle = React.useMemo(
() => ({
right: "auto",
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
}),
[width, theme.sidebarCollapsedWidth, collapsed]
);
return (
<>
<Container
ref={ref}
style={style}
$isHovering={isHovering}
$isAnimating={isAnimating}
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
className={className}
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
column
>
{ui.mobileSidebarVisible && (
@@ -187,32 +175,26 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{user && (
<AccountMenu>
{(props: SidebarButtonProps) => (
<SidebarButton
{(props: HeaderButtonProps) => (
<HeaderButton
{...props}
showMoreMenu
title={user.name}
position="bottom"
image={
<Avatar
<StyledAvatar
alt={user.name}
model={user}
size={24}
showBorder={false}
style={{ marginLeft: 4 }}
/>
}
>
<NotificationsPopover>
{(rest: SidebarButtonProps) => (
<SidebarButton
{...rest}
position="bottom"
image={<NotificationIcon />}
/>
{(rest: HeaderButtonProps) => (
<HeaderButton {...rest} image={<NotificationIcon />} />
)}
</NotificationsPopover>
</SidebarButton>
</HeaderButton>
)}
</AccountMenu>
)}
@@ -220,11 +202,28 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
onMouseDown={handleMouseDown}
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
/>
{ui.sidebarIsClosed && (
<Toggle
onClick={ui.toggleCollapsedSidebar}
direction={"right"}
aria-label={t("Expand")}
/>
)}
</Container>
<Toggle
style={toggleStyle}
onClick={ui.toggleCollapsedSidebar}
direction={ui.sidebarIsClosed ? "right" : "left"}
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
/>
</>
);
});
const StyledAvatar = styled(Avatar)`
margin-left: 4px;
`;
const Backdrop = styled.a`
animation: ${fadeIn} 250ms ease-in-out;
position: fixed;
@@ -241,33 +240,16 @@ type ContainerProps = {
$mobileSidebarVisible: boolean;
$isAnimating: boolean;
$isSmallerThanMinimum: boolean;
$isHovering: boolean;
$collapsed: boolean;
};
const hoverStyles = (props: ContainerProps) => `
transform: none;
box-shadow: ${
props.$collapsed
? "rgba(0, 0, 0, 0.2) 1px 0 4px"
: props.$isSmallerThanMinimum
? "rgba(0, 0, 0, 0.1) inset -1px 0 2px"
: "none"
};
${ToggleButton} {
opacity: 1;
}
`;
const Container = styled(Flex)<ContainerProps>`
position: fixed;
top: 0;
bottom: 0;
width: 100%;
background: ${s("sidebarBackground")};
transition: box-shadow 100ms ease-in-out, opacity 100ms ease-in-out,
transform 100ms ease-out,
transition: box-shadow 100ms ease-in-out, transform 100ms ease-out,
${s("backgroundTransition")}
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
@@ -277,17 +259,19 @@ const Container = styled(Flex)<ContainerProps>`
z-index: ${depths.sidebar};
max-width: 80%;
min-width: 280px;
padding-top: ${Desktop.hasInsetTitlebar() ? 36 : 0}px;
${draggableOnDesktop()}
${fadeOnDesktopBackgrounded()}
${Positioner} {
display: none;
}
@media print {
display: none;
transform: none;
}
& > div {
opacity: ${(props) => (props.$collapsed && !props.$isHovering ? "0" : "1")};
}
${breakpoint("tablet")`
margin: 0;
min-width: 0;
@@ -296,14 +280,28 @@ const Container = styled(Flex)<ContainerProps>`
? `calc(-100% + ${Desktop.hasInsetTitlebar() ? 8 : 16}px)`
: 0});
${(props: ContainerProps) => props.$isHovering && css(hoverStyles)}
&:hover,
&:focus-within {
${hoverStyles}
transform: none;
box-shadow: ${(props: ContainerProps) =>
props.$collapsed
? "rgba(0, 0, 0, 0.2) 1px 0 4px"
: props.$isSmallerThanMinimum
? "rgba(0, 0, 0, 0.1) inset -1px 0 2px"
: "none"};
& > div {
${Positioner} {
display: block;
}
${ToggleButton} {
opacity: 1;
}
}
}
&:not(:hover):not(:focus-within) > div {
opacity: ${(props: ContainerProps) => (props.$collapsed ? "0" : "1")};
transition: opacity 100ms ease-in-out;
}
`};
`;
@@ -1,66 +1,58 @@
import { MoreIcon } from "outline-icons";
import { ExpandedIcon, MoreIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import { draggableOnDesktop, undraggableOnDesktop } from "~/styles";
import Desktop from "~/utils/Desktop";
import { undraggableOnDesktop } from "~/styles";
export type SidebarButtonProps = React.ComponentProps<typeof Button> & {
position: "top" | "bottom";
export type HeaderButtonProps = React.ComponentProps<typeof Button> & {
title: React.ReactNode;
image: React.ReactNode;
minHeight?: number;
rounded?: boolean;
showDisclosure?: boolean;
showMoreMenu?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
};
const SidebarButton = React.forwardRef<HTMLButtonElement, SidebarButtonProps>(
function _SidebarButton(
const HeaderButton = React.forwardRef<HTMLButtonElement, HeaderButtonProps>(
function _HeaderButton(
{
position = "top",
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: SidebarButtonProps,
}: HeaderButtonProps,
ref
) {
return (
<Container
justify="space-between"
align="center"
shrink={false}
$position={position}
>
<Flex justify="space-between" align="center" shrink={false}>
<Button
{...rest}
$position={position}
minHeight={minHeight}
as="button"
ref={ref}
role="button"
>
<Title gap={8} align="center">
{image}
{title && <Text as="span">{title}</Text>}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Container>
</Flex>
);
}
);
const Container = styled(Flex)<{ $position: "top" | "bottom" }>`
padding-top: ${(props) =>
props.$position === "top" && Desktop.hasInsetTitlebar() ? 36 : 0}px;
${draggableOnDesktop()}
`;
const Title = styled(Flex)`
color: ${s("text")};
flex-shrink: 1;
flex-grow: 1;
text-overflow: ellipsis;
@@ -68,20 +60,19 @@ const Title = styled(Flex)`
overflow: hidden;
`;
const Button = styled(Flex)<{
$position: "top" | "bottom";
}>`
const Button = styled(Flex)<{ minHeight: number }>`
flex: 1;
color: ${s("textTertiary")};
align-items: center;
padding: 4px;
padding: 8px 4px;
font-size: 15px;
font-weight: 500;
border-radius: 4px;
margin: 8px 0;
border: 0;
margin: ${(props) => (props.$position === "top" ? 16 : 8)}px 0;
background: none;
flex-shrink: 0;
min-height: ${(props) => props.minHeight}px;
-webkit-appearance: none;
text-decoration: none;
@@ -108,4 +99,4 @@ const Button = styled(Flex)<{
}
`;
export default SidebarButton;
export default HeaderButton;
@@ -3,12 +3,12 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { isMac } from "@shared/utils/browser";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useKeyDown from "~/hooks/useKeyDown";
import Desktop from "~/utils/Desktop";
import { isMac } from "~/utils/browser";
function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
const { t } = useTranslation();
@@ -1,4 +1,3 @@
import includes from "lodash/includes";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -7,15 +6,14 @@ import Collection from "~/models/Collection";
import Document from "~/models/Document";
import useStores from "~/hooks/useStores";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import { descendants } from "~/utils/tree";
import Disclosure from "./Disclosure";
import SidebarLink from "./SidebarLink";
type Props = {
node: NavigationNode;
collection?: Collection;
activeDocumentId?: string;
activeDocument?: Document;
activeDocumentId: string | undefined;
activeDocument: Document | undefined;
isDraft?: boolean;
depth: number;
index: number;
@@ -43,19 +41,10 @@ function DocumentLink(
const hasChildDocuments =
!!node.children.length || activeDocument?.parentDocumentId === node.id;
const document = documents.get(node.id);
const showChildren = React.useMemo(
() =>
!!(
hasChildDocuments &&
((activeDocumentId &&
includes(
descendants(node).map((n) => n.id),
activeDocumentId
)) ||
isActiveDocument ||
depth <= 1)
),
[hasChildDocuments, activeDocumentId, isActiveDocument, depth, node]
() => !!hasChildDocuments,
[hasChildDocuments]
);
const [expanded, setExpanded] = React.useState(showChildren);
@@ -66,6 +55,12 @@ function DocumentLink(
}
}, [showChildren]);
React.useEffect(() => {
if (isActiveDocument) {
setExpanded(true);
}
}, [isActiveDocument]);
const handleDisclosureClick = React.useCallback(
(ev: React.SyntheticEvent) => {
ev.preventDefault();
@@ -292,7 +292,7 @@ const Label = styled.div`
position: relative;
width: 100%;
max-height: 4.8em;
line-height: 24px;
line-height: 1.6;
* {
unicode-bidi: plaintext;
@@ -0,0 +1,106 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import Arrow from "~/components/Arrow";
import useEventListener from "~/hooks/useEventListener";
type Props = {
direction: "left" | "right";
style?: React.CSSProperties;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
};
const Toggle = React.forwardRef<HTMLButtonElement, Props>(function Toggle_(
{ direction = "left", onClick, style }: Props,
ref
) {
const { t } = useTranslation();
const [hovering, setHovering] = React.useState(false);
const positionRef = React.useRef<HTMLDivElement>(null);
// Not using CSS hover here so that we can disable pointer events on this
// div and allow click through to the editor elements behind.
useEventListener("mousemove", (event: MouseEvent) => {
if (!positionRef.current) {
return;
}
const bound = positionRef.current.getBoundingClientRect();
const withinBounds =
event.clientX >= bound.left && event.clientX <= bound.right;
if (withinBounds !== hovering) {
setHovering(withinBounds);
}
});
return (
<Positioner style={style} ref={positionRef} $hovering={hovering}>
<ToggleButton
ref={ref}
$direction={direction}
onClick={onClick}
aria-label={t("Toggle sidebar")}
>
<Arrow />
</ToggleButton>
</Positioner>
);
});
export const ToggleButton = styled.button<{ $direction?: "left" | "right" }>`
opacity: 0;
background: none;
transition: opacity 100ms ease-in-out;
transform: translateY(-50%)
scaleX(${(props) => (props.$direction === "left" ? 1 : -1)});
position: fixed;
top: 50vh;
padding: 8px;
border: 0;
pointer-events: none;
color: ${s("divider")};
&:active {
color: ${s("sidebarText")};
}
${breakpoint("tablet")`
pointer-events: all;
cursor: var(--pointer);
`}
@media (hover: none) {
opacity: 1;
}
`;
export const Positioner = styled.div<{ $hovering: boolean }>`
display: none;
z-index: 2;
position: absolute;
top: 0;
bottom: 0;
right: -30px;
width: 30px;
pointer-events: none;
&:focus-within ${ToggleButton} {
opacity: 1;
}
${(props) =>
props.$hovering &&
css`
${ToggleButton} {
opacity: 1;
}
`}
${breakpoint("tablet")`
display: block;
`}
`;
export default Toggle;
@@ -1,15 +0,0 @@
import styled from "styled-components";
import { hover } from "~/styles";
import SidebarButton from "./SidebarButton";
const ToggleButton = styled(SidebarButton)`
opacity: 0;
transition: opacity 100ms ease-in-out;
&:${hover},
&:active {
opacity: 1;
}
`;
export default ToggleButton;
+1 -1
View File
@@ -1,4 +1,4 @@
import isEqual from "lodash/isEqual";
import { isEqual } from "lodash";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
+1 -2
View File
@@ -4,8 +4,7 @@ import Avatar from "./Avatar";
const TeamLogo = styled(Avatar)`
border-radius: 4px;
box-shadow: inset 0 0 0 1px ${s("divider")};
border: 0;
border: 1px solid ${s("divider")};
`;
export default TeamLogo;
+8 -13
View File
@@ -1,4 +1,4 @@
import styled, { css } from "styled-components";
import styled from "styled-components";
type Props = {
type?: "secondary" | "tertiary" | "danger";
@@ -14,7 +14,7 @@ type Props = {
*/
const Text = styled.p<Props>`
margin-top: 0;
text-align: ${(props) => (props.dir ? props.dir : "inherit")};
text-align: ${(props) => (props.dir ? props.dir : "initial")};
color: ${(props) =>
props.type === "secondary"
? props.theme.textSecondary
@@ -31,17 +31,12 @@ const Text = styled.p<Props>`
: props.size === "xsmall"
? "13px"
: "inherit"};
${(props) =>
props.weight &&
css`
font-weight: ${props.weight === "bold"
? 500
: props.weight === "normal"
? 400
: "inherit"};
`}
font-weight: ${(props) =>
props.weight === "bold"
? 500
: props.weight === "normal"
? "normal"
: "inherit"};
white-space: normal;
user-select: ${(props) => (props.selectable ? "text" : "none")};
`;
+2 -3
View File
@@ -1,6 +1,5 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { UserRole } from "@shared/types";
import User from "~/models/User";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Input from "~/components/Input";
@@ -16,7 +15,7 @@ export function UserChangeToViewerDialog({ user, onSubmit }: Props) {
const { users } = useStores();
const handleSubmit = async () => {
await users.demote(user, UserRole.Viewer);
await users.demote(user, "viewer");
onSubmit();
};
@@ -42,7 +41,7 @@ export function UserChangeToMemberDialog({ user, onSubmit }: Props) {
const { users } = useStores();
const handleSubmit = async () => {
await users.demote(user, UserRole.Member);
await users.demote(user, "member");
onSubmit();
};
+1 -1
View File
@@ -1,5 +1,5 @@
import invariant from "invariant";
import find from "lodash/find";
import { find } from "lodash";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
+1 -3
View File
@@ -9,7 +9,6 @@ import { Portal } from "~/components/Portal";
import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
import useMediaQuery from "~/hooks/useMediaQuery";
import useMobile from "~/hooks/useMobile";
import useViewportHeight from "~/hooks/useViewportHeight";
import Logger from "~/utils/Logger";
import { useEditor } from "./EditorContext";
@@ -41,7 +40,6 @@ function usePosition({
const { selection } = view.state;
const { width: menuWidth, height: menuHeight } = useComponentSize(menuRef);
const viewportHeight = useViewportHeight();
const isMobile = useMobile();
const isTouchDevice = useMediaQuery("(hover: none) and (pointer: coarse)");
if (!active || !menuWidth || !menuHeight || !menuRef.current) {
@@ -50,7 +48,7 @@ function usePosition({
// If we're on a mobile device then stick the floating toolbar to the bottom
// of the screen above the virtual keyboard.
if (isTouchDevice && isMobile && viewportHeight) {
if (isTouchDevice && viewportHeight) {
return {
left: 0,
right: 0,
+2 -2
View File
@@ -1,4 +1,4 @@
import some from "lodash/some";
import { some } from "lodash";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import * as React from "react";
import createAndInsertLink from "@shared/editor/commands/createAndInsertLink";
@@ -231,7 +231,7 @@ export default function SelectionToolbar(props: Props) {
} else if (rowIndex !== undefined) {
items = getTableRowMenuItems(state, rowIndex, dictionary);
} else if (isImageSelection) {
items = readOnly ? [] : getImageMenuItems(state, dictionary);
items = getImageMenuItems(state, dictionary);
} else if (isDividerSelection) {
items = getDividerMenuItems(state, dictionary);
} else if (readOnly) {
+1 -1
View File
@@ -1,5 +1,5 @@
import commandScore from "command-score";
import capitalize from "lodash/capitalize";
import { capitalize } from "lodash";
import * as React from "react";
import { Trans } from "react-i18next";
import { VisuallyHidden } from "reakit/VisuallyHidden";
-6
View File
@@ -8,10 +8,4 @@ declare global {
const env = window.env;
if (!env) {
throw new Error(
"Config could not be be parsed. \nSee: https://docs.getoutline.com/s/hosting/doc/troubleshooting-HXckrzCqDJ#h-config-could-not-be-parsed"
);
}
export default env;
+1 -1
View File
@@ -1,5 +1,5 @@
import { useRegisterActions } from "kbar";
import flattenDeep from "lodash/flattenDeep";
import { flattenDeep } from "lodash";
import { useLocation } from "react-router-dom";
import { actionToKBar } from "~/actions";
import { Action } from "~/types";
+1 -1
View File
@@ -1,4 +1,4 @@
import find from "lodash/find";
import { find } from "lodash";
import * as React from "react";
import embeds, { EmbedDescriptor } from "@shared/editor/embeds";
import { IntegrationType } from "@shared/types";
+1 -1
View File
@@ -1,4 +1,4 @@
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import * as React from "react";
import { Minute } from "@shared/utils/time";
+1 -1
View File
@@ -1,4 +1,4 @@
import noop from "lodash/noop";
import { noop } from "lodash";
import React from "react";
type MenuContextType = {
+1 -1
View File
@@ -1,4 +1,4 @@
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import * as React from "react";
import useEventListener from "./useEventListener";
import useIsMounted from "./useIsMounted";
+2 -2
View File
@@ -1,8 +1,8 @@
// Based on https://github.com/rehooks/window-scroll-position which is no longer
// maintained.
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import { useState, useEffect } from "react";
import { supportsPassiveListener } from "@shared/utils/browser";
import { supportsPassiveListener } from "~/utils/browser";
const getPosition = () => ({
x: window.pageXOffset,
+1 -1
View File
@@ -1,4 +1,4 @@
import pick from "lodash/pick";
import { pick } from "lodash";
import { set, observable } from "mobx";
import Logger from "~/utils/Logger";
import { getFieldsForModel } from "./decorators/Field";
+1 -1
View File
@@ -1,4 +1,4 @@
import trim from "lodash/trim";
import { trim } from "lodash";
import { action, computed, observable, reaction, runInAction } from "mobx";
import {
CollectionPermission,
+1 -1
View File
@@ -1,5 +1,5 @@
import { addDays, differenceInDays } from "date-fns";
import floor from "lodash/floor";
import { floor } from "lodash";
import { action, autorun, computed, observable, set } from "mobx";
import { ExportContentType } from "@shared/types";
import type { NavigationNode } from "@shared/types";
+5 -6
View File
@@ -7,9 +7,8 @@ import {
NotificationEventType,
UserPreference,
UserPreferences,
UserRole,
} from "@shared/types";
import type { NotificationSettings } from "@shared/types";
import type { Role, NotificationSettings } from "@shared/types";
import { client } from "~/utils/ApiClient";
import ParanoidModel from "./ParanoidModel";
import Field from "./decorators/Field";
@@ -75,13 +74,13 @@ class User extends ParanoidModel {
}
@computed
get role(): UserRole {
get role(): Role {
if (this.isAdmin) {
return UserRole.Admin;
return "admin";
} else if (this.isViewer) {
return UserRole.Viewer;
return "viewer";
} else {
return UserRole.Member;
return "member";
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import sortBy from "lodash/sortBy";
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+1 -2
View File
@@ -1,4 +1,4 @@
import intersection from "lodash/intersection";
import { intersection } from "lodash";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
@@ -56,7 +56,6 @@ class CollectionNew extends React.Component<Props> {
icon: this.icon,
color: this.color,
permission: this.permission,
documents: [],
},
this.props.collections
);
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -1,4 +1,4 @@
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+1 -1
View File
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { AllSelection } from "prosemirror-state";
+57 -63
View File
@@ -104,6 +104,7 @@ function DocumentHeader({
const { isDeleted, isTemplate } = document;
const can = usePolicy(document?.id);
const canToggleEmbeds = team?.documentEmbeds;
const canEdit = can.update && !isEditing;
const toc = (
<Tooltip
tooltip={ui.tocVisible ? t("Hide contents") : t("Show contents")}
@@ -137,7 +138,7 @@ function DocumentHeader({
to={documentEditPath(document)}
neutral
>
{isMobile ? null : t("Edit")}
{t("Edit")}
</Button>
</Tooltip>
</Action>
@@ -184,7 +185,7 @@ function DocumentHeader({
actions={
<>
{appearanceAction}
{can.update && !isEditing ? editAction : <div />}
{canEdit ? editAction : <div />}
</>
}
/>
@@ -249,56 +250,45 @@ function DocumentHeader({
disabled={savingIsDisabled}
neutral={isDraft}
>
{isDraft ? t("Save draft") : t("Done editing")}
{isDraft ? t("Save Draft") : t("Done Editing")}
</Button>
</Tooltip>
</Action>
</>
)}
{can.update &&
!isEditing &&
!team?.seamlessEditing &&
!isRevision &&
editAction}
{can.update &&
can.createChildDocument &&
!isRevision &&
!isMobile && (
<Action>
<NewChildDocumentMenu
document={document}
label={(props) => (
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
{t("New doc")}
</Button>
</Tooltip>
)}
/>
</Action>
)}
{can.update &&
!isEditing &&
isTemplate &&
!isDraft &&
!isRevision && (
<Action>
<Button
icon={<PlusIcon />}
as={Link}
to={newDocumentPath(document.collectionId, {
templateId: document.id,
})}
>
{t("New from template")}
</Button>
</Action>
)}
{canEdit && !team?.seamlessEditing && !isRevision && editAction}
{canEdit && can.createChildDocument && !isRevision && !isMobile && (
<Action>
<NewChildDocumentMenu
document={document}
label={(props) => (
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
{t("New doc")}
</Button>
</Tooltip>
)}
/>
</Action>
)}
{canEdit && isTemplate && !isDraft && !isRevision && (
<Action>
<Button
icon={<PlusIcon />}
as={Link}
to={newDocumentPath(document.collectionId, {
templateId: document.id,
})}
>
{t("New from template")}
</Button>
</Action>
)}
{revision && revision.createdAt !== document.updatedAt && (
<Action>
<Tooltip
@@ -328,23 +318,27 @@ function DocumentHeader({
{document.collectionId ? t("Publish") : `${t("Publish")}`}
</Button>
</Action>
{!isDeleted && <Separator />}
<Action>
<DocumentMenu
document={document}
isRevision={isRevision}
label={(props) => (
<Button
icon={<MoreIcon />}
{...props}
borderOnHover
neutral
{!isEditing && (
<>
{!isDeleted && <Separator />}
<Action>
<DocumentMenu
document={document}
isRevision={isRevision}
label={(props) => (
<Button
icon={<MoreIcon />}
{...props}
borderOnHover
neutral
/>
)}
showToggleEmbeds={canToggleEmbeds}
showDisplayOptions
/>
)}
showToggleEmbeds={canToggleEmbeds}
showDisplayOptions
/>
</Action>
</Action>
</>
)}
</>
}
/>
@@ -1,31 +1,36 @@
import { observer } from "mobx-react";
import { KeyboardIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import Guide from "~/components/Guide";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useStores from "~/hooks/useStores";
import useBoolean from "~/hooks/useBoolean";
function KeyboardShortcutsButton() {
const { t } = useTranslation();
const { dialogs } = useStores();
const handleOpenKeyboardShortcuts = () => {
dialogs.openGuide({
title: t("Keyboard shortcuts"),
content: <KeyboardShortcuts />,
});
};
const [
keyboardShortcutsOpen,
handleOpenKeyboardShortcuts,
handleCloseKeyboardShortcuts,
] = useBoolean();
return (
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
<Button onClick={handleOpenKeyboardShortcuts}>
<KeyboardIcon />
</Button>
</Tooltip>
<>
<Guide
isOpen={keyboardShortcutsOpen}
onRequestClose={handleCloseKeyboardShortcuts}
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Guide>
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
<Button onClick={handleOpenKeyboardShortcuts}>
<KeyboardIcon />
</Button>
</Tooltip>
</>
);
}
@@ -44,4 +49,4 @@ const Button = styled(NudeButton)`
}
`;
export default observer(KeyboardShortcutsButton);
export default KeyboardShortcutsButton;
@@ -1,12 +1,11 @@
import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider";
import throttle from "lodash/throttle";
import { throttle } from "lodash";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs";
import MultiplayerExtension from "@shared/editor/extensions/Multiplayer";
import { supportsPassiveListener } from "@shared/utils/browser";
import Editor, { Props as EditorProps } from "~/components/Editor";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -17,6 +16,7 @@ import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { AwarenessChangeEvent } from "~/types";
import Logger from "~/utils/Logger";
import { supportsPassiveListener } from "~/utils/browser";
import { homePath } from "~/utils/routeHelpers";
type Props = EditorProps & {
@@ -135,10 +135,13 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
});
provider.on("close", (ev: MessageEvent) => {
if ("code" in ev.event) {
provider.shouldConnect =
ev.event.code !== 1009 && ev.event.code !== 4401;
ui.setMultiplayerStatus("disconnected", ev.event.code);
if ("code" in ev.event && ev.event.code === 1009) {
provider.shouldConnect = false;
showToast(
t(
"Sorry, this document is too large - edits will no longer be persisted."
)
);
}
});
@@ -161,11 +164,9 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
);
}
provider.on("status", (ev: ConnectionStatusEvent) => {
if (ui.multiplayerStatus !== ev.status) {
ui.setMultiplayerStatus(ev.status, undefined);
}
});
provider.on("status", (ev: ConnectionStatusEvent) =>
ui.setMultiplayerStatus(ev.status)
);
setRemoteProvider(provider);
@@ -176,7 +177,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
provider?.destroy();
void localProvider?.destroy();
setRemoteProvider(null);
ui.setMultiplayerStatus(undefined, undefined);
ui.setMultiplayerStatus(undefined);
};
}, [
history,
@@ -49,7 +49,8 @@ const Title = styled.div`
line-height: 1.25;
padding-top: 3px;
color: ${s("text")};
font-family: ${s("fontFamily")};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
function ReferenceListItem({
@@ -1,6 +1,5 @@
import invariant from "invariant";
import debounce from "lodash/debounce";
import isEmpty from "lodash/isEmpty";
import { debounce, isEmpty } from "lodash";
import { observer } from "mobx-react";
import { ExpandedIcon, GlobeIcon, PadlockIcon } from "outline-icons";
import * as React from "react";
+1 -1
View File
@@ -1,4 +1,4 @@
import flatten from "lodash/flatten";
import { flatten } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
+1 -1
View File
@@ -1,4 +1,4 @@
import flatten from "lodash/flatten";
import { flatten } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
+1 -1
View File
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+2 -2
View File
@@ -32,7 +32,7 @@ function Home() {
void pins.fetchPage();
}, [pins]);
const can = usePolicy(team);
const canManageTeam = usePolicy(team).manage;
return (
<Scene
@@ -49,7 +49,7 @@ function Home() {
>
{!ui.languagePromptDismissed && <LanguagePrompt />}
<Heading>{t("Home")}</Heading>
<PinnedDocuments pins={pins.home} canUpdate={can.update} />
<PinnedDocuments pins={pins.home} canUpdate={canManageTeam} />
<Documents>
<Tabs>
<Tab to="/home" exact>
+15 -18
View File
@@ -5,7 +5,7 @@ import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import { UserRole } from "@shared/types";
import { Role } from "@shared/types";
import { UserValidation } from "@shared/validations";
import Button from "~/components/Button";
import CopyToClipboard from "~/components/CopyToClipboard";
@@ -28,7 +28,7 @@ type Props = {
type InviteRequest = {
email: string;
name: string;
role: UserRole;
role: Role;
};
function Invite({ onSubmit }: Props) {
@@ -38,17 +38,17 @@ function Invite({ onSubmit }: Props) {
{
email: "",
name: "",
role: UserRole.Member,
role: "member",
},
{
email: "",
name: "",
role: UserRole.Member,
role: "member",
},
{
email: "",
name: "",
role: UserRole.Member,
role: "member",
},
]);
const { users } = useStores();
@@ -65,7 +65,7 @@ function Invite({ onSubmit }: Props) {
setIsSaving(true);
try {
const data = await users.invite(invites.filter((i) => i.email));
const data = await users.invite(invites);
onSubmit();
if (data.sent.length > 0) {
@@ -113,7 +113,7 @@ function Invite({ onSubmit }: Props) {
newInvites.push({
email: "",
name: "",
role: UserRole.Member,
role: "member",
});
return newInvites;
});
@@ -138,16 +138,13 @@ function Invite({ onSubmit }: Props) {
});
}, [showToast, t]);
const handleRoleChange = React.useCallback(
(role: UserRole, index: number) => {
setInvites((prevInvites) => {
const newInvites = [...prevInvites];
newInvites[index]["role"] = role;
return newInvites;
});
},
[]
);
const handleRoleChange = React.useCallback((role: Role, index: number) => {
setInvites((prevInvites) => {
const newInvites = [...prevInvites];
newInvites[index]["role"] = role;
return newInvites;
});
}, []);
return (
<form onSubmit={handleSubmit}>
@@ -227,7 +224,7 @@ function Invite({ onSubmit }: Props) {
flex
/>
<InputSelectRole
onChange={(role: UserRole) => handleRoleChange(role, index)}
onChange={(role: Role) => handleRoleChange(role, index)}
value={invite.role}
labelHidden={index !== 0}
short
+1 -2
View File
@@ -2,10 +2,10 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { isMac } from "@shared/utils/browser";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import Key from "~/components/Key";
import { isMac } from "~/utils/browser";
import { metaDisplay, altDisplay } from "~/utils/keyboard";
function KeyboardShortcuts() {
@@ -442,7 +442,6 @@ const List = styled.dl`
overflow: hidden;
padding: 0;
margin: 0;
user-select: none;
`;
const Keys = styled.dt`
+1 -7
View File
@@ -37,13 +37,7 @@ export default function Notices() {
Please use a Google Workspaces account instead.
</Trans>
)}
{notice === "pending-deletion" && (
<Trans>
The workspace associated with your user is scheduled for deletion and
cannot at accessed at this time.
</Trans>
)}
{notice === "maximum-reached" && (
{notice === "maximum-teams" && (
<Trans>
The workspace you authenticated with is not authorized on this
installation. Try another?
+1 -1
View File
@@ -1,4 +1,4 @@
import find from "lodash/find";
import { find } from "lodash";
import { observer } from "mobx-react";
import { BackIcon, EmailIcon } from "outline-icons";
import * as React from "react";
+2 -2
View File
@@ -1,4 +1,4 @@
import isEqual from "lodash/isEqual";
import { isEqual } from "lodash";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import queryString from "query-string";
@@ -464,7 +464,7 @@ const SearchTitlesFilter = styled(Switch)`
margin-left: 8px;
margin-top: 2px;
font-size: 14px;
font-weight: 400;
font-weight: 500;
`;
export default withTranslation()(withStores(withRouter(Search)));
+4 -38
View File
@@ -1,5 +1,5 @@
import { isHexColor } from "class-validator";
import pickBy from "lodash/pickBy";
import { pickBy } from "lodash";
import { observer } from "mobx-react";
import { TeamIcon } from "outline-icons";
import { useRef, useState } from "react";
@@ -18,23 +18,20 @@ import InputColor from "~/components/InputColor";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import isCloudHosted from "~/utils/isCloudHosted";
import TeamDelete from "../TeamDelete";
import ImageInput from "./components/ImageInput";
import SettingRow from "./components/SettingRow";
function Details() {
const { auth, dialogs, ui } = useStores();
const { auth, ui } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const team = useCurrentTeam();
const theme = useTheme();
const can = usePolicy(team);
const form = useRef<HTMLFormElement>(null);
const [accent, setAccent] = useState<null | undefined | string>(
team.preferences?.customTheme?.accent
@@ -128,14 +125,6 @@ function Details() {
[showToast, t]
);
const showDeleteWorkspace = () => {
dialogs.openModal({
title: t("Delete workspace"),
content: <TeamDelete onSubmit={dialogs.closeAllModals} />,
isCentered: true,
});
};
const onSelectCollection = React.useCallback(async (value: string) => {
const defaultCollectionId = value === "home" ? null : value;
setDefaultCollectionId(defaultCollectionId);
@@ -233,7 +222,6 @@ function Details() {
</SettingRow>
{team.avatarUrl && (
<SettingRow
border={false}
name={TeamPreference.PublicBranding}
label={t("Public branding")}
description={t(
@@ -254,7 +242,7 @@ function Details() {
<Heading as="h2">{t("Behavior")}</Heading>
<SettingRow
visible={isCloudHosted}
visible={env.SUBDOMAINS_ENABLED && isCloudHosted}
label={t("Subdomain")}
name="subdomain"
description={
@@ -299,28 +287,6 @@ function Details() {
<Button type="submit" disabled={auth.isSaving || !isValid}>
{auth.isSaving ? `${t("Saving")}` : t("Save")}
</Button>
{can.delete && (
<>
<p>&nbsp;</p>
<Heading as="h2">{t("Danger")}</Heading>
<SettingRow
name="delete"
border={false}
label={t("Delete workspace")}
description={t(
"You can delete this entire workspace including collections, documents, and users."
)}
>
<span>
<Button onClick={showDeleteWorkspace} neutral>
{t("Delete workspace")}
</Button>
</span>
</SettingRow>
</>
)}
</form>
</Scene>
</ThemeProvider>
+1 -1
View File
@@ -1,4 +1,4 @@
import find from "lodash/find";
import { find } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useForm } from "react-hook-form";
+6 -9
View File
@@ -1,4 +1,4 @@
import sortBy from "lodash/sortBy";
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import { PlusIcon, UserIcon } from "outline-icons";
import * as React from "react";
@@ -39,8 +39,8 @@ function Members() {
const [totalPages, setTotalPages] = React.useState(0);
const [userIds, setUserIds] = React.useState<string[]>([]);
const can = usePolicy(team);
const query = params.get("query") || undefined;
const filter = params.get("filter") || undefined;
const query = params.get("query") || "";
const filter = params.get("filter") || "";
const sort = params.get("sort") || "name";
const direction = (params.get("direction") || "asc").toUpperCase() as
| "ASC"
@@ -176,18 +176,15 @@ function Members() {
<Flex gap={8}>
<InputSearch
short
value={query ?? ""}
value={query}
placeholder={`${t("Filter")}`}
onChange={handleSearch}
/>
<LargeUserStatusFilter
activeKey={filter ?? ""}
onSelect={handleFilter}
/>
<LargeUserStatusFilter activeKey={filter} onSelect={handleFilter} />
</Flex>
<PeopleTable
data={data}
canManage={can.update}
canManage={can.manage}
isLoading={isLoading}
page={page}
pageSize={limit}
+1 -1
View File
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { observer } from "mobx-react";
import {
AcademicCapIcon,
+2 -2
View File
@@ -47,7 +47,6 @@ function Preferences() {
dialogs.openModal({
title: t("Delete account"),
content: <UserDelete />,
isCentered: true,
});
};
@@ -132,7 +131,8 @@ function Preferences() {
/>
</SettingRow>
<Heading as="h2">{t("Danger")}</Heading>
<p>&nbsp;</p>
<SettingRow
name="delete"
label={t("Delete account")}
+1 -1
View File
@@ -1,4 +1,4 @@
import debounce from "lodash/debounce";
import { debounce } from "lodash";
import { observer } from "mobx-react";
import { CheckboxIcon, EmailIcon, PadlockIcon } from "outline-icons";
import { useState } from "react";
+8 -44
View File
@@ -1,4 +1,4 @@
import find from "lodash/find";
import { find } from "lodash";
import { observer } from "mobx-react";
import { BuildingBlocksIcon } from "outline-icons";
import * as React from "react";
@@ -16,7 +16,6 @@ import SettingRow from "./components/SettingRow";
type FormData = {
drawIoUrl: string;
gristUrl: string;
};
function SelfHosted() {
@@ -24,16 +23,11 @@ function SelfHosted() {
const { t } = useTranslation();
const { showToast } = useToasts();
const integrationDiagrams = find(integrations.orderedData, {
const integration = find(integrations.orderedData, {
type: IntegrationType.Embed,
service: IntegrationService.Diagrams,
}) as Integration<IntegrationType.Embed> | undefined;
const integrationGrist = find(integrations.orderedData, {
type: IntegrationType.Embed,
service: IntegrationService.Grist,
}) as Integration<IntegrationType.Embed> | undefined;
const {
register,
reset,
@@ -42,8 +36,7 @@ function SelfHosted() {
} = useForm<FormData>({
mode: "all",
defaultValues: {
drawIoUrl: integrationDiagrams?.settings.url,
gristUrl: integrationGrist?.settings.url,
drawIoUrl: integration?.settings.url,
},
});
@@ -54,18 +47,15 @@ function SelfHosted() {
}, [integrations]);
React.useEffect(() => {
reset({
drawIoUrl: integrationDiagrams?.settings.url,
gristUrl: integrationGrist?.settings.url,
});
}, [integrationDiagrams, integrationGrist, reset]);
reset({ drawIoUrl: integration?.settings.url });
}, [integration, reset]);
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
if (data.drawIoUrl) {
await integrations.save({
id: integrationDiagrams?.id,
id: integration?.id,
type: IntegrationType.Embed,
service: IntegrationService.Diagrams,
settings: {
@@ -73,20 +63,7 @@ function SelfHosted() {
},
});
} else {
await integrationDiagrams?.delete();
}
if (data.gristUrl) {
await integrations.save({
id: integrationGrist?.id,
type: IntegrationType.Embed,
service: IntegrationService.Grist,
settings: {
url: data.gristUrl,
},
});
} else {
await integrationGrist?.delete();
await integration?.delete();
}
showToast(t("Settings saved"), {
@@ -98,7 +75,7 @@ function SelfHosted() {
});
}
},
[integrations, integrationDiagrams, integrationGrist, t, showToast]
[integrations, integration, t, showToast]
);
return (
@@ -121,19 +98,6 @@ function SelfHosted() {
/>
</SettingRow>
<SettingRow
label={t("Grist deployment")}
name="gristUrl"
description={t("Add your self-hosted grist installation URL here.")}
border={false}
>
<Input
placeholder="https://docs.getgrist.com/"
pattern="https?://.*"
{...register("gristUrl")}
/>
</SettingRow>
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? `${t("Saving")}` : t("Save")}
</Button>
+3 -3
View File
@@ -1,4 +1,4 @@
import sortBy from "lodash/sortBy";
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import { LinkIcon, WarningIcon } from "outline-icons";
import * as React from "react";
@@ -70,7 +70,7 @@ function Shares() {
<Scene title={t("Shared Links")} icon={<LinkIcon />}>
<Heading>{t("Shared Links")}</Heading>
{can.update && !canShareDocuments && (
{can.manage && !canShareDocuments && (
<>
<Notice icon={<WarningIcon />}>
{t("Sharing is currently disabled.")}{" "}
@@ -95,7 +95,7 @@ function Shares() {
<SharesTable
data={data}
canManage={can.update}
canManage={can.manage}
isLoading={isLoading}
page={page}
pageSize={limit}
@@ -1,4 +1,4 @@
import compact from "lodash/compact";
import { compact } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
-122
View File
@@ -1,122 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useForm } from "react-hook-form";
import { useTranslation, Trans } from "react-i18next";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type FormData = {
code: string;
};
type Props = {
onSubmit: () => void;
};
function TeamDelete({ onSubmit }: Props) {
const [isWaitingCode, setWaitingCode] = React.useState(false);
const { auth } = useStores();
const { showToast } = useToasts();
const team = useCurrentTeam();
const { t } = useTranslation();
const {
register,
handleSubmit: formHandleSubmit,
formState,
} = useForm<FormData>();
const handleRequestDelete = React.useCallback(
async (ev: React.SyntheticEvent) => {
ev.preventDefault();
try {
await auth.requestDeleteTeam();
setWaitingCode(true);
} catch (error) {
showToast(error.message, {
type: "error",
});
}
},
[auth, showToast]
);
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
await auth.deleteTeam(data);
await auth.logout();
onSubmit();
} catch (error) {
showToast(error.message, {
type: "error",
});
}
},
[auth, onSubmit, showToast]
);
const inputProps = register("code", {
required: true,
});
const appName = env.APP_NAME;
const workspaceName = team.name;
return (
<Flex column>
<form onSubmit={formHandleSubmit(handleSubmit)}>
{isWaitingCode ? (
<>
<Text type="secondary">
<Trans>
A confirmation code has been sent to your email address, please
enter the code below to permanantly destroy this workspace.
</Trans>
</Text>
<Input
placeholder={t("Confirmation code")}
autoComplete="off"
autoFocus
maxLength={8}
required
{...inputProps}
/>
</>
) : (
<>
<Text type="secondary">
<Trans>
Deleting the <strong>{{ workspaceName }}</strong> workspace will
destroy all collections, documents, users, and associated data.
You will be immediately logged out of {{ appName }}.
</Trans>
</Text>
</>
)}
{env.EMAIL_ENABLED && !isWaitingCode ? (
<Button type="submit" onClick={handleRequestDelete} neutral>
{t("Continue")}
</Button>
) : (
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
danger
>
{formState.isSubmitting
? `${t("Deleting")}`
: t("Delete workspace")}
</Button>
)}
</form>
</Flex>
);
}
export default observer(TeamDelete);
+14 -22
View File
@@ -5,7 +5,6 @@ import User from "~/models/User";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import Notice from "~/components/Notice";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -47,14 +46,17 @@ function TeamNew({ user }: Props) {
return (
<>
<form onSubmit={handleSubmit}>
<Notice>
<Trans>
Please note that workspaces are completely separate. They can have a
different domain, settings, users, and billing.
</Trans>
</Notice>
<p />
<Text type="secondary">
<Trans
defaults="Your are creating a new workspace using your current account — <em>{{email}}</em>"
values={{
email: user.email,
}}
components={{
em: <strong />,
}}
/>
</Text>
<Flex>
<Input
@@ -67,21 +69,11 @@ function TeamNew({ user }: Props) {
flex
/>
</Flex>
<Text type="secondary">
<Trans
defaults="Your are creating a new workspace using your current account — <em>{{email}}</em>"
values={{
email: user.email,
}}
components={{
em: <strong />,
}}
/>
.{" "}
<Trans>
To create a workspace under another email please sign up from the
homepage
When your new workspace is created, you will be the admin, meaning
you will have the highest level of permissions and the ability to
invite others.
</Trans>
</Text>
+12 -8
View File
@@ -30,7 +30,7 @@ function UserDelete() {
ev.preventDefault();
try {
await auth.requestDeleteUser();
await auth.requestDelete();
setWaitingCode(true);
} catch (error) {
showToast(error.message, {
@@ -71,8 +71,16 @@ function UserDelete() {
enter the code below to permanantly destroy your account.
</Trans>
</Text>
<Text type="secondary">
<Trans
defaults="<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned."
components={{
em: <strong />,
}}
/>
</Text>
<Input
placeholder={t("Confirmation code")}
placeholder="CODE"
autoComplete="off"
autoFocus
maxLength={8}
@@ -97,14 +105,10 @@ function UserDelete() {
{t("Continue")}
</Button>
) : (
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
danger
>
<Button type="submit" disabled={formState.isSubmitting} danger>
{formState.isSubmitting
? `${t("Deleting")}`
: t("Delete my account")}
: t("Delete My Account")}
</Button>
)}
</form>
+5 -19
View File
@@ -13,7 +13,6 @@ import env from "~/env";
import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted";
const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home", "/logout"];
@@ -213,7 +212,7 @@ export default class AuthStore {
return;
}
} else if (
isCloudHosted &&
env.SUBDOMAINS_ENABLED &&
parseDomain(hostname).teamSubdomain !== (team.subdomain ?? "")
) {
window.location.href = `${team.url}${pathname}`;
@@ -242,14 +241,13 @@ export default class AuthStore {
}
};
requestDeleteUser = () => client.post(`/users.requestDelete`);
requestDeleteTeam = () => client.post(`/teams.requestDelete`);
@action
requestDelete = () => client.post(`/users.requestDelete`);
@action
deleteUser = async (data: { code: string }) => {
await client.post(`/users.delete`, data);
runInAction("AuthStore#deleteUser", () => {
runInAction("AuthStore#updateUser", () => {
this.user = null;
this.team = null;
this.collaborationToken = null;
@@ -260,18 +258,6 @@ export default class AuthStore {
});
};
@action
deleteTeam = async (data: { code: string }) => {
await client.post(`/teams.delete`, data);
runInAction("AuthStore#deleteTeam", () => {
this.user = null;
this.availableTeams = this.availableTeams?.filter(
(team) => team.id !== this.team?.id
);
this.policies = [];
});
};
@action
updateUser = async (params: {
name?: string;
@@ -373,7 +359,7 @@ export default class AuthStore {
const sessions = JSON.parse(getCookie("sessions") || "{}");
delete sessions[team.id];
setCookie("sessions", JSON.stringify(sessions), {
domain: getCookieDomain(window.location.hostname, isCloudHosted),
domain: getCookieDomain(window.location.hostname),
});
}
+1 -2
View File
@@ -1,6 +1,5 @@
import invariant from "invariant";
import lowerFirst from "lodash/lowerFirst";
import orderBy from "lodash/orderBy";
import { lowerFirst, orderBy } from "lodash";
import { observable, action, computed, runInAction } from "mobx";
import { Class } from "utility-types";
import RootStore from "~/stores/RootStore";
+1 -4
View File
@@ -1,8 +1,5 @@
import invariant from "invariant";
import concat from "lodash/concat";
import find from "lodash/find";
import last from "lodash/last";
import sortBy from "lodash/sortBy";
import { concat, find, last, sortBy } from "lodash";
import { computed, action } from "mobx";
import {
CollectionPermission,
+1 -2
View File
@@ -1,6 +1,5 @@
import invariant from "invariant";
import filter from "lodash/filter";
import orderBy from "lodash/orderBy";
import { filter, orderBy } from "lodash";
import { action, runInAction, computed } from "mobx";
import Comment from "~/models/Comment";
import Document from "~/models/Document";
+1 -5
View File
@@ -1,9 +1,5 @@
import invariant from "invariant";
import compact from "lodash/compact";
import filter from "lodash/filter";
import find from "lodash/find";
import omitBy from "lodash/omitBy";
import orderBy from "lodash/orderBy";
import { find, orderBy, filter, compact, omitBy } from "lodash";
import { observable, action, computed, runInAction } from "mobx";
import { DateFilter, NavigationNode, PublicTeam } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
+1 -2
View File
@@ -1,5 +1,4 @@
import filter from "lodash/filter";
import sortBy from "lodash/sortBy";
import { sortBy, filter } from "lodash";
import { computed } from "mobx";
import Event from "~/models/Event";
import BaseStore, { RPCAction } from "./BaseStore";
+1 -1
View File
@@ -1,4 +1,4 @@
import orderBy from "lodash/orderBy";
import { orderBy } from "lodash";
import { computed } from "mobx";
import { FileOperationType } from "@shared/types";
import FileOperation from "~/models/FileOperation";
+1 -1
View File
@@ -1,5 +1,5 @@
import invariant from "invariant";
import filter from "lodash/filter";
import { filter } from "lodash";
import { action, runInAction } from "mobx";
import GroupMembership from "~/models/GroupMembership";
import { PaginationParams } from "~/types";
+1 -1
View File
@@ -1,5 +1,5 @@
import invariant from "invariant";
import filter from "lodash/filter";
import { filter } from "lodash";
import { action, runInAction, computed } from "mobx";
import naturalSort from "@shared/utils/naturalSort";
import Group from "~/models/Group";
+1 -1
View File
@@ -1,4 +1,4 @@
import filter from "lodash/filter";
import { filter } from "lodash";
import { computed } from "mobx";
import { IntegrationService } from "@shared/types";
import naturalSort from "@shared/utils/naturalSort";
+1 -2
View File
@@ -1,6 +1,5 @@
import invariant from "invariant";
import orderBy from "lodash/orderBy";
import sortBy from "lodash/sortBy";
import { orderBy, sortBy } from "lodash";
import { action, computed, runInAction } from "mobx";
import Notification from "~/models/Notification";
import { PaginationParams } from "~/types";

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