feat: Collaborative revision restore (#2721)

This commit is contained in:
Tom Moor 2021-11-07 08:58:44 -08:00 committed by GitHub
parent 5dd5df6268
commit b2a1e6b309
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 17 deletions

View File

@ -137,7 +137,8 @@ const ListItem = styled(Item)`
&:nth-child(2)::before { &:nth-child(2)::before {
height: 50%; height: 50%;
top: 50%; top: auto;
bottom: -4px;
} }
&:last-child::before { &:last-child::before {

View File

@ -32,11 +32,19 @@ function RevisionMenu({ document, revisionId, className }: Props) {
const handleRestore = React.useCallback( const handleRestore = React.useCallback(
async (ev: SyntheticEvent<>) => { async (ev: SyntheticEvent<>) => {
ev.preventDefault(); ev.preventDefault();
await document.restore({ revisionId });
showToast(t("Document restored"), { type: "success" }); if (team.collaborativeEditing) {
history.push(document.url); history.push(document.url, {
restore: true,
revisionId,
});
} else {
await document.restore({ revisionId });
showToast(t("Document restored"), { type: "success" });
history.push(document.url);
}
}, },
[history, showToast, t, document, revisionId] [history, showToast, t, team.collaborativeEditing, document, revisionId]
); );
const handleCopy = React.useCallback(() => { const handleCopy = React.useCallback(() => {
@ -57,11 +65,7 @@ function RevisionMenu({ document, revisionId, className }: Props) {
{...menu} {...menu}
/> />
<ContextMenu {...menu} aria-label={t("Revision options")}> <ContextMenu {...menu} aria-label={t("Revision options")}>
<MenuItem <MenuItem {...menu} onClick={handleRestore}>
{...menu}
onClick={handleRestore}
disabled={team.collaborativeEditing}
>
<MenuIconWrapper> <MenuIconWrapper>
<RestoreIcon /> <RestoreIcon />
</MenuIconWrapper> </MenuIconWrapper>

View File

@ -24,6 +24,7 @@ import { type LocationWithState, type NavigationNode } from "types";
import { NotFoundError, OfflineError } from "utils/errors"; import { NotFoundError, OfflineError } from "utils/errors";
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers"; import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
import { isInternalUrl } from "utils/urls"; import { isInternalUrl } from "utils/urls";
type Props = {| type Props = {|
match: Match, match: Match,
auth: AuthStore, auth: AuthStore,
@ -45,6 +46,7 @@ class DataLoader extends React.Component<Props> {
sharedTree: ?NavigationNode; sharedTree: ?NavigationNode;
@observable document: ?Document; @observable document: ?Document;
@observable revision: ?Revision; @observable revision: ?Revision;
@observable shapshot: ?Blob;
@observable error: ?Error; @observable error: ?Error;
componentDidMount() { componentDidMount() {
@ -223,7 +225,8 @@ class DataLoader extends React.Component<Props> {
}; };
render() { render() {
const { location, policies, auth, ui } = this.props; const { location, policies, auth, match, ui } = this.props;
const { revisionId } = match.params;
if (this.error) { if (this.error) {
return this.error instanceof OfflineError ? ( return this.error instanceof OfflineError ? (
@ -237,7 +240,7 @@ class DataLoader extends React.Component<Props> {
const document = this.document; const document = this.document;
const revision = this.revision; const revision = this.revision;
if (!document || !team) { if (!document || !team || (revisionId && !revision)) {
return ( return (
<> <>
<Loading location={location} /> <Loading location={location} />

View File

@ -37,6 +37,7 @@ import MarkAsViewed from "./MarkAsViewed";
import PublicReferences from "./PublicReferences"; import PublicReferences from "./PublicReferences";
import References from "./References"; import References from "./References";
import { type LocationWithState, type NavigationNode, type Theme } from "types"; import { type LocationWithState, type NavigationNode, type Theme } from "types";
import { client } from "utils/ApiClient";
import { isCustomDomain } from "utils/domains"; import { isCustomDomain } from "utils/domains";
import { emojiToUrl } from "utils/emoji"; import { emojiToUrl } from "utils/emoji";
import { isModKey } from "utils/keyboard"; import { isModKey } from "utils/keyboard";
@ -125,7 +126,7 @@ class DocumentScene extends React.Component<Props> {
} }
} }
onSelectTemplate = (template: Document) => { replaceDocument = (template: Document | Revision) => {
this.title = template.title; this.title = template.title;
this.isDirty = true; this.isDirty = true;
@ -141,13 +142,36 @@ class DocumentScene extends React.Component<Props> {
.replaceSelectionWith(parser.parse(template.text)) .replaceSelectionWith(parser.parse(template.text))
); );
this.props.document.templateId = template.id; if (template instanceof Document) {
this.props.document.templateId = template.id;
}
this.props.document.title = template.title; this.props.document.title = template.title;
this.props.document.text = template.text; this.props.document.text = template.text;
this.updateIsDirty(); this.updateIsDirty();
}; };
onSynced = async () => {
const { toasts, history, location, t } = this.props;
const restore = location.state?.restore;
const revisionId = location.state?.revisionId;
const editorRef = this.editor.current;
if (!editorRef || !restore) {
return;
}
const response = await client.post("/revisions.info", {
id: revisionId,
});
if (response) {
this.replaceDocument(response.data);
toasts.showToast(t("Document restored"));
history.replace(this.props.document.url);
}
};
goToMove = (ev) => { goToMove = (ev) => {
if (!this.props.readOnly) return; if (!this.props.readOnly) return;
@ -457,7 +481,7 @@ class DocumentScene extends React.Component<Props> {
savingIsDisabled={document.isSaving || this.isEmpty} savingIsDisabled={document.isSaving || this.isEmpty}
sharedTree={this.props.sharedTree} sharedTree={this.props.sharedTree}
goBack={this.goBack} goBack={this.goBack}
onSelectTemplate={this.onSelectTemplate} onSelectTemplate={this.replaceDocument}
onSave={this.onSave} onSave={this.onSave}
headings={headings} headings={headings}
/> />
@ -530,6 +554,7 @@ class DocumentScene extends React.Component<Props> {
value={readOnly ? value : undefined} value={readOnly ? value : undefined}
defaultValue={value} defaultValue={value}
disableEmbeds={disableEmbeds} disableEmbeds={disableEmbeds}
onSynced={this.onSynced}
onImageUploadStart={this.onImageUploadStart} onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop} onImageUploadStop={this.onImageUploadStop}
onSearchLink={this.props.onSearchLink} onSearchLink={this.props.onSearchLink}

View File

@ -20,9 +20,10 @@ import { homePath } from "utils/routeHelpers";
type Props = {| type Props = {|
...EditorProps, ...EditorProps,
id: string, id: string,
onSynced?: () => void,
|}; |};
function MultiplayerEditor({ ...props }: Props, ref: any) { function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
const documentId = props.id; const documentId = props.id;
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@ -154,6 +155,12 @@ function MultiplayerEditor({ ...props }: Props, ref: any) {
]; ];
}, [remoteProvider, user, ydoc]); }, [remoteProvider, user, ydoc]);
React.useEffect(() => {
if (isLocalSynced && isRemoteSynced) {
onSynced?.();
}
}, [onSynced, isLocalSynced, isRemoteSynced]);
// Disconnect the realtime connection while idle. `isIdle` also checks for // Disconnect the realtime connection while idle. `isIdle` also checks for
// page visibility and will immediately disconnect when a tab is hidden. // page visibility and will immediately disconnect when a tab is hidden.
React.useEffect(() => { React.useEffect(() => {

View File

@ -10,7 +10,7 @@
"build:webpack": "webpack --config webpack.config.prod.js", "build:webpack": "webpack --config webpack.config.prod.js",
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
"start": "node ./build/server/index.js", "start": "node ./build/server/index.js",
"dev": "yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"", "dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"",
"dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/", "dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/",
"lint": "eslint app server shared", "lint": "eslint app server shared",
"deploy": "git push heroku master", "deploy": "git push heroku master",