fix: 'shared with me' optimistic updates (#10547)

* optimistic state updates when documents under 'shared with me' section are created

* optimistic updates for other 'shared with me' document actions

* update top level document

* use action decorator
This commit is contained in:
Salihu
2025-11-08 17:47:38 +01:00
committed by GitHub
parent 636153a56b
commit 265f2721f8
6 changed files with 90 additions and 10 deletions
@@ -302,6 +302,7 @@ function InnerDocumentLink(
{ publish: true }
);
collection?.addDocument(newDocument, node.id);
membership?.addDocument(newDocument, node.id);
closeAddingNewChild();
history.push({
-4
View File
@@ -20,9 +20,6 @@ class GroupMembership extends NavigableModel {
@Relation(() => Group, { onDelete: "cascade" })
group: Group;
/** The document ID that this membership grants the group access to. */
documentId: string | undefined;
/** The document that this membership grants the group access to. */
@Relation(() => Document, { onDelete: "cascade" })
document: Document | undefined;
@@ -46,7 +43,6 @@ class GroupMembership extends NavigableModel {
permission: CollectionPermission | DocumentPermission;
// methods
/**
* Fetches the child documents structure from the server.
*/
-3
View File
@@ -20,9 +20,6 @@ class UserMembership extends NavigableModel {
@observable
permission: DocumentPermission;
/** The document ID that this membership grants the user access to. */
documentId?: string;
/** The document that this membership grants the user access to. */
@Relation(() => Document, { onDelete: "cascade" })
document?: Document;
+68 -1
View File
@@ -1,11 +1,15 @@
import { computed, observable, runInAction } from "mobx";
import { action, computed, observable, runInAction } from "mobx";
import { JSONObject, type NavigationNode } from "@shared/types";
import { client } from "~/utils/ApiClient";
import ParanoidModel from "./ParanoidModel";
import type Document from "../Document";
export default abstract class NavigableModel extends ParanoidModel {
private isFetching = false;
/** The document ID associated with this model. */
documentId?: string;
@observable
node?: NavigationNode;
@@ -45,6 +49,13 @@ export default abstract class NavigableModel extends ParanoidModel {
return this.node?.children;
}
@action
setDocuments(value: NavigationNode[] | undefined) {
if (this.node && value) {
this.node.children = value;
}
}
/**
* Returns the document path from the original document shared with this membership.
*/
@@ -109,4 +120,60 @@ export default abstract class NavigableModel extends ParanoidModel {
return result;
}
/**
* Adds the document identified by the given id to the model in
* memory. Does not add the document to the database or store.
*
* @param document The document to add.
* @param parentDocumentId The id of the document to add the new document to.
*/
@action
addDocument(document: Document, parentDocumentId: string) {
if (!this.documents || !document || !parentDocumentId?.trim()) {
return;
}
if (this.documentId && parentDocumentId === this.documentId) {
this.setDocuments([document.asNavigationNode, ...(this.documents ?? [])]);
}
const travelNodes = (nodes: NavigationNode[]) =>
nodes.forEach((node) => {
if (node.id === parentDocumentId) {
node.children = [document.asNavigationNode, ...(node.children ?? [])];
} else {
travelNodes(node.children);
}
});
travelNodes(this.documents);
}
/**
* Removes the document identified by the given id from the model in
* memory. Does not remove the document from the database.
*
* @param documentId The id of the document to remove.
*/
@action
removeDocument(documentId: string) {
if (!this.documents) {
return;
}
this.setDocuments(
this.documents.filter(function f(node): boolean {
if (node.id === documentId) {
return false;
}
if (node.children) {
node.children = node.children.filter(f);
}
return true;
})
);
}
}
+9 -1
View File
@@ -21,7 +21,8 @@ type Props = {
function DocumentDelete({ document, onSubmit }: Props) {
const { t } = useTranslation();
const { ui, documents, collections } = useStores();
const { ui, documents, collections, userMemberships, groupMemberships } =
useStores();
const history = useHistory();
const [isDeleting, setDeleting] = React.useState(false);
const [isArchiving, setArchiving] = React.useState(false);
@@ -41,6 +42,13 @@ function DocumentDelete({ document, onSubmit }: Props) {
try {
await document.delete();
userMemberships
.getByDocumentId(document.id)
?.removeDocument(document.id);
groupMemberships
.getByDocumentId(document.id)
?.removeDocument(document.id);
// only redirect if we're currently viewing the document that's deleted
if (ui.activeDocumentId === document.id) {
// If the document has a parent and it's available in the store then
+12 -1
View File
@@ -25,7 +25,8 @@ function DocumentNew({ template }: Props) {
const user = useCurrentUser();
const match = useRouteMatch<{ id?: string }>();
const { t } = useTranslation();
const { documents, collections } = useStores();
const { documents, collections, userMemberships, groupMemberships } =
useStores();
const id = match.params.id || query.get("collectionId");
useEffect(() => {
@@ -55,6 +56,16 @@ function DocumentNew({ template }: Props) {
{ publish: collection?.id || parentDocumentId ? true : undefined }
);
if (parentDocumentId) {
userMemberships
.getByDocumentId(document.id)
?.addDocument(document, parentDocumentId);
groupMemberships
.getByDocumentId(document.id)
?.addDocument(document, parentDocumentId);
}
history.replace(
template || !user.separateEditMode
? documentPath(document)