Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce0936ebb6 | |||
| bb72774f2d | |||
| 76868a3083 | |||
| 0865052bb8 | |||
| de6bc9beca | |||
| e97944ab40 | |||
| 5cfea207e6 | |||
| 95f0c42d56 | |||
| ee7738c141 | |||
| 76701e35ec | |||
| ae8c2aae15 | |||
| a9fa2ed72b | |||
| 0deb7e7f09 | |||
| a544559de2 | |||
| 79fe08e9b6 | |||
| c8d8ba3914 | |||
| 7a148b0353 | |||
| ca891a56da | |||
| 294d3e896a | |||
| d947f8fda2 | |||
| 6dd228a533 | |||
| c7d847215c | |||
| 6995ca8521 | |||
| 8a3452e664 | |||
| f6315875b4 | |||
| f4e53da1bf |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -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,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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 977 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 693 B After Width: | Height: | Size: 638 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 239 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 764 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1013 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 513 B |
|
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, {
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export default abstract class ExportTask extends BaseTask<Props> {
|
||||
: {
|
||||
teamId: user.teamId,
|
||||
archivedAt: {
|
||||
[Op.ne]: null,
|
||||
[Op.eq]: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
})}`
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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",
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
``
|
||||
);
|
||||
@@ -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(
|
||||
``
|
||||
);
|
||||
@@ -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(
|
||||
``
|
||||
);
|
||||
@@ -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(
|
||||
``
|
||||
);
|
||||
@@ -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(`
|
||||
|
||||
@@ -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}" />`
|
||||
);
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 you’re 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 you’re 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",
|
||||
|
||||
@@ -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 you’re online": "Edits you make will sync once you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re 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",
|
||||
|
||||
@@ -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 you’re online": "Edits you make will sync once you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re online": "Edits you make will sync once you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re 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",
|
||||
|
||||
@@ -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 you’re online": "Edits you make will sync once you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re looking for cannot be found. It might have been deleted or the link is incorrect.": "The page you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re 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",
|
||||
|
||||
@@ -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 you’re 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 you’re 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": "警告",
|
||||
|
||||
@@ -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 you’re 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 you’re 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": "경고 표시",
|
||||
|
||||