From 32adf839491fa03a6e23edeb751909c0149e2bc7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 12 Nov 2024 11:03:31 +0100 Subject: [PATCH] Switch to Tanstack Query --- biome.json | 11 ++- frontend/.storybook/preview.tsx | 4 +- frontend/codegen.ts | 1 + frontend/package-lock.json | 83 +++++++++++++++-- frontend/package.json | 3 + frontend/src/components/BrowserSession.tsx | 28 ++++-- frontend/src/components/CompatSession.tsx | 24 +++-- frontend/src/components/Layout/Layout.tsx | 12 ++- frontend/src/components/Layout/index.ts | 2 +- frontend/src/components/OAuth2Session.tsx | 21 +++-- .../Session/DeviceTypeIcon.stories.tsx | 2 - .../SessionDetail/CompatSessionDetail.tsx | 20 +++-- .../SessionDetail/OAuth2SessionDetail.tsx | 21 +++-- .../src/components/UserEmail/UserEmail.tsx | 41 +++++---- .../components/UserGreeting/UserGreeting.tsx | 40 +++++---- .../components/UserProfile/AddEmailForm.tsx | 44 ++++++---- .../components/UserProfile/UserEmailList.tsx | 27 +++--- .../components/VerifyEmail/VerifyEmail.tsx | 53 ++++++----- frontend/src/gql/gql.ts | 8 +- frontend/src/gql/graphql.ts | 11 +-- frontend/src/graphql.ts | 30 +------ frontend/src/main.tsx | 19 ++-- frontend/src/pagination.ts | 12 +-- frontend/src/routes/__root.tsx | 20 +++-- frontend/src/routes/_account.index.lazy.tsx | 22 +++-- frontend/src/routes/_account.index.tsx | 21 +++-- frontend/src/routes/_account.lazy.tsx | 14 ++- .../src/routes/_account.sessions.$id.lazy.tsx | 13 ++- frontend/src/routes/_account.sessions.$id.tsx | 24 ++--- .../_account.sessions.browsers.lazy.tsx | 29 +++--- .../src/routes/_account.sessions.browsers.tsx | 45 +++++----- .../routes/_account.sessions.index.lazy.tsx | 31 +++---- .../src/routes/_account.sessions.index.tsx | 63 +++++++------ frontend/src/routes/_account.tsx | 24 +++-- frontend/src/routes/clients.$id.lazy.tsx | 19 ++-- frontend/src/routes/clients.$id.tsx | 24 ++--- frontend/src/routes/devices.$.tsx | 56 +++++++----- .../src/routes/emails.$id.verify.lazy.tsx | 15 ++-- frontend/src/routes/emails.$id.verify.tsx | 26 +++--- .../src/routes/password.change.index.lazy.tsx | 88 ++++++++++--------- frontend/src/routes/password.change.index.tsx | 23 +++-- .../routes/password.recovery.index.lazy.tsx | 79 +++++++++-------- .../src/routes/password.recovery.index.tsx | 18 ++-- .../src/routes/reset-cross-signing.index.tsx | 79 ++++++++++------- 44 files changed, 715 insertions(+), 535 deletions(-) diff --git a/biome.json b/biome.json index 22be272e0..d1021d150 100644 --- a/biome.json +++ b/biome.json @@ -17,19 +17,24 @@ "crates/**", "frontend/src/gql/**", "frontend/src/routeTree.gen.ts", + "frontend/.storybook/locales.ts", + "frontend/locales/*.json", "**/coverage/**", "**/dist/**" ] }, "formatter": { "enabled": true, - "useEditorconfig": true, - "ignore": ["frontend/.storybook/locales.ts", "frontend/locales/*.json"] + "useEditorconfig": true }, "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" + } } } } diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index fd824edea..0c188c877 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -74,7 +74,7 @@ const withThemeProvider: Decorator = (Story, context) => { ); }; -const withDummyRouter: Decorator = (Story, context) => { +const withDummyRouter: Decorator = (Story, _context) => { return ( @@ -82,7 +82,7 @@ const withDummyRouter: Decorator = (Story, context) => { ); }; -const withTooltipProvider: Decorator = (Story, context) => { +const withTooltipProvider: Decorator = (Story, _context) => { return ( diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 78b8ff235..0c56ca77d 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -18,6 +18,7 @@ const config: CodegenConfig = { enumsAsTypes: true, // By default, unknown scalars are generated as `any`. This is not ideal for catching potential bugs. defaultScalarType: "unknown", + maybeValue: "T | null | undefined", scalars: { DateTime: "string", Url: "string", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a1eb9dc0a..f7a40ddc3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@fontsource/inter": "^5.1.0", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", + "@tanstack/react-query": "^5.59.20", "@tanstack/react-router": "^1.81.5", "@tanstack/router-zod-adapter": "^1.81.5", "@urql/core": "^5.0.8", @@ -25,6 +26,7 @@ "classnames": "^2.5.1", "date-fns": "^4.1.0", "graphql": "^16.9.0", + "graphql-request": "^7.1.2", "i18next": "^23.16.5", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -47,6 +49,7 @@ "@storybook/addon-essentials": "^8.4.3", "@storybook/react": "^8.4.3", "@storybook/react-vite": "^8.4.3", + "@tanstack/react-query-devtools": "^5.59.20", "@tanstack/router-devtools": "^1.81.5", "@tanstack/router-vite-plugin": "^1.79.0", "@testing-library/react": "^16.0.1", @@ -2566,6 +2569,20 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@graphql-tools/prisma-loader/node_modules/graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", + "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "cross-fetch": "^3.1.5" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "node_modules/@graphql-tools/relay-operation-optimizer": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.2.tgz", @@ -2673,7 +2690,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "dev": true, "license": "MIT", "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" @@ -5080,6 +5096,61 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-core": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.20.tgz", + "integrity": "sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.59.20.tgz", + "integrity": "sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.20.tgz", + "integrity": "sha512-Zly0egsK0tFdfSbh5/mapSa+Zfc3Et0Zkar7Wo5sQkFzWyB3p3uZWOHR2wrlAEEV2L953eLuDBtbgFvMYiLvUw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.20.tgz", + "integrity": "sha512-AL/eQS1NFZhwwzq2Bq9Gd8wTTH+XhPNOJlDFpzPMu9NC5CQVgA0J8lWrte/sXpdWNo5KA4hgHnEdImZsF4h6Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.59.20", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-router": { "version": "1.81.5", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.81.5.tgz", @@ -8494,14 +8565,12 @@ } }, "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "dev": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.1.2.tgz", + "integrity": "sha512-+XE3iuC55C2di5ZUrB4pjgwe+nIQBuXVIK9J98wrVwojzDW3GMdSBZfxUk8l4j9TieIpjpggclxhNEU9ebGF8w==", "license": "MIT", "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" + "@graphql-typed-document-node/core": "^3.2.0" }, "peerDependencies": { "graphql": "14 - 16" diff --git a/frontend/package.json b/frontend/package.json index 4431ec29e..41c55843a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@fontsource/inter": "^5.1.0", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", + "@tanstack/react-query": "^5.59.20", "@tanstack/react-router": "^1.81.5", "@tanstack/router-zod-adapter": "^1.81.5", "@urql/core": "^5.0.8", @@ -34,6 +35,7 @@ "classnames": "^2.5.1", "date-fns": "^4.1.0", "graphql": "^16.9.0", + "graphql-request": "^7.1.2", "i18next": "^23.16.5", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -56,6 +58,7 @@ "@storybook/addon-essentials": "^8.4.3", "@storybook/react": "^8.4.3", "@storybook/react-vite": "^8.4.3", + "@tanstack/react-query-devtools": "^5.59.20", "@tanstack/router-devtools": "^1.81.5", "@tanstack/router-vite-plugin": "^1.79.0", "@testing-library/react": "^16.0.1", diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index c2ca4d2f8..ceecddb28 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -11,11 +11,11 @@ import { Badge } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; import { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; import { type FragmentType, graphql, useFragment } from "../gql"; -import type { DeviceType } from "../gql/graphql"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { graphqlClient } from "../graphql"; import DateTime from "./DateTime"; import EndSessionButton from "./Session/EndSessionButton"; import LastActive from "./Session/LastActive"; @@ -58,14 +58,26 @@ export const useEndBrowserSession = ( sessionId: string, isCurrent: boolean, ): (() => Promise) => { - const [, endSession] = useMutation(END_SESSION_MUTATION); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(END_SESSION_MUTATION, { id }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["browserSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id], + }); + + if (isCurrent) { + window.location.reload(); + } + }, + }); const onSessionEnd = useCallback(async (): Promise => { - await endSession({ id: sessionId }); - if (isCurrent) { - window.location.reload(); - } - }, [isCurrent, endSession, sessionId]); + await endSession.mutateAsync(sessionId); + }, [endSession.mutateAsync, sessionId]); return onSessionEnd; }; diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx index 136ffb0db..cca02d90d 100644 --- a/frontend/src/components/CompatSession.tsx +++ b/frontend/src/components/CompatSession.tsx @@ -4,13 +4,11 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - import { type FragmentType, graphql, useFragment } from "../gql"; -import { DeviceType } from "../gql/graphql"; - +import { graphqlClient } from "../graphql"; import { browserLogoUri } from "./BrowserSession"; import DateTime from "./DateTime"; import EndSessionButton from "./Session/EndSessionButton"; @@ -44,7 +42,6 @@ export const END_SESSION_MUTATION = graphql(/* GraphQL */ ` status compatSession { id - finishedAt } } } @@ -54,7 +51,7 @@ export const simplifyUrl = (url: string): string => { let parsed: URL; try { parsed = new URL(url); - } catch (e) { + } catch (_e) { // Not a valid URL, return the original return url; } @@ -76,10 +73,21 @@ const CompatSession: React.FC<{ }> = ({ session }) => { const { t } = useTranslation(); const data = useFragment(FRAGMENT, session); - const [, endCompatSession] = useMutation(END_SESSION_MUTATION); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(END_SESSION_MUTATION, { id }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id], + }); + }, + }); const onSessionEnd = async (): Promise => { - await endCompatSession({ id: data.id }); + await endSession.mutateAsync(data.id); }; const clientName = data.ssoLogin?.redirectUri diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx index cf9316f65..4f60a2439 100644 --- a/frontend/src/components/Layout/Layout.tsx +++ b/frontend/src/components/Layout/Layout.tsx @@ -6,11 +6,12 @@ import cx from "classnames"; import { Suspense } from "react"; -import { useQuery } from "urql"; import { graphql } from "../../gql"; import Footer from "../Footer"; +import { queryOptions, useQuery } from "@tanstack/react-query"; +import { graphqlClient } from "../../graphql"; import styles from "./Layout.module.css"; const QUERY = graphql(/* GraphQL */ ` @@ -22,10 +23,13 @@ const QUERY = graphql(/* GraphQL */ ` } `); +export const query = queryOptions({ + queryKey: ["footer"], + queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }), +}); + const AsyncFooter: React.FC = () => { - const [result] = useQuery({ - query: QUERY, - }); + const result = useQuery(query); if (result.error) { // We probably prefer to render an empty footer in case of an error diff --git a/frontend/src/components/Layout/index.ts b/frontend/src/components/Layout/index.ts index 9fff7e5a3..303e6cf06 100644 --- a/frontend/src/components/Layout/index.ts +++ b/frontend/src/components/Layout/index.ts @@ -4,4 +4,4 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -export { default } from "./Layout"; +export { default, query } from "./Layout"; diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index 2b771033b..9f2d171dd 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -4,14 +4,13 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - import { type FragmentType, graphql, useFragment } from "../gql"; import type { DeviceType, Oauth2ApplicationType } from "../gql/graphql"; +import { graphqlClient } from "../graphql"; import { getDeviceIdFromScope } from "../utils/deviceIdFromScope"; - import DateTime from "./DateTime"; import EndSessionButton from "./Session/EndSessionButton"; import LastActive from "./Session/LastActive"; @@ -49,7 +48,6 @@ export const END_SESSION_MUTATION = graphql(/* GraphQL */ ` status oauth2Session { id - ...OAuth2Session_session } } } @@ -74,10 +72,21 @@ type Props = { const OAuth2Session: React.FC = ({ session }) => { const { t } = useTranslation(); const data = useFragment(FRAGMENT, session); - const [, endSession] = useMutation(END_SESSION_MUTATION); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(END_SESSION_MUTATION, { id }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id], + }); + }, + }); const onSessionEnd = async (): Promise => { - await endSession({ id: data.id }); + await endSession.mutateAsync(data.id); }; const deviceId = getDeviceIdFromScope(data.scope); diff --git a/frontend/src/components/Session/DeviceTypeIcon.stories.tsx b/frontend/src/components/Session/DeviceTypeIcon.stories.tsx index 19b9c13c5..bae7ec783 100644 --- a/frontend/src/components/Session/DeviceTypeIcon.stories.tsx +++ b/frontend/src/components/Session/DeviceTypeIcon.stories.tsx @@ -6,8 +6,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { DeviceType } from "../../gql/graphql"; - import DeviceTypeIcon from "./DeviceTypeIcon"; const meta = { diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 5b3a38fca..b698380d9 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -4,17 +4,16 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlClient } from "../../graphql"; import BlockList from "../BlockList/BlockList"; import { END_SESSION_MUTATION, simplifyUrl } from "../CompatSession"; import DateTime from "../DateTime"; import ExternalLink from "../ExternalLink/ExternalLink"; import EndSessionButton from "../Session/EndSessionButton"; - import SessionDetails from "./SessionDetails"; import SessionHeader from "./SessionHeader"; @@ -44,11 +43,22 @@ type Props = { const CompatSessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); - const [, endSession] = useMutation(END_SESSION_MUTATION); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(END_SESSION_MUTATION, { id }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id], + }); + }, + }); const { t } = useTranslation(); const onSessionEnd = async (): Promise => { - await endSession({ id: data.id }); + await endSession.mutateAsync(data.id); }; const finishedAt = data.finishedAt diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 9b467796c..b1df1ba6b 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -4,11 +4,11 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlClient } from "../../graphql"; import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope"; import BlockList from "../BlockList/BlockList"; import DateTime from "../DateTime"; @@ -16,7 +16,6 @@ import { Link } from "../Link"; import { END_SESSION_MUTATION } from "../OAuth2Session"; import ClientAvatar from "../Session/ClientAvatar"; import EndSessionButton from "../Session/EndSessionButton"; - import SessionDetails from "./SessionDetails"; import SessionHeader from "./SessionHeader"; @@ -44,11 +43,23 @@ type Props = { const OAuth2SessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); - const [, endSession] = useMutation(END_SESSION_MUTATION); + const queryClient = useQueryClient(); + const endSession = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(END_SESSION_MUTATION, { id }), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + queryClient.invalidateQueries({ queryKey: ["appSessionList"] }); + queryClient.invalidateQueries({ + queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id], + }); + }, + }); + const { t } = useTranslation(); const onSessionEnd = async (): Promise => { - await endSession({ id: data.id }); + await endSession.mutateAsync(data.id); }; const deviceId = getDeviceIdFromScope(data.scope); diff --git a/frontend/src/components/UserEmail/UserEmail.tsx b/frontend/src/components/UserEmail/UserEmail.tsx index 8997103de..c6bb2d299 100644 --- a/frontend/src/components/UserEmail/UserEmail.tsx +++ b/frontend/src/components/UserEmail/UserEmail.tsx @@ -9,12 +9,12 @@ import IconEmail from "@vector-im/compound-design-tokens/assets/web/icons/email" import { Button, Form, IconButton, Tooltip } from "@vector-im/compound-web"; import type { ComponentProps, ReactNode } from "react"; import { Translation, useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - import { type FragmentType, graphql, useFragment } from "../../gql"; import { Close, Description, Dialog, Title } from "../Dialog"; import { Link } from "../Link"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { graphqlClient } from "../../graphql"; import styles from "./UserEmail.module.css"; // This component shows a single user email address, with controls to verify it, @@ -132,24 +132,33 @@ const UserEmail: React.FC<{ const { t } = useTranslation(); const data = useFragment(FRAGMENT, email); const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig); + const queryClient = useQueryClient(); - const [setPrimaryResult, setPrimary] = useMutation( - SET_PRIMARY_EMAIL_MUTATION, - ); - const [removeResult, removeEmail] = useMutation(REMOVE_EMAIL_MUTATION); - // Handle errors with the error boundary - if (setPrimaryResult.error) throw setPrimaryResult.error; - if (removeResult.error) throw removeResult.error; + const setPrimary = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(SET_PRIMARY_EMAIL_MUTATION, { id }), + onSuccess: (_data) => { + queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] }); + queryClient.invalidateQueries({ queryKey: ["userEmails"] }); + }, + }); + + const removeEmail = useMutation({ + mutationFn: (id: string) => + graphqlClient.request(REMOVE_EMAIL_MUTATION, { id }), + onSuccess: (_data) => { + onRemove?.(); + queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] }); + queryClient.invalidateQueries({ queryKey: ["userEmails"] }); + }, + }); const onRemoveClick = (): void => { - removeEmail({ id: data.id }).then(() => { - // Call the onRemove callback if provided - onRemove?.(); - }); + removeEmail.mutate(data.id); }; const onSetPrimaryClick = (): void => { - setPrimary({ id: data.id }); + setPrimary.mutate(data.id); }; return ( @@ -171,7 +180,7 @@ const UserEmail: React.FC<{ {!isPrimary && emailChangeAllowed && ( )} @@ -188,7 +197,7 @@ const UserEmail: React.FC<{