From 35b1c54bdece076beeb5b6cb4e19d2da345177cb Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Thu, 2 May 2024 17:57:53 +0200
Subject: [PATCH] [frontend] Do optimistic update when
 approving/rejecting/suspending account (#2892)

---
 web/source/settings/lib/query/admin/index.ts | 67 +++++++++++++++-----
 web/source/settings/lib/types/account.ts     |  7 ++
 2 files changed, 59 insertions(+), 15 deletions(-)

diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts
index 3e7b1a0a0..fd3e1ca1b 100644
--- a/web/source/settings/lib/query/admin/index.ts
+++ b/web/source/settings/lib/query/admin/index.ts
@@ -20,7 +20,7 @@
 import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
 import { gtsApi } from "../gts-api";
 import { listToKeyedObject } from "../transforms";
-import { AdminAccount, HandleSignupParams, SearchAccountParams, SearchAccountResp } from "../../types/account";
+import { ActionAccountParams, AdminAccount, HandleSignupParams, SearchAccountParams, SearchAccountResp } from "../../types/account";
 import { InstanceRule, MappedRules } from "../../types/rules";
 import parse from "parse-link-header";
 
@@ -84,22 +84,19 @@ const extended = gtsApi.injectEndpoints({
 					url: `/api/v2/admin/accounts${query}`
 				};
 			},
+			// Headers required for paging.
 			transformResponse: (apiResp: AdminAccount[], meta) => {
 				const accounts = apiResp;
 				const linksStr = meta?.response?.headers.get("Link");
 				const links = parse(linksStr);
 				return { accounts, links };
 			},
-			providesTags: (res) =>
-				res
-					? [
-						...res.accounts.map(({ id }) => ({ type: 'Account' as const, id })),
-						{ type: 'Account', id: 'LIST' },
-					  ]
-					: [{ type: 'Account', id: 'LIST' }],
+			// Only provide LIST tag id since this model is not the
+			// same as getAccount model (due to transformResponse).
+			providesTags: [{ type: "Account", id: "TRANSFORMED" }]
 		}),
 
-		actionAccount: build.mutation<string, { id: string, action: string, reason: string }>({
+		actionAccount: build.mutation<string, ActionAccountParams>({
 			query: ({ id, action, reason }) => ({
 				method: "POST",
 				url: `/api/v1/admin/accounts/${id}/action`,
@@ -109,9 +106,26 @@ const extended = gtsApi.injectEndpoints({
 					text: reason
 				}
 			}),
-			invalidatesTags: (_result, _error, { id }) => [
-				{ type: 'Account', id },
-			],
+			// Do an optimistic update on this account to mark
+			// it according to whatever action was submitted.
+			async onQueryStarted({ id, action }, { dispatch, queryFulfilled }) {
+				const patchResult = dispatch(
+					extended.util.updateQueryData("getAccount", id, (draft) => {
+						if (action === "suspend") {
+							draft.suspended = true;
+							draft.account.suspended = true;
+						}
+					})
+				);
+
+				// Revert optimistic
+				// update if query fails.
+				try {
+					await queryFulfilled;
+				} catch {
+					patchResult.undo();
+				}
+			}
 		}),
 
 		handleSignup: build.mutation<AdminAccount, HandleSignupParams>({
@@ -123,9 +137,32 @@ const extended = gtsApi.injectEndpoints({
 					body: approve_or_reject === "reject" ?? formData,
 				};
 			},
-			invalidatesTags: (_result, _error, { id }) => [
-				{ type: 'Account', id },
-			],
+			// Do an optimistic update on this account to mark it approved
+			// if approved was true, else just invalidate getAccount.
+			async onQueryStarted({ id, approve_or_reject }, { dispatch, queryFulfilled }) {
+				if (approve_or_reject === "reject") {
+					// Just invalidate this ID and getAccounts.
+					dispatch(extended.util.invalidateTags([
+						{ type: "Account", id: id },
+						{ type: "Account", id: "TRANSFORMED" }
+					]));
+					return;
+				}
+				
+				const patchResult = dispatch(
+					extended.util.updateQueryData("getAccount", id, (draft) => {
+						draft.approved = true;
+					})
+				);
+
+				// Revert optimistic
+				// update if query fails.
+				try {
+					await queryFulfilled;
+				} catch {
+					patchResult.undo();
+				}
+			}
 		}),
 
 		instanceRules: build.query<MappedRules, void>({
diff --git a/web/source/settings/lib/types/account.ts b/web/source/settings/lib/types/account.ts
index db97001ac..8fd4e0356 100644
--- a/web/source/settings/lib/types/account.ts
+++ b/web/source/settings/lib/types/account.ts
@@ -63,6 +63,7 @@ export interface Account {
 	fields: [],
 	enable_rss: boolean,
 	role: any,
+	suspended?: boolean,
 }
 
 export interface SearchAccountParams {
@@ -92,3 +93,9 @@ export interface HandleSignupParams {
 	message?: string,
 	send_email?: boolean,
 }
+
+export interface ActionAccountParams {
+	id: string;
+	action: "suspend";
+	reason: string;
+}