Compare commits

...

26 Commits

Author SHA1 Message Date
tommoor ce0936ebb6 chore: Compressed inefficient images automatically 2025-10-12 20:06:11 +00:00
Tom Moor bb72774f2d fix: Issue introduced when document.editorVersion is null (#10352) 2025-10-12 13:52:45 -04:00
Tom Moor 76868a3083 chore: Replace UUID package with standard module (#10351)
* fix: Missing replacements

* More
2025-10-12 13:15:53 -04:00
Tom Moor 0865052bb8 fix: Missing replacements (#10350) 2025-10-12 12:48:51 -04:00
Tom Moor de6bc9beca fix: Mispositioned toolbar (#10343)
* fix: Mispositioned toolbar

* tsc
2025-10-10 22:53:44 -04:00
Translate-O-Tron e97944ab40 New Crowdin updates (#10294)
* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Hebrew translations from Crowdin [ci skip]

* fix: New Hungarian translations from Crowdin [ci skip]

* fix: New Polish translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Romanian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Czech translations from Crowdin [ci skip]

* fix: New Danish translations from Crowdin [ci skip]

* fix: New Italian translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Swedish translations from Crowdin [ci skip]

* fix: New Turkish translations from Crowdin [ci skip]

* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Traditional translations from Crowdin [ci skip]

* fix: New Vietnamese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Indonesian translations from Crowdin [ci skip]

* fix: New Persian translations from Crowdin [ci skip]

* fix: New Thai translations from Crowdin [ci skip]

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Hebrew translations from Crowdin [ci skip]

* fix: New Hungarian translations from Crowdin [ci skip]

* fix: New Polish translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Romanian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Czech translations from Crowdin [ci skip]

* fix: New Danish translations from Crowdin [ci skip]

* fix: New Italian translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Swedish translations from Crowdin [ci skip]

* fix: New Turkish translations from Crowdin [ci skip]

* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Traditional translations from Crowdin [ci skip]

* fix: New Vietnamese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Indonesian translations from Crowdin [ci skip]

* fix: New Persian translations from Crowdin [ci skip]

* fix: New Thai translations from Crowdin [ci skip]

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Hebrew translations from Crowdin [ci skip]

* fix: New Hungarian translations from Crowdin [ci skip]

* fix: New Polish translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Romanian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Czech translations from Crowdin [ci skip]

* fix: New Danish translations from Crowdin [ci skip]

* fix: New Italian translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Swedish translations from Crowdin [ci skip]

* fix: New Turkish translations from Crowdin [ci skip]

* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Traditional translations from Crowdin [ci skip]

* fix: New Vietnamese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Indonesian translations from Crowdin [ci skip]

* fix: New Persian translations from Crowdin [ci skip]

* fix: New Thai translations from Crowdin [ci skip]

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Hebrew translations from Crowdin [ci skip]

* fix: New Hungarian translations from Crowdin [ci skip]

* fix: New Polish translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Romanian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Czech translations from Crowdin [ci skip]

* fix: New Danish translations from Crowdin [ci skip]

* fix: New Italian translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Swedish translations from Crowdin [ci skip]

* fix: New Turkish translations from Crowdin [ci skip]

* fix: New Ukrainian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Traditional translations from Crowdin [ci skip]

* fix: New Vietnamese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Indonesian translations from Crowdin [ci skip]

* fix: New Persian translations from Crowdin [ci skip]

* fix: New Thai translations from Crowdin [ci skip]

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2025-10-10 20:16:29 -04:00
Salihu 5cfea207e6 restore comment content on error (#10342) 2025-10-10 20:16:10 -04:00
Apoorv Mishra 95f0c42d56 Mention chip for regular URLs (#10327)
* fix: replace oembed with iframely

* feat: wip

fix: favicon

* fix: missing icon in API response
2025-10-10 19:40:05 -04:00
Tom Moor ee7738c141 fix: RedisAdapter does not respect url arg (#10341) 2025-10-10 18:09:24 -04:00
Alex 76701e35ec fix: replace uuid package with standard module (#10318) 2025-10-10 17:06:51 -04:00
codegen-sh[bot] ae8c2aae15 fix: Default destination path for nested document duplication (#10339)
* fix: Default destination path for nested document duplication

When duplicating nested documents, the DocumentExplorer component was only
searching top-level items to find the default node, causing the parent
document to not be found and selected as the default destination.

This fix updates the component to use flattenTree utility to search through
all nodes in the tree hierarchy, ensuring nested parent documents are
properly found and selected as the default destination.

Fixes #10333

* fix: Move flatten import to correct position to resolve TypeScript error

The flatten function from lodash was being used before it was imported,
causing a TypeScript compilation error. This commit moves the import
statement to the proper location with other lodash imports.

* fix: Move nodes declaration before useEffect to resolve temporal dead zone error

The nodes variable was being used in a useEffect dependency array before it was declared, causing TypeScript compilation errors. This commit moves the getNodes function and nodes declaration before the useEffect that uses them, resolving the temporal dead zone issue.

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-10 17:06:37 -04:00
Tom Moor a9fa2ed72b fix: User name should be selectable in members table (#10338) 2025-10-10 13:09:12 +00:00
Tom Moor 0deb7e7f09 fix: editorVersion property on document should be updated through collaborative service (#10325) 2025-10-10 09:07:15 -04:00
Tom Moor a544559de2 fix: Prevent reload loop when collaborative service editor version is ahead (#10326) 2025-10-10 09:07:07 -04:00
Nico Hülscher 79fe08e9b6 fix: mobile safari sidebar navigation issue (#10329)
* fix: mobile safari sidebar navigation issue

* fix: readd hover for possible edge cases
2025-10-10 09:06:57 -04:00
codegen-sh[bot] c8d8ba3914 Fix Redis reusing same property as (#10336)
The collaborationClient getter was incorrectly reusing the same this.client
property as defaultClient, causing it to return the already-initialized
connection to the main Redis instead of creating a new connection to
REDIS_COLLABORATION_URL.

This fix adds a separate private static collabClient property to maintain
a separate connection for collaboration operations.

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-10 09:00:20 -04:00
codegen-sh[bot] 7a148b0353 Fix autolink when text is within inline code marks (#10322)
* Fix autolink when text is within inline code marks

- Use isInCode with inclusive: true option to properly detect when cursor is within inline code marks
- Prevents autolink from converting URLs to links when typing within backticks
- Fixes issue #10321

Co-authored-by: Tom Moor <tom@getoutline.com>

* Update Link.tsx

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-08 01:59:40 +00:00
Salihu ca891a56da change list children to match list parent when list style changes (#10315) 2025-10-07 21:16:11 -04:00
Apoorv Mishra 294d3e896a Pan & Zoom (#10271)
* feat: pan and zoom inside lightbox

* fix: cleanup

* fix: edge-to-edge panning

* fix: restore closing animation when lightbox is closed while it's still opening

* fix: zoom in/out action buttons

* fix: swipe

* fix: bg for action buttons

* fix: image err

* fix: comment

* fix: being explicit

* trigger ci

* Lockfile

* Update app/components/Lightbox.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-10-07 20:36:53 -04:00
dependabot[bot] d947f8fda2 chore(deps): bump @bull-board/koa from 6.12.0 to 6.13.0 (#10312)
Bumps [@bull-board/koa](https://github.com/felixmosh/bull-board/tree/HEAD/packages/koa) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/felixmosh/bull-board/releases)
- [Changelog](https://github.com/felixmosh/bull-board/blob/master/CHANGELOG.md)
- [Commits](https://github.com/felixmosh/bull-board/commits/v6.13.0/packages/koa)

---
updated-dependencies:
- dependency-name: "@bull-board/koa"
  dependency-version: 6.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:53:23 -04:00
dependabot[bot] 6dd228a533 chore(deps): bump the fortawesome group with 3 updates (#10310)
Bumps the fortawesome group with 3 updates: [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome), [@fortawesome/free-brands-svg-icons](https://github.com/FortAwesome/Font-Awesome) and [@fortawesome/free-solid-svg-icons](https://github.com/FortAwesome/Font-Awesome).


Updates `@fortawesome/fontawesome-svg-core` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

Updates `@fortawesome/free-brands-svg-icons` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

Updates `@fortawesome/free-solid-svg-icons` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-svg-core"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
- dependency-name: "@fortawesome/free-brands-svg-icons"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
- dependency-name: "@fortawesome/free-solid-svg-icons"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:53:15 -04:00
dependabot[bot] c7d847215c chore(deps): bump the aws group with 5 updates (#10311)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.896.0` | `3.901.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.896.0` | `3.903.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.896.0` | `3.901.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.896.0` | `3.901.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.896.0` | `3.901.0` |


Updates `@aws-sdk/client-s3` from 3.896.0 to 3.901.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.901.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.896.0 to 3.903.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.903.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.896.0 to 3.901.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.901.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.896.0 to 3.901.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.901.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.896.0 to 3.901.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.901.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.901.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.903.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.901.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.901.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.901.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:50:06 -04:00
dependabot[bot] 6995ca8521 chore(deps-dev): bump @types/validator from 13.15.2 to 13.15.3 (#10314)
Bumps [@types/validator](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/validator) from 13.15.2 to 13.15.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/validator)

---
updated-dependencies:
- dependency-name: "@types/validator"
  dependency-version: 13.15.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:49:57 -04:00
dependabot[bot] 8a3452e664 chore(deps): bump nodemailer from 6.10.1 to 7.0.7 (#10320)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.10.1 to 7.0.7.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.7)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 16:43:49 -04:00
Tom Moor f6315875b4 fix: CSRF validation issues on Firefox (#10317) 2025-10-06 19:10:25 -04:00
Tom Moor f4e53da1bf fix: Flipped logic in export all (#10305) 2025-10-05 21:35:44 -04:00
115 changed files with 1820 additions and 1209 deletions
+6 -7
View File
@@ -2,7 +2,6 @@ import { LocationDescriptor } from "history";
import flattenDeep from "lodash/flattenDeep";
import { toast } from "sonner";
import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import {
Action,
ActionContext,
@@ -46,7 +45,7 @@ export function createAction(definition: Optional<Action, "id">): Action {
return definition.perform?.(context);
}
: undefined,
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -202,7 +201,7 @@ export function createActionV2(
return definition.perform(context);
}
: () => {},
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -213,7 +212,7 @@ export function createInternalLinkActionV2(
...definition,
type: "action",
variant: "internal_link",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -224,7 +223,7 @@ export function createExternalLinkActionV2(
...definition,
type: "action",
variant: "external_link",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -235,7 +234,7 @@ export function createActionV2WithChildren(
...definition,
type: "action",
variant: "action_with_children",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -252,7 +251,7 @@ export function createRootMenuAction(
actions: (ActionV2Variant | ActionV2Group | TActionV2Separator)[]
): ActionV2WithChildren {
return {
id: uuidv4(),
id: crypto.randomUUID(),
type: "action",
variant: "action_with_children",
name: "root_action",
+24 -17
View File
@@ -3,6 +3,7 @@ import concat from "lodash/concat";
import difference from "lodash/difference";
import fill from "lodash/fill";
import filter from "lodash/filter";
import flatten from "lodash/flatten";
import includes from "lodash/includes";
import map from "lodash/map";
import { observer } from "mobx-react";
@@ -27,7 +28,6 @@ import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { ancestors, descendants, flattenTree } from "~/utils/tree";
import flatten from "lodash/flatten";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
@@ -49,8 +49,13 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [searchTerm, setSearchTerm] = React.useState<string>();
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
() => {
const node =
defaultValue && items.find((item) => item.id === defaultValue);
if (!defaultValue) {
return null;
}
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
return node || null;
}
);
@@ -59,7 +64,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((ancestorNode) => ancestorNode.id);
}
@@ -104,19 +111,6 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
);
}, [items.length]);
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, []);
function getNodes() {
function includeDescendants(item: NavigationNode): NavigationNode[] {
return expandedNodes.includes(item.id)
@@ -130,6 +124,19 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
}
const nodes = getNodes();
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, [defaultValue, selectedNode, nodes]);
const baseDepth = nodes.reduce(
(min, node) => (node.depth ? Math.min(min, node.depth) : min),
Infinity
+339 -53
View File
@@ -1,7 +1,17 @@
import { observer } from "mobx-react";
import * as Dialog from "@radix-ui/react-dialog";
import styled, { css, Keyframes, keyframes } from "styled-components";
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import {
ComponentProps,
createContext,
forwardRef,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { isInternalUrl } from "@shared/utils/urls";
import { Error as ImageError } from "@shared/editor/components/Image";
import {
@@ -11,6 +21,8 @@ import {
DownloadIcon,
LinkIcon,
NextIcon,
ZoomInIcon,
ZoomOutIcon,
} from "outline-icons";
import { depths, extraArea, s } from "@shared/styles";
import NudeButton from "./NudeButton";
@@ -28,6 +40,13 @@ import useSwipe from "~/hooks/useSwipe";
import { toast } from "sonner";
import { findIndex } from "lodash";
import { LightboxImage } from "@shared/editor/lib/Lightbox";
import {
TransformWrapper,
TransformComponent,
useTransformEffect,
ReactZoomPanPinchRef,
} from "react-zoom-pan-pinch";
import { transparentize } from "polished";
export enum LightboxStatus {
READY_TO_OPEN,
@@ -42,6 +61,9 @@ export enum ImageStatus {
LOADING,
ERROR,
LOADED,
MIN_ZOOM,
MAX_ZOOM,
ZOOMED,
}
type Status = {
lightbox: LightboxStatus | null;
@@ -69,11 +91,102 @@ type Props = {
onClose: () => void;
};
const ZoomPanPinchContext = createContext({ isImagePanning: false });
type ZoomablePannablePinchableProps = {
children: ReactNode;
panningDisabled: boolean;
disabled: boolean;
};
const ZoomablePannablePinchable = forwardRef<
ReactZoomPanPinchRef,
ZoomablePannablePinchableProps
>(({ children, panningDisabled, disabled }, ref) => {
const { isPanning, ...panningHandlers } = usePanning();
return (
<ZoomPanPinchContext.Provider value={{ isImagePanning: isPanning }}>
<TransformWrapper
ref={ref}
disabled={disabled}
doubleClick={{ disabled: true }}
minScale={1}
maxScale={8}
panning={{
disabled: panningDisabled,
}}
{...panningHandlers}
>
<TransformComponent
wrapperStyle={{ width: "100%", height: "100%" }}
contentStyle={{
width: "100%",
height: "100%",
padding: "56px",
justifyContent: "center",
alignItems: "center",
}}
>
{children}
</TransformComponent>
</TransformWrapper>
</ZoomPanPinchContext.Provider>
);
});
function usePanning() {
const [isPanning, setPanning] = useState(false);
const dragged = useRef(false);
const onPanningStart: ComponentProps<
typeof TransformWrapper
>["onPanningStart"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
const zoomedIn = ref.state.scale > 1;
if (zoomedIn) {
setPanning(ref.instance.isPanning);
}
};
const onPanning: ComponentProps<
typeof TransformWrapper
>["onPanning"] = () => {
dragged.current = true;
};
const onPanningStop: ComponentProps<
typeof TransformWrapper
>["onPanningStop"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
setPanning(ref.instance.isPanning);
if (dragged.current) {
dragged.current = false;
} else {
const zoomedOut = Math.abs(ref.state.scale - 1) < 0.001;
if (zoomedOut) {
ref.zoomIn();
} else {
ref.resetTransform();
}
}
};
return {
isPanning,
onPanningStart,
onPanning,
onPanningStop,
};
}
function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
const isIdle = useIdle(3 * Second.ms);
const { t } = useTranslation();
const imgRef = useRef<HTMLImageElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(null);
const [status, setStatus] = useState<Status>({ lightbox: null, image: null });
const animation = useRef<Animation | null>(null);
const finalImage = useRef<{
@@ -81,6 +194,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
width: number;
height: number;
} | null>(null);
const zoomPanPinchRef = useRef<ReactZoomPanPinchRef>(null);
const currentImageIndex = findIndex(
images,
@@ -131,6 +245,18 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
}
}, [status.image, status.lightbox]);
useEffect(() => {
if (
status.lightbox === LightboxStatus.OPENED &&
status.image === ImageStatus.LOADED
) {
setStatus({
lightbox: LightboxStatus.OPENED,
image: ImageStatus.MIN_ZOOM,
});
}
}, [status.lightbox, status.image]);
useEffect(() => {
if (status.lightbox === LightboxStatus.READY_TO_CLOSE) {
setupFadeOut();
@@ -148,6 +274,15 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
}
}, [status.lightbox]);
useEffect(() => {
if (status.image === ImageStatus.MIN_ZOOM) {
// It was observed that focus went to `body` as the zoom out button was disabled
// upon clicking it. This stopped navigating to next/previous image using arrow keys.
// So focusing the content div here to restore the functionality.
contentRef.current?.focus();
}
}, [status.image]);
const rememberImagePosition = () => {
if (imgRef.current) {
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
@@ -261,7 +396,13 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const setupZoomOut = () => {
if (imgRef.current) {
if (
imgRef.current &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
) {
// in lightbox
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
const {
@@ -356,7 +497,11 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const prev = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (
status.lightbox === LightboxStatus.OPENED &&
(status.image === ImageStatus.MIN_ZOOM ||
status.image === ImageStatus.ERROR)
) {
const prevIndex = currentImageIndex - 1;
if (prevIndex < 0) {
return;
@@ -366,7 +511,11 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const next = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (
status.lightbox === LightboxStatus.OPENED &&
(status.image === ImageStatus.MIN_ZOOM ||
status.image === ImageStatus.ERROR)
) {
const nextIndex = currentImageIndex + 1;
if (nextIndex >= images.length) {
return;
@@ -500,7 +649,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
onAnimationStart={handleFadeStart}
onAnimationEnd={handleFadeEnd}
/>
<StyledContent onKeyDown={handleKeyDown}>
<StyledContent onKeyDown={handleKeyDown} ref={contentRef}>
<VisuallyHidden.Root>
<Dialog.Title>{t("Lightbox")}</Dialog.Title>
<Dialog.Description>
@@ -508,10 +657,52 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</Dialog.Description>
</VisuallyHidden.Root>
<Actions animation={animation.current}>
<Tooltip content={t("Zoom in")} placement="bottom">
<ActionButton
tabIndex={-1}
disabled={
status.image === ImageStatus.MAX_ZOOM ||
status.image === ImageStatus.ERROR
}
onClick={() => {
if (zoomPanPinchRef.current) {
zoomPanPinchRef.current.zoomIn();
}
}}
aria-label={t("Zoom in")}
size={32}
icon={<ZoomInIcon />}
borderOnHover
neutral
/>
</Tooltip>
<Tooltip content={t("Zoom out")} placement="bottom">
<ActionButton
tabIndex={-1}
disabled={
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
}
onClick={() => {
if (zoomPanPinchRef.current) {
zoomPanPinchRef.current.zoomOut();
}
}}
aria-label={t("Zoom out")}
size={32}
icon={<ZoomOutIcon />}
borderOnHover
neutral
/>
</Tooltip>
<Separator />
<Tooltip content={t("Copy link")} placement="bottom">
<CopyToClipboard text={imgRef.current?.src ?? ""}>
<Button
<ActionButton
tabIndex={-1}
disabled={status.image === ImageStatus.ERROR}
aria-label={t("Copy link")}
size={32}
icon={<LinkIcon />}
@@ -521,8 +712,9 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</CopyToClipboard>
</Tooltip>
<Tooltip content={t("Download")} placement="bottom">
<Button
<ActionButton
tabIndex={-1}
disabled={status.image === ImageStatus.ERROR}
onClick={download}
aria-label={t("Download")}
size={32}
@@ -534,7 +726,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
<Separator />
<Dialog.Close asChild>
<Tooltip content={t("Close")} shortcut="Esc" placement="bottom">
<Button
<ActionButton
tabIndex={-1}
onClick={close}
aria-label={t("Close")}
@@ -546,49 +738,86 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</Tooltip>
</Dialog.Close>
</Actions>
{currentImageIndex > 0 && (
<Nav dir="left" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={prev} size={32} aria-label={t("Previous")}>
<BackIcon size={32} />
</NavButton>
</Nav>
)}
<Image
ref={imgRef}
src={activeImage.getSrc()}
alt={activeImage.getAlt()}
onLoading={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADING,
})
{currentImageIndex > 0 &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
) && (
<Nav dir="left" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={prev} size={32} aria-label={t("Previous")}>
<BackIcon size={32} />
</NavButton>
</Nav>
)}
<ZoomablePannablePinchable
panningDisabled={
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
}
onLoad={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADED,
})
}
onError={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.ERROR,
})
}
onSwipeRight={prev}
onSwipeLeft={next}
onSwipeUp={close}
onSwipeDown={close}
status={status}
animation={animation.current}
/>
{currentImageIndex < images.length - 1 && (
<Nav dir="right" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={next} size={32} aria-label={t("Next")}>
<NextIcon size={32} />
</NavButton>
</Nav>
)}
disabled={status.image === ImageStatus.ERROR}
ref={zoomPanPinchRef}
>
<Image
ref={imgRef}
src={activeImage.getSrc()}
alt={activeImage.getAlt()}
onLoading={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADING,
})
}
onLoad={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADED,
})
}
onError={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.ERROR,
})
}
onSwipeRight={prev}
onSwipeLeft={next}
onSwipeUp={close}
onSwipeDown={close}
status={status}
animation={animation.current}
onMinZoom={() => {
setStatus({
lightbox: status.lightbox,
image: ImageStatus.MIN_ZOOM,
});
}}
onZoom={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.ZOOMED,
})
}
onMaxZoom={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.MAX_ZOOM,
})
}
/>
</ZoomablePannablePinchable>
{currentImageIndex < images.length - 1 &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
) && (
<Nav dir="right" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={next} size={32} aria-label={t("Next")}>
<NextIcon size={32} />
</NavButton>
</Nav>
)}
</StyledContent>
</Dialog.Portal>
</Dialog.Root>
@@ -607,6 +836,9 @@ type ImageProps = {
onSwipeDown: () => void;
status: Status;
animation: Animation | null;
onMinZoom: () => void;
onZoom: () => void;
onMaxZoom: () => void;
};
const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
@@ -622,6 +854,9 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onSwipeDown,
status,
animation,
onMinZoom,
onZoom,
onMaxZoom,
}: ImageProps,
ref
) {
@@ -634,6 +869,25 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onSwipeDown,
});
const { isImagePanning } = useContext(ZoomPanPinchContext);
useTransformEffect(({ state, instance }) => {
const minScale = instance.props.minScale ?? 1;
const maxScale = instance.props.maxScale ?? 8;
const { scale } = state;
if (scale === minScale && status.image === ImageStatus.ZOOMED) {
onMinZoom();
} else if (scale === maxScale && status.image === ImageStatus.ZOOMED) {
onMaxZoom();
} else if (
scale > minScale &&
scale < maxScale &&
status.image !== ImageStatus.ZOOMED
) {
onZoom();
}
});
const [hidden, setHidden] = useState(
status.image === null || status.image === ImageStatus.LOADING
);
@@ -668,9 +922,15 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onError={onError}
onLoad={onLoad}
$hidden={hidden}
$zoomedIn={
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
}
$zoomedOut={status.image === ImageStatus.MIN_ZOOM}
$panning={isImagePanning}
/>
<Caption>
{status.image === ImageStatus.LOADED &&
{status.image === ImageStatus.MIN_ZOOM &&
status.lightbox === LightboxStatus.OPENED ? (
<Fade>{alt}</Fade>
) : null}
@@ -726,12 +986,25 @@ const StyledOverlay = styled(Dialog.Overlay)<{
const StyledImg = styled.img<{
$hidden: boolean;
$zoomedIn: boolean;
$zoomedOut: boolean;
$panning: boolean;
animation: Animation | null;
}>`
visibility: ${(props) => (props.$hidden ? "hidden" : "visible")};
pointer-events: auto !important;
max-width: 100%;
min-height: 0;
object-fit: contain;
cursor: ${(props) =>
props.$panning
? "grab"
: props.$zoomedOut
? "zoom-in"
: props.$zoomedIn
? "zoom-out"
: "default"};
${(props) =>
props.animation?.zoomIn
? css`
@@ -743,7 +1016,12 @@ const StyledImg = styled.img<{
animation: ${props.animation.zoomOut.apply()}
${props.animation.zoomOut.duration}ms;
`
: ""}
: props.animation?.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const StyledContent = styled(Dialog.Content)`
@@ -754,7 +1032,10 @@ const StyledContent = styled(Dialog.Content)`
justify-content: center;
align-items: center;
outline: none;
padding: 56px;
`;
const ActionButton = styled(Button)`
background: transparent;
`;
const Actions = styled.div<{
@@ -767,6 +1048,10 @@ const Actions = styled.div<{
display: flex;
align-items: center;
gap: 8px;
z-index: ${depths.modal};
background: ${(props) => transparentize(0.2, props.theme.background)};
backdrop-filter: blur(4px);
border-radius: 6px;
${(props) =>
props.animation === null
@@ -794,6 +1079,7 @@ const Nav = styled.div<{
position: absolute;
${(props) => (props.dir === "left" ? "left: 0;" : "right: 0;")}
transition: opacity 500ms ease-in-out;
z-index: ${depths.modal};
${(props) => props.$hidden && "opacity: 0;"}
${(props) =>
props.animation === null
+3 -1
View File
@@ -146,7 +146,8 @@ const StyledSidebar = styled(Sidebar)<{ $hoverTransition: boolean }>`
${({ $hoverTransition }) =>
$hoverTransition &&
`
&: ${hover} {
@media (hover: hover) {
&:${hover} {
${StyledSearchPopover} {
width: 85%;
}
@@ -154,6 +155,7 @@ const StyledSidebar = styled(Sidebar)<{ $hoverTransition: boolean }>`
${ToggleWrapper} {
opacity: 1;
transform: translateX(0);
}
}
}
`}
+15 -9
View File
@@ -48,11 +48,7 @@ function usePosition({
const { view } = useEditor();
const { selection } = view.state;
const menuWidth = menuRef.current?.offsetWidth ?? 0;
const menuHeight = menuRef.current?.offsetHeight ?? 0;
if (!active || !menuRef.current) {
return defaultPosition;
}
const menuHeight = 36;
// based on the start and end of the selection calculate the position at
// the center top
@@ -74,7 +70,7 @@ function usePosition({
right: Math.max(fromPos.right, toPos.right),
};
const offsetParent = menuRef.current.offsetParent
const offsetParent = menuRef.current?.offsetParent
? menuRef.current.offsetParent.getBoundingClientRect()
: ({
width: window.innerWidth,
@@ -99,12 +95,16 @@ function usePosition({
if (position !== null) {
const element = view.nodeDOM(position);
const bounds = (element as HTMLElement).getBoundingClientRect();
selectionBounds.top = bounds.top;
selectionBounds.top = bounds.top + menuHeight;
selectionBounds.left = bounds.right;
selectionBounds.right = bounds.right;
}
}
if (!active || !menuRef.current || !menuHeight) {
return defaultPosition;
}
// tables are an oddity, and need their own positioning logic
const isColSelection =
selection instanceof ColumnSelection && selection.isColSelection();
@@ -166,6 +166,8 @@ function usePosition({
top: Math.round(top - menuHeight - offsetParent.top),
offset: 0,
visible: true,
blockSelection: false,
maxWidth: width,
};
}
}
@@ -210,8 +212,12 @@ function usePosition({
top: Math.round(top - offsetParent.top),
offset: Math.round(offset),
maxWidth: Math.min(window.innerWidth, offsetParent.width) - margin * 2,
blockSelection:
codeBlock || isColSelection || isRowSelection || noticeBlock,
blockSelection: !!(
codeBlock ||
isColSelection ||
isRowSelection ||
noticeBlock
),
visible: true,
};
}
+5 -6
View File
@@ -5,7 +5,6 @@ import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { toast } from "sonner";
import { v4 } from "uuid";
import Icon from "@shared/components/Icon";
import { MenuItem } from "@shared/editor/types";
import { MentionType } from "@shared/types";
@@ -92,7 +91,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: UserSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.User,
modelId: user.id,
actorId,
@@ -124,7 +123,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: DocumentsSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Document,
modelId: doc.id,
actorId,
@@ -152,7 +151,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: CollectionsSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Collection,
modelId: collection.id,
actorId,
@@ -172,9 +171,9 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
priority: -1,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Document,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId,
label: search,
},
+3 -4
View File
@@ -2,7 +2,6 @@ import { observer } from "mobx-react";
import { EmailIcon, LinkIcon } from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";
import { EmbedDescriptor } from "@shared/editor/embeds";
import { MenuItem } from "@shared/editor/types";
import { MentionType } from "@shared/types";
@@ -82,7 +81,7 @@ function useItems({
mentionType = integration
? determineMentionType({ url, integration })
: undefined;
: MentionType.URL;
}
return [
@@ -97,11 +96,11 @@ function useItems({
icon: <EmailIcon />,
visible: !!mentionType,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: mentionType,
label: pastedText,
href: pastedText,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId: user?.id,
},
appendSpace: true,
+2 -3
View File
@@ -8,7 +8,6 @@ import {
TextSelection,
} from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { v4 } from "uuid";
import Extension, { WidgetProps } from "@shared/editor/lib/Extension";
import { codeLanguages } from "@shared/editor/lib/code";
import isMarkdown from "@shared/editor/lib/isMarkdown";
@@ -144,7 +143,7 @@ export default class PasteHandler extends Extension {
type: MentionType.Document,
modelId: document.id,
label: document.titleWithDefault,
id: v4(),
id: crypto.randomUUID(),
})
)
);
@@ -189,7 +188,7 @@ export default class PasteHandler extends Extension {
type: MentionType.Collection,
modelId: collection.id,
label: collection.name,
id: v4(),
id: crypto.randomUUID(),
})
)
);
+18 -9
View File
@@ -26,13 +26,22 @@ export default function useSwipe({
touchYEnd.current = undefined;
};
const onTouchStart = (e: React.TouchEvent<HTMLImageElement>) => {
touchXStart.current = e.changedTouches[0].screenX;
touchYStart.current = e.changedTouches[0].screenY;
const onTouchStartCapture = (e: React.TouchEvent<HTMLImageElement>) => {
if (e.touches.length === 1) {
// Stop propagation only for single touch gestures, otherwise it prevents
// multi-touch gestures like pinch to zoom to take effect
e.stopPropagation();
touchXStart.current = e.changedTouches[0].screenX;
touchYStart.current = e.changedTouches[0].screenY;
}
};
const onTouchMove = (e: React.TouchEvent<HTMLImageElement>) => {
if (isNumber(touchXStart.current) && isNumber(touchYStart.current)) {
const onTouchMoveCapture = (e: React.TouchEvent<HTMLImageElement>) => {
if (
isNumber(touchXStart.current) &&
isNumber(touchYStart.current) &&
e.touches.length === 1
) {
touchXEnd.current = e.changedTouches[0].screenX;
touchYEnd.current = e.changedTouches[0].screenY;
const dx = touchXEnd.current - touchXStart.current;
@@ -64,13 +73,13 @@ export default function useSwipe({
}
};
const onTouchCancel = () => {
const onTouchCancelCapture = () => {
resetTouchPoints();
};
return {
onTouchStart,
onTouchMove,
onTouchCancel,
onTouchStartCapture,
onTouchMoveCapture,
onTouchCancelCapture,
};
}
@@ -7,7 +7,6 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useTheme } from "styled-components";
import { v4 as uuidv4 } from "uuid";
import { ProsemirrorData } from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import { AttachmentValidation, CommentValidation } from "@shared/validations";
@@ -107,6 +106,7 @@ function CommentForm({
setForceRender((s) => ++s);
setInputFocused(false);
const commentDraft = draft;
const comment =
thread ??
new Comment(
@@ -126,6 +126,9 @@ function CommentForm({
})
.then(() => onSubmit?.())
.catch(() => {
onSaveDraft(commentDraft);
setForceRender((s) => ++s);
comment.isNew = true;
toast.error(t("Error creating comment"));
});
@@ -142,6 +145,7 @@ function CommentForm({
return;
}
const commentDraft = draft;
onSaveDraft(undefined);
setForceRender((s) => ++s);
@@ -156,13 +160,16 @@ function CommentForm({
comments
);
comment.id = uuidv4();
comment.id = crypto.randomUUID();
comments.add(comment);
comment
.save()
.then(() => onSubmit?.())
.catch(() => {
onSaveDraft(commentDraft);
setForceRender((s) => ++s);
comments.remove(comment.id);
comment.isNew = true;
toast.error(t("Error creating comment"));
@@ -7,6 +7,7 @@ import {
AuthenticationFailed,
AuthorizationFailed,
DocumentTooLarge,
EditorUpdateError,
TooManyConnections,
} from "@shared/collaboration/CloseEvents";
import Fade from "~/components/Fade";
@@ -37,6 +38,10 @@ function ConnectionStatus() {
title: t("Too many users connected to document"),
body: t("Your edits will sync once other users leave the document"),
},
[EditorUpdateError.code]: {
title: t("New version available"),
body: t("Please reload the page to update to the latest version"),
},
};
const message = ui.multiplayerErrorCode
@@ -63,20 +68,29 @@ function ConnectionStatus() {
}
placement="bottom"
>
<Button>
<Fade>
<Fade>
<Button width="auto">
{message?.title ?? t("Offline")}
<DisconnectedIcon />
</Fade>
</Button>
</Button>
</Fade>
</Tooltip>
) : null;
}
const Button = styled(NudeButton)`
display: none;
background: ${(props) => props.theme.backgroundTertiary};
color: ${(props) => props.theme.textSecondary};
font-size: 14px;
font-weight: 500;
padding-left: 6px;
padding-right: 6px;
${breakpoint("tablet")`
display: block;
display: flex;
gap: 4px;
align-items: center;
`};
@media print {
+1 -1
View File
@@ -10,9 +10,9 @@ type Props = {
export const Footer = ({ document }: Props) => (
<FooterWrapper>
<KeyboardShortcutsButton />
<ConnectionStatus />
<SizeWarning document={document} />
<KeyboardShortcutsButton />
</FooterWrapper>
);
@@ -57,6 +57,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
const { presence, auth, ui } = useStores();
const [editorVersionBehind, setEditorVersionBehind] = useState(false);
const [showCursorNames, setShowCursorNames] = useState(false);
const [remoteProvider, setRemoteProvider] =
useState<HocuspocusProvider | null>(null);
@@ -161,7 +162,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
ui.setMultiplayerStatus("disconnected", ev.event.code);
if (ev.event.code === EditorUpdateError.code) {
window.location.reload();
setEditorVersionBehind(true);
}
}
});
@@ -309,6 +310,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
)}
<Editor
{...props}
readOnly={props.readOnly || editorVersionBehind}
value={undefined}
defaultValue={undefined}
extensions={extensions}
+1 -2
View File
@@ -6,7 +6,6 @@ import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { v4 as uuidv4 } from "uuid";
import { Pagination } from "@shared/constants";
import { hideScrollbars } from "@shared/styles";
import {
@@ -105,7 +104,7 @@ function Search() {
// without a flash of loading.
if (query) {
searches.add({
id: uuidv4(),
id: crypto.randomUUID(),
query,
createdAt: new Date().toISOString(),
});
@@ -43,11 +43,13 @@ export function MembersTable({ canManage, ...rest }: Props) {
<Flex align="center" gap={8}>
<Avatar model={user} size={AvatarSize.Large} />{" "}
<Flex column>
<Text>
<Text selectable>
{user.name} {currentUser.id === user.id && `(${t("You")})`}
</Text>
{isMobile && canManage && (
<Text type="tertiary">{user.email}</Text>
<Text type="tertiary" selectable>
{user.email}
</Text>
)}
</Flex>
</Flex>
+1 -2
View File
@@ -1,6 +1,5 @@
import { observable, action } from "mobx";
import * as React from "react";
import { v4 as uuidv4 } from "uuid";
type DialogDefinition = {
title: string;
@@ -66,7 +65,7 @@ export default class DialogsStore {
this.modalStack.clear();
}
this.modalStack.set(id ?? replaceId ?? uuidv4(), {
this.modalStack.set(id ?? replaceId ?? crypto.randomUUID(), {
title,
content,
style,
+13 -13
View File
@@ -51,11 +51,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@aws-sdk/client-s3": "3.896.0",
"@aws-sdk/lib-storage": "3.896.0",
"@aws-sdk/s3-presigned-post": "3.896.0",
"@aws-sdk/s3-request-presigner": "3.896.0",
"@aws-sdk/signature-v4-crt": "^3.896.0",
"@aws-sdk/client-s3": "3.901.0",
"@aws-sdk/lib-storage": "3.903.0",
"@aws-sdk/s3-presigned-post": "3.901.0",
"@aws-sdk/s3-request-presigner": "3.901.0",
"@aws-sdk/signature-v4-crt": "^3.901.0",
"@babel/core": "^7.28.4",
"@babel/plugin-proposal-decorators": "^7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
@@ -65,7 +65,7 @@
"@babel/preset-react": "^7.27.1",
"@benrbray/prosemirror-math": "^0.2.2",
"@bull-board/api": "^6.7.10",
"@bull-board/koa": "^6.12.0",
"@bull-board/koa": "^6.13.0",
"@css-inline/css-inline-wasm": "^0.17.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^6.0.1",
@@ -73,9 +73,9 @@
"@dotenvx/dotenvx": "^1.49.0",
"@emoji-mart/data": "^1.2.1",
"@fast-csv/parse": "^5.0.5",
"@fortawesome/fontawesome-svg-core": "^7.0.1",
"@fortawesome/free-brands-svg-icons": "^7.0.1",
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^0.2.6",
"@getoutline/react-roving-tabindex": "^3.2.4",
"@hocuspocus/extension-redis": "1.1.2",
@@ -179,9 +179,9 @@
"mobx-utils": "^4.0.1",
"natural-sort": "^1.0.0",
"node-fetch": "2.7.0",
"nodemailer": "^6.10.1",
"nodemailer": "^7.0.7",
"octokit": "^3.2.2",
"outline-icons": "^3.12.1",
"outline-icons": "^3.13.0",
"oy-vey": "^0.12.1",
"passport": "^0.7.0",
"passport-google-oauth2": "^0.2.0",
@@ -228,6 +228,7 @@
"react-virtualized-auto-sizer": "^1.0.26",
"react-waypoint": "^10.3.0",
"react-window": "^1.8.11",
"react-zoom-pan-pinch": "^3.7.0",
"redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.2.2",
"refractor": "^3.6.0",
@@ -261,7 +262,6 @@
"ukkonen": "^2.2.0",
"umzug": "^3.8.2",
"utility-types": "^3.11.0",
"uuid": "^8.3.2",
"validator": "13.15.15",
"vaul": "^1.1.2",
"vite": "npm:rolldown-vite@latest",
@@ -342,7 +342,7 @@
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.5",
"@types/utf8": "^3.0.3",
"@types/validator": "^13.15.2",
"@types/validator": "^13.15.3",
"@types/yauzl": "^2.10.3",
"babel-jest": "^29.7.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
+2 -2
View File
@@ -9,7 +9,7 @@ class Iframely {
public static async requestResource(
url: string,
type = "oembed"
type = "iframely"
): Promise<JSONObject | UnfurlError> {
const isDefaultHost = env.IFRAMELY_URL === this.defaultUrl;
@@ -38,7 +38,7 @@ class Iframely {
const data = await Iframely.requestResource(url);
return "error" in data // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
? ({ error: data.error } as UnfurlError)
: { ...data, type: UnfurlResourceType.OEmbed };
: { ...data, type: UnfurlResourceType.URL };
};
}
@@ -1,4 +1,3 @@
import { v4 as uuidv4 } from "uuid";
import { IntegrationService, IntegrationType } from "@shared/types";
import { Integration } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
@@ -27,7 +26,7 @@ export default class UploadLinearWorkspaceLogoTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.logoUrl,
`${Buckets.avatars}/${integration.teamId}/${uuidv4()}`,
`${Buckets.avatars}/${integration.teamId}/${crypto.randomUUID()}`,
"public-read",
{
headers: {
+5 -6
View File
@@ -3,7 +3,6 @@ import { readFile } from "fs/promises";
import path from "path";
import FormData from "form-data";
import { ensureDirSync } from "fs-extra";
import { v4 as uuidV4 } from "uuid";
import { FileOperationState, FileOperationType } from "@shared/types";
import env from "@server/env";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
@@ -135,7 +134,7 @@ describe("#files.get", () => {
it("should fail with status 404 if existing file is requested with key", async () => {
const user = await buildUser();
const fileName = "images.docx";
const key = path.join("uploads", user.id, uuidV4(), fileName);
const key = path.join("uploads", user.id, crypto.randomUUID(), fileName);
ensureDirSync(
path.dirname(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key))
@@ -153,7 +152,7 @@ describe("#files.get", () => {
it("should fail with status 404 if non-existing file is requested with key", async () => {
const user = await buildUser();
const fileName = "images.docx";
const key = path.join("uploads", user.id, uuidV4(), fileName);
const key = path.join("uploads", user.id, crypto.randomUUID(), fileName);
const res = await server.get(`/api/files.get?key=${key}`);
expect(res.status).toEqual(404);
});
@@ -279,7 +278,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when avatar is requested using key", async () => {
const user = await buildUser();
const key = path.join("avatars", user.id, uuidV4());
const key = path.join("avatars", user.id, crypto.randomUUID());
const attachment = await buildAttachment({
key,
teamId: user.teamId,
@@ -308,7 +307,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when avatar is requested using key", async () => {
const user = await buildUser();
const key = path.join("avatars", user.id, uuidV4());
const key = path.join("avatars", user.id, crypto.randomUUID());
await buildAttachment({
key,
teamId: user.teamId,
@@ -335,7 +334,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when exported file is requested using signature", async () => {
const user = await buildUser();
const fileName = "export-markdown.zip";
const key = `${Buckets.uploads}/${user.teamId}/${uuidV4()}/${fileName}`;
const key = `${Buckets.uploads}/${user.teamId}/${crypto.randomUUID()}/${fileName}`;
await buildFileOperation({
userId: user.id,
@@ -1,5 +1,4 @@
import fetchMock from "jest-fetch-mock";
import { v4 as uuidv4 } from "uuid";
import { WebhookDelivery } from "@server/models";
import {
buildUser,
@@ -99,7 +98,7 @@ describe("DeliverWebhookTask", () => {
url: "http://example.com",
events: ["*"],
});
const deletedUserId = uuidv4();
const deletedUserId = crypto.randomUUID();
const signedInUser = await buildUser({ teamId: subscription.teamId });
const task = new DeliverWebhookTask();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 B

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 803 B

@@ -94,8 +94,10 @@ export default class PersistenceExtension implements Extension {
context,
documentName,
clientsCount,
requestParameters,
}: onStoreDocumentPayload) {
const [, documentId] = documentName.split(".");
const clientVersion = requestParameters.get("editorVersion");
const key = Document.getCollaboratorKey(documentId);
const sessionCollaboratorIds = await Redis.defaultClient.smembers(key);
@@ -110,6 +112,7 @@ export default class PersistenceExtension implements Extension {
ydoc: document,
sessionCollaboratorIds,
isLastConnection: clientsCount === 0,
clientVersion,
});
} catch (err) {
Logger.error("Unable to persist document", err, {
+9 -9
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import { TeamDomain } from "@server/models";
import Collection from "@server/models/Collection";
@@ -35,7 +35,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -137,7 +137,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -271,7 +271,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -313,7 +313,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -361,7 +361,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -405,7 +405,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -458,7 +458,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -491,7 +491,7 @@ describe("accountProvisioner", () => {
providerId: domain,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { AttachmentPreset } from "@shared/types";
import { Attachment, User } from "@server/models";
import AttachmentHelper from "@server/models/helpers/AttachmentHelper";
@@ -47,7 +47,7 @@ export default async function attachmentCreator({
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
id: uuidv4(),
id: randomUUID(),
name,
userId: user.id,
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import {
FileOperationFormat,
FileOperationType,
@@ -25,7 +25,7 @@ function getKeyForFileOp(
) {
return `${
Buckets.uploads
}/${teamId}/${uuidv4()}/${name}-export.${format.replace(/outline-/, "")}.zip`;
}/${teamId}/${randomUUID()}/${name}-export.${format.replace(/outline-/, "")}.zip`;
}
async function collectionExporter({
@@ -7,6 +7,7 @@ import Logger from "@server/logging/Logger";
import { Document, Event } from "@server/models";
import { sequelize } from "@server/storage/database";
import { AuthenticationType } from "@server/types";
import semver from "semver";
type Props = {
/** The document ID to update. */
@@ -17,6 +18,8 @@ type Props = {
sessionCollaboratorIds: string[];
/** Whether the last connection to the document left. */
isLastConnection: boolean;
/** The client version, if available. */
clientVersion: string | null;
};
export default async function documentCollaborativeUpdater({
@@ -24,6 +27,7 @@ export default async function documentCollaborativeUpdater({
ydoc,
sessionCollaboratorIds,
isLastConnection,
clientVersion,
}: Props) {
return sequelize.transaction(async (transaction) => {
const document = await Document.unscoped()
@@ -68,12 +72,24 @@ export default async function documentCollaborativeUpdater({
...pudIds,
]);
// Either the client or server version could be null, or they could both be
// set. In that case we want to use the greater (newer) version.
const editorVersion =
document.editorVersion && clientVersion
? semver.gt(clientVersion, document.editorVersion)
? clientVersion
: document.editorVersion
: clientVersion
? clientVersion
: document.editorVersion;
await document.update(
{
content,
state: Buffer.from(state),
lastModifiedById,
collaboratorIds,
editorVersion,
},
{
transaction,
+4 -4
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { UserRole } from "@shared/types";
import { TeamDomain } from "@server/models";
import {
@@ -60,7 +60,7 @@ describe("userProvisioner", () => {
teamId: existing.teamId,
authentication: {
authenticationProviderId: authenticationProvider.id,
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -94,7 +94,7 @@ describe("userProvisioner", () => {
teamId: existing.teamId,
authentication: {
authenticationProviderId: authenticationProvider.id,
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -148,7 +148,7 @@ describe("userProvisioner", () => {
email: "test@example.com",
teamId: existing.teamId,
authentication: {
authenticationProviderId: uuidv4(),
authenticationProviderId: randomUUID(),
providerId: existingAuth.providerId,
accessToken: "123",
scopes: ["read"],
@@ -1,7 +1,5 @@
"use strict";
const { v4 } = require("uuid");
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("pins", {
@@ -85,7 +83,7 @@ module.exports = {
`,
{
replacements: {
id: v4(),
id: crypto.randomUUID(),
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -1,7 +1,5 @@
"use strict";
const { v4 } = require("uuid");
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.transaction(async (transaction) => {
@@ -80,7 +78,7 @@ module.exports = {
`,
{
replacements: {
id: v4(),
id: crypto.randomUUID(),
teamId: team.id,
createdById: adminUserID,
name: domain,
+8 -8
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomString } from "@shared/random";
import slugify from "@shared/utils/slugify";
import {
@@ -101,7 +101,7 @@ describe("getDocumentTree", () => {
describe("#addDocumentToStructure", () => {
it("should add as last element without index", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -119,7 +119,7 @@ describe("#addDocumentToStructure", () => {
it("should add with an index", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -136,7 +136,7 @@ describe("#addDocumentToStructure", () => {
const document = await buildDocument({ collectionId: collection.id });
await collection.reload();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -156,12 +156,12 @@ describe("#addDocumentToStructure", () => {
await collection.reload();
const newDocument = await buildDocument({
id: uuidv4(),
id: randomUUID(),
title: "node",
parentDocumentId: document.id,
teamId: collection.teamId,
});
const id = uuidv4();
const id = randomUUID();
const secondDocument = await buildDocument({
id,
title: "New start node",
@@ -239,9 +239,9 @@ describe("#addDocumentToStructure", () => {
describe("options: documentJson", () => {
it("should append supplied json over document's own", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id: uuidv4(),
id: randomUUID(),
title: "New end node",
parentDocumentId: null,
teamId: collection.teamId,
+1 -1
View File
@@ -315,7 +315,7 @@ class Document extends ArchivableModel<
msg: `editorVersion must be 255 characters or less`,
})
@Column
editorVersion: string;
editorVersion: string | null;
/** An icon to use as the document icon. */
@Length({
+1 -1
View File
@@ -48,7 +48,7 @@ class Revision extends ParanoidModel<
})
@Column
@SkipChangeset
editorVersion: string;
editorVersion: string | null;
/** The document title at the time of the revision */
@Length({
+2 -2
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuid } from "uuid";
import { randomUUID } from "crypto";
import { TeamPreference } from "@shared/types";
import { buildDocument, buildTeam } from "@server/test/factories";
import User from "../User";
@@ -40,7 +40,7 @@ describe("Model", () => {
});
it("should return full array if value changed", async () => {
const collaboratorId = uuid();
const collaboratorId = randomUUID();
const document = await buildDocument();
const prev = document.collaboratorIds;
@@ -1,12 +1,12 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import env from "@server/env";
import SubscriptionHelper from "./SubscriptionHelper";
describe("SubscriptionHelper", () => {
describe("unsubscribeUrl", () => {
it("should return a valid unsubscribe URL", () => {
const userId = uuidv4();
const documentId = uuidv4();
const userId = randomUUID();
const documentId = randomUUID();
const unsubscribeUrl = SubscriptionHelper.unsubscribeUrl(
userId,
+8 -7
View File
@@ -19,18 +19,19 @@ async function presentUnfurl(
case UnfurlResourceType.Issue:
return presentIssue(data);
default:
return presentOEmbed(data);
return presentURL(data);
}
}
const presentOEmbed = (
const presentURL = (
data: Record<string, any>
): UnfurlResponse[UnfurlResourceType.OEmbed] => ({
type: UnfurlResourceType.OEmbed,
): UnfurlResponse[UnfurlResourceType.URL] => ({
type: UnfurlResourceType.URL,
url: data.url,
title: data.title,
description: data.description,
thumbnailUrl: data.thumbnail_url,
title: data.meta.title,
description: data.meta.description,
thumbnailUrl: (data.links.thumbnail ?? [])[0]?.href ?? "",
faviconUrl: (data.links.icon ?? [])[0]?.href ?? "",
});
const presentMention = async (
+3 -3
View File
@@ -9,7 +9,7 @@ import {
Transaction,
UniqueConstraintError,
} from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomElement } from "@shared/random";
import { ImportInput, ImportTaskInput } from "@shared/schema";
import {
@@ -514,7 +514,7 @@ export default abstract class ImportsProcessor<
const json = node.toJSON() as ProsemirrorData;
const attrs = json.attrs ?? {};
attrs.id = uuidv4();
attrs.id = randomUUID();
attrs.actorId = actorId;
const externalId = attrs.modelId as string;
@@ -597,7 +597,7 @@ export default abstract class ImportsProcessor<
}
}
idMap[externalId] = internalId ?? uuidv4();
idMap[externalId] = internalId ?? randomUUID();
return idMap[externalId];
}
+2 -2
View File
@@ -4,7 +4,7 @@ import truncate from "lodash/truncate";
import uniqBy from "lodash/uniqBy";
import { Fragment, Node } from "prosemirror-model";
import { Transaction, WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { ImportTaskInput, ImportTaskOutput } from "@shared/schema";
import {
AttachmentPreset,
@@ -290,7 +290,7 @@ export default abstract class APIImportTask<
await sequelize.transaction(async (transaction) => {
const dbPromises = attachmentsData.map(async (item) => {
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(
AttachmentPreset.DocumentAttachment
);
+1 -1
View File
@@ -49,7 +49,7 @@ export default abstract class ExportTask extends BaseTask<Props> {
: {
teamId: user.teamId,
archivedAt: {
[Op.ne]: null,
[Op.eq]: null,
},
};
+4 -4
View File
@@ -3,7 +3,7 @@ import fs from "fs-extra";
import find from "lodash/find";
import mime from "mime-types";
import { Fragment, Node } from "prosemirror-model";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { ProsemirrorData } from "@shared/types";
import { schema, serializer } from "@server/editor";
import Logger from "@server/logging/Logger";
@@ -72,7 +72,7 @@ export default class ImportJSONTask extends ImportTask {
collectionId: string
) {
Object.values(documents).forEach((node) => {
const id = uuidv4();
const id = randomUUID();
output.documents.push({
...node,
path: "",
@@ -101,7 +101,7 @@ export default class ImportJSONTask extends ImportTask {
[id: string]: AttachmentJSONExport;
}) {
Object.values(attachments).forEach((node) => {
const id = uuidv4();
const id = randomUUID();
const mimeType = mime.lookup(node.key) || "application/octet-stream";
output.attachments.push({
@@ -128,7 +128,7 @@ export default class ImportJSONTask extends ImportTask {
throw new Error(`Could not parse ${node.path}. ${err.message}`);
}
const collectionId = uuidv4();
const collectionId = randomUUID();
output.collections.push({
...item.collection,
+3 -3
View File
@@ -2,7 +2,7 @@ import path from "path";
import fs from "fs-extra";
import escapeRegExp from "lodash/escapeRegExp";
import mime from "mime-types";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import documentImporter from "@server/commands/documentImporter";
import { createContext } from "@server/context";
import Logger from "@server/logging/Logger";
@@ -66,7 +66,7 @@ export default class ImportMarkdownZipTask extends ImportTask {
return parseNodeChildren(child.children, collectionId);
}
const id = uuidv4();
const id = randomUUID();
// this is an attachment
if (
@@ -144,7 +144,7 @@ export default class ImportMarkdownZipTask extends ImportTask {
// All nodes in the root level should be collections
for (const node of tree) {
if (node.children.length > 0) {
const collectionId = uuidv4();
const collectionId = randomUUID();
output.collections.push({
id: collectionId,
name: node.title,
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { Team } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import FileStorage from "@server/storage/files";
@@ -23,7 +23,7 @@ export default class UploadTeamAvatarTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.avatarUrl,
`${Buckets.avatars}/${team.id}/${uuidv4()}`,
`${Buckets.avatars}/${team.id}/${randomUUID()}`,
"public-read"
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { User } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import FileStorage from "@server/storage/files";
@@ -23,7 +23,7 @@ export default class UploadUserAvatarTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.avatarUrl,
`${Buckets.avatars}/${user.id}/${uuidv4()}`,
`${Buckets.avatars}/${user.id}/${randomUUID()}`,
"public-read"
);
+3 -3
View File
@@ -1,6 +1,6 @@
import Router from "koa-router";
import { WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { AttachmentPreset } from "@shared/types";
import { bytesToHumanReadable, getFileNameFromUrl } from "@shared/utils/files";
import { AttachmentValidation } from "@shared/validations";
@@ -113,7 +113,7 @@ router.post(
);
}
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
@@ -185,7 +185,7 @@ router.post(
authorize(user, "update", document);
const name = getFileNameFromUrl(url) ?? "file";
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
+8 -8
View File
@@ -1,9 +1,9 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { buildUser, buildTeam } from "@server/test/factories";
import { getTestServer, setSelfHosted } from "@server/test/support";
const mockTeamInSessionId = uuidv4();
const mockTeamInSessionId = randomUUID();
jest.mock("@server/utils/authentication", () => ({
getSessionsInCookie() {
@@ -107,7 +107,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -130,7 +130,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -153,7 +153,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -177,7 +177,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
enabled: false,
},
],
@@ -201,7 +201,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -220,7 +220,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { buildUser, buildAdmin, buildTeam } from "@server/test/factories";
import { getTestServer, setSelfHosted } from "@server/test/support";
@@ -77,7 +77,7 @@ describe("#authenticationProviders.update", () => {
});
const googleProvider = await team.$create("authenticationProvider", {
name: "google",
providerId: uuidv4(),
providerId: randomUUID(),
});
const res = await server.post("/api/authenticationProviders.update", {
body: {
+2 -2
View File
@@ -12,7 +12,7 @@ import remove from "lodash/remove";
import uniq from "lodash/uniq";
import mime from "mime-types";
import { Op, ScopeOptions, Sequelize, WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { NavigationNode, StatusFilter, UserRole } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
import slugify from "@shared/utils/slugify";
@@ -1607,7 +1607,7 @@ router.post(
const key = AttachmentHelper.getKey({
acl,
id: uuidv4(),
id: randomUUID(),
name: fileName,
userId: user.id,
});
@@ -1,5 +1,5 @@
import queryString from "query-string";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomElement } from "@shared/random";
import { NotificationEventType } from "@shared/types";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
@@ -335,7 +335,7 @@ describe("#notifications.pixel", () => {
it("should return 404 for notification that does not exist", async () => {
const res = await server.get(
`/api/notifications.pixel?${queryString.stringify({
id: uuidv4(),
id: randomUUID(),
token: "invalid-token",
})}`
);
+28 -7
View File
@@ -162,11 +162,32 @@ describe("#urls.unfurl", () => {
Promise.resolve({
url: "https://www.flickr.com",
type: "rich",
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
thumbnail_url:
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg",
meta: {
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
},
links: {
thumbnail: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg",
type: "image/jpg",
rel: ["twitter", "thumbnail", "ssl", "og"],
content_length: 412824,
media: {
width: 1200,
height: 630,
},
},
],
icon: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/67167dd041b0982f0f230dab_flickr-webclip.png",
rel: ["apple-touch-icon", "icon", "ssl"],
type: "image/png",
},
],
},
})
);
@@ -182,13 +203,13 @@ describe("#urls.unfurl", () => {
expect(res.status).toEqual(200);
expect(body.url).toEqual("https://www.flickr.com");
expect(body.type).toEqual(UnfurlResourceType.OEmbed);
expect(body.type).toEqual(UnfurlResourceType.URL);
expect(body.title).toEqual("Flickr");
expect(body.description).toEqual(
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!"
);
expect(body.thumbnailUrl).toEqual(
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg"
"https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg"
);
});
+3 -1
View File
@@ -46,7 +46,6 @@ export default function init(app: Koa = new Koa(), server?: Server) {
}
app.use(compress());
app.use(attachCSRFToken());
// Monitor server connections
if (server) {
@@ -66,6 +65,9 @@ export default function init(app: Koa = new Koa(), server?: Server) {
app.use(mount("/api", api));
// Generate and attach a CSRF token to the session on non-API requests
app.use(attachCSRFToken());
// Apply CSP middleware after API as these responses are rendered in the browser
app.use(csp());
+8 -3
View File
@@ -47,7 +47,7 @@ export default class RedisAdapter extends Redis {
if (!url || !url.startsWith("ioredis://")) {
super(
env.REDIS_URL ?? "",
url || env.REDIS_URL || "",
defaults(options, { connectionName }, defaultOptions)
);
} else {
@@ -76,6 +76,7 @@ export default class RedisAdapter extends Redis {
private static client: RedisAdapter;
private static subscriber: RedisAdapter;
private static collabClient: RedisAdapter;
public static get defaultClient(): RedisAdapter {
return (
@@ -100,9 +101,13 @@ export default class RedisAdapter extends Redis {
* A Redis adapter for collaboration-related operations.
*/
public static get collaborationClient(): RedisAdapter {
if (!env.REDIS_COLLABORATION_URL) {
return this.defaultClient;
}
return (
this.client ||
(this.client = new this(env.REDIS_COLLABORATION_URL, {
this.collabClient ||
(this.collabClient = new this(env.REDIS_COLLABORATION_URL, {
connectionNameSuffix: "collab",
}))
);
+3 -3
View File
@@ -4,7 +4,7 @@ import isNull from "lodash/isNull";
import { Node } from "prosemirror-model";
import { InferCreationAttributes } from "sequelize";
import { DeepPartial } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomString } from "@shared/random";
import {
CollectionPermission,
@@ -282,7 +282,7 @@ export async function buildIntegration(overrides: Partial<Integration> = {}) {
type: IntegrationType.Post,
events: ["documents.update", "documents.publish"],
settings: {
serviceTeamId: uuidv4(),
serviceTeamId: randomUUID(),
},
authenticationId: authentication.id,
...overrides,
@@ -559,7 +559,7 @@ export async function buildAttachment(
overrides.documentId = document.id;
}
const id = uuidv4();
const id = randomUUID();
const acl = overrides.acl || "public-read";
const name = fileName || faker.system.fileName();
return Attachment.create({
+3 -3
View File
@@ -1,4 +1,4 @@
import { v4 } from "uuid";
import { randomUUID } from "crypto";
import { Scope } from "@shared/types";
import { OAuthInterface } from "./OAuthInterface";
import {
@@ -9,10 +9,10 @@ import {
describe("OAuthInterface", () => {
const user = {
id: v4(),
id: randomUUID(),
};
const client = {
id: v4(),
id: randomUUID(),
grants: ["authorization_code", "refresh_token"],
redirectUris: ["https://example.com/callback"],
};
+10 -10
View File
@@ -1,5 +1,5 @@
import { expect } from "@jest/globals";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import env from "@server/env";
import parseAttachmentIds from "./parseAttachmentIds";
@@ -8,7 +8,7 @@ it("should return an empty array with no matches", () => {
});
it("should not return orphaned UUID's", () => {
const uuid = uuidv4();
const uuid = randomUUID();
expect(
parseAttachmentIds(`some random text with a uuid ${uuid}
@@ -17,7 +17,7 @@ it("should not return orphaned UUID's", () => {
});
it("should parse attachment ID from markdown", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid})`
);
@@ -26,7 +26,7 @@ it("should parse attachment ID from markdown", () => {
});
it("should parse attachment ID from markdown with additional query params", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid}&size=2)`
);
@@ -35,7 +35,7 @@ it("should parse attachment ID from markdown with additional query params", () =
});
it("should parse attachment ID from markdown with fully qualified url", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](${env.URL}/api/attachments.redirect?id=${uuid})`
);
@@ -44,7 +44,7 @@ it("should parse attachment ID from markdown with fully qualified url", () => {
});
it("should parse attachment ID from markdown with title", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid} "align-left")`
);
@@ -53,8 +53,8 @@ it("should parse attachment ID from markdown with title", () => {
});
it("should parse multiple attachment IDs from markdown", () => {
const uuid = uuidv4();
const uuid2 = uuidv4();
const uuid = randomUUID();
const uuid2 = randomUUID();
const results =
parseAttachmentIds(`![caption text](/api/attachments.redirect?id=${uuid})
@@ -67,7 +67,7 @@ some text
});
it("should parse attachment ID from html", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`<img src="/api/attachments.redirect?id=${uuid}" />`
);
@@ -76,7 +76,7 @@ it("should parse attachment ID from html", () => {
});
it("should parse attachment ID from html with fully qualified url", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`<img src="${env.URL}/api/attachments.redirect?id=${uuid}" />`
);
+15 -15
View File
@@ -1,65 +1,65 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { Buckets } from "./models/helpers/AttachmentHelper";
import { ValidateKey } from "./validation";
describe("#ValidateKey.isValid", () => {
it("should return false if number of key components are not equal to 4", () => {
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}`)
).toBe(false);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}/foo/bar`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo/bar`)
).toBe(false);
});
it("should return false if the first key component is not a valid bucket", () => {
expect(ValidateKey.isValid(`foo/${uuidv4()}/${uuidv4()}/bar.png`)).toBe(
expect(ValidateKey.isValid(`foo/${randomUUID()}/${randomUUID()}/bar.png`)).toBe(
false
);
});
it("should return false if second and third key components are not UUID", () => {
expect(
ValidateKey.isValid(`${Buckets.uploads}/foo/${uuidv4()}/bar.png`)
ValidateKey.isValid(`${Buckets.uploads}/foo/${randomUUID()}/bar.png`)
).toBe(false);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/foo/bar.png`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/foo/bar.png`)
).toBe(false);
});
it("should return true successfully validating key", () => {
expect(
ValidateKey.isValid(`${Buckets.public}/${uuidv4()}/${uuidv4()}/foo.png`)
ValidateKey.isValid(`${Buckets.public}/${randomUUID()}/${randomUUID()}/foo.png`)
).toBe(true);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}/foo.png`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo.png`)
).toBe(true);
expect(
ValidateKey.isValid(`${Buckets.avatars}/${uuidv4()}/${uuidv4()}`)
ValidateKey.isValid(`${Buckets.avatars}/${randomUUID()}/${randomUUID()}`)
).toBe(true);
});
});
describe("#ValidateKey.sanitize", () => {
it("should sanitize malicious looking keys", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~\.\u0000\malicious_key`)
).toEqual(`public/${uuid1}/${uuid2}/~.malicious_key`);
});
it("should remove potential path traversal", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/../../malicious_key`)
).toEqual(`public/${uuid1}/${uuid2}/malicious_key`);
});
it("should remove problematic characters", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(ValidateKey.sanitize(`public/${uuid1}/${uuid2}/test#:*?`)).toEqual(
`public/${uuid1}/${uuid2}/test`
);
+1 -2
View File
@@ -1,7 +1,6 @@
import * as Sentry from "@sentry/react";
import { EditorView } from "prosemirror-view";
import { toast } from "sonner";
import { v4 as uuidv4 } from "uuid";
import type { Dictionary } from "~/hooks/useDictionary";
import FileHelper from "../lib/FileHelper";
import uploadPlaceholderPlugin, {
@@ -72,7 +71,7 @@ const insertFiles = async function (
: undefined;
return {
id: `upload-${uuidv4()}`,
id: `upload-${crypto.randomUUID()}`,
dimensions: await getDimensions?.(file),
isImage,
isVideo,
+8 -4
View File
@@ -39,7 +39,7 @@ export default function toggleList(
const currentItemType = parentList.node.content.firstChild?.type;
const differentType = currentItemType && currentItemType !== itemType;
if (differentType || differentListStyle) {
if (differentType) {
return chainTransactions(
clearNodes(),
wrapInList(listType, { listStyle })
@@ -50,10 +50,14 @@ export default function toggleList(
isList(parentList.node, schema) &&
listType.validContent(parentList.node.content)
) {
tr.setNodeMarkup(
tr.doc.nodesBetween(
parentList.pos,
listType,
listStyle ? { listStyle } : {}
parentList.pos + parentList.node.nodeSize,
(node, pos) => {
if (isList(node, schema)) {
tr.setNodeMarkup(pos, listType, listStyle ? { listStyle } : {});
}
}
);
dispatch?.(tr);
+64
View File
@@ -28,6 +28,7 @@ import {
} from "../../types";
import { cn } from "../styles/utils";
import { ComponentProps } from "../types";
import { sanitizeUrl } from "@shared/utils/urls";
type Attrs = {
className: string;
@@ -143,6 +144,64 @@ type IssuePrProps = ComponentProps & {
) => void;
};
export const MentionURL = (props: ComponentProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
const [loaded, setLoaded] = React.useState(false);
const { isSelected, node } = props;
const {
className,
unfurl: unfurlAttr,
...attrs
} = getAttributesFromNode(node);
const unfurl = unfurls.get(attrs.href)?.data ?? unfurlAttr;
React.useEffect(() => {
const fetchUnfurl = async () => {
await unfurls.fetchUnfurl({ url: attrs.href });
if (!isMounted()) {
return;
}
setLoaded(true);
};
void fetchUnfurl();
}, [unfurls, attrs.href, isMounted]);
if (!unfurl) {
return !loaded ? (
<MentionLoading className={className} />
) : (
<MentionError className={className} />
);
}
return (
<a
{...attrs}
className={cn(className, {
"ProseMirror-selectednode": isSelected,
})}
href={attrs.href as string}
target="_blank"
rel="noopener noreferrer nofollow"
>
<Flex align="center" gap={6}>
{unfurl.faviconUrl ? (
<Logo src={sanitizeUrl(unfurl.faviconUrl)} alt="" />
) : null}
<Text>
<Backticks content={unfurl.title} />
</Text>
</Flex>
</a>
);
};
export const MentionIssue = observer((props: IssuePrProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
@@ -316,3 +375,8 @@ const MentionError = ({ className }: { className: string }) => {
const StyledWarningIcon = styled(WarningIcon)`
margin: 0 -2px;
`;
const Logo = styled.img`
width: 16px;
height: 16px;
`;
+1 -2
View File
@@ -10,7 +10,6 @@ import {
Transaction,
} from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { v4 as uuidv4 } from "uuid";
import { isCode } from "../lib/isCode";
import { isRemoteTransaction } from "../lib/multiplayer";
import { findBlockNodes } from "../queries/findChildren";
@@ -54,7 +53,7 @@ class MermaidRenderer {
readonly editor: Editor;
constructor(editor: Editor) {
this.diagramId = uuidv4();
this.diagramId = crypto.randomUUID();
this.elementId = `mermaid-diagram-wrapper-${this.diagramId}`;
this.element =
document.getElementById(this.elementId) || document.createElement("div");
+2 -3
View File
@@ -1,6 +1,5 @@
import { Node, Schema } from "prosemirror-model";
import { Primitive } from "utility-types";
import { v4 } from "uuid";
import { isList } from "../queries/isList";
export function transformListToMentions(
@@ -34,11 +33,11 @@ function transformListItemToMentions(
node.type.create(
node.attrs,
schema.nodes.mention.create({
id: v4(),
id: crypto.randomUUID(),
type: mentionType,
label: link,
href: link,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId: attrs.actorId,
})
)
+2 -3
View File
@@ -1,7 +1,6 @@
import { toggleMark } from "prosemirror-commands";
import { MarkSpec, MarkType, Schema, Mark as PMMark } from "prosemirror-model";
import { Command, Plugin } from "prosemirror-state";
import { v4 as uuidv4 } from "uuid";
import { addMark } from "../commands/addMark";
import { collapseSelection } from "../commands/collapseSelection";
import { chainTransactions } from "../lib/chainTransactions";
@@ -82,7 +81,7 @@ export default class Comment extends Mark {
chainTransactions(
toggleMark(type, {
id: uuidv4(),
id: crypto.randomUUID(),
userId: this.options.userId,
draft: true,
}),
@@ -112,7 +111,7 @@ export default class Comment extends Mark {
chainTransactions(
addMark(type, {
id: uuidv4(),
id: crypto.randomUUID(),
userId: this.options.userId,
draft: true,
}),
+9 -1
View File
@@ -268,7 +268,15 @@ export default class Link extends Mark {
if (!words.length) {
return false;
}
if (isInCode(view.state)) {
// check if there is a code mark at the current cursor position
const hasCodeMark = schema.marks.code_inline.isInSet(selection.$from.marks());
if (hasCodeMark) {
return false;
}
// check if we are in a code block or code fence
if (isInCode(view.state, { onlyBlock: true })) {
return false;
}
+1 -2
View File
@@ -9,7 +9,6 @@ import toggleCheckboxItem from "../commands/toggleCheckboxItem";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import checkboxRule from "../rules/checkboxes";
import Node from "./Node";
import { v4 } from "uuid";
export default class CheckboxItem extends Node {
get name() {
@@ -35,7 +34,7 @@ export default class CheckboxItem extends Node {
},
],
toDOM: (node) => {
const id = `checkbox-${v4()}`;
const id = `checkbox-${crypto.randomUUID()}`;
const checked = node.attrs.checked.toString();
let input;
if (typeof document !== "undefined") {
+4 -3
View File
@@ -12,9 +12,7 @@ import {
Plugin,
TextSelection,
} from "prosemirror-state";
import * as React from "react";
import { Primitive } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import env from "../../env";
import { MentionType, UnfurlResourceType, UnfurlResponse } from "../../types";
import {
@@ -22,6 +20,7 @@ import {
MentionDocument,
MentionIssue,
MentionPullRequest,
MentionURL,
MentionUser,
} from "../components/Mentions";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
@@ -145,6 +144,8 @@ export default class Mention extends Node {
onChangeUnfurl={this.handleChangeUnfurl(props)}
/>
);
case MentionType.URL:
return <MentionURL {...props} />;
default:
return null;
}
@@ -169,7 +170,7 @@ export default class Mention extends Node {
node.type.name === this.name &&
(!nodeId || existingIds.has(nodeId))
) {
nodeId = uuidv4();
nodeId = crypto.randomUUID();
modified = true;
tr.setNodeAttribute(pos, "id", nodeId);
}
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Oprávnění",
"Change Language": "Změnit jazyk",
"Dismiss": "Zavřít",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Zavřít",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archivované sbírky",
"New doc": "Nový dokument",
"Empty": "Prázdné",
"No collections": "No collections",
"Collapse": "Sbalit",
"Expand": "Rozbalit",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokument není podporován zkuste Markdown, Plain text, HTML nebo Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Možná jste ztratili přístup k tomuto dokumentu, zkuste jej znovu načíst",
"Too many users connected to document": "Příliš mnoho uživatelů připojených k dokumentu",
"Your edits will sync once other users leave the document": "Vaše úpravy budou synchronizovány, jakmile ostatní uživatelé opustí dokument",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Připojení k serveru bylo ztraceno",
"Edits you make will sync once youre online": "Úpravy, které provedete, se synchronizují, jakmile budete online",
"Offline": "Offline",
"Document restored": "Dokument obnoven",
"Images are still uploading.\nAre you sure you want to discard them?": "Obrázky se stále nahrávají.\nOpravdu je chcete zahodit?",
"{{ count }} comment": "{{ count }} komentář",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Nenalezeno",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "Stránka, kterou hledáte, nebyla nalezena. Možná byla odstraněna nebo odkaz není správný.",
"Offline": "Offline",
"We were unable to load the document while offline.": "V režimu offline se nepodařilo načíst dokument.",
"Your account has been suspended": "Váš účet byl pozastaven",
"Warning Sign": "Výstraha",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Skift sprog",
"Dismiss": "Afvis",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "Nyt dokument",
"Empty": "Tom",
"No collections": "No collections",
"Collapse": "Collapse",
"Expand": "Expand",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokumentet understøttes ikke prøv Markdown, Almindelig tekst, HTML, eller Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Berechtigung",
"Change Language": "Sprache ändern",
"Dismiss": "Ablehnen",
"Unable to download image": "Unable to download image",
"Lightbox": "LightBox",
"View, navigate, or download images in the document": "Bilder im Dokument anzeigen, navigieren oder herunterladen",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Schließen",
"Previous": "Zurück",
"Next": "Nächste",
@@ -418,6 +421,7 @@
"Archived collections": "Archivierte Sammlungen",
"New doc": "Neues Dokument",
"Empty": "Leer",
"No collections": "No collections",
"Collapse": "Zusammenklappen",
"Expand": "Ausklappen",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokument nicht unterstützt - versuche Markdown, Klartext, HTML oder Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Sie haben möglicherweise den Zugriff auf dieses Dokument verloren, versuchen Sie es neu zu laden",
"Too many users connected to document": "Zu viele Benutzer sind mit dem Dokument verbunden",
"Your edits will sync once other users leave the document": "Ihre Änderungen werden synchronisiert, sobald andere Benutzer das Dokument verlassen",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Verbindung zum Server verloren",
"Edits you make will sync once youre online": "Änderungen, die du vornimmst, werden synchronisiert, sobald du online bist",
"Offline": "Offline",
"Document restored": "Dokument wiederhergestellt",
"Images are still uploading.\nAre you sure you want to discard them?": "Bilder werden noch hochgeladen.\nMöchtest du sie wirklich verwerfen?",
"{{ count }} comment": "{{ count }} Kommentar",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Bitte fordern Sie den Zugriff beim Eigentümer des Dokuments an.",
"Not found": "Nicht gefunden",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "Die von Ihnen gesuchte Seite kann nicht gefunden werden. Möglicherweise wurde sie gelöscht oder der Link ist fehlerhaft.",
"Offline": "Offline",
"We were unable to load the document while offline.": "Wir konnten das Dokument nicht offline laden.",
"Your account has been suspended": "Ihr Konto wurde gesperrt",
"Warning Sign": "Warnzeichen",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Change Language",
"Dismiss": "Dismiss",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "New doc",
"Empty": "Empty",
"No collections": "No collections",
"Collapse": "Collapse",
"Expand": "Expand",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, please try reloading",
"Too many users connected to document": "Too many users connected to the document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+5 -1
View File
@@ -322,6 +322,8 @@
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -686,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -763,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permiso",
"Change Language": "Cambiar Idioma",
"Dismiss": "Descartar",
"Unable to download image": "Unable to download image",
"Lightbox": "Caja de luz\t",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Cerrar",
"Previous": "Previous",
"Next": "Siguiente",
@@ -418,6 +421,7 @@
"Archived collections": "Colecciones archivadas",
"New doc": "Nuevo doc",
"Empty": "Vacío",
"No collections": "No collections",
"Collapse": "Colapsar",
"Expand": "Expandir",
"Document not supported try Markdown, Plain text, HTML, or Word": "Documento no compatible intenta Markdown, Texto sin formato, HTML o Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Puede que hayas perdido acceso a este documento, intenta recargar la página",
"Too many users connected to document": "Demasiados usuarios conectados al documento",
"Your edits will sync once other users leave the document": "Tus ediciones se sincronizarán una vez los demás usuarios salgan del documento",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Conexión al servidor perdida",
"Edits you make will sync once youre online": "Las ediciones que realices se sincronizarán una vez que estés en línea",
"Offline": "Sin conexión",
"Document restored": "Documento restaurado",
"Images are still uploading.\nAre you sure you want to discard them?": "Las imágenes aún se están cargando.\n¿Estás seguro de que quieres descartarlas?",
"{{ count }} comment": "{{ count }} comentario",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Solicita acceso al propietario del documento.",
"Not found": "No encontrado",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "No se puede encontrar la página que estás buscando. Es posible que haya sido eliminada o que el enlace sea incorrecto.",
"Offline": "Sin conexión",
"We were unable to load the document while offline.": "No pudimos cargar el documento sin conexión.",
"Your account has been suspended": "Tu cuenta ha sido suspendida",
"Warning Sign": "Señal de Advertencia",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "مجوز",
"Change Language": "تغییر زبان",
"Dismiss": "رد کردن",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "سند جدید",
"Empty": "خالی",
"No collections": "No collections",
"Collapse": "جمع کردن",
"Expand": "باز کردن",
"Document not supported try Markdown, Plain text, HTML, or Word": "نوع سند پشتیبانی نمی‌شود - از Markdown، متن ساده، HTML، یا Word استفاده کنید",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "اتصال سرور قطع شد",
"Edits you make will sync once youre online": "ویرایش هایی که انجام می دهید پس از آنلاین بودن همگام سازی می شوند",
"Offline": "آفلاین",
"Document restored": "سند بازیابی شد",
"Images are still uploading.\nAre you sure you want to discard them?": "تصاویر هنوز در حال بارگذاری هستند.\nآیا مطمئن هستید که می خواهید آنها را نادیده بگیرید؟",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "آفلاین",
"We were unable to load the document while offline.": "امکان بارگیری سند در حالت آفلاین وجود نداشت.",
"Your account has been suspended": "حساب شما معلق شده است",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Changer de langue",
"Dismiss": "Fermer",
"Unable to download image": "Unable to download image",
"Lightbox": "Visionneuse",
"View, navigate, or download images in the document": "Afficher, naviguer ou télécharger des images dans le document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Fermer",
"Previous": "Précédent",
"Next": "Suivant",
@@ -418,6 +421,7 @@
"Archived collections": "Collections archivées",
"New doc": "Nouveau doc",
"Empty": "Vide",
"No collections": "No collections",
"Collapse": "Réduire",
"Expand": "Développer",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document non pris en charge - essayez un format Markdown, Text, HTML ou Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Vous avez peut-être perdu l'accès à ce document, essayez de recharger",
"Too many users connected to document": "Trop d'utilisateurs connectés au document",
"Your edits will sync once other users leave the document": "Vos modifications seront synchronisées une fois que les autres utilisateurs quitteront le document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Connexion au serveur perdue",
"Edits you make will sync once youre online": "Les modifications que vous effectuez seront synchronisées lorsque vous serez en ligne",
"Offline": "Hors-ligne",
"Document restored": "Document restauré",
"Images are still uploading.\nAre you sure you want to discard them?": "Des images sont toujours en cours de téléchargement.\nÊtes-vous sûr de vouloir les supprimer ?",
"{{ count }} comment": "{{ count }} commentaire",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Veuillez demander l'accès au propriétaire du document.",
"Not found": "Non trouvé",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "La page que vous cherchez est introuvable. Elle a peut-être été supprimée ou le lien est incorrect.",
"Offline": "Hors-ligne",
"We were unable to load the document while offline.": "Impossible de charger le document en mode hors-ligne.",
"Your account has been suspended": "Votre compte a été suspendu",
"Warning Sign": "Signe d'avertissement",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Change Language",
"Dismiss": "Dismiss",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "New doc",
"Empty": "Empty",
"No collections": "No collections",
"Collapse": "Collapse",
"Expand": "Expand",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Jogosultságok",
"Change Language": "Nyelv megváltoztatása",
"Dismiss": "Bezár",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Bezár",
"Previous": "Előző",
"Next": "Következő",
@@ -418,6 +421,7 @@
"Archived collections": "Keresés a gyűjteményben",
"New doc": "Új doku",
"Empty": "Üres",
"No collections": "No collections",
"Collapse": "Összezárás",
"Expand": "Kinyitás",
"Document not supported try Markdown, Plain text, HTML, or Word": "Ez a dokumentum nem támogatott próbáljon Markdown, egyszerű szöveg, HTML vagy Word formátumot",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Lehet, hogy nincs hozzáférése a dokumentumhoz, próbálja meg újra betölteni",
"Too many users connected to document": "Túl sok felhasználó kapcsolódik a dokumentumhoz",
"Your edits will sync once other users leave the document": "A módosításai szinkronizálódnak, amikor a többi felhasználó elhagyja a dokumentumot",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "A szerverrel a kapcsolat megszakadt",
"Edits you make will sync once youre online": "Az elvégzett módosítások szinkronizálódnak, amint online lesz",
"Offline": "Offline",
"Document restored": "Dokumentum visszaállítva",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} tag",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "A fiókja felfüggesztésre került",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Ubah Bahasa",
"Dismiss": "Menutup",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Menutup",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "Dokumen baru",
"Empty": "Kosong",
"No collections": "No collections",
"Collapse": "Persingkat",
"Expand": "Perlengkap",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokumen tidak didukung coba Markdown, teks biasa, HTML, atau Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Koneksi server terputus",
"Edits you make will sync once youre online": "Suntingan yang Anda buat akan disinkronkan saat Anda daring",
"Offline": "Luring",
"Document restored": "Dokumen dipulihkan",
"Images are still uploading.\nAre you sure you want to discard them?": "Gambar masih diunggah.\nApa Anda yakin ingin membatalkannya?",
"{{ count }} comment": "{{ count }} komentar",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Luring",
"We were unable to load the document while offline.": "Kami tidak dapat memuat dokumen saat luring.",
"Your account has been suspended": "Akun Anda telah disuspen",
"Warning Sign": "Tanda peringatan",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permessi",
"Change Language": "Cambia Lingua",
"Dismiss": "Chiudi",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "Visualizza, naviga, o scarica immagini nel documento",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Chiudi",
"Previous": "Precedente",
"Next": "Successivo",
@@ -418,6 +421,7 @@
"Archived collections": "Collezioni archiviate",
"New doc": "Nuovo documento",
"Empty": "Vuoto",
"No collections": "No collections",
"Collapse": "Raggruppa",
"Expand": "Espandi",
"Document not supported try Markdown, Plain text, HTML, or Word": "Documento non supportato prova Markdown, testo semplice, HTML o Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Potresti aver perso l'accesso a questo documento, prova a ricaricarlo",
"Too many users connected to document": "Troppi utenti connessi al documento",
"Your edits will sync once other users leave the document": "Le tue modifiche verranno sincronizzate non appena gli altri utenti avranno lasciato il documento",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Connessione al server interrotta",
"Edits you make will sync once youre online": "Le modifiche apportate verranno sincronizzate una volta che sarai online",
"Offline": "Non in linea",
"Document restored": "Documento ripristinato",
"Images are still uploading.\nAre you sure you want to discard them?": "Le immagini sono ancora in caricamento.\nVuoi davvero scartarle?",
"{{ count }} comment": "{{ count }} commento",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Richiedi l'accesso al proprietario del documento",
"Not found": "Non trovato",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "La pagina che stai cercando non è stata trovata. Potrebbe essere stata eliminata oppure il link non è corretto.",
"Offline": "Non in linea",
"We were unable to load the document while offline.": "Impossibile caricare il documento offline.",
"Your account has been suspended": "Il tuo account è stato sospeso",
"Warning Sign": "Simbolo di avvertenza",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "権限",
"Change Language": "言語を変更",
"Dismiss": "却下",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "閉じる",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "アーカイブされたコレクション",
"New doc": "ドキュメントを新規作成",
"Empty": "空",
"No collections": "No collections",
"Collapse": "折りたたむ",
"Expand": "展開",
"Document not supported try Markdown, Plain text, HTML, or Word": "ドキュメントはサポートされていません。\nMarkdown、プレーンテキスト、HTML、または Word をお試しください。",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "このドキュメントへのアクセス権を失った可能性があります。再読み込みしてください。",
"Too many users connected to document": "ドキュメントを閲覧しているユーザーが多すぎます",
"Your edits will sync once other users leave the document": "他のユーザーがドキュメントを離れると、あなたの編集内容が同期されます。",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "サーバーへの接続が失われました",
"Edits you make will sync once youre online": "編集内容はオンラインになると同期されます",
"Offline": "オフライン",
"Document restored": "ドキュメントが復元されました",
"Images are still uploading.\nAre you sure you want to discard them?": "画像はまだアップロード中です\nこの操作を取り消しますか?",
"{{ count }} comment": "{{ count }} 件のコメント",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "見つかりません",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "お探しのページが見つかりません。削除されたか、リンクが正しくありません。",
"Offline": "オフライン",
"We were unable to load the document while offline.": "インターネットに接続していない状態でドキュメントを読み込むことができません。",
"Your account has been suspended": "あなたのアカウントは凍結されています。",
"Warning Sign": "警告",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "권한",
"Change Language": "언어 변경",
"Dismiss": "닫기",
"Unable to download image": "Unable to download image",
"Lightbox": "라이트박스",
"View, navigate, or download images in the document": "문서에서 이미지 보기, 탐색 또는 다운로드",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "닫기",
"Previous": "이전",
"Next": "다음",
@@ -418,6 +421,7 @@
"Archived collections": "보관된 컬렉션",
"New doc": "새 문서",
"Empty": "비어 있음",
"No collections": "No collections",
"Collapse": "감추기",
"Expand": "펼치기",
"Document not supported try Markdown, Plain text, HTML, or Word": "이 문서는 지원되지 않습니다 Markdown, Plain Text, HTML이나 Word를 이용해주세요",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "이 문서에 대한 액세스 권한을 잃었을 수 있습니다. 새로고침해 보세요.",
"Too many users connected to document": "문서에 연결된 사용자가 너무 많습니다.",
"Your edits will sync once other users leave the document": "다른 사용자가 문서를 떠나면 편집 내용이 동기화됩니다.",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "서버 연결 끊김",
"Edits you make will sync once youre online": "온라인 상태가 되면 수정 사항이 동기화됩니다",
"Offline": "오프라인",
"Document restored": "문서가 복원되었습니다",
"Images are still uploading.\nAre you sure you want to discard them?": "이미지가 아직 업로드 중입니다.\n변경 내용을 삭제하시겠습니까?",
"{{ count }} comment": "댓글 {{ count }} 개",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "문서 소유자에게 접근 권한을 요청하세요.",
"Not found": "찾을 수 없음",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "찾고 있는 페이지를 찾을 수 없습니다. 삭제되었거나 링크가 올바르지 않을 수 있습니다.",
"Offline": "오프라인",
"We were unable to load the document while offline.": "오프라인 상태에서는 문서를 불러 올 수 없습니다.",
"Your account has been suspended": "계정사용이 중지 되었습니다",
"Warning Sign": "경고 표시",

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