feat: Collaborative revision restore (#2721)
This commit is contained in:
parent
5dd5df6268
commit
b2a1e6b309
|
@ -137,7 +137,8 @@ const ListItem = styled(Item)`
|
|||
|
||||
&:nth-child(2)::before {
|
||||
height: 50%;
|
||||
top: 50%;
|
||||
top: auto;
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
&:last-child::before {
|
||||
|
|
|
@ -32,11 +32,19 @@ function RevisionMenu({ document, revisionId, className }: Props) {
|
|||
const handleRestore = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
await document.restore({ revisionId });
|
||||
showToast(t("Document restored"), { type: "success" });
|
||||
history.push(document.url);
|
||||
|
||||
if (team.collaborativeEditing) {
|
||||
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(() => {
|
||||
|
@ -57,11 +65,7 @@ function RevisionMenu({ document, revisionId, className }: Props) {
|
|||
{...menu}
|
||||
/>
|
||||
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={handleRestore}
|
||||
disabled={team.collaborativeEditing}
|
||||
>
|
||||
<MenuItem {...menu} onClick={handleRestore}>
|
||||
<MenuIconWrapper>
|
||||
<RestoreIcon />
|
||||
</MenuIconWrapper>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { type LocationWithState, type NavigationNode } from "types";
|
|||
import { NotFoundError, OfflineError } from "utils/errors";
|
||||
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||
import { isInternalUrl } from "utils/urls";
|
||||
|
||||
type Props = {|
|
||||
match: Match,
|
||||
auth: AuthStore,
|
||||
|
@ -45,6 +46,7 @@ class DataLoader extends React.Component<Props> {
|
|||
sharedTree: ?NavigationNode;
|
||||
@observable document: ?Document;
|
||||
@observable revision: ?Revision;
|
||||
@observable shapshot: ?Blob;
|
||||
@observable error: ?Error;
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -223,7 +225,8 @@ class DataLoader extends React.Component<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { location, policies, auth, ui } = this.props;
|
||||
const { location, policies, auth, match, ui } = this.props;
|
||||
const { revisionId } = match.params;
|
||||
|
||||
if (this.error) {
|
||||
return this.error instanceof OfflineError ? (
|
||||
|
@ -237,7 +240,7 @@ class DataLoader extends React.Component<Props> {
|
|||
const document = this.document;
|
||||
const revision = this.revision;
|
||||
|
||||
if (!document || !team) {
|
||||
if (!document || !team || (revisionId && !revision)) {
|
||||
return (
|
||||
<>
|
||||
<Loading location={location} />
|
||||
|
|
|
@ -37,6 +37,7 @@ import MarkAsViewed from "./MarkAsViewed";
|
|||
import PublicReferences from "./PublicReferences";
|
||||
import References from "./References";
|
||||
import { type LocationWithState, type NavigationNode, type Theme } from "types";
|
||||
import { client } from "utils/ApiClient";
|
||||
import { isCustomDomain } from "utils/domains";
|
||||
import { emojiToUrl } from "utils/emoji";
|
||||
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.isDirty = true;
|
||||
|
||||
|
@ -141,13 +142,36 @@ class DocumentScene extends React.Component<Props> {
|
|||
.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.text = template.text;
|
||||
|
||||
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) => {
|
||||
if (!this.props.readOnly) return;
|
||||
|
||||
|
@ -457,7 +481,7 @@ class DocumentScene extends React.Component<Props> {
|
|||
savingIsDisabled={document.isSaving || this.isEmpty}
|
||||
sharedTree={this.props.sharedTree}
|
||||
goBack={this.goBack}
|
||||
onSelectTemplate={this.onSelectTemplate}
|
||||
onSelectTemplate={this.replaceDocument}
|
||||
onSave={this.onSave}
|
||||
headings={headings}
|
||||
/>
|
||||
|
@ -530,6 +554,7 @@ class DocumentScene extends React.Component<Props> {
|
|||
value={readOnly ? value : undefined}
|
||||
defaultValue={value}
|
||||
disableEmbeds={disableEmbeds}
|
||||
onSynced={this.onSynced}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onSearchLink={this.props.onSearchLink}
|
||||
|
|
|
@ -20,9 +20,10 @@ import { homePath } from "utils/routeHelpers";
|
|||
type Props = {|
|
||||
...EditorProps,
|
||||
id: string,
|
||||
onSynced?: () => void,
|
||||
|};
|
||||
|
||||
function MultiplayerEditor({ ...props }: Props, ref: any) {
|
||||
function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
||||
const documentId = props.id;
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
@ -154,6 +155,12 @@ function MultiplayerEditor({ ...props }: Props, ref: any) {
|
|||
];
|
||||
}, [remoteProvider, user, ydoc]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLocalSynced && isRemoteSynced) {
|
||||
onSynced?.();
|
||||
}
|
||||
}, [onSynced, isLocalSynced, isRemoteSynced]);
|
||||
|
||||
// Disconnect the realtime connection while idle. `isIdle` also checks for
|
||||
// page visibility and will immediately disconnect when a tab is hidden.
|
||||
React.useEffect(() => {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"build:webpack": "webpack --config webpack.config.prod.js",
|
||||
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
||||
"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/",
|
||||
"lint": "eslint app server shared",
|
||||
"deploy": "git push heroku master",
|
||||
|
|
Reference in New Issue