Compare commits

...

61 Commits

Author SHA1 Message Date
Tom Moor 706ce9fcef Update README.md 2023-08-27 17:15:44 -07:00
Tom Moor 78ad1b867a fix: Handle base64 and remote images when creating a file (#5740) 2023-08-26 06:15:14 -07:00
Tom Moor c643f62d96 fix: Show more header options in edit mode 2023-08-24 21:51:42 -04:00
Tom Moor 79ff9309fd fix: Write revision when leaving editing mode 2023-08-24 21:18:38 -04:00
Tom Moor 9256c59e60 Add C++ to code language options, closes #5736 2023-08-24 19:52:35 -04:00
Tom Moor 1d90f98a29 fix: Remove overly aggressive AWS_ env variable validation prevents use with IAM roles 2023-08-24 18:16:52 -04:00
Tom Moor 10ec8a59b4 fix: Disable previews in notification items 2023-08-23 22:34:21 -04:00
Tom Moor dfbd89ad53 fix: Improve error message when an individual document in a large import is too large 2023-08-23 21:49:35 -04:00
Tom Moor da9a8af543 fix: Prevent rendering of undefined SVG placeholder 2023-08-23 21:35:05 -04:00
Tom Moor aada5c20cd fix: Clarify separate billing on workspaces 2023-08-23 19:44:42 -04:00
Apoorv Mishra 8f86eadc5d fix: tsc (#5732) 2023-08-23 19:23:41 +05:30
Apoorv Mishra 53c6c5599a Go-To Actions with transactions emails (#5728)
* feat: go-to actions for emails

* fix: comment

* fix: tsc without previewText

* fix: goToAction

* fix: link to original template

* fix: final comments
2023-08-23 18:43:52 +05:30
dependabot[bot] e3ba87dcb0 chore(deps-dev): bump eslint from 8.45.0 to 8.47.0 (#5722)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 07:07:37 -07:00
dependabot[bot] 3c5753621c chore(deps-dev): bump babel-jest from 29.6.1 to 29.6.3 (#5723)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 07:07:24 -07:00
Tom Moor 3366fb46cd fix: Copy Mermaid toolbar incorrectly positioned in read-only
closes #5714
2023-08-21 22:20:42 -04:00
Tom Moor 89bf5373aa chore: Add pointer to troubleshooting when nonce prevents script execution
closes #5718
2023-08-21 21:38:30 -04:00
dependabot[bot] e6b0e434ea chore(deps): bump rate-limiter-flexible from 2.4.1 to 2.4.2 (#5721)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 17:25:33 -07:00
dependabot[bot] 225f0dbf11 chore(deps): bump dd-trace from 3.32.1 to 3.33.0 (#5725)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 17:25:00 -07:00
Tom Moor 418d3305b2 feat: Add team deletion flow for cloud-hosted (#5717) 2023-08-21 17:24:46 -07:00
Tom Moor 5c07694f6b Refactor 'uploadFromUrl' to base storage implementation
Add safety around using fetch implementation
2023-08-20 13:13:17 -04:00
Tom Moor 74722b80f2 chore: Refactor file storage (#5711) 2023-08-20 07:04:34 -07:00
Apoorv Mishra 4354e1055e Keep nested docs in shared sidebar collapsed by default (#5208)
Co-authored-by: Tom Moor <tom@getoutline.com>
2023-08-20 07:04:04 -07:00
Apoorv Mishra c3a8858c6b fix: re-position hover preview correctly to prevent going out of page bounds (#5702) 2023-08-20 16:42:05 +05:30
Tom Moor 546022e5d6 fix: Allow webhooks to connct to private IPs when self-hosting, restore proxy compatability closes #5709 2023-08-20 07:03:33 -04:00
Tom Moor 33e532847e feat: Add Ukranian language support 2023-08-20 06:52:07 -04:00
Apoorv Mishra c9d62420c8 feat: send header (#5707) 2023-08-20 10:55:04 +05:30
Tom Moor cc2a1865c5 perf: Do not render KeyboardShortcuts guide unless clicked 2023-08-18 19:00:59 +02:00
Tom Moor 1ec87da8a9 fix: Add additional check for mobile device to bottom toolbar
closes #5703
2023-08-18 18:51:39 +02:00
Tom Moor d820b2a617 fix: Scrolling in desktop app sidebar is finicky 2023-08-18 18:45:38 +02:00
Tom Moor 5e7ea165b4 0.71.0 2023-08-18 11:11:32 +02:00
Tom Moor c68d55f49b fix: Inopperable image toolbar appears in read-only mode 2023-08-17 23:30:42 +02:00
Tom Moor 7e349c9db1 perf: Do not load state to calculate navigation node 2023-08-17 23:14:44 +02:00
Tom Moor 13b067fb3f fix: Document importer only replaces first base64 encoded image when there are multiple identical images in a document
closes #5653
2023-08-17 22:46:40 +02:00
Tom Moor 41c346d105 Revert "chore: Update browserslist"
This reverts commit fce90df3aa.
2023-08-17 10:41:36 +02:00
Tom Moor 4788ab3bd6 fix: Add support for Airtable share links with app ID 2023-08-16 22:20:55 +02:00
Tom Moor 5f00b4f744 fix: Incorrect error shown to user when connection limit is reached (#5695) 2023-08-16 12:39:56 -07:00
Tom Moor fd600ced09 fix: Flash of 'not found' screen when deleting a collection 2023-08-15 21:39:01 +02:00
Tom Moor 0047384d70 fix: Code blocks nested in list do not get line numbers 2023-08-15 19:52:16 +02:00
Tom Moor 8bff566c30 fix: Sidebar item misalignment 2023-08-15 11:32:19 +02:00
Tom Moor fce90df3aa chore: Update browserslist 2023-08-15 11:26:48 +02:00
Tom Moor 28ae1af2a3 fix: ctrl+a does not work on Windows in code block (#5692) 2023-08-14 13:16:12 -07:00
Tom Moor 9f0534d544 chore: Bump vite
Reduces full page reloads in dev, increase perf
2023-08-14 20:51:08 +02:00
Tom Moor 4edfab20fe fix: Bug with local dynamic reloading since moving to SSL 2023-08-14 20:48:49 +02:00
Philip Standt c38e045df2 feat: support self hosted grist (#5655)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-08-14 11:46:24 -07:00
Tom Moor b7bfc4bb1a chore: Remove optimize imports to allow vite upgrade (#5691) 2023-08-14 11:44:58 -07:00
dependabot[bot] a71ad43c31 chore(deps): bump nodemailer and @types/nodemailer (#5689)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 10:15:32 -07:00
dependabot[bot] 199fa5844e chore(deps): bump react-window from 1.8.7 to 1.8.9 (#5685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 10:15:19 -07:00
dependabot[bot] b466f1c8bb chore(deps): bump ws from 7.5.6 to 7.5.9 (#5686)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 10:14:56 -07:00
dependabot[bot] 503e4e1f71 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 5.61.0 to 5.62.0 (#5688)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 10:14:40 -07:00
dependabot[bot] 2bc52be2cf chore(deps): bump email-providers from 1.13.1 to 1.14.0 (#5687)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 09:28:58 -07:00
github-actions[bot] 3ba730943c chore: Auto Compress Images (#5682)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2023-08-12 02:47:12 -07:00
Jack Woodgate 6828718cf0 feat: Add Google Maps Embed (#5667) (#5673)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-08-12 02:45:21 -07:00
Tom Moor 9749a53558 feat: Handle pasting iframe for supported embed 2023-08-12 11:44:16 +02:00
Tom Moor f4e4992508 fix: Remove fetch-with-proxy from DeliverWebhookTask 2023-08-11 22:35:38 +02:00
Tom Moor cf2f0b1b5c fix: Flash of empty state in sidebar when creating a new collection 2023-08-11 22:34:14 +02:00
Tom Moor 4a4ea0e531 fix: Text alignment on collection empty state 2023-08-11 22:30:48 +02:00
Tom Moor 8830773acb fix: Mobile styling bugs 2023-08-11 22:26:40 +02:00
Tom Moor f5d2c7890a fix: Unable to create collection with no access permission 2023-08-10 15:13:40 +02:00
Apoorv Mishra 434812dbe3 Allow vite to serve files from workspace's parent directory (#5675)
* fix: allow vite to serve files from workspace parent dir

* trigger ci

* trigger ci
2023-08-09 21:52:44 +05:30
Tom Moor ed5671209a New Crowdin updates (#5647) 2023-08-09 04:23:00 -07:00
Tom Moor c32cec7bff Add support for SSL in development (#5668) 2023-08-09 04:21:41 -07:00
326 changed files with 2993 additions and 1473 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ REDIS_URL=redis://localhost:6379
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
URL=http://localhost:3000
URL=https://app.outline.dev:3000
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
+2 -1
View File
@@ -21,7 +21,7 @@
"eslint-plugin-import",
"eslint-plugin-node",
"eslint-plugin-react",
"import"
"eslint-plugin-lodash"
],
"rules": {
"eqeqeq": 2,
@@ -55,6 +55,7 @@
],
"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,
+1
View File
@@ -1,5 +1,6 @@
up:
docker-compose up -d redis postgres s3
yarn install-local-ssl
yarn install --pure-lockfile
yarn dev:watch
+4
View File
@@ -96,6 +96,10 @@ 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
Outline is [BSL 1.1 licensed](LICENSE).
+1 -1
View File
@@ -14,6 +14,7 @@ import {
BrowserIcon,
} from "outline-icons";
import * as React from "react";
import { isMac } from "@shared/utils/browser";
import {
developersUrl,
changelogUrl,
@@ -26,7 +27,6 @@ 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";
import flattenDeep from "lodash/flattenDeep";
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";
import escape from "lodash/escape";
import * as React from "react";
import { IntegrationService } from "@shared/types";
import env from "~/env";
+4 -1
View File
@@ -1,4 +1,7 @@
import { sortBy, filter, uniq, isEqual } from "lodash";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import sortBy from "lodash/sortBy";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+7 -2
View File
@@ -7,6 +7,7 @@ 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 = {
@@ -17,16 +18,20 @@ 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 (
+38 -5
View File
@@ -14,15 +14,48 @@ 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={
<Centered>
<strong>{t("Server connection lost")}</strong>
<br />
{t("Edits you make will sync once youre online")}
</Centered>
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>
)
}
placement="bottom"
>
+1 -1
View File
@@ -151,7 +151,7 @@ const ContextMenu: React.FC<Props> = ({
ref={backgroundRef}
hiddenScrollbars
style={
topAnchor
topAnchor && !isMobile
? {
maxHeight,
}
+6 -1
View File
@@ -1,5 +1,10 @@
import FuzzySearch from "fuzzy-search";
import { includes, difference, concat, filter, map, fill } from "lodash";
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 { observer } from "mobx-react";
import { StarredIcon, DocumentIcon } from "outline-icons";
import * as React from "react";
+1 -1
View File
@@ -1,4 +1,4 @@
import { sortBy } from "lodash";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+6 -2
View File
@@ -1,4 +1,6 @@
import { deburr, difference, sortBy } from "lodash";
import deburr from "lodash/deburr";
import difference from "lodash/difference";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
@@ -46,6 +48,7 @@ export type Props = Optional<
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
previewsDisabled?: boolean;
onHeadingsChange?: (headings: Heading[]) => void;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => any;
@@ -60,6 +63,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
onHeadingsChange,
onCreateCommentMark,
onDeleteCommentMark,
previewsDisabled,
} = props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
@@ -337,7 +341,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
userPreferences={preferences}
dictionary={dictionary}
{...props}
onHoverLink={handleLinkActive}
onHoverLink={previewsDisabled ? undefined : handleLinkActive}
onClickLink={handleClickLink}
onSearchLink={handleSearchLink}
onChange={handleChange}
+2 -2
View File
@@ -1,4 +1,4 @@
import { throttle } from "lodash";
import throttle from "lodash/throttle";
import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import { transparentize } from "polished";
@@ -6,6 +6,7 @@ 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";
@@ -14,7 +15,6 @@ 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";
import escapeRegExp from "lodash/escapeRegExp";
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 = 16;
export const CARD_MARGIN = 10;
const NUMBER_OF_LINES = 10;
+59 -27
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, s } from "@shared/styles";
import { depths } from "@shared/styles";
import { UnfurlType } from "@shared/types";
import LoadingIndicator from "~/components/LoadingIndicator";
import useEventListener from "~/hooks/useEventListener";
@@ -27,6 +27,14 @@ 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);
@@ -36,31 +44,46 @@ function HoverPreviewInternal({ element, onClose }: Props) {
const stores = useStores();
const [cardLeft, setCardLeft] = React.useState(0);
const [cardTop, setCardTop] = React.useState(0);
const [pointerOffset, setPointerOffset] = React.useState(0);
const [pointerLeft, setPointerLeft] = React.useState(0);
const [pointerTop, setPointerTop] = React.useState(0);
const [pointerDir, setPointerDir] = React.useState(Direction.UP);
React.useLayoutEffect(() => {
if (isVisible && cardRef.current) {
const elem = element.getBoundingClientRect();
const card = cardRef.current.getBoundingClientRect();
const top = elem.bottom + window.scrollY;
setCardTop(top);
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);
let left = elem.left;
let pointerOffset = elem.width / 2;
if (left + card.width > window.innerWidth) {
let cLeft = elem.left;
let pLeft = elem.width / 2;
if (cLeft + card.width > window.innerWidth) {
// shift card leftwards by the amount it went out of screen
let shiftBy = left + card.width - window.innerWidth;
// shift a littler further to leave some margin between card and window boundary
let shiftBy = cLeft + card.width - window.innerWidth;
// shift a little further to leave some margin between card and window boundary
shiftBy += CARD_MARGIN;
left -= shiftBy;
cLeft -= shiftBy;
// shift pointer rightwards by same amount so as to position it back correctly
pointerOffset += shiftBy;
pLeft += shiftBy;
}
setCardLeft(left);
setPointerOffset(pointerOffset);
setCardLeft(cLeft);
setPointerLeft(pLeft);
}
}, [isVisible, element]);
@@ -193,7 +216,11 @@ function HoverPreviewInternal({ element, onClose }: Props) {
description={data.description}
/>
)}
<Pointer offset={pointerOffset} />
<Pointer
top={pointerTop}
left={pointerLeft}
direction={pointerDir}
/>
</Animate>
) : null}
</Position>
@@ -217,7 +244,6 @@ 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;
@@ -227,11 +253,11 @@ const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`;
const Pointer = styled.div<{ offset: number }>`
top: -22px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
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;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
@@ -241,20 +267,26 @@ const Pointer = styled.div<{ offset: number }>`
content: "";
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
${({ direction }) => (direction === Direction.UP ? "bottom: 0" : "top: 0")};
${({ direction }) => (direction === Direction.UP ? "right: 0" : "left: 0")};
}
&:before {
border: 8px solid transparent;
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
${({ 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"};
}
&:after {
border: 7px solid transparent;
border-bottom-color: ${s("menuBackground")};
${({ direction, theme }) =>
direction === Direction.UP
? `border-bottom-color: ${theme.menuBackground}`
: `border-top-color: ${theme.menuBackground}`};
}
`;
@@ -26,9 +26,9 @@ const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
) {
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column>
<Flex column ref={ref}>
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
<Card ref={ref}>
<Card>
<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 = "";
value = null;
}
onChange?.(value);
+1 -1
View File
@@ -1,4 +1,4 @@
import { find } from "lodash";
import find from "lodash/find";
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";
import times from "lodash/times";
import * as React from "react";
import styled from "styled-components";
import Fade from "~/components/Fade";
@@ -64,6 +64,7 @@ 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";
import isEqual from "lodash/isEqual";
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";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+5 -3
View File
@@ -26,7 +26,9 @@ 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 FullWidthButton, {
FullWidthButtonProps,
} from "./components/FullWidthButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarAction from "./components/SidebarAction";
@@ -65,8 +67,8 @@ function AppSidebar() {
<DragPlaceholder />
<OrganizationMenu>
{(props: HeaderButtonProps) => (
<HeaderButton
{(props: FullWidthButtonProps) => (
<FullWidthButton
{...props}
title={team.name}
image={
+3 -3
View File
@@ -1,4 +1,4 @@
import { groupBy } from "lodash";
import groupBy from "lodash/groupBy";
import { observer } from "mobx-react";
import { BackIcon } from "outline-icons";
import * as React from "react";
@@ -11,8 +11,8 @@ import useSettingsConfig from "~/hooks/useSettingsConfig";
import Desktop from "~/utils/Desktop";
import isCloudHosted from "~/utils/isCloudHosted";
import Sidebar from "./Sidebar";
import FullWidthButton from "./components/FullWidthButton";
import Header from "./components/Header";
import HeaderButton from "./components/HeaderButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
@@ -31,7 +31,7 @@ function SettingsSidebar() {
return (
<Sidebar>
<HistoryNavigation />
<HeaderButton
<FullWidthButton
title={t("Return to App")}
image={<StyledBackIcon />}
onClick={returnToApp}
+2 -2
View File
@@ -11,7 +11,7 @@ import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import HeaderButton from "./components/HeaderButton";
import FullWidthButton from "./components/FullWidthButton";
import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
@@ -28,7 +28,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
return (
<Sidebar>
{team && (
<HeaderButton
<FullWidthButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
onClick={() =>
+14 -9
View File
@@ -11,13 +11,15 @@ import useMenuContext from "~/hooks/useMenuContext";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import AccountMenu from "~/menus/AccountMenu";
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import { 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 FullWidthButton, {
FullWidthButtonProps,
} from "./components/FullWidthButton";
import ResizeBorder from "./components/ResizeBorder";
import Toggle, { ToggleButton, Positioner } from "./components/Toggle";
@@ -175,11 +177,12 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{user && (
<AccountMenu>
{(props: HeaderButtonProps) => (
<HeaderButton
{(props: FullWidthButtonProps) => (
<FullWidthButton
{...props}
showMoreMenu
title={user.name}
position="bottom"
image={
<StyledAvatar
alt={user.name}
@@ -190,11 +193,15 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
}
>
<NotificationsPopover>
{(rest: HeaderButtonProps) => (
<HeaderButton {...rest} image={<NotificationIcon />} />
{(rest: FullWidthButtonProps) => (
<FullWidthButton
{...rest}
position="bottom"
image={<NotificationIcon />}
/>
)}
</NotificationsPopover>
</HeaderButton>
</FullWidthButton>
)}
</AccountMenu>
)}
@@ -259,8 +266,6 @@ 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} {
@@ -3,9 +3,11 @@ import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import { undraggableOnDesktop } from "~/styles";
import { draggableOnDesktop, undraggableOnDesktop } from "~/styles";
import Desktop from "~/utils/Desktop";
export type HeaderButtonProps = React.ComponentProps<typeof Button> & {
export type FullWidthButtonProps = React.ComponentProps<typeof Button> & {
position: "top" | "bottom";
title: React.ReactNode;
image: React.ReactNode;
minHeight?: number;
@@ -16,40 +18,53 @@ export type HeaderButtonProps = React.ComponentProps<typeof Button> & {
children?: React.ReactNode;
};
const HeaderButton = React.forwardRef<HTMLButtonElement, HeaderButtonProps>(
function _HeaderButton(
{
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: HeaderButtonProps,
ref
) {
return (
<Flex justify="space-between" align="center" shrink={false}>
<Button
{...rest}
minHeight={minHeight}
as="button"
ref={ref}
role="button"
>
<Title gap={8} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Flex>
);
}
);
const FullWidthButton = React.forwardRef<
HTMLButtonElement,
FullWidthButtonProps
>(function _FullWidthButton(
{
position = "top",
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: FullWidthButtonProps,
ref
) {
return (
<Container
justify="space-between"
align="center"
shrink={false}
$position={position}
>
<Button
{...rest}
minHeight={minHeight}
as="button"
ref={ref}
role="button"
>
<Title gap={8} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Container>
);
});
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")};
@@ -99,4 +114,4 @@ const Button = styled(Flex)<{ minHeight: number }>`
}
`;
export default HeaderButton;
export default FullWidthButton;
@@ -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,3 +1,4 @@
import includes from "lodash/includes";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -6,14 +7,15 @@ 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 | undefined;
activeDocument: Document | undefined;
activeDocumentId?: string;
activeDocument?: Document;
isDraft?: boolean;
depth: number;
index: number;
@@ -41,10 +43,19 @@ function DocumentLink(
const hasChildDocuments =
!!node.children.length || activeDocument?.parentDocumentId === node.id;
const document = documents.get(node.id);
const showChildren = React.useMemo(
() => !!hasChildDocuments,
[hasChildDocuments]
() =>
!!(
hasChildDocuments &&
((activeDocumentId &&
includes(
descendants(node).map((n) => n.id),
activeDocumentId
)) ||
isActiveDocument ||
depth <= 1)
),
[hasChildDocuments, activeDocumentId, isActiveDocument, depth, node]
);
const [expanded, setExpanded] = React.useState(showChildren);
@@ -55,12 +66,6 @@ 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: 1.6;
line-height: 24px;
* {
unicode-bidi: plaintext;
+1 -1
View File
@@ -1,4 +1,4 @@
import { isEqual } from "lodash";
import isEqual from "lodash/isEqual";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
+1 -1
View File
@@ -14,7 +14,7 @@ type Props = {
*/
const Text = styled.p<Props>`
margin-top: 0;
text-align: ${(props) => (props.dir ? props.dir : "initial")};
text-align: ${(props) => (props.dir ? props.dir : "inherit")};
color: ${(props) =>
props.type === "secondary"
? props.theme.textSecondary
+1 -1
View File
@@ -1,5 +1,5 @@
import invariant from "invariant";
import { find } from "lodash";
import find from "lodash/find";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
+3 -1
View File
@@ -9,6 +9,7 @@ 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";
@@ -40,6 +41,7 @@ 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) {
@@ -48,7 +50,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 && viewportHeight) {
if (isTouchDevice && isMobile && viewportHeight) {
return {
left: 0,
right: 0,
+2 -2
View File
@@ -1,4 +1,4 @@
import { some } from "lodash";
import some from "lodash/some";
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 = getImageMenuItems(state, dictionary);
items = readOnly ? [] : 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";
import capitalize from "lodash/capitalize";
import * as React from "react";
import { Trans } from "react-i18next";
import { VisuallyHidden } from "reakit/VisuallyHidden";
+6
View File
@@ -8,4 +8,10 @@ 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";
import flattenDeep from "lodash/flattenDeep";
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";
import find from "lodash/find";
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";
import throttle from "lodash/throttle";
import * as React from "react";
import { Minute } from "@shared/utils/time";
+1 -1
View File
@@ -1,4 +1,4 @@
import { noop } from "lodash";
import noop from "lodash/noop";
import React from "react";
type MenuContextType = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { throttle } from "lodash";
import throttle from "lodash/throttle";
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";
import throttle from "lodash/throttle";
import { useState, useEffect } from "react";
import { supportsPassiveListener } from "~/utils/browser";
import { supportsPassiveListener } from "@shared/utils/browser";
const getPosition = () => ({
x: window.pageXOffset,
+1 -1
View File
@@ -1,4 +1,4 @@
import { pick } from "lodash";
import pick from "lodash/pick";
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";
import trim from "lodash/trim";
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";
import floor from "lodash/floor";
import { action, autorun, computed, observable, set } from "mobx";
import { ExportContentType } from "@shared/types";
import type { NavigationNode } from "@shared/types";
+1 -1
View File
@@ -1,4 +1,4 @@
import { sortBy } from "lodash";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+2 -1
View File
@@ -1,4 +1,4 @@
import { intersection } from "lodash";
import intersection from "lodash/intersection";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
@@ -56,6 +56,7 @@ 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";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -1,4 +1,4 @@
import { throttle } from "lodash";
import throttle from "lodash/throttle";
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";
import debounce from "lodash/debounce";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { AllSelection } from "prosemirror-state";
+63 -57
View File
@@ -104,7 +104,6 @@ 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")}
@@ -138,7 +137,7 @@ function DocumentHeader({
to={documentEditPath(document)}
neutral
>
{t("Edit")}
{isMobile ? null : t("Edit")}
</Button>
</Tooltip>
</Action>
@@ -185,7 +184,7 @@ function DocumentHeader({
actions={
<>
{appearanceAction}
{canEdit ? editAction : <div />}
{can.update && !isEditing ? editAction : <div />}
</>
}
/>
@@ -250,45 +249,56 @@ function DocumentHeader({
disabled={savingIsDisabled}
neutral={isDraft}
>
{isDraft ? t("Save Draft") : t("Done Editing")}
{isDraft ? t("Save draft") : t("Done editing")}
</Button>
</Tooltip>
</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>
)}
{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>
)}
{revision && revision.createdAt !== document.updatedAt && (
<Action>
<Tooltip
@@ -318,27 +328,23 @@ function DocumentHeader({
{document.collectionId ? t("Publish") : `${t("Publish")}`}
</Button>
</Action>
{!isEditing && (
<>
{!isDeleted && <Separator />}
<Action>
<DocumentMenu
document={document}
isRevision={isRevision}
label={(props) => (
<Button
icon={<MoreIcon />}
{...props}
borderOnHover
neutral
/>
)}
showToggleEmbeds={canToggleEmbeds}
showDisplayOptions
{!isDeleted && <Separator />}
<Action>
<DocumentMenu
document={document}
isRevision={isRevision}
label={(props) => (
<Button
icon={<MoreIcon />}
{...props}
borderOnHover
neutral
/>
</Action>
</>
)}
)}
showToggleEmbeds={canToggleEmbeds}
showDisplayOptions
/>
</Action>
</>
}
/>
@@ -1,36 +1,31 @@
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 useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
function KeyboardShortcutsButton() {
const { t } = useTranslation();
const [
keyboardShortcutsOpen,
handleOpenKeyboardShortcuts,
handleCloseKeyboardShortcuts,
] = useBoolean();
const { dialogs } = useStores();
const handleOpenKeyboardShortcuts = () => {
dialogs.openGuide({
title: t("Keyboard shortcuts"),
content: <KeyboardShortcuts />,
});
};
return (
<>
<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>
</>
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
<Button onClick={handleOpenKeyboardShortcuts}>
<KeyboardIcon />
</Button>
</Tooltip>
);
}
@@ -49,4 +44,4 @@ const Button = styled(NudeButton)`
}
`;
export default KeyboardShortcutsButton;
export default observer(KeyboardShortcutsButton);
@@ -1,11 +1,12 @@
import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider";
import { throttle } from "lodash";
import throttle from "lodash/throttle";
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";
@@ -16,7 +17,6 @@ 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,13 +135,10 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
});
provider.on("close", (ev: MessageEvent) => {
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."
)
);
if ("code" in ev.event) {
provider.shouldConnect =
ev.event.code !== 1009 && ev.event.code !== 4401;
ui.setMultiplayerStatus("disconnected", ev.event.code);
}
});
@@ -164,9 +161,11 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
);
}
provider.on("status", (ev: ConnectionStatusEvent) =>
ui.setMultiplayerStatus(ev.status)
);
provider.on("status", (ev: ConnectionStatusEvent) => {
if (ui.multiplayerStatus !== ev.status) {
ui.setMultiplayerStatus(ev.status, undefined);
}
});
setRemoteProvider(provider);
@@ -177,7 +176,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
provider?.destroy();
void localProvider?.destroy();
setRemoteProvider(null);
ui.setMultiplayerStatus(undefined);
ui.setMultiplayerStatus(undefined, undefined);
};
}, [
history,
@@ -1,5 +1,6 @@
import invariant from "invariant";
import { debounce, isEmpty } from "lodash";
import debounce from "lodash/debounce";
import isEmpty from "lodash/isEmpty";
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";
import flatten from "lodash/flatten";
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";
import flatten from "lodash/flatten";
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";
import debounce from "lodash/debounce";
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 canManageTeam = usePolicy(team).manage;
const can = usePolicy(team);
return (
<Scene
@@ -49,7 +49,7 @@ function Home() {
>
{!ui.languagePromptDismissed && <LanguagePrompt />}
<Heading>{t("Home")}</Heading>
<PinnedDocuments pins={pins.home} canUpdate={canManageTeam} />
<PinnedDocuments pins={pins.home} canUpdate={can.update} />
<Documents>
<Tabs>
<Tab to="/home" exact>
+2 -1
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,6 +442,7 @@ const List = styled.dl`
overflow: hidden;
padding: 0;
margin: 0;
user-select: none;
`;
const Keys = styled.dt`
+7 -1
View File
@@ -37,7 +37,13 @@ export default function Notices() {
Please use a Google Workspaces account instead.
</Trans>
)}
{notice === "maximum-teams" && (
{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" && (
<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";
import find from "lodash/find";
import { observer } from "mobx-react";
import { BackIcon, EmailIcon } from "outline-icons";
import * as React from "react";
+1 -1
View File
@@ -1,4 +1,4 @@
import { isEqual } from "lodash";
import isEqual from "lodash/isEqual";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import queryString from "query-string";
+37 -2
View File
@@ -1,5 +1,5 @@
import { isHexColor } from "class-validator";
import { pickBy } from "lodash";
import pickBy from "lodash/pickBy";
import { observer } from "mobx-react";
import { TeamIcon } from "outline-icons";
import { useRef, useState } from "react";
@@ -20,18 +20,22 @@ 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, ui } = useStores();
const { auth, dialogs, 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
@@ -125,6 +129,14 @@ 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);
@@ -222,6 +234,7 @@ function Details() {
</SettingRow>
{team.avatarUrl && (
<SettingRow
border={false}
name={TeamPreference.PublicBranding}
label={t("Public branding")}
description={t(
@@ -287,6 +300,28 @@ 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";
import find from "lodash/find";
import { observer } from "mobx-react";
import * as React from "react";
import { useForm } from "react-hook-form";
+2 -2
View File
@@ -1,4 +1,4 @@
import { sortBy } from "lodash";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import { PlusIcon, UserIcon } from "outline-icons";
import * as React from "react";
@@ -184,7 +184,7 @@ function Members() {
</Flex>
<PeopleTable
data={data}
canManage={can.manage}
canManage={can.update}
isLoading={isLoading}
page={page}
pageSize={limit}
+1 -1
View File
@@ -1,4 +1,4 @@
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import {
AcademicCapIcon,
+2 -2
View File
@@ -47,6 +47,7 @@ function Preferences() {
dialogs.openModal({
title: t("Delete account"),
content: <UserDelete />,
isCentered: true,
});
};
@@ -131,8 +132,7 @@ function Preferences() {
/>
</SettingRow>
<p>&nbsp;</p>
<Heading as="h2">{t("Danger")}</Heading>
<SettingRow
name="delete"
label={t("Delete account")}
+1 -1
View File
@@ -1,4 +1,4 @@
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { CheckboxIcon, EmailIcon, PadlockIcon } from "outline-icons";
import { useState } from "react";
+44 -8
View File
@@ -1,4 +1,4 @@
import { find } from "lodash";
import find from "lodash/find";
import { observer } from "mobx-react";
import { BuildingBlocksIcon } from "outline-icons";
import * as React from "react";
@@ -16,6 +16,7 @@ import SettingRow from "./components/SettingRow";
type FormData = {
drawIoUrl: string;
gristUrl: string;
};
function SelfHosted() {
@@ -23,11 +24,16 @@ function SelfHosted() {
const { t } = useTranslation();
const { showToast } = useToasts();
const integration = find(integrations.orderedData, {
const integrationDiagrams = 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,
@@ -36,7 +42,8 @@ function SelfHosted() {
} = useForm<FormData>({
mode: "all",
defaultValues: {
drawIoUrl: integration?.settings.url,
drawIoUrl: integrationDiagrams?.settings.url,
gristUrl: integrationGrist?.settings.url,
},
});
@@ -47,15 +54,18 @@ function SelfHosted() {
}, [integrations]);
React.useEffect(() => {
reset({ drawIoUrl: integration?.settings.url });
}, [integration, reset]);
reset({
drawIoUrl: integrationDiagrams?.settings.url,
gristUrl: integrationGrist?.settings.url,
});
}, [integrationDiagrams, integrationGrist, reset]);
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
if (data.drawIoUrl) {
await integrations.save({
id: integration?.id,
id: integrationDiagrams?.id,
type: IntegrationType.Embed,
service: IntegrationService.Diagrams,
settings: {
@@ -63,7 +73,20 @@ function SelfHosted() {
},
});
} else {
await integration?.delete();
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();
}
showToast(t("Settings saved"), {
@@ -75,7 +98,7 @@ function SelfHosted() {
});
}
},
[integrations, integration, t, showToast]
[integrations, integrationDiagrams, integrationGrist, t, showToast]
);
return (
@@ -98,6 +121,19 @@ 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";
import sortBy from "lodash/sortBy";
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.manage && !canShareDocuments && (
{can.update && !canShareDocuments && (
<>
<Notice icon={<WarningIcon />}>
{t("Sharing is currently disabled.")}{" "}
@@ -95,7 +95,7 @@ function Shares() {
<SharesTable
data={data}
canManage={can.manage}
canManage={can.update}
isLoading={isLoading}
page={page}
pageSize={limit}
@@ -1,4 +1,4 @@
import { compact } from "lodash";
import compact from "lodash/compact";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
+122
View File
@@ -0,0 +1,122 @@
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);
+22 -14
View File
@@ -5,6 +5,7 @@ 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";
@@ -46,17 +47,14 @@ function TeamNew({ user }: Props) {
return (
<>
<form onSubmit={handleSubmit}>
<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>
<Notice>
<Trans>
Please note that workspaces are completely separate. They can have a
different domain, settings, users, and billing.
</Trans>
</Notice>
<p />
<Flex>
<Input
@@ -69,11 +67,21 @@ 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>
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.
To create a workspace under another email please sign up from the
homepage
</Trans>
</Text>
+8 -12
View File
@@ -30,7 +30,7 @@ function UserDelete() {
ev.preventDefault();
try {
await auth.requestDelete();
await auth.requestDeleteUser();
setWaitingCode(true);
} catch (error) {
showToast(error.message, {
@@ -71,16 +71,8 @@ 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="CODE"
placeholder={t("Confirmation code")}
autoComplete="off"
autoFocus
maxLength={8}
@@ -105,10 +97,14 @@ function UserDelete() {
{t("Continue")}
</Button>
) : (
<Button type="submit" disabled={formState.isSubmitting} danger>
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
danger
>
{formState.isSubmitting
? `${t("Deleting")}`
: t("Delete My Account")}
: t("Delete my account")}
</Button>
)}
</form>
+16 -3
View File
@@ -241,13 +241,14 @@ export default class AuthStore {
}
};
@action
requestDelete = () => client.post(`/users.requestDelete`);
requestDeleteUser = () => client.post(`/users.requestDelete`);
requestDeleteTeam = () => client.post(`/teams.requestDelete`);
@action
deleteUser = async (data: { code: string }) => {
await client.post(`/users.delete`, data);
runInAction("AuthStore#updateUser", () => {
runInAction("AuthStore#deleteUser", () => {
this.user = null;
this.team = null;
this.collaborationToken = null;
@@ -258,6 +259,18 @@ 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;
+2 -1
View File
@@ -1,5 +1,6 @@
import invariant from "invariant";
import { lowerFirst, orderBy } from "lodash";
import lowerFirst from "lodash/lowerFirst";
import orderBy from "lodash/orderBy";
import { observable, action, computed, runInAction } from "mobx";
import { Class } from "utility-types";
import RootStore from "~/stores/RootStore";
+4 -1
View File
@@ -1,5 +1,8 @@
import invariant from "invariant";
import { concat, find, last, sortBy } from "lodash";
import concat from "lodash/concat";
import find from "lodash/find";
import last from "lodash/last";
import sortBy from "lodash/sortBy";
import { computed, action } from "mobx";
import {
CollectionPermission,
+2 -1
View File
@@ -1,5 +1,6 @@
import invariant from "invariant";
import { filter, orderBy } from "lodash";
import filter from "lodash/filter";
import orderBy from "lodash/orderBy";
import { action, runInAction, computed } from "mobx";
import Comment from "~/models/Comment";
import Document from "~/models/Document";
+5 -1
View File
@@ -1,5 +1,9 @@
import invariant from "invariant";
import { find, orderBy, filter, compact, omitBy } from "lodash";
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 { observable, action, computed, runInAction } from "mobx";
import { DateFilter, NavigationNode, PublicTeam } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
+2 -1
View File
@@ -1,4 +1,5 @@
import { sortBy, filter } from "lodash";
import filter from "lodash/filter";
import sortBy from "lodash/sortBy";
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";
import orderBy from "lodash/orderBy";
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";
import filter from "lodash/filter";
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";
import filter from "lodash/filter";
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";
import filter from "lodash/filter";
import { computed } from "mobx";
import { IntegrationService } from "@shared/types";
import naturalSort from "@shared/utils/naturalSort";
+2 -1
View File
@@ -1,5 +1,6 @@
import invariant from "invariant";
import { orderBy, sortBy } from "lodash";
import orderBy from "lodash/orderBy";
import sortBy from "lodash/sortBy";
import { action, computed, runInAction } from "mobx";
import Notification from "~/models/Notification";
import { PaginationParams } from "~/types";
+1 -1
View File
@@ -1,5 +1,5 @@
import invariant from "invariant";
import { filter } from "lodash";
import filter from "lodash/filter";
import { action, runInAction } from "mobx";
import BaseStore, { RPCAction } from "~/stores/BaseStore";
import RootStore from "~/stores/RootStore";
+1 -1
View File
@@ -1,4 +1,4 @@
import { uniqBy } from "lodash";
import uniqBy from "lodash/uniqBy";
import { computed } from "mobx";
import SearchQuery from "~/models/SearchQuery";
import BaseStore, { RPCAction } from "./BaseStore";
+4 -1
View File
@@ -1,5 +1,8 @@
import invariant from "invariant";
import { sortBy, filter, find, isUndefined } from "lodash";
import filter from "lodash/filter";
import find from "lodash/find";
import isUndefined from "lodash/isUndefined";
import sortBy from "lodash/sortBy";
import { action, computed } from "mobx";
import Share from "~/models/Share";
import { client } from "~/utils/ApiClient";
+1 -1
View File
@@ -1,4 +1,4 @@
import { orderBy } from "lodash";
import orderBy from "lodash/orderBy";
import { observable, action, computed } from "mobx";
import { v4 as uuidv4 } from "uuid";
import { Toast, ToastOptions } from "~/types";
+8 -1
View File
@@ -69,6 +69,9 @@ class UiStore {
@observable
multiplayerStatus: ConnectionStatus;
@observable
multiplayerErrorCode?: number;
constructor() {
// Rehydrate
const data: Partial<UiStore> = Storage.get(UI_STORE) || {};
@@ -133,8 +136,12 @@ class UiStore {
};
@action
setMultiplayerStatus = (status: ConnectionStatus): void => {
setMultiplayerStatus = (
status: ConnectionStatus,
errorCode?: number
): void => {
this.multiplayerStatus = status;
this.multiplayerErrorCode = errorCode;
};
@action
+2 -1
View File
@@ -1,5 +1,6 @@
import invariant from "invariant";
import { filter, orderBy } from "lodash";
import filter from "lodash/filter";
import orderBy from "lodash/orderBy";
import { observable, computed, action, runInAction } from "mobx";
import { Role } from "@shared/types";
import User from "~/models/User";
+4 -1
View File
@@ -1,4 +1,7 @@
import { reduce, filter, find, orderBy } from "lodash";
import filter from "lodash/filter";
import find from "lodash/find";
import orderBy from "lodash/orderBy";
import reduce from "lodash/reduce";
import View from "~/models/View";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
+1 -1
View File
@@ -1,5 +1,5 @@
import { isTouchDevice } from "@shared/utils/browser";
import Desktop from "~/utils/Desktop";
import { isTouchDevice } from "~/utils/browser";
/**
* Returns "hover" on a non-touch device and "active" on a touch device. To
+1 -1
View File
@@ -1,5 +1,5 @@
import retry from "fetch-retry";
import { trim } from "lodash";
import trim from "lodash/trim";
import queryString from "query-string";
import EDITOR_VERSION from "@shared/editor/version";
import stores from "~/stores";
+1 -1
View File
@@ -1,4 +1,4 @@
import { isMac, isWindows } from "./browser";
import { isMac, isWindows } from "@shared/utils/browser";
export default class Desktop {
/**
+1 -1
View File
@@ -7,7 +7,7 @@ import {
format as formatDate,
} from "date-fns";
import { TFunction } from "i18next";
import { startCase } from "lodash";
import startCase from "lodash/startCase";
import {
getCurrentDateAsString,
getCurrentDateTimeAsString,

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