feat: Seamless Edit (#2701)
* feat: Remove explicit edit * Restore revision remains disabled for now * Bump RME, better differentiation of focused state * fix: Star not visible in edit mode * remove stray log * fix: Occassional user context not available in collaborative persistence
This commit is contained in:
parent
37be7f99c4
commit
c597f2d9a2
|
@ -0,0 +1,105 @@
|
||||||
|
// @flow
|
||||||
|
import isPrintableKeyEvent from "is-printable-key-event";
|
||||||
|
import * as React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
disabled?: boolean,
|
||||||
|
onChange?: (text: string) => void,
|
||||||
|
onBlur?: (event: SyntheticInputEvent<>) => void,
|
||||||
|
onInput?: (event: SyntheticInputEvent<>) => void,
|
||||||
|
onKeyDown?: (event: SyntheticInputEvent<>) => void,
|
||||||
|
placeholder?: string,
|
||||||
|
maxLength?: number,
|
||||||
|
autoFocus?: boolean,
|
||||||
|
className?: string,
|
||||||
|
children?: React.Node,
|
||||||
|
value: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a content editable component with the same interface as a native
|
||||||
|
* HTMLInputElement (or, as close as we can get).
|
||||||
|
*/
|
||||||
|
function ContentEditable({
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
onInput,
|
||||||
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
maxLength,
|
||||||
|
autoFocus,
|
||||||
|
placeholder,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const ref = React.useRef<?HTMLSpanElement>();
|
||||||
|
const [innerHTML, setInnerHTML] = React.useState<string>(value);
|
||||||
|
const lastValue = React.useRef("");
|
||||||
|
|
||||||
|
const wrappedEvent = (callback) => (
|
||||||
|
event: SyntheticInputEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const text = ref.current?.innerText || "";
|
||||||
|
|
||||||
|
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text !== lastValue.current) {
|
||||||
|
lastValue.current = text;
|
||||||
|
onChange && onChange(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback && callback(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (autoFocus) {
|
||||||
|
ref.current?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (value !== ref.current?.innerText) {
|
||||||
|
setInnerHTML(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Content
|
||||||
|
contentEditable={!disabled}
|
||||||
|
onInput={wrappedEvent(onInput)}
|
||||||
|
onBlur={wrappedEvent(onBlur)}
|
||||||
|
onKeyDown={wrappedEvent(onKeyDown)}
|
||||||
|
ref={ref}
|
||||||
|
data-placeholder={placeholder}
|
||||||
|
role="textbox"
|
||||||
|
dangerouslySetInnerHTML={{ __html: innerHTML }}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = styled.span`
|
||||||
|
&:empty {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty::before {
|
||||||
|
display: inline-block;
|
||||||
|
color: ${(props) => props.theme.placeholder};
|
||||||
|
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default React.memo<Props>(ContentEditable);
|
|
@ -325,7 +325,7 @@ function DocumentMenu({
|
||||||
{
|
{
|
||||||
title: t("Edit"),
|
title: t("Edit"),
|
||||||
to: editDocumentUrl(document),
|
to: editDocumentUrl(document),
|
||||||
visible: !!can.update,
|
visible: !!can.update && !team.collaborativeEditing,
|
||||||
icon: <EditIcon />,
|
icon: <EditIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -88,7 +88,10 @@ class DataLoader extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEditing() {
|
get isEditing() {
|
||||||
return this.props.match.path === matchDocumentEdit;
|
return (
|
||||||
|
this.props.match.path === matchDocumentEdit ||
|
||||||
|
this.props.auth?.team?.collaborativeEditing
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchLink = async (term: string) => {
|
onSearchLink = async (term: string) => {
|
||||||
|
@ -244,7 +247,9 @@ class DataLoader extends React.Component<Props> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Loading location={location} />
|
<Loading location={location} />
|
||||||
{this.isEditing && <HideSidebar ui={ui} />}
|
{this.isEditing && !team?.collaborativeEditing && (
|
||||||
|
<HideSidebar ui={ui} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -261,7 +266,9 @@ class DataLoader extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={key}>
|
<React.Fragment key={key}>
|
||||||
{this.isEditing && <HideSidebar ui={ui} />}
|
{this.isEditing && !team.collaborativeEditing && (
|
||||||
|
<HideSidebar ui={ui} />
|
||||||
|
)}
|
||||||
{this.props.children({
|
{this.props.children({
|
||||||
document,
|
document,
|
||||||
revision,
|
revision,
|
||||||
|
|
|
@ -357,8 +357,8 @@ class DocumentScene extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTitle = (event) => {
|
onChangeTitle = (value) => {
|
||||||
this.title = event.target.value;
|
this.title = value;
|
||||||
this.updateIsDirtyDebounced();
|
this.updateIsDirtyDebounced();
|
||||||
this.autosave();
|
this.autosave();
|
||||||
};
|
};
|
||||||
|
@ -389,7 +389,8 @@ class DocumentScene extends React.Component<Props> {
|
||||||
const headings = this.editor.current
|
const headings = this.editor.current
|
||||||
? this.editor.current.getHeadings()
|
? this.editor.current.getHeadings()
|
||||||
: [];
|
: [];
|
||||||
const showContents = ui.tocVisible && readOnly;
|
const showContents =
|
||||||
|
ui.tocVisible && (readOnly || team?.collaborativeEditing);
|
||||||
|
|
||||||
const collaborativeEditing =
|
const collaborativeEditing =
|
||||||
team?.collaborativeEditing &&
|
team?.collaborativeEditing &&
|
||||||
|
@ -473,7 +474,7 @@ class DocumentScene extends React.Component<Props> {
|
||||||
shareId={shareId}
|
shareId={shareId}
|
||||||
isRevision={!!revision}
|
isRevision={!!revision}
|
||||||
isDraft={document.isDraft}
|
isDraft={document.isDraft}
|
||||||
isEditing={!readOnly}
|
isEditing={!readOnly && !team?.collaborativeEditing}
|
||||||
isSaving={this.isSaving}
|
isSaving={this.isSaving}
|
||||||
isPublishing={this.isPublishing}
|
isPublishing={this.isPublishing}
|
||||||
publishingIsDisabled={
|
publishingIsDisabled={
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
// @flow
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
import { MAX_TITLE_LENGTH } from "shared/constants";
|
||||||
|
import { light } from "shared/theme";
|
||||||
|
import parseTitle from "shared/utils/parseTitle";
|
||||||
|
import Document from "models/Document";
|
||||||
|
import ContentEditable from "components/ContentEditable";
|
||||||
|
import Star, { AnimatedStar } from "components/Star";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
import { isModKey } from "utils/keyboard";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string,
|
||||||
|
document: Document,
|
||||||
|
readOnly: boolean,
|
||||||
|
onChange: (text: string) => void,
|
||||||
|
onGoToNextInput: (insertParagraph?: boolean) => void,
|
||||||
|
onSave: (options: { publish?: boolean, done?: boolean }) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function EditableTitle({
|
||||||
|
value,
|
||||||
|
document,
|
||||||
|
readOnly,
|
||||||
|
onChange,
|
||||||
|
onSave,
|
||||||
|
onGoToNextInput,
|
||||||
|
}: Props) {
|
||||||
|
const ref = React.useRef();
|
||||||
|
const { policies } = useStores();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const can = policies.abilities(document.id);
|
||||||
|
const { emoji } = parseTitle(value);
|
||||||
|
const startsWithEmojiAndSpace = !!(emoji && value.startsWith(`${emoji} `));
|
||||||
|
const normalizedTitle =
|
||||||
|
!value && readOnly ? document.titleWithDefault : value;
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: SyntheticKeyboardEvent<>) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isModKey(event)) {
|
||||||
|
onSave({ done: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onGoToNextInput(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Tab" || event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
onGoToNextInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "p" && isModKey(event) && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSave({ publish: true, done: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "s" && isModKey(event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSave({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onGoToNextInput, onSave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Title
|
||||||
|
ref={ref}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={
|
||||||
|
document.isTemplate
|
||||||
|
? t("Start your template…")
|
||||||
|
: t("Start with a title…")
|
||||||
|
}
|
||||||
|
value={normalizedTitle}
|
||||||
|
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
||||||
|
$isStarred={document.isStarred}
|
||||||
|
autoFocus={!value}
|
||||||
|
maxLength={MAX_TITLE_LENGTH}
|
||||||
|
readOnly={readOnly}
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
{(can.star || can.unstar) && <StarButton document={document} size={32} />}
|
||||||
|
</Title>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StarButton = styled(Star)`
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled(ContentEditable)`
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
background: ${(props) => props.theme.background};
|
||||||
|
transition: ${(props) => props.theme.backgroundTransition};
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
-webkit-text-fill-color: ${(props) => props.theme.text};
|
||||||
|
font-size: 2.25em;
|
||||||
|
font-weight: 500;
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${(props) => props.theme.placeholder};
|
||||||
|
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
||||||
|
}
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
margin-left: ${(props) => (props.$startsWithEmojiAndSpace ? "-1.2em" : 0)};
|
||||||
|
`};
|
||||||
|
|
||||||
|
${AnimatedStar} {
|
||||||
|
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
${AnimatedStar} {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
color: ${(props) => light.text};
|
||||||
|
-webkit-text-fill-color: ${(props) => light.text};
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(EditableTitle);
|
|
@ -2,13 +2,7 @@
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { inject, observer } from "mobx-react";
|
import { inject, observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Textarea from "react-autosize-textarea";
|
|
||||||
import { type TFunction, withTranslation } from "react-i18next";
|
import { type TFunction, withTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
|
||||||
import breakpoint from "styled-components-breakpoint";
|
|
||||||
import { MAX_TITLE_LENGTH } from "shared/constants";
|
|
||||||
import { light } from "shared/theme";
|
|
||||||
import parseTitle from "shared/utils/parseTitle";
|
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import ClickablePadding from "components/ClickablePadding";
|
import ClickablePadding from "components/ClickablePadding";
|
||||||
|
@ -16,14 +10,13 @@ import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||||
import Editor, { type Props as EditorProps } from "components/Editor";
|
import Editor, { type Props as EditorProps } from "components/Editor";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import HoverPreview from "components/HoverPreview";
|
import HoverPreview from "components/HoverPreview";
|
||||||
import Star, { AnimatedStar } from "components/Star";
|
import EditableTitle from "./EditableTitle";
|
||||||
import MultiplayerEditor from "./MultiplayerEditor";
|
import MultiplayerEditor from "./MultiplayerEditor";
|
||||||
import { isModKey } from "utils/keyboard";
|
|
||||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
...EditorProps,
|
...EditorProps,
|
||||||
onChangeTitle: (event: SyntheticInputEvent<>) => void,
|
onChangeTitle: (text: string) => void,
|
||||||
title: string,
|
title: string,
|
||||||
document: Document,
|
document: Document,
|
||||||
isDraft: boolean,
|
isDraft: boolean,
|
||||||
|
@ -61,35 +54,6 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
if (isModKey(event)) {
|
|
||||||
this.props.onSave({ done: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.insertParagraph();
|
|
||||||
this.focusAtStart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === "Tab" || event.key === "ArrowDown") {
|
|
||||||
event.preventDefault();
|
|
||||||
this.focusAtStart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === "p" && isModKey(event) && event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.onSave({ publish: true, done: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === "s" && isModKey(event)) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.onSave({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLinkActive = (event: MouseEvent) => {
|
handleLinkActive = (event: MouseEvent) => {
|
||||||
this.activeLinkEvent = event;
|
this.activeLinkEvent = event;
|
||||||
};
|
};
|
||||||
|
@ -98,6 +62,13 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
this.activeLinkEvent = null;
|
this.activeLinkEvent = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleGoToNextInput = (insertParagraph: boolean) => {
|
||||||
|
if (insertParagraph) {
|
||||||
|
this.insertParagraph();
|
||||||
|
}
|
||||||
|
this.focusAtStart();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
document,
|
document,
|
||||||
|
@ -115,45 +86,16 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
|
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
|
||||||
const can = policies.abilities(document.id);
|
|
||||||
const { emoji } = parseTitle(title);
|
|
||||||
const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `));
|
|
||||||
const normalizedTitle =
|
|
||||||
!title && readOnly ? document.titleWithDefault : title;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex auto column>
|
<Flex auto column>
|
||||||
{readOnly ? (
|
<EditableTitle
|
||||||
<Title
|
value={title}
|
||||||
as="div"
|
readOnly={readOnly}
|
||||||
ref={this.ref}
|
document={document}
|
||||||
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
onGoToNextInput={this.handleGoToNextInput}
|
||||||
$isStarred={document.isStarred}
|
onChange={onChangeTitle}
|
||||||
dir="auto"
|
/>
|
||||||
>
|
|
||||||
<span>{normalizedTitle}</span>{" "}
|
|
||||||
{(can.star || can.unstar) && (
|
|
||||||
<StarButton document={document} size={32} />
|
|
||||||
)}
|
|
||||||
</Title>
|
|
||||||
) : (
|
|
||||||
<Title
|
|
||||||
type="text"
|
|
||||||
ref={this.ref}
|
|
||||||
onChange={onChangeTitle}
|
|
||||||
onKeyDown={this.handleTitleKeyDown}
|
|
||||||
placeholder={
|
|
||||||
document.isTemplate
|
|
||||||
? t("Start your template…")
|
|
||||||
: t("Start with a title…")
|
|
||||||
}
|
|
||||||
value={normalizedTitle}
|
|
||||||
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
|
|
||||||
autoFocus={!title}
|
|
||||||
maxLength={MAX_TITLE_LENGTH}
|
|
||||||
dir="auto"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!shareId && (
|
{!shareId && (
|
||||||
<DocumentMetaWithViews
|
<DocumentMetaWithViews
|
||||||
isDraft={isDraft}
|
isDraft={isDraft}
|
||||||
|
@ -191,56 +133,6 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const StarButton = styled(Star)`
|
|
||||||
position: relative;
|
|
||||||
top: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled(Textarea)`
|
|
||||||
line-height: 1.25;
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
background: ${(props) => props.theme.background};
|
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
|
||||||
color: ${(props) => props.theme.text};
|
|
||||||
-webkit-text-fill-color: ${(props) => props.theme.text};
|
|
||||||
font-size: 2.25em;
|
|
||||||
font-weight: 500;
|
|
||||||
outline: none;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
resize: none;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: ${(props) => props.theme.placeholder};
|
|
||||||
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
|
||||||
}
|
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
margin-left: ${(props) => (props.$startsWithEmojiAndSpace ? "-1.2em" : 0)};
|
|
||||||
`};
|
|
||||||
|
|
||||||
${AnimatedStar} {
|
|
||||||
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
${AnimatedStar} {
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
color: ${(props) => light.text};
|
|
||||||
-webkit-text-fill-color: ${(props) => light.text};
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default withTranslation()<DocumentEditor>(
|
export default withTranslation()<DocumentEditor>(
|
||||||
inject("policies")(DocumentEditor)
|
inject("policies")(DocumentEditor)
|
||||||
);
|
);
|
||||||
|
|
|
@ -230,7 +230,7 @@ function DocumentHeader({
|
||||||
</Action>
|
</Action>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canEdit && editAction}
|
{canEdit && !team.collaborativeEditing && editAction}
|
||||||
{canEdit && can.createChildDocument && !isMobile && (
|
{canEdit && can.createChildDocument && !isMobile && (
|
||||||
<Action>
|
<Action>
|
||||||
<NewChildDocumentMenu
|
<NewChildDocumentMenu
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
// flow-typed signature: 4739272fd9d8d2ec5c9881791bce7104
|
|
||||||
// flow-typed version: <<STUB>>/react-autosize-textarea_v^6.0.0/flow_v0.104.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an autogenerated libdef stub for:
|
|
||||||
*
|
|
||||||
* 'react-autosize-textarea'
|
|
||||||
*
|
|
||||||
* Fill this stub out by replacing all the `any` types.
|
|
||||||
*
|
|
||||||
* Once filled out, we encourage you to share your work with the
|
|
||||||
* community by sending a pull request to:
|
|
||||||
* https://github.com/flowtype/flow-typed
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module 'react-autosize-textarea' {
|
|
||||||
declare module.exports: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We include stubs for each file inside this npm package in case you need to
|
|
||||||
* require those files directly. Feel free to delete any files that aren't
|
|
||||||
* needed.
|
|
||||||
*/
|
|
||||||
declare module 'react-autosize-textarea/lib' {
|
|
||||||
declare module.exports: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'react-autosize-textarea/lib/TextareaAutosize' {
|
|
||||||
declare module.exports: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filename aliases
|
|
||||||
declare module 'react-autosize-textarea/lib/index' {
|
|
||||||
declare module.exports: $Exports<'react-autosize-textarea/lib'>;
|
|
||||||
}
|
|
||||||
declare module 'react-autosize-textarea/lib/index.js' {
|
|
||||||
declare module.exports: $Exports<'react-autosize-textarea/lib'>;
|
|
||||||
}
|
|
||||||
declare module 'react-autosize-textarea/lib/TextareaAutosize.js' {
|
|
||||||
declare module.exports: $Exports<'react-autosize-textarea/lib/TextareaAutosize'>;
|
|
||||||
}
|
|
|
@ -92,6 +92,7 @@
|
||||||
"imports-loader": "0.6.5",
|
"imports-loader": "0.6.5",
|
||||||
"invariant": "^2.2.2",
|
"invariant": "^2.2.2",
|
||||||
"ioredis": "^4.24.3",
|
"ioredis": "^4.24.3",
|
||||||
|
"is-printable-key-event": "^1.0.0",
|
||||||
"joplin-turndown-plugin-gfm": "^1.0.12",
|
"joplin-turndown-plugin-gfm": "^1.0.12",
|
||||||
"js-search": "^1.4.2",
|
"js-search": "^1.4.2",
|
||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
|
@ -131,7 +132,6 @@
|
||||||
"randomstring": "1.1.5",
|
"randomstring": "1.1.5",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-autosize-textarea": "^7.1.0",
|
|
||||||
"react-avatar-editor": "^11.1.0",
|
"react-avatar-editor": "^11.1.0",
|
||||||
"react-color": "^2.17.3",
|
"react-color": "^2.17.3",
|
||||||
"react-dnd": "^14.0.1",
|
"react-dnd": "^14.0.1",
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default class Persistence {
|
||||||
documentName,
|
documentName,
|
||||||
}: {
|
}: {
|
||||||
document: Y.Doc,
|
document: Y.Doc,
|
||||||
context: { user: User },
|
context: { user: ?User },
|
||||||
documentName: string,
|
documentName: string,
|
||||||
}) => {
|
}) => {
|
||||||
const [, documentId] = documentName.split(".");
|
const [, documentId] = documentName.split(".");
|
||||||
|
@ -63,7 +63,7 @@ export default class Persistence {
|
||||||
await documentUpdater({
|
await documentUpdater({
|
||||||
documentId,
|
documentId,
|
||||||
ydoc: document,
|
ydoc: document,
|
||||||
userId: context.user.id,
|
userId: context.user?.id,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error("Unable to persist document", err, {
|
Logger.error("Unable to persist document", err, {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default async function documentUpdater({
|
||||||
}: {
|
}: {
|
||||||
documentId: string,
|
documentId: string,
|
||||||
ydoc: Y.Doc,
|
ydoc: Y.Doc,
|
||||||
userId: string,
|
userId?: string,
|
||||||
}) {
|
}) {
|
||||||
const document = await Document.findByPk(documentId);
|
const document = await Document.findByPk(documentId);
|
||||||
const state = Y.encodeStateAsUpdate(ydoc);
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
|
@ -38,7 +38,8 @@ export default async function documentUpdater({
|
||||||
text,
|
text,
|
||||||
state: Buffer.from(state),
|
state: Buffer.from(state),
|
||||||
updatedAt: isUnchanged ? document.updatedAt : new Date(),
|
updatedAt: isUnchanged ? document.updatedAt : new Date(),
|
||||||
lastModifiedById: isUnchanged ? document.lastModifiedById : userId,
|
lastModifiedById:
|
||||||
|
isUnchanged || !userId ? document.lastModifiedById : userId,
|
||||||
collaboratorIds,
|
collaboratorIds,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -449,6 +449,7 @@
|
||||||
"Send Invites": "Send Invites",
|
"Send Invites": "Send Invites",
|
||||||
"Edit current document": "Edit current document",
|
"Edit current document": "Edit current document",
|
||||||
"Move current document": "Move current document",
|
"Move current document": "Move current document",
|
||||||
|
"Open document history": "Open document history",
|
||||||
"Jump to search": "Jump to search",
|
"Jump to search": "Jump to search",
|
||||||
"Jump to home": "Jump to home",
|
"Jump to home": "Jump to home",
|
||||||
"Toggle navigation": "Toggle navigation",
|
"Toggle navigation": "Toggle navigation",
|
||||||
|
|
33
yarn.lock
33
yarn.lock
|
@ -3530,11 +3530,6 @@ auto-bind@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-1.2.1.tgz#807f7910b0210db9eefe133f3492c28e89698b96"
|
resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-1.2.1.tgz#807f7910b0210db9eefe133f3492c28e89698b96"
|
||||||
integrity sha512-/W9yj1yKmBLwpexwAujeD9YHwYmRuWFGV8HWE7smQab797VeHa4/cnE2NFeDhA+E+5e/OGBI8763EhLjfZ/MXA==
|
integrity sha512-/W9yj1yKmBLwpexwAujeD9YHwYmRuWFGV8HWE7smQab797VeHa4/cnE2NFeDhA+E+5e/OGBI8763EhLjfZ/MXA==
|
||||||
|
|
||||||
autosize@^4.0.2:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
|
|
||||||
integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
|
|
||||||
|
|
||||||
autotrack@^2.4.1:
|
autotrack@^2.4.1:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/autotrack/-/autotrack-2.4.1.tgz#ccbf010e3d95ef23c8dd6db4e8df025135c82ee6"
|
resolved "https://registry.yarnpkg.com/autotrack/-/autotrack-2.4.1.tgz#ccbf010e3d95ef23c8dd6db4e8df025135c82ee6"
|
||||||
|
@ -4897,11 +4892,6 @@ compute-scroll-into-view@^1.0.16:
|
||||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088"
|
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088"
|
||||||
integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==
|
integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==
|
||||||
|
|
||||||
computed-style@~0.1.3:
|
|
||||||
version "0.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/computed-style/-/computed-style-0.1.4.tgz#7f344fd8584b2e425bedca4a1afc0e300bb05d74"
|
|
||||||
integrity sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ=
|
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
@ -8353,6 +8343,11 @@ is-potential-custom-element-name@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
|
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
|
||||||
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
|
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
|
||||||
|
|
||||||
|
is-printable-key-event@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-printable-key-event/-/is-printable-key-event-1.0.0.tgz#1ea47b8abe1a2e53a1f5ea6aecbd6d24da707c66"
|
||||||
|
integrity sha512-C/GJ8ApSdY6/RGQrSSkBzuWDtYI9/mOTRLCOu/5iYH46pI7Ki6y6B71kPL7OWRzqv9KkWSEmskKdq5IvgAGPHA==
|
||||||
|
|
||||||
is-promise@^2.1:
|
is-promise@^2.1:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||||
|
@ -9568,13 +9563,6 @@ limiter@^1.1.4:
|
||||||
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
|
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
|
||||||
integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==
|
integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==
|
||||||
|
|
||||||
line-height@^0.3.1:
|
|
||||||
version "0.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/line-height/-/line-height-0.3.1.tgz#4b1205edde182872a5efa3c8f620b3187a9c54c9"
|
|
||||||
integrity sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk=
|
|
||||||
dependencies:
|
|
||||||
computed-style "~0.1.3"
|
|
||||||
|
|
||||||
lines-and-columns@^1.1.6:
|
lines-and-columns@^1.1.6:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||||
|
@ -11688,7 +11676,7 @@ prop-types-exact@^1.2.0:
|
||||||
object.assign "^4.1.0"
|
object.assign "^4.1.0"
|
||||||
reflect.ownkeys "^0.2.0"
|
reflect.ownkeys "^0.2.0"
|
||||||
|
|
||||||
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
|
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||||
|
@ -12044,15 +12032,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-autosize-textarea@^7.1.0:
|
|
||||||
version "7.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz#902c84fc395a689ca3a484dfb6bc2be9ba3694d1"
|
|
||||||
integrity sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==
|
|
||||||
dependencies:
|
|
||||||
autosize "^4.0.2"
|
|
||||||
line-height "^0.3.1"
|
|
||||||
prop-types "^15.5.6"
|
|
||||||
|
|
||||||
react-avatar-editor@^11.1.0:
|
react-avatar-editor@^11.1.0:
|
||||||
version "11.1.0"
|
version "11.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.1.0.tgz#0eaec7970b1fbbd90d42a1955be440ea27f598ea"
|
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.1.0.tgz#0eaec7970b1fbbd90d42a1955be440ea27f598ea"
|
||||||
|
|
Reference in New Issue