From e44ab89ffcb3d97ecd8a0c21c507449674eed30a Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Wed, 29 Nov 2023 08:08:11 +0000 Subject: [PATCH] use useUpdateCurrentUserMutation and make it fetch current state first --- components/dashboard/src/AppNotifications.tsx | 71 ++++++++++--------- .../dashboard/src/components/AuthorizeGit.tsx | 4 +- .../src/data/current-user/update-mutation.ts | 15 +++- .../src/dedicated-setup/DedicatedSetup.tsx | 2 +- .../src/onboarding/UserOnboarding.tsx | 2 +- .../dashboard/src/user-settings/Account.tsx | 6 +- .../src/user-settings/Notifications.tsx | 13 ++-- .../src/user-settings/Preferences.tsx | 16 +++-- .../dashboard/src/user-settings/SelectIDE.tsx | 2 +- .../src/workspaces/CreateWorkspacePage.tsx | 10 +-- 10 files changed, 82 insertions(+), 59 deletions(-) diff --git a/components/dashboard/src/AppNotifications.tsx b/components/dashboard/src/AppNotifications.tsx index d6a31d54858b37..d24f2e7754c0ab 100644 --- a/components/dashboard/src/AppNotifications.tsx +++ b/components/dashboard/src/AppNotifications.tsx @@ -8,9 +8,11 @@ import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; import Alert, { AlertType } from "./components/Alert"; import { useUserLoader } from "./hooks/use-user-loader"; -import { getGitpodService } from "./service/service"; import { isGitpodIo } from "./utils"; import { trackEvent } from "./Analytics"; +import { useUpdateCurrentUserMutation } from "./data/current-user/update-mutation"; +import { User as UserProtocol } from "@gitpod/gitpod-protocol"; +import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb"; const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed"; const PRIVACY_POLICY_LAST_UPDATED = "2023-10-17"; @@ -23,41 +25,44 @@ interface Notification { onClose?: () => void; } -const UPDATED_PRIVACY_POLICY: Notification = { - id: "privacy-policy-update", - type: "info", - preventDismiss: true, - onClose: async () => { - let dismissSuccess = false; - try { - const updatedUser = await getGitpodService().server.updateLoggedInUser({ - additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } }, - }); - dismissSuccess = !!updatedUser; - } catch (err) { - console.error("Failed to update user's privacy policy acceptance date", err); - dismissSuccess = false; - } finally { - trackEvent("privacy_policy_update_accepted", { - path: window.location.pathname, - success: dismissSuccess, - }); - } - }, - message: ( - - We've updated our Privacy Policy. You can review it{" "} - - here - - . - - ), +const UPDATED_PRIVACY_POLICY = (updateUser: (user: Partial) => Promise) => { + return { + id: "privacy-policy-update", + type: "info", + preventDismiss: true, + onClose: async () => { + let dismissSuccess = false; + try { + const updatedUser = await updateUser({ + additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } }, + }); + dismissSuccess = !!updatedUser; + } catch (err) { + console.error("Failed to update user's privacy policy acceptance date", err); + dismissSuccess = false; + } finally { + trackEvent("privacy_policy_update_accepted", { + path: window.location.pathname, + success: dismissSuccess, + }); + } + }, + message: ( + + We've updated our Privacy Policy. You can review it{" "} + + here + + . + + ), + } as Notification; }; export function AppNotifications() { const [topNotification, setTopNotification] = useState(undefined); const { user, loading } = useUserLoader(); + const updateUser = useUpdateCurrentUserMutation(); useEffect(() => { const notifications = []; @@ -66,14 +71,14 @@ export function AppNotifications() { !user?.profile?.acceptedPrivacyPolicyDate || new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.profile.acceptedPrivacyPolicyDate) ) { - notifications.push(UPDATED_PRIVACY_POLICY); + notifications.push(UPDATED_PRIVACY_POLICY((u: Partial) => updateUser.mutateAsync(u))); } } const dismissedNotifications = getDismissedNotifications(); const topNotification = notifications.find((n) => !dismissedNotifications.includes(n.id)); setTopNotification(topNotification); - }, [loading, setTopNotification, user]); + }, [loading, updateUser, setTopNotification, user]); const dismissNotification = useCallback(() => { if (!topNotification) { diff --git a/components/dashboard/src/components/AuthorizeGit.tsx b/components/dashboard/src/components/AuthorizeGit.tsx index 9a65b4d0f9ccb5..606a9437cc7347 100644 --- a/components/dashboard/src/components/AuthorizeGit.tsx +++ b/components/dashboard/src/components/AuthorizeGit.tsx @@ -29,8 +29,8 @@ export const AuthorizeGit: FC<{ className?: string }> = ({ className }) => { const { refetch: reloadUser } = useAuthenticatedUser(); const owner = useIsOwner(); const { data: authProviders } = useAuthProviderDescriptions(); - const updateUser = useCallback(() => { - reloadUser(); + const updateUser = useCallback(async () => { + await reloadUser(); }, [reloadUser]); const connect = useCallback( diff --git a/components/dashboard/src/data/current-user/update-mutation.ts b/components/dashboard/src/data/current-user/update-mutation.ts index 6990ed01959b46..43b5ab798bc1ad 100644 --- a/components/dashboard/src/data/current-user/update-mutation.ts +++ b/components/dashboard/src/data/current-user/update-mutation.ts @@ -4,19 +4,28 @@ * See License.AGPL.txt in the project root for license information. */ -import { User } from "@gitpod/gitpod-protocol"; +import { User as UserProtocol } from "@gitpod/gitpod-protocol"; import { useMutation } from "@tanstack/react-query"; import { trackEvent } from "../../Analytics"; import { getGitpodService } from "../../service/service"; import { useAuthenticatedUser } from "./authenticated-user-query"; import { converter } from "../../service/public-api"; -type UpdateCurrentUserArgs = Partial; +type UpdateCurrentUserArgs = Partial; export const useUpdateCurrentUserMutation = () => { return useMutation({ mutationFn: async (partialUser: UpdateCurrentUserArgs) => { - const user = await getGitpodService().server.updateLoggedInUser(partialUser); + const current = await getGitpodService().server.getLoggedInUser(); + const update: UpdateCurrentUserArgs = { + id: current.id, + fullName: partialUser.fullName || current.fullName, + additionalData: { + ...current.additionalData, + ...partialUser.additionalData, + }, + }; + const user = await getGitpodService().server.updateLoggedInUser(update); return converter.toUser(user); }, }); diff --git a/components/dashboard/src/dedicated-setup/DedicatedSetup.tsx b/components/dashboard/src/dedicated-setup/DedicatedSetup.tsx index 8b3dd7c8fe6b9e..fc66fa3b137bc5 100644 --- a/components/dashboard/src/dedicated-setup/DedicatedSetup.tsx +++ b/components/dashboard/src/dedicated-setup/DedicatedSetup.tsx @@ -97,7 +97,7 @@ const DedicatedSetupSteps: FC = ({ org, ssoConfig, onC const updateUser = useCallback(async () => { await getGitpodService().reconnect(); - reloadUser(); + await reloadUser(); }, [reloadUser]); const handleEndSetup = useCallback(async () => { diff --git a/components/dashboard/src/onboarding/UserOnboarding.tsx b/components/dashboard/src/onboarding/UserOnboarding.tsx index 44eb1a0a1dda86..b29a4cf04adaf4 100644 --- a/components/dashboard/src/onboarding/UserOnboarding.tsx +++ b/components/dashboard/src/onboarding/UserOnboarding.tsx @@ -82,7 +82,7 @@ const UserOnboarding: FunctionComponent = ({ user }) => { }); dropConfetti(); - reloadUser(); + await reloadUser(); // Look for the `onboarding=force` query param, and remove if present const queryParams = new URLSearchParams(location.search); diff --git a/components/dashboard/src/user-settings/Account.tsx b/components/dashboard/src/user-settings/Account.tsx index c1691dd4eb7adc..7edc59e1039680 100644 --- a/components/dashboard/src/user-settings/Account.tsx +++ b/components/dashboard/src/user-settings/Account.tsx @@ -20,6 +20,7 @@ import { useAuthenticatedUser } from "../data/current-user/authenticated-user-qu import { getPrimaryEmail, getProfile, isOrganizationOwned } from "@gitpod/gitpod-protocol/lib/public-api-utils"; import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb"; import { User as UserProtocol } from "@gitpod/gitpod-protocol"; +import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; export default function Account() { const { data: user, refetch: reloadUser } = useAuthenticatedUser(); @@ -30,6 +31,7 @@ export default function Account() { const [errorMessage, setErrorMessage] = useState(""); const canUpdateEmail = user && !isOrganizationOwned(user); const { toast } = useToast(); + const updateUser = useUpdateCurrentUserMutation(); const saveProfileState = useCallback(async () => { if (!user || !profileState) { @@ -54,14 +56,14 @@ export default function Account() { profileState.email = getPrimaryEmail(user) || ""; } - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { profile: profileState, }, }); reloadUser(); toast("Your profile information has been updated."); - }, [canUpdateEmail, profileState, reloadUser, toast, user]); + }, [updateUser, canUpdateEmail, profileState, reloadUser, toast, user]); const deleteAccount = useCallback(async () => { await getGitpodService().server.deleteAccount(); diff --git a/components/dashboard/src/user-settings/Notifications.tsx b/components/dashboard/src/user-settings/Notifications.tsx index 61f7c5bc907b98..f70c9b886c57f0 100644 --- a/components/dashboard/src/user-settings/Notifications.tsx +++ b/components/dashboard/src/user-settings/Notifications.tsx @@ -5,31 +5,32 @@ */ import { useState } from "react"; -import { getGitpodService } from "../service/service"; import { CheckboxInputField } from "../components/forms/CheckboxInputField"; import { identifyUser } from "../Analytics"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; import { Heading2 } from "../components/typography/headings"; import { useAuthenticatedUser } from "../data/current-user/authenticated-user-query"; +import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; export default function Notifications() { const { data: user, refetch: reloadUser } = useAuthenticatedUser(); const [isOnboardingMail, setOnboardingMail] = useState(!!user?.emailNotificationSettings?.allowsOnboardingMail); const [isChangelogMail, setChangelogMail] = useState(!!user?.emailNotificationSettings?.allowsChangelogMail); const [isDevXMail, setDevXMail] = useState(!!user?.emailNotificationSettings?.allowsDevxMail); + const updateUser = useUpdateCurrentUserMutation(); const toggleOnboardingMail = async () => { if (user && user.emailNotificationSettings) { const newIsOnboardingMail = !isOnboardingMail; user.emailNotificationSettings.allowsOnboardingMail = newIsOnboardingMail; - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { emailNotificationSettings: { allowsOnboardingMail: newIsOnboardingMail, }, }, }); - reloadUser(); + await reloadUser(); identifyUser({ unsubscribed_onboarding: !newIsOnboardingMail }); setOnboardingMail(newIsOnboardingMail); } @@ -39,14 +40,14 @@ export default function Notifications() { if (user && user.emailNotificationSettings) { const newIsChangelogMail = !isChangelogMail; user.emailNotificationSettings.allowsChangelogMail = newIsChangelogMail; - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { emailNotificationSettings: { allowsChangelogMail: newIsChangelogMail, }, }, }); - reloadUser(); + await reloadUser(); identifyUser({ unsubscribed_changelog: !newIsChangelogMail }); setChangelogMail(newIsChangelogMail); } @@ -56,7 +57,7 @@ export default function Notifications() { if (user && user.emailNotificationSettings) { const newIsDevXMail = !isDevXMail; user.emailNotificationSettings.allowsDevxMail = newIsDevXMail; - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { emailNotificationSettings: { allowsDevXMail: newIsDevXMail, diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx index 38fb9c8781e096..32bfa6fb0200b4 100644 --- a/components/dashboard/src/user-settings/Preferences.tsx +++ b/components/dashboard/src/user-settings/Preferences.tsx @@ -15,7 +15,10 @@ import SelectIDE from "./SelectIDE"; import { InputField } from "../components/forms/InputField"; import { TextInput } from "../components/forms/TextInputField"; import { useToast } from "../components/toasts/Toasts"; -import { useUpdateCurrentUserDotfileRepoMutation } from "../data/current-user/update-mutation"; +import { + useUpdateCurrentUserDotfileRepoMutation, + useUpdateCurrentUserMutation, +} from "../data/current-user/update-mutation"; import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query"; import { useAuthenticatedUser } from "../data/current-user/authenticated-user-query"; import { converter } from "../service/public-api"; @@ -25,6 +28,7 @@ export type IDEChangedTrackLocation = "workspace_list" | "workspace_start" | "pr export default function Preferences() { const { toast } = useToast(); const { data: user, refetch: reloadUser } = useAuthenticatedUser(); + const updateUser = useUpdateCurrentUserMutation(); const billingMode = useOrgBillingMode(); const updateDotfileRepo = useUpdateCurrentUserDotfileRepoMutation(); @@ -41,7 +45,7 @@ export default function Preferences() { e.preventDefault(); await updateDotfileRepo.mutateAsync(dotfileRepo); - reloadUser(); + await reloadUser(); toast("Your dotfiles repository was updated."); }, [updateDotfileRepo, dotfileRepo, reloadUser, toast], @@ -57,7 +61,7 @@ export default function Preferences() { await getGitpodService().server.updateWorkspaceTimeoutSetting({ workspaceTimeout: workspaceTimeout }); // TODO: Once current user is in react-query, we can instead invalidate the query vs. refetching here - reloadUser(); + await reloadUser(); let toastMessage = <>Default workspace timeout was updated.; if (billingMode.data?.mode === "usage-based") { @@ -89,14 +93,14 @@ export default function Preferences() { if (!user) { return; } - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { workspaceAutostartOptions: [], }, }); - reloadUser(); + await reloadUser(); toast("Workspace options have been cleared."); - }, [reloadUser, toast, user]); + }, [updateUser, reloadUser, toast, user]); return (
diff --git a/components/dashboard/src/user-settings/SelectIDE.tsx b/components/dashboard/src/user-settings/SelectIDE.tsx index 9e8e5cd9dda6af..730ac3049ccc4b 100644 --- a/components/dashboard/src/user-settings/SelectIDE.tsx +++ b/components/dashboard/src/user-settings/SelectIDE.tsx @@ -35,7 +35,7 @@ export default function SelectIDE(props: SelectIDEProps) { }, }; await updateUser.mutateAsync(updates); - reloadUser(); + await reloadUser(); }, [reloadUser, updateUser], ); diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index eb698ebfdd45f5..e0d19874625fec 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -28,7 +28,7 @@ import { useListWorkspacesQuery } from "../data/workspaces/list-workspaces-query import { useWorkspaceContext } from "../data/workspaces/resolve-context-query"; import { useDirtyState } from "../hooks/use-dirty-state"; import { openAuthorizeWindow } from "../provider-utils"; -import { getGitpodService, gitpodHostUrl } from "../service/service"; +import { gitpodHostUrl } from "../service/service"; import { StartWorkspaceError } from "../start/StartPage"; import { VerifyModal } from "../start/VerifyModal"; import { StartWorkspaceOptions } from "../start/start-workspace-options"; @@ -45,9 +45,11 @@ import { useAuthenticatedUser } from "../data/current-user/authenticated-user-qu import { User_WorkspaceAutostartOption } from "@gitpod/public-api/lib/gitpod/v1/user_pb"; import { EditorReference } from "@gitpod/public-api/lib/gitpod/v1/editor_pb"; import { converter } from "../service/public-api"; +import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; export function CreateWorkspacePage() { const { data: user, refetch: reloadUser } = useAuthenticatedUser(); + const updateUser = useUpdateCurrentUserMutation(); const currentOrg = useCurrentOrg().data; const projects = useListAllProjectsQuery(); const workspaces = useListWorkspacesQuery({ limit: 50 }); @@ -106,15 +108,15 @@ export function CreateWorkspacePage() { }), }), ); - await getGitpodService().server.updateLoggedInUser({ + await updateUser.mutateAsync({ additionalData: { workspaceAutostartOptions: workspaceAutoStartOptions.map((o) => converter.fromWorkspaceAutostartOption(o), ), }, }); - reloadUser(); - }, [currentOrg, selectedIde, selectedWsClass, reloadUser, useLatestIde, user, workspaceContext.data]); + await reloadUser(); + }, [updateUser, currentOrg, selectedIde, selectedWsClass, reloadUser, useLatestIde, user, workspaceContext.data]); // see if we have a matching project based on context url and project's repo url const project = useMemo(() => {