Switch to Tanstack Query
This commit is contained in:
11
biome.json
11
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ const withThemeProvider: Decorator = (Story, context) => {
|
||||
);
|
||||
};
|
||||
|
||||
const withDummyRouter: Decorator = (Story, context) => {
|
||||
const withDummyRouter: Decorator = (Story, _context) => {
|
||||
return (
|
||||
<DummyRouter>
|
||||
<Story />
|
||||
@@ -82,7 +82,7 @@ const withDummyRouter: Decorator = (Story, context) => {
|
||||
);
|
||||
};
|
||||
|
||||
const withTooltipProvider: Decorator = (Story, context) => {
|
||||
const withTooltipProvider: Decorator = (Story, _context) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Story />
|
||||
|
||||
@@ -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",
|
||||
|
||||
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void>) => {
|
||||
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<void> => {
|
||||
await endSession({ id: sessionId });
|
||||
if (isCurrent) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [isCurrent, endSession, sessionId]);
|
||||
await endSession.mutateAsync(sessionId);
|
||||
}, [endSession.mutateAsync, sessionId]);
|
||||
|
||||
return onSessionEnd;
|
||||
};
|
||||
|
||||
@@ -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<void> => {
|
||||
await endCompatSession({ id: data.id });
|
||||
await endSession.mutateAsync(data.id);
|
||||
};
|
||||
|
||||
const clientName = data.ssoLogin?.redirectUri
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Props> = ({ 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<void> => {
|
||||
await endSession({ id: data.id });
|
||||
await endSession.mutateAsync(data.id);
|
||||
};
|
||||
|
||||
const deviceId = getDeviceIdFromScope(data.scope);
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DeviceType } from "../../gql/graphql";
|
||||
|
||||
import DeviceTypeIcon from "./DeviceTypeIcon";
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -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<Props> = ({ 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<void> => {
|
||||
await endSession({ id: data.id });
|
||||
await endSession.mutateAsync(data.id);
|
||||
};
|
||||
|
||||
const finishedAt = data.finishedAt
|
||||
|
||||
@@ -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<Props> = ({ 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<void> => {
|
||||
await endSession({ id: data.id });
|
||||
await endSession.mutateAsync(data.id);
|
||||
};
|
||||
|
||||
const deviceId = getDeviceIdFromScope(data.scope);
|
||||
|
||||
@@ -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 && (
|
||||
<DeleteButtonWithConfirmation
|
||||
email={data.email}
|
||||
disabled={removeResult.fetching}
|
||||
disabled={removeEmail.isPending}
|
||||
onClick={onRemoveClick}
|
||||
/>
|
||||
)}
|
||||
@@ -188,7 +197,7 @@ const UserEmail: React.FC<{
|
||||
<button
|
||||
type="button"
|
||||
className={styles.link}
|
||||
disabled={setPrimaryResult.fetching}
|
||||
disabled={setPrimary.isPending}
|
||||
onClick={onSetPrimaryClick}
|
||||
>
|
||||
{t("frontend.user_email.make_primary_button")}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// 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 IconClose from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||
import IconEdit from "@vector-im/compound-design-tokens/assets/web/icons/edit";
|
||||
import {
|
||||
@@ -21,13 +22,10 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "urql";
|
||||
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { SetDisplayNameStatus } from "../../gql/graphql";
|
||||
import { graphqlClient } from "../../graphql";
|
||||
import * as Dialog from "../Dialog";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
|
||||
import styles from "./UserGreeting.module.css";
|
||||
|
||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
@@ -88,28 +86,32 @@ const UserGreeting: React.FC<Props> = ({ user, siteConfig }) => {
|
||||
const fieldRef = useRef<HTMLInputElement>(null);
|
||||
const data = useFragment(FRAGMENT, user);
|
||||
const { displayNameChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [setDisplayNameResult, setDisplayName] = useMutation(
|
||||
SET_DISPLAYNAME_MUTATION,
|
||||
);
|
||||
const setDisplayName = useMutation({
|
||||
mutationFn: ({
|
||||
userId,
|
||||
displayName,
|
||||
}: { userId: string; displayName: string | null }) =>
|
||||
graphqlClient.request(SET_DISPLAYNAME_MUTATION, { userId, displayName }),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
|
||||
if (data.setDisplayName.status === "SET") {
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onSubmit = async (
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const displayName = (formData.get("displayname") as string) || null;
|
||||
|
||||
const result = await setDisplayName({ displayName, userId: data.id });
|
||||
|
||||
if (result.data?.setDisplayName.status === "SET") {
|
||||
setOpen(false);
|
||||
}
|
||||
setDisplayName.mutate({ displayName, userId: data.id });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -163,7 +165,7 @@ const UserGreeting: React.FC<Props> = ({ user, siteConfig }) => {
|
||||
<Form.Field
|
||||
name="displayname"
|
||||
serverInvalid={
|
||||
setDisplayNameResult.data?.setDisplayName.status === "INVALID"
|
||||
setDisplayName.data?.setDisplayName.status === "INVALID"
|
||||
}
|
||||
>
|
||||
<Form.Label>
|
||||
@@ -198,8 +200,8 @@ const UserGreeting: React.FC<Props> = ({ user, siteConfig }) => {
|
||||
</Form.Field>
|
||||
</div>
|
||||
|
||||
<Form.Submit disabled={setDisplayNameResult.fetching}>
|
||||
{setDisplayNameResult.fetching && <LoadingSpinner inline />}
|
||||
<Form.Submit disabled={setDisplayName.isPending}>
|
||||
{setDisplayName.isPending && <LoadingSpinner inline />}
|
||||
{t("action.save")}
|
||||
</Form.Submit>
|
||||
</Form.Root>
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
// 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 {
|
||||
EditInPlace,
|
||||
ErrorMessage,
|
||||
HelpMessage,
|
||||
} from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "urql";
|
||||
|
||||
import { graphql } from "../../gql";
|
||||
import { graphqlClient } from "../../graphql";
|
||||
|
||||
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation AddEmail($userId: ID!, $email: String!) {
|
||||
@@ -32,8 +32,26 @@ const AddEmailForm: React.FC<{
|
||||
onAdd: (id: string) => Promise<void>;
|
||||
}> = ({ userId, onAdd }) => {
|
||||
const { t } = useTranslation();
|
||||
const [addEmailResult, addEmail] = useMutation(ADD_EMAIL_MUTATION);
|
||||
if (addEmailResult.error) throw addEmailResult.error;
|
||||
const queryClient = useQueryClient();
|
||||
const addEmail = useMutation({
|
||||
mutationFn: ({ userId, email }: { userId: string; email: string }) =>
|
||||
graphqlClient.request(ADD_EMAIL_MUTATION, { userId, email }),
|
||||
onSuccess: async (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
|
||||
// Don't clear the form if the email was invalid or already exists
|
||||
if (data.addEmail.status !== "ADDED") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.addEmail.email?.id) {
|
||||
throw new Error("Unexpected response from server");
|
||||
}
|
||||
|
||||
// Call the onAdd callback
|
||||
await onAdd(data.addEmail.email?.id);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
@@ -42,23 +60,11 @@ const AddEmailForm: React.FC<{
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
const result = await addEmail({ userId, email });
|
||||
|
||||
// Don't clear the form if the email was invalid or already exists
|
||||
if (result.data?.addEmail.status !== "ADDED") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data?.addEmail.email?.id) {
|
||||
throw new Error("Unexpected response from server");
|
||||
}
|
||||
|
||||
// Call the onAdd callback
|
||||
await onAdd(result.data?.addEmail.email?.id);
|
||||
addEmail.mutate({ userId, email });
|
||||
};
|
||||
|
||||
const status = addEmailResult.data?.addEmail.status ?? null;
|
||||
const violations = addEmailResult.data?.addEmail.violations ?? [];
|
||||
const status = addEmail.data?.addEmail.status ?? null;
|
||||
const violations = addEmail.data?.addEmail.violations ?? [];
|
||||
|
||||
return (
|
||||
<EditInPlace
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTransition } from "react";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlClient } from "../../graphql";
|
||||
import {
|
||||
type AnyPagination,
|
||||
FIRST_PAGE,
|
||||
type Pagination,
|
||||
usePages,
|
||||
@@ -73,13 +74,20 @@ const UserEmailList: React.FC<{
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [pagination, setPagination] = usePagination();
|
||||
const [result, refreshList] = useQuery({
|
||||
query: QUERY,
|
||||
variables: { userId: data.id, ...pagination },
|
||||
const result = useSuspenseQuery({
|
||||
queryKey: ["userEmails", pagination],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: QUERY,
|
||||
variables: {
|
||||
userId: data.id,
|
||||
...(pagination as AnyPagination),
|
||||
},
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
const emails = result.data?.user?.emails;
|
||||
if (!emails) throw new Error(); // Suspense mode is enabled
|
||||
const emails = result.data.user?.emails;
|
||||
if (!emails) throw new Error();
|
||||
|
||||
const [prevPage, nextPage] = usePages(pagination, emails.pageInfo);
|
||||
|
||||
@@ -91,11 +99,10 @@ const UserEmailList: React.FC<{
|
||||
});
|
||||
};
|
||||
|
||||
// When removing an email, we want to refresh the list and go back to the first page
|
||||
// When removing an email, we want to go back to the first page
|
||||
const onRemove = (): void => {
|
||||
startTransition(() => {
|
||||
setPagination(FIRST_PAGE);
|
||||
refreshList();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
// 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 { useLinkProps, useNavigate } from "@tanstack/react-router";
|
||||
import IconArrowLeft from "@vector-im/compound-design-tokens/assets/web/icons/arrow-left";
|
||||
import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send-solid";
|
||||
import { Alert, Button, Form, H1, Text } from "@vector-im/compound-web";
|
||||
import { useRef } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useMutation } from "urql";
|
||||
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
|
||||
import { graphqlClient } from "../../graphql";
|
||||
import styles from "./VerifyEmail.module.css";
|
||||
|
||||
const FRAGMENT = graphql(/* GraphQL */ `
|
||||
@@ -78,10 +77,28 @@ const VerifyEmail: React.FC<{
|
||||
email: FragmentType<typeof FRAGMENT>;
|
||||
}> = ({ email }) => {
|
||||
const data = useFragment(FRAGMENT, email);
|
||||
const [verifyEmailResult, verifyEmail] = useMutation(VERIFY_EMAIL_MUTATION);
|
||||
const [resendVerificationEmailResult, resendVerificationEmail] = useMutation(
|
||||
RESEND_VERIFICATION_EMAIL_MUTATION,
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
const verifyEmail = useMutation({
|
||||
mutationFn: ({ id, code }: { id: string; code: string }) =>
|
||||
graphqlClient.request(VERIFY_EMAIL_MUTATION, { id, code }),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["userProfile"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
|
||||
if (data.verifyEmail.status === "VERIFIED") {
|
||||
navigate({ to: "/" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const resendVerificationEmail = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
graphqlClient.request(RESEND_VERIFICATION_EMAIL_MUTATION, { id }),
|
||||
onSuccess: () => {
|
||||
fieldRef.current?.focus();
|
||||
},
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const fieldRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -91,26 +108,16 @@ const VerifyEmail: React.FC<{
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const code = formData.get("code") as string;
|
||||
verifyEmail({ id: data.id, code }).then((result) => {
|
||||
// Clear the form
|
||||
form.reset();
|
||||
|
||||
if (result.data?.verifyEmail.status === "VERIFIED") {
|
||||
navigate({ to: "/" });
|
||||
}
|
||||
});
|
||||
verifyEmail.mutateAsync({ id: data.id, code }).finally(() => form.reset());
|
||||
};
|
||||
|
||||
const onResendClick = (): void => {
|
||||
resendVerificationEmail({ id: data.id }).then(() => {
|
||||
fieldRef.current?.focus();
|
||||
});
|
||||
resendVerificationEmail.mutate(data.id);
|
||||
};
|
||||
|
||||
const emailSent =
|
||||
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
|
||||
const invalidCode =
|
||||
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
|
||||
resendVerificationEmail.data?.sendVerificationEmail.status === "SENT";
|
||||
const invalidCode = verifyEmail.data?.verifyEmail.status === "INVALID_CODE";
|
||||
const { email: codeEmail } = data;
|
||||
|
||||
return (
|
||||
@@ -163,13 +170,13 @@ const VerifyEmail: React.FC<{
|
||||
</Form.ErrorMessage>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Submit type="submit" disabled={verifyEmailResult.fetching}>
|
||||
<Form.Submit type="submit" disabled={verifyEmail.isPending}>
|
||||
{t("action.continue")}
|
||||
</Form.Submit>
|
||||
<Button
|
||||
type="button"
|
||||
kind="secondary"
|
||||
disabled={resendVerificationEmailResult.fetching}
|
||||
disabled={resendVerificationEmail.isPending}
|
||||
onClick={onResendClick}
|
||||
>
|
||||
{t("frontend.verify_email.resend_code")}
|
||||
|
||||
@@ -19,11 +19,11 @@ const documents = {
|
||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": types.EndBrowserSessionDocument,
|
||||
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": types.OAuth2Client_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc,
|
||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n": types.EndCompatSessionDocument,
|
||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": types.EndCompatSessionDocument,
|
||||
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc,
|
||||
"\n query FooterQuery {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterQueryDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n": types.EndOAuth2SessionDocument,
|
||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument,
|
||||
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc,
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
@@ -98,7 +98,7 @@ export function graphql(source: "\n fragment CompatSession_session on CompatSes
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n"): (typeof documents)["\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"): (typeof documents)["\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -114,7 +114,7 @@ export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Ses
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"): (typeof documents)["\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"): (typeof documents)["\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1439,7 +1439,7 @@ export type EndCompatSessionMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string, finishedAt?: string | null } | null } };
|
||||
export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string } | null } };
|
||||
|
||||
export type Footer_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, imprint?: string | null, tosUri?: string | null, policyUri?: string | null } & { ' $fragmentName'?: 'Footer_SiteConfigFragment' };
|
||||
|
||||
@@ -1458,10 +1458,7 @@ export type EndOAuth2SessionMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EndOAuth2SessionMutation = { __typename?: 'Mutation', endOauth2Session: { __typename?: 'EndOAuth2SessionPayload', status: EndOAuth2SessionStatus, oauth2Session?: (
|
||||
{ __typename?: 'Oauth2Session', id: string }
|
||||
& { ' $fragmentRefs'?: { 'OAuth2Session_SessionFragment': OAuth2Session_SessionFragment } }
|
||||
) | null } };
|
||||
export type EndOAuth2SessionMutation = { __typename?: 'Mutation', endOauth2Session: { __typename?: 'EndOAuth2SessionPayload', status: EndOAuth2SessionStatus, oauth2Session?: { __typename?: 'Oauth2Session', id: string } | null } };
|
||||
|
||||
export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
|
||||
|
||||
@@ -1733,9 +1730,9 @@ export const UserEmailList_SiteConfigFragmentDoc = {"kind":"Document","definitio
|
||||
export const BrowserSessionsOverview_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BrowserSessionsOverview_user"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"browserSessions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"0"}},{"kind":"Argument","name":{"kind":"Name","value":"state"},"value":{"kind":"EnumValue","value":"ACTIVE"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode<BrowserSessionsOverview_UserFragment, unknown>;
|
||||
export const UserEmail_VerifyEmailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserEmail_verifyEmail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserEmail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
|
||||
export const EndBrowserSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndBrowserSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endBrowserSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"browserSessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"browserSession"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"BrowserSession_session"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BrowserSession_session"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BrowserSession"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"raw"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"deviceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastAuthentication"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<EndBrowserSessionMutation, EndBrowserSessionMutationVariables>;
|
||||
export const EndCompatSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndCompatSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCompatSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"compatSessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"compatSession"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}}]}}]}}]}}]} as unknown as DocumentNode<EndCompatSessionMutation, EndCompatSessionMutationVariables>;
|
||||
export const EndCompatSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndCompatSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCompatSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"compatSessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"compatSession"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<EndCompatSessionMutation, EndCompatSessionMutationVariables>;
|
||||
export const FooterQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FooterQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"Footer_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Footer_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imprint"}},{"kind":"Field","name":{"kind":"Name","value":"tosUri"}},{"kind":"Field","name":{"kind":"Name","value":"policyUri"}}]}}]} as unknown as DocumentNode<FooterQueryQuery, FooterQueryQueryVariables>;
|
||||
export const EndOAuth2SessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndOAuth2Session"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endOauth2Session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"oauth2SessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"oauth2Session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"OAuth2Session_session"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OAuth2Session_session"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Oauth2Session"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"scope"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"deviceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"clientId"}},{"kind":"Field","name":{"kind":"Name","value":"clientName"}},{"kind":"Field","name":{"kind":"Name","value":"applicationType"}},{"kind":"Field","name":{"kind":"Name","value":"logoUri"}}]}}]}}]} as unknown as DocumentNode<EndOAuth2SessionMutation, EndOAuth2SessionMutationVariables>;
|
||||
export const EndOAuth2SessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EndOAuth2Session"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endOauth2Session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"oauth2SessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"oauth2Session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<EndOAuth2SessionMutation, EndOAuth2SessionMutationVariables>;
|
||||
export const RemoveEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userEmailId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<RemoveEmailMutation, RemoveEmailMutationVariables>;
|
||||
export const SetPrimaryEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetPrimaryEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPrimaryEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userEmailId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"primaryEmail"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode<SetPrimaryEmailMutation, SetPrimaryEmailMutationVariables>;
|
||||
export const SetDisplayNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetDisplayName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"displayName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setDisplayName"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"displayName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"displayName"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"matrix"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}}]}}]}}]}}]} as unknown as DocumentNode<SetDisplayNameMutation, SetDisplayNameMutationVariables>;
|
||||
|
||||
@@ -4,32 +4,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { cacheExchange, createClient, fetchExchange } from "@urql/core";
|
||||
import { devtoolsExchange } from "@urql/devtools";
|
||||
import { refocusExchange } from "@urql/exchange-refocus";
|
||||
import { requestPolicyExchange } from "@urql/exchange-request-policy";
|
||||
import { GraphQLClient } from "graphql-request";
|
||||
|
||||
import appConfig from "./config";
|
||||
|
||||
const exchanges = [
|
||||
// This sets the policy to 'cache-and-network' after 5 minutes
|
||||
requestPolicyExchange({
|
||||
ttl: 1000 * 60 * 5, // 5 minute
|
||||
}),
|
||||
|
||||
// This refetches all queries when the tab is refocused
|
||||
refocusExchange(),
|
||||
|
||||
// Simple cache
|
||||
cacheExchange,
|
||||
|
||||
// Use `fetch` to execute the requests
|
||||
fetchExchange,
|
||||
];
|
||||
|
||||
export const client = createClient({
|
||||
url: appConfig.graphqlEndpoint,
|
||||
suspense: true,
|
||||
// Add the devtools exchange in development
|
||||
exchanges: import.meta.env.DEV ? [devtoolsExchange, ...exchanges] : exchanges,
|
||||
});
|
||||
export const graphqlClient = new GraphQLClient(
|
||||
new URL(appConfig.graphqlEndpoint, window.location.toString()).toString(),
|
||||
);
|
||||
|
||||
@@ -4,22 +4,29 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { StrictMode, Suspense } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { Provider as UrqlProvider } from "urql";
|
||||
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import GenericError from "./components/GenericError";
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import config from "./config";
|
||||
import { client } from "./graphql";
|
||||
import i18n from "./i18n";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./shared.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: {
|
||||
throwOnError: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
@@ -27,7 +34,7 @@ const router = createRouter({
|
||||
defaultErrorComponent: GenericError,
|
||||
defaultPreload: "intent",
|
||||
defaultPendingMinMs: 0,
|
||||
context: { client },
|
||||
context: { queryClient },
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
@@ -39,16 +46,16 @@ declare module "@tanstack/react-router" {
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<UrqlProvider value={client}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorBoundary>
|
||||
<TooltipProvider>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RouterProvider router={router} context={{ client }} />
|
||||
<RouterProvider router={router} context={{ queryClient }} />
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
</ErrorBoundary>
|
||||
</UrqlProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -19,20 +19,20 @@ export const FIRST_PAGE = Symbol("FIRST_PAGE");
|
||||
export const LAST_PAGE = Symbol("LAST_PAGE");
|
||||
|
||||
export const anyPaginationSchema = z.object({
|
||||
first: z.number().optional(),
|
||||
after: z.string().optional(),
|
||||
last: z.number().optional(),
|
||||
before: z.string().optional(),
|
||||
first: z.number().nullish(),
|
||||
after: z.string().nullish(),
|
||||
last: z.number().nullish(),
|
||||
before: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const forwardPaginationSchema = z.object({
|
||||
first: z.number(),
|
||||
after: z.string().optional(),
|
||||
after: z.string().nullish(),
|
||||
});
|
||||
|
||||
const backwardPaginationSchema = z.object({
|
||||
last: z.number(),
|
||||
before: z.string().optional(),
|
||||
before: z.string().nullish(),
|
||||
});
|
||||
|
||||
const paginationSchema = z.union([
|
||||
|
||||
@@ -4,28 +4,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import {
|
||||
Outlet,
|
||||
ScrollRestoration,
|
||||
createRootRouteWithContext,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
||||
import type { Client } from "urql";
|
||||
|
||||
import Layout from "../components/Layout";
|
||||
import Layout, { query } from "../components/Layout";
|
||||
import NotFound from "../components/NotFound";
|
||||
|
||||
export const Route = createRootRouteWithContext<{
|
||||
client: Client;
|
||||
queryClient: QueryClient;
|
||||
}>()({
|
||||
component: () => (
|
||||
<>
|
||||
<ScrollRestoration />
|
||||
<Outlet />
|
||||
{import.meta.env.DEV && <TanStackRouterDevtools />}
|
||||
|
||||
{import.meta.env.DEV && (
|
||||
<>
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<ReactQueryDevtools buttonPosition="top-right" />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
|
||||
loader({ context }) {
|
||||
context.queryClient.ensureQueryData(query);
|
||||
},
|
||||
|
||||
notFoundComponent: () => (
|
||||
<Layout>
|
||||
<NotFound />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
createLazyFileRoute,
|
||||
notFound,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
import { Alert, Heading, Separator, Text } from "@vector-im/compound-web";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
|
||||
import BlockList from "../components/BlockList";
|
||||
@@ -23,7 +23,7 @@ import UserEmail from "../components/UserEmail";
|
||||
import AddEmailForm from "../components/UserProfile/AddEmailForm";
|
||||
import UserEmailList from "../components/UserProfile/UserEmailList";
|
||||
|
||||
import { QUERY } from "./_account.index";
|
||||
import { query } from "./_account.index";
|
||||
|
||||
export const Route = createLazyFileRoute("/_account/")({
|
||||
component: Index,
|
||||
@@ -32,12 +32,10 @@ export const Route = createLazyFileRoute("/_account/")({
|
||||
function Index(): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [result] = useQuery({ query: QUERY });
|
||||
if (result.error) throw result.error;
|
||||
const user = result.data?.viewer;
|
||||
if (user?.__typename !== "User") throw notFound();
|
||||
const siteConfig = result.data?.siteConfig;
|
||||
if (!siteConfig) throw Error(); // This should never happen
|
||||
const {
|
||||
data: { viewer, siteConfig },
|
||||
} = useSuspenseQuery(query);
|
||||
if (viewer?.__typename !== "User") throw notFound();
|
||||
|
||||
// When adding an email, we want to go to the email verification form
|
||||
const onAdd = async (id: string): Promise<void> => {
|
||||
@@ -49,9 +47,9 @@ function Index(): React.ReactElement {
|
||||
<BlockList>
|
||||
{/* This wrapper is only needed for the anchor link */}
|
||||
<div className="flex flex-col gap-4" id="emails">
|
||||
{user.primaryEmail ? (
|
||||
{viewer.primaryEmail ? (
|
||||
<UserEmail
|
||||
email={user.primaryEmail}
|
||||
email={viewer.primaryEmail}
|
||||
isPrimary
|
||||
siteConfig={siteConfig}
|
||||
/>
|
||||
@@ -63,11 +61,11 @@ function Index(): React.ReactElement {
|
||||
)}
|
||||
|
||||
<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
|
||||
<UserEmailList siteConfig={siteConfig} user={user} />
|
||||
<UserEmailList siteConfig={siteConfig} user={viewer} />
|
||||
</Suspense>
|
||||
|
||||
{siteConfig.emailChangeAllowed && (
|
||||
<AddEmailForm userId={user.id} onAdd={onAdd} />
|
||||
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
|
||||
import * as z from "zod";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query UserProfileQuery {
|
||||
viewer {
|
||||
__typename
|
||||
@@ -37,6 +39,11 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = queryOptions({
|
||||
queryKey: ["userProfile"],
|
||||
queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }),
|
||||
});
|
||||
|
||||
const actionSchema = z
|
||||
.discriminatedUnion("action", [
|
||||
z.object({
|
||||
@@ -101,13 +108,5 @@ export const Route = createFileRoute("/_account/")({
|
||||
}
|
||||
},
|
||||
|
||||
async loader({ context, abortController: { signal } }) {
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.data?.viewer.__typename !== "User") throw notFound();
|
||||
},
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(query),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { Outlet, createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { Heading } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { useEndBrowserSession } from "../components/BrowserSession";
|
||||
import Layout from "../components/Layout";
|
||||
@@ -17,7 +16,8 @@ import EndSessionButton from "../components/Session/EndSessionButton";
|
||||
import UnverifiedEmailAlert from "../components/UnverifiedEmailAlert";
|
||||
import UserGreeting from "../components/UserGreeting";
|
||||
|
||||
import { QUERY } from "./_account";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { query } from "./_account";
|
||||
|
||||
export const Route = createLazyFileRoute("/_account")({
|
||||
component: Account,
|
||||
@@ -25,14 +25,10 @@ export const Route = createLazyFileRoute("/_account")({
|
||||
|
||||
function Account(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [result] = useQuery({
|
||||
query: QUERY,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
const session = result.data?.viewerSession;
|
||||
const result = useSuspenseQuery(query);
|
||||
const session = result.data.viewerSession;
|
||||
if (session?.__typename !== "BrowserSession") throw notFound();
|
||||
const siteConfig = result.data?.siteConfig;
|
||||
if (!siteConfig) throw Error(); // This should never happen
|
||||
const siteConfig = result.data.siteConfig;
|
||||
const onSessionEnd = useEndBrowserSession(session.id, true);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { Alert } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { Link } from "../components/Link";
|
||||
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
|
||||
import CompatSessionDetail from "../components/SessionDetail/CompatSessionDetail";
|
||||
import OAuth2SessionDetail from "../components/SessionDetail/OAuth2SessionDetail";
|
||||
|
||||
import { QUERY } from "./_account.sessions.$id";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { query } from "./_account.sessions.$id";
|
||||
|
||||
export const Route = createLazyFileRoute("/_account/sessions/$id")({
|
||||
notFoundComponent: NotFound,
|
||||
@@ -38,11 +38,10 @@ function NotFound(): React.ReactElement {
|
||||
|
||||
function SessionDetail(): React.ReactElement {
|
||||
const { id } = Route.useParams();
|
||||
const [result] = useQuery({ query: QUERY, variables: { id } });
|
||||
if (result.error) throw result.error;
|
||||
const node = result.data?.node;
|
||||
const {
|
||||
data: { node, viewerSession },
|
||||
} = useSuspenseQuery(query(id));
|
||||
if (!node) throw notFound();
|
||||
const currentSessionId = result.data?.viewerSession?.id;
|
||||
|
||||
switch (node.__typename) {
|
||||
case "CompatSession":
|
||||
@@ -53,7 +52,7 @@ function SessionDetail(): React.ReactElement {
|
||||
return (
|
||||
<BrowserSessionDetail
|
||||
session={node}
|
||||
isCurrent={node.id === currentSessionId}
|
||||
isCurrent={node.id === viewerSession.id}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query SessionDetailQuery($id: ID!) {
|
||||
viewerSession {
|
||||
... on Node {
|
||||
@@ -26,14 +28,14 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["sessionDetail", id],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({ document: QUERY, signal, variables: { id } }),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/_account/sessions/$id")({
|
||||
async loader({ context, params, abortController: { signal } }) {
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{ id: params.id },
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (!result.data?.node) throw notFound();
|
||||
},
|
||||
loader: ({ context, params }) =>
|
||||
context.queryClient.ensureQueryData(query(params.id)),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { H5 } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import BlockList from "../components/BlockList";
|
||||
import BrowserSession from "../components/BrowserSession";
|
||||
@@ -15,9 +14,9 @@ import { ButtonLink } from "../components/ButtonLink";
|
||||
import EmptyState from "../components/EmptyState";
|
||||
import Filter from "../components/Filter";
|
||||
import { usePages } from "../pagination";
|
||||
import { getNinetyDaysAgo } from "../utils/dates";
|
||||
|
||||
import { QUERY } from "./_account.sessions.browsers";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { query } from "./_account.sessions.browsers";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
@@ -29,27 +28,19 @@ function BrowserSessions(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { inactive, pagination } = Route.useLoaderDeps();
|
||||
|
||||
const variables = {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const [list] = useQuery({ query: QUERY, variables });
|
||||
if (list.error) throw list.error;
|
||||
const currentSession =
|
||||
list.data?.viewerSession.__typename === "BrowserSession"
|
||||
? list.data.viewerSession
|
||||
: null;
|
||||
if (currentSession === null) throw notFound();
|
||||
const {
|
||||
data: { viewerSession },
|
||||
} = useSuspenseQuery(query(pagination, inactive));
|
||||
if (viewerSession.__typename !== "BrowserSession") throw notFound();
|
||||
|
||||
const [backwardPage, forwardPage] = usePages(
|
||||
pagination,
|
||||
currentSession.user.browserSessions.pageInfo,
|
||||
viewerSession.user.browserSessions.pageInfo,
|
||||
PAGE_SIZE,
|
||||
);
|
||||
|
||||
// We reverse the list as we are paginating backwards
|
||||
const edges = [...currentSession.user.browserSessions.edges].reverse();
|
||||
const edges = [...viewerSession.user.browserSessions.edges].reverse();
|
||||
return (
|
||||
<BlockList>
|
||||
<H5>{t("frontend.browser_sessions_overview.heading")}</H5>
|
||||
@@ -68,11 +59,11 @@ function BrowserSessions(): React.ReactElement {
|
||||
<BrowserSession
|
||||
key={n.cursor}
|
||||
session={n.node}
|
||||
isCurrent={currentSession.id === n.node.id}
|
||||
isCurrent={viewerSession.id === n.node.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
{currentSession.user.browserSessions.totalCount === 0 && (
|
||||
{viewerSession.user.browserSessions.totalCount === 0 && (
|
||||
<EmptyState>
|
||||
{inactive
|
||||
? t(
|
||||
|
||||
@@ -4,17 +4,23 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
|
||||
import * as z from "zod";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { anyPaginationSchema, normalizePagination } from "../pagination";
|
||||
import { graphqlClient } from "../graphql";
|
||||
import {
|
||||
type AnyPagination,
|
||||
anyPaginationSchema,
|
||||
normalizePagination,
|
||||
} from "../pagination";
|
||||
import { getNinetyDaysAgo } from "../utils/dates";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query BrowserSessionList(
|
||||
$first: Int
|
||||
$after: String
|
||||
@@ -61,6 +67,20 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = (pagination: AnyPagination, inactive: true | undefined) =>
|
||||
queryOptions({
|
||||
queryKey: ["browserSessionList", inactive, pagination],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: QUERY,
|
||||
variables: {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
},
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
|
||||
const searchSchema = z
|
||||
.object({
|
||||
inactive: z.literal(true).optional(),
|
||||
@@ -75,21 +95,6 @@ export const Route = createFileRoute("/_account/sessions/browsers")({
|
||||
pagination: normalizePagination(pagination, PAGE_SIZE, "backward"),
|
||||
}),
|
||||
|
||||
async loader({
|
||||
context,
|
||||
deps: { inactive, pagination },
|
||||
abortController: { signal },
|
||||
}) {
|
||||
const variables = {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const result = await context.client.query(QUERY, variables, {
|
||||
fetchOptions: { signal },
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.data?.viewerSession?.__typename !== "BrowserSession")
|
||||
throw notFound();
|
||||
},
|
||||
loader: ({ context, deps: { inactive, pagination } }) =>
|
||||
context.queryClient.ensureQueryData(query(pagination, inactive)),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { H3, Separator } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import BlockList from "../components/BlockList";
|
||||
import { ButtonLink } from "../components/ButtonLink";
|
||||
@@ -17,9 +16,9 @@ import Filter from "../components/Filter";
|
||||
import OAuth2Session from "../components/OAuth2Session";
|
||||
import BrowserSessionsOverview from "../components/UserSessionsOverview/BrowserSessionsOverview";
|
||||
import { usePages } from "../pagination";
|
||||
import { getNinetyDaysAgo } from "../utils/dates";
|
||||
|
||||
import { LIST_QUERY, QUERY } from "./_account.sessions.index";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { listQuery, query } from "./_account.sessions.index";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
@@ -35,24 +34,14 @@ export const Route = createLazyFileRoute("/_account/sessions/")({
|
||||
function Sessions(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { inactive, pagination } = Route.useLoaderDeps();
|
||||
const [overview] = useQuery({ query: QUERY });
|
||||
if (overview.error) throw overview.error;
|
||||
const user =
|
||||
overview.data?.viewer.__typename === "User" ? overview.data.viewer : null;
|
||||
if (user === null) throw notFound();
|
||||
const {
|
||||
data: { viewer },
|
||||
} = useSuspenseQuery(query);
|
||||
if (viewer.__typename !== "User") throw notFound();
|
||||
|
||||
const variables = {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const [list] = useQuery({ query: LIST_QUERY, variables });
|
||||
if (list.error) throw list.error;
|
||||
const appSessions =
|
||||
list.data?.viewer.__typename === "User"
|
||||
? list.data.viewer.appSessions
|
||||
: null;
|
||||
if (appSessions === null) throw notFound();
|
||||
const { data } = useSuspenseQuery(listQuery(pagination, inactive));
|
||||
if (data.viewer.__typename !== "User") throw notFound();
|
||||
const appSessions = data.viewer.appSessions;
|
||||
|
||||
const [backwardPage, forwardPage] = usePages(
|
||||
pagination,
|
||||
@@ -66,7 +55,7 @@ function Sessions(): React.ReactElement {
|
||||
return (
|
||||
<BlockList>
|
||||
<H3>{t("frontend.user_sessions_overview.heading")}</H3>
|
||||
<BrowserSessionsOverview user={user} />
|
||||
<BrowserSessionsOverview user={viewer} />
|
||||
<Separator />
|
||||
<div className="flex gap-2 justify-start items-center">
|
||||
<Filter
|
||||
|
||||
@@ -4,17 +4,23 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
|
||||
import * as z from "zod";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { anyPaginationSchema, normalizePagination } from "../pagination";
|
||||
import { graphqlClient } from "../graphql";
|
||||
import {
|
||||
type AnyPagination,
|
||||
anyPaginationSchema,
|
||||
normalizePagination,
|
||||
} from "../pagination";
|
||||
import { getNinetyDaysAgo } from "../utils/dates";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query SessionsOverviewQuery {
|
||||
viewer {
|
||||
__typename
|
||||
@@ -27,7 +33,12 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const LIST_QUERY = graphql(/* GraphQL */ `
|
||||
export const query = queryOptions({
|
||||
queryKey: ["sessionsOverview"],
|
||||
queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }),
|
||||
});
|
||||
|
||||
const LIST_QUERY = graphql(/* GraphQL */ `
|
||||
query AppSessionsListQuery(
|
||||
$before: String
|
||||
$after: String
|
||||
@@ -70,6 +81,23 @@ export const LIST_QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const listQuery = (
|
||||
pagination: AnyPagination,
|
||||
inactive: true | undefined,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryKey: ["appSessionList", inactive, pagination],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: LIST_QUERY,
|
||||
variables: {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
},
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
|
||||
const searchSchema = z
|
||||
.object({
|
||||
inactive: z.literal(true).optional(),
|
||||
@@ -84,26 +112,9 @@ export const Route = createFileRoute("/_account/sessions/")({
|
||||
pagination: normalizePagination(pagination, PAGE_SIZE, "backward"),
|
||||
}),
|
||||
|
||||
async loader({
|
||||
context,
|
||||
deps: { inactive, pagination },
|
||||
abortController: { signal },
|
||||
}) {
|
||||
const variables = {
|
||||
lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined,
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const [overview, list] = await Promise.all([
|
||||
context.client.query(QUERY, {}, { fetchOptions: { signal } }),
|
||||
context.client.query(LIST_QUERY, variables, {
|
||||
fetchOptions: { signal },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (overview.error) throw overview.error;
|
||||
if (list.error) throw list.error;
|
||||
if (overview.data?.viewer?.__typename !== "User") throw notFound();
|
||||
if (list.data?.viewer?.__typename !== "User") throw notFound();
|
||||
},
|
||||
loader: ({ context, deps: { inactive, pagination } }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(query),
|
||||
context.queryClient.ensureQueryData(listQuery(pagination, inactive)),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentUserGreeting {
|
||||
viewerSession {
|
||||
__typename
|
||||
@@ -31,15 +33,11 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const Route = createFileRoute("/_account")({
|
||||
loader: async ({ context, abortController: { signal } }) => {
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.data?.viewerSession.__typename !== "BrowserSession")
|
||||
throw notFound();
|
||||
},
|
||||
export const query = queryOptions({
|
||||
queryKey: ["currentUserGreeting"],
|
||||
queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/_account")({
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(query),
|
||||
});
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useQuery } from "urql";
|
||||
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
|
||||
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
|
||||
import Layout from "../components/Layout";
|
||||
|
||||
import { QUERY } from "./clients.$id";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { query } from "./clients.$id";
|
||||
|
||||
export const Route = createLazyFileRoute("/clients/$id")({
|
||||
component: ClientDetail,
|
||||
@@ -18,17 +18,14 @@ export const Route = createLazyFileRoute("/clients/$id")({
|
||||
|
||||
function ClientDetail(): React.ReactElement {
|
||||
const { id } = Route.useParams();
|
||||
const [result] = useQuery({
|
||||
query: QUERY,
|
||||
variables: { id },
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
const client = result.data?.oauth2Client;
|
||||
if (!client) throw new Error(); // Should have been caught by the loader
|
||||
const {
|
||||
data: { oauth2Client },
|
||||
} = useSuspenseQuery(query(id));
|
||||
if (!oauth2Client) throw notFound();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<OAuth2ClientDetail client={client} />
|
||||
<OAuth2ClientDetail client={oauth2Client} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query OAuth2ClientQuery($id: ID!) {
|
||||
oauth2Client(id: $id) {
|
||||
...OAuth2Client_detail
|
||||
@@ -16,14 +18,14 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["oauth2Client", id],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({ document: QUERY, variables: { id }, signal }),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/clients/$id")({
|
||||
loader: async ({ context, params, abortController: { signal } }) => {
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{ id: params.id },
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (!result.data?.oauth2Client) throw notFound();
|
||||
},
|
||||
loader: ({ context, params }) =>
|
||||
context.queryClient.ensureQueryData(query(params.id)),
|
||||
});
|
||||
|
||||
@@ -8,9 +8,11 @@ import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
import { Alert } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import Layout from "../components/Layout";
|
||||
import { Link } from "../components/Link";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerQuery {
|
||||
@@ -23,6 +25,15 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const currentViewerQuery = queryOptions({
|
||||
queryKey: ["currentViewer"],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: CURRENT_VIEWER_QUERY,
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query DeviceRedirectQuery($deviceId: String!, $userId: ID!) {
|
||||
session(deviceId: $deviceId, userId: $userId) {
|
||||
@@ -34,33 +45,34 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const Route = createFileRoute("/devices/$")({
|
||||
async loader({ context, params, abortController: { signal } }) {
|
||||
const viewer = await context.client.query(
|
||||
CURRENT_VIEWER_QUERY,
|
||||
{},
|
||||
{
|
||||
fetchOptions: { signal },
|
||||
},
|
||||
);
|
||||
if (viewer.error) throw viewer.error;
|
||||
if (viewer.data?.viewer.__typename !== "User") throw notFound();
|
||||
const query = (deviceId: string, userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["deviceRedirect", deviceId, userId],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: QUERY,
|
||||
variables: { deviceId, userId },
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{
|
||||
deviceId: params._splat || "",
|
||||
userId: viewer.data.viewer.id,
|
||||
},
|
||||
{ fetchOptions: { signal } },
|
||||
export const Route = createFileRoute("/devices/$")({
|
||||
async loader({ context, params }) {
|
||||
const data = await context.queryClient.fetchQuery(currentViewerQuery);
|
||||
if (data.viewer.__typename !== "User")
|
||||
throw notFound({
|
||||
global: true,
|
||||
});
|
||||
|
||||
const result = await context.queryClient.fetchQuery(
|
||||
query(params._splat || "", data.viewer.id),
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
const session = result.data?.session;
|
||||
if (!session) throw notFound();
|
||||
|
||||
if (!result.session) throw notFound();
|
||||
|
||||
throw redirect({
|
||||
to: "/sessions/$id",
|
||||
params: { id: session.id },
|
||||
params: { id: result.session.id },
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import Layout from "../components/Layout";
|
||||
import VerifyEmailComponent from "../components/VerifyEmail";
|
||||
|
||||
import { QUERY } from "./emails.$id.verify";
|
||||
import { query } from "./emails.$id.verify";
|
||||
|
||||
export const Route = createLazyFileRoute("/emails/$id/verify")({
|
||||
component: EmailVerify,
|
||||
@@ -18,15 +18,14 @@ export const Route = createLazyFileRoute("/emails/$id/verify")({
|
||||
|
||||
function EmailVerify(): React.ReactElement {
|
||||
const { id } = Route.useParams();
|
||||
const [result] = useQuery({ query: QUERY, variables: { id } });
|
||||
|
||||
if (result.error) throw result.error;
|
||||
const email = result.data?.userEmail;
|
||||
if (email == null) throw notFound();
|
||||
const {
|
||||
data: { userEmail },
|
||||
} = useSuspenseQuery(query(id));
|
||||
if (!userEmail) throw notFound();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<VerifyEmailComponent email={email} />
|
||||
<VerifyEmailComponent email={userEmail} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query VerifyEmailQuery($id: ID!) {
|
||||
userEmail(id: $id) {
|
||||
...UserEmail_verifyEmail
|
||||
@@ -16,16 +18,14 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["verifyEmail", id],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({ document: QUERY, signal, variables: { id } }),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/emails/$id/verify")({
|
||||
async loader({ context, params, abortController: { signal } }) {
|
||||
const result = await context.client.query(
|
||||
QUERY,
|
||||
{
|
||||
id: params.id,
|
||||
},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (!result.data?.userEmail) throw notFound();
|
||||
},
|
||||
loader: ({ context, params }) =>
|
||||
context.queryClient.ensureQueryData(query(params.id)),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
createLazyFileRoute,
|
||||
notFound,
|
||||
@@ -13,7 +14,6 @@ import IconLockSolid from "@vector-im/compound-design-tokens/assets/web/icons/lo
|
||||
import { Alert, Form, Separator } from "@vector-im/compound-web";
|
||||
import { type FormEvent, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery } from "urql";
|
||||
|
||||
import BlockList from "../components/BlockList";
|
||||
import { ButtonLink } from "../components/ButtonLink";
|
||||
@@ -22,10 +22,10 @@ import LoadingSpinner from "../components/LoadingSpinner";
|
||||
import PageHeading from "../components/PageHeading";
|
||||
import PasswordCreationDoubleInput from "../components/PasswordCreationDoubleInput";
|
||||
import { graphql } from "../gql";
|
||||
import { SetPasswordStatus } from "../gql/graphql";
|
||||
import { translateSetPasswordError } from "../i18n/password_changes";
|
||||
|
||||
import { QUERY } from "./password.change.index";
|
||||
import { graphqlClient } from "../graphql";
|
||||
import { query } from "./password.change.index";
|
||||
|
||||
const CHANGE_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation ChangePassword(
|
||||
@@ -51,43 +51,52 @@ export const Route = createLazyFileRoute("/password/change/")({
|
||||
|
||||
function ChangePassword(): React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const [queryResult] = useQuery({ query: QUERY });
|
||||
const {
|
||||
data: { viewer, siteConfig },
|
||||
} = useSuspenseQuery(query);
|
||||
const router = useRouter();
|
||||
if (queryResult.error) throw queryResult.error;
|
||||
if (queryResult.data?.viewer.__typename !== "User") throw notFound();
|
||||
const userId = queryResult.data.viewer.id;
|
||||
const siteConfig = queryResult.data?.siteConfig;
|
||||
if (!siteConfig) throw Error(); // This should never happen
|
||||
if (viewer.__typename !== "User") throw notFound();
|
||||
const userId = viewer.id;
|
||||
|
||||
const currentPasswordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [result, changePassword] = useMutation(CHANGE_PASSWORD_MUTATION);
|
||||
const mutation = useMutation({
|
||||
async mutationFn(formData: FormData) {
|
||||
const oldPassword = formData.get("current_password") as string;
|
||||
const newPassword = formData.get("new_password") as string;
|
||||
const newPasswordAgain = formData.get("new_password_again") as string;
|
||||
|
||||
if (newPassword !== newPasswordAgain) {
|
||||
throw new Error(
|
||||
"passwords mismatch; this should be checked by the form",
|
||||
);
|
||||
}
|
||||
|
||||
const response = await graphqlClient.request(CHANGE_PASSWORD_MUTATION, {
|
||||
userId,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
});
|
||||
|
||||
if (response.setPassword.status === "ALLOWED") {
|
||||
router.navigate({ to: "/password/change/success" });
|
||||
}
|
||||
|
||||
return response.setPassword;
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const oldPassword = formData.get("current_password") as string;
|
||||
const newPassword = formData.get("new_password") as string;
|
||||
const newPasswordAgain = formData.get("new_password_again") as string;
|
||||
|
||||
if (newPassword !== newPasswordAgain) {
|
||||
throw new Error("passwords mismatch; this should be checked by the form");
|
||||
}
|
||||
|
||||
const response = await changePassword({ userId, oldPassword, newPassword });
|
||||
|
||||
if (response.data?.setPassword.status === "ALLOWED") {
|
||||
router.navigate({ to: "/password/change/success" });
|
||||
}
|
||||
mutation.mutate(formData);
|
||||
};
|
||||
|
||||
const unhandleableError = result.error !== undefined;
|
||||
const unhandleableError = mutation.error !== null;
|
||||
|
||||
const errorMsg: string | undefined = translateSetPasswordError(
|
||||
t,
|
||||
result.data?.setPassword.status,
|
||||
mutation.data?.status,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -125,7 +134,7 @@ function ChangePassword(): React.ReactNode {
|
||||
|
||||
<Form.Field
|
||||
name="current_password"
|
||||
serverInvalid={result.data?.setPassword.status === "WRONG_PASSWORD"}
|
||||
serverInvalid={mutation.data?.status === "WRONG_PASSWORD"}
|
||||
>
|
||||
<Form.Label>
|
||||
{t("frontend.password_change.current_password_label")}
|
||||
@@ -141,14 +150,13 @@ function ChangePassword(): React.ReactNode {
|
||||
{t("frontend.errors.field_required")}
|
||||
</Form.ErrorMessage>
|
||||
|
||||
{result.data &&
|
||||
result.data.setPassword.status === "WRONG_PASSWORD" && (
|
||||
<Form.ErrorMessage>
|
||||
{t(
|
||||
"frontend.password_change.failure.description.wrong_password",
|
||||
)}
|
||||
</Form.ErrorMessage>
|
||||
)}
|
||||
{mutation.data && mutation.data.status === "WRONG_PASSWORD" && (
|
||||
<Form.ErrorMessage>
|
||||
{t(
|
||||
"frontend.password_change.failure.description.wrong_password",
|
||||
)}
|
||||
</Form.ErrorMessage>
|
||||
)}
|
||||
</Form.Field>
|
||||
|
||||
<Separator />
|
||||
@@ -156,14 +164,14 @@ function ChangePassword(): React.ReactNode {
|
||||
<PasswordCreationDoubleInput
|
||||
siteConfig={siteConfig}
|
||||
forceShowNewPasswordInvalid={
|
||||
(result.data &&
|
||||
result.data.setPassword.status === "INVALID_NEW_PASSWORD") ||
|
||||
(mutation.data &&
|
||||
mutation.data.status === "INVALID_NEW_PASSWORD") ||
|
||||
false
|
||||
}
|
||||
/>
|
||||
|
||||
<Form.Submit kind="primary" disabled={result.fetching}>
|
||||
{!!result.fetching && <LoadingSpinner inline />}
|
||||
<Form.Submit kind="primary" disabled={mutation.isPending}>
|
||||
{!!mutation.isPending && <LoadingSpinner inline />}
|
||||
{t("action.save")}
|
||||
</Form.Submit>
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordChangeQuery {
|
||||
viewer {
|
||||
__typename
|
||||
@@ -23,14 +25,11 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const Route = createFileRoute("/password/change/")({
|
||||
async loader({ context, abortController: { signal } }) {
|
||||
const queryResult = await context.client.query(
|
||||
QUERY,
|
||||
{},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (queryResult.error) throw queryResult.error;
|
||||
if (queryResult.data?.viewer.__typename !== "User") throw notFound();
|
||||
},
|
||||
export const query = queryOptions({
|
||||
queryKey: ["passwordChange"],
|
||||
queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/password/change/")({
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(query),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
createLazyFileRoute,
|
||||
useRouter,
|
||||
@@ -13,7 +14,6 @@ import IconLockSolid from "@vector-im/compound-design-tokens/assets/web/icons/lo
|
||||
import { Alert, Form } from "@vector-im/compound-web";
|
||||
import type { FormEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery } from "urql";
|
||||
|
||||
import BlockList from "../components/BlockList";
|
||||
import Layout from "../components/Layout";
|
||||
@@ -21,10 +21,10 @@ import LoadingSpinner from "../components/LoadingSpinner";
|
||||
import PageHeading from "../components/PageHeading";
|
||||
import PasswordCreationDoubleInput from "../components/PasswordCreationDoubleInput";
|
||||
import { graphql } from "../gql";
|
||||
import { SetPasswordStatus } from "../gql/graphql";
|
||||
import { translateSetPasswordError } from "../i18n/password_changes";
|
||||
|
||||
import { QUERY } from "./password.recovery.index";
|
||||
import { graphqlClient } from "../graphql";
|
||||
import { query } from "./password.recovery.index";
|
||||
|
||||
const RECOVER_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
|
||||
@@ -45,43 +45,55 @@ function RecoverPassword(): React.ReactNode {
|
||||
const { ticket } = useSearch({
|
||||
from: "/password/recovery/",
|
||||
});
|
||||
const [queryResult] = useQuery({ query: QUERY });
|
||||
const {
|
||||
data: { siteConfig },
|
||||
} = useSuspenseQuery(query);
|
||||
const router = useRouter();
|
||||
if (queryResult.error) throw queryResult.error;
|
||||
const siteConfig = queryResult.data?.siteConfig;
|
||||
if (!siteConfig) throw Error(); // This should never happen
|
||||
|
||||
const [result, changePassword] = useMutation(RECOVER_PASSWORD_MUTATION);
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
ticket,
|
||||
form,
|
||||
}: { ticket: string; form: FormData }) => {
|
||||
const newPassword = form.get("new_password") as string;
|
||||
const newPasswordAgain = form.get("new_password_again") as string;
|
||||
|
||||
if (newPassword !== newPasswordAgain) {
|
||||
throw new Error(
|
||||
"passwords mismatch; this should be checked by the form",
|
||||
);
|
||||
}
|
||||
|
||||
const response = await graphqlClient.request(RECOVER_PASSWORD_MUTATION, {
|
||||
ticket,
|
||||
newPassword,
|
||||
});
|
||||
|
||||
if (response.setPasswordByRecovery.status === "ALLOWED") {
|
||||
// Redirect to the application root using a full page load
|
||||
// The MAS backend will then redirect to the login page
|
||||
// Unfortunately this won't work in dev mode (`npm run dev`)
|
||||
// as the backend isn't involved there.
|
||||
const location = router.buildLocation({ to: "/" });
|
||||
window.location.href = location.href;
|
||||
}
|
||||
|
||||
return response.setPasswordByRecovery;
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const newPassword = formData.get("new_password") as string;
|
||||
const newPasswordAgain = formData.get("new_password_again") as string;
|
||||
|
||||
if (newPassword !== newPasswordAgain) {
|
||||
throw new Error("passwords mismatch; this should be checked by the form");
|
||||
}
|
||||
|
||||
const response = await changePassword({ ticket, newPassword });
|
||||
|
||||
if (response.data?.setPasswordByRecovery.status === "ALLOWED") {
|
||||
// Redirect to the application root using a full page load
|
||||
// The MAS backend will then redirect to the login page
|
||||
// Unfortunately this won't work in dev mode (`npm run dev`)
|
||||
// as the backend isn't involved there.
|
||||
const location = router.buildLocation({ to: "/" });
|
||||
window.location.href = location.href;
|
||||
}
|
||||
const form = new FormData(event.currentTarget);
|
||||
mutation.mutate({ ticket, form });
|
||||
};
|
||||
|
||||
const unhandleableError = result.error !== undefined;
|
||||
const unhandleableError = mutation.error !== undefined;
|
||||
|
||||
const errorMsg: string | undefined = translateSetPasswordError(
|
||||
t,
|
||||
result.data?.setPasswordByRecovery.status,
|
||||
mutation.data?.status,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -120,15 +132,12 @@ function RecoverPassword(): React.ReactNode {
|
||||
<PasswordCreationDoubleInput
|
||||
siteConfig={siteConfig}
|
||||
forceShowNewPasswordInvalid={
|
||||
(result.data &&
|
||||
result.data.setPasswordByRecovery.status ===
|
||||
"INVALID_NEW_PASSWORD") ||
|
||||
false
|
||||
mutation.data?.status === "INVALID_NEW_PASSWORD" || false
|
||||
}
|
||||
/>
|
||||
|
||||
<Form.Submit kind="primary" disabled={result.fetching}>
|
||||
{!!result.fetching && <LoadingSpinner inline />}
|
||||
<Form.Submit kind="primary" disabled={mutation.isPending}>
|
||||
{!!mutation.isPending && <LoadingSpinner inline />}
|
||||
{t("action.save_and_continue")}
|
||||
</Form.Submit>
|
||||
</Form.Root>
|
||||
|
||||
@@ -8,9 +8,11 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
|
||||
import * as z from "zod";
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordRecoveryQuery {
|
||||
siteConfig {
|
||||
id
|
||||
@@ -19,6 +21,11 @@ export const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const query = queryOptions({
|
||||
queryKey: ["passwordRecovery"],
|
||||
queryFn: ({ signal }) => graphqlClient.request({ document: QUERY, signal }),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
ticket: z.string(),
|
||||
});
|
||||
@@ -26,12 +33,5 @@ const schema = z.object({
|
||||
export const Route = createFileRoute("/password/recovery/")({
|
||||
validateSearch: zodSearchValidator(schema),
|
||||
|
||||
async loader({ context, abortController: { signal } }) {
|
||||
const queryResult = await context.client.query(
|
||||
QUERY,
|
||||
{},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (queryResult.error) throw queryResult.error;
|
||||
},
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(query),
|
||||
});
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import IconCheck from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||
import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info";
|
||||
import { Button, Text } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery } from "urql";
|
||||
import { ButtonLink } from "../components/ButtonLink";
|
||||
import LoadingSpinner from "../components/LoadingSpinner";
|
||||
import PageHeading from "../components/PageHeading";
|
||||
@@ -19,6 +23,7 @@ import {
|
||||
VisualListItem,
|
||||
} from "../components/VisualList/VisualList";
|
||||
import { graphql } from "../gql";
|
||||
import { graphqlClient } from "../graphql";
|
||||
|
||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerQuery {
|
||||
@@ -31,16 +36,18 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const currentViewerQuery = queryOptions({
|
||||
queryKey: ["currentViewer"],
|
||||
queryFn: ({ signal }) =>
|
||||
graphqlClient.request({
|
||||
document: CURRENT_VIEWER_QUERY,
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/reset-cross-signing/")({
|
||||
async loader({ context, abortController: { signal } }) {
|
||||
const viewer = await context.client.query(
|
||||
CURRENT_VIEWER_QUERY,
|
||||
{},
|
||||
{ fetchOptions: { signal } },
|
||||
);
|
||||
if (viewer.error) throw viewer.error;
|
||||
if (viewer.data?.viewer.__typename !== "User") throw notFound();
|
||||
},
|
||||
loader: ({ context }) =>
|
||||
context.queryClient.ensureQueryData(currentViewerQuery),
|
||||
|
||||
component: ResetCrossSigning,
|
||||
});
|
||||
@@ -68,30 +75,36 @@ function ResetCrossSigning(): React.ReactNode {
|
||||
const { deepLink } = Route.useSearch();
|
||||
const navigate = Route.useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [viewer] = useQuery({ query: CURRENT_VIEWER_QUERY });
|
||||
if (viewer.error) throw viewer.error;
|
||||
if (viewer.data?.viewer.__typename !== "User") throw notFound();
|
||||
const userId = viewer.data.viewer.id;
|
||||
const {
|
||||
data: { viewer },
|
||||
} = useSuspenseQuery(currentViewerQuery);
|
||||
if (viewer.__typename !== "User") throw notFound();
|
||||
const userId = viewer.id;
|
||||
|
||||
const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION);
|
||||
if (result.error) throw result.error;
|
||||
const success = !!result.data;
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (userId: string) =>
|
||||
graphqlClient.request(ALLOW_CROSS_SIGING_RESET_MUTATION, {
|
||||
userId,
|
||||
}),
|
||||
|
||||
onSuccess: () => {
|
||||
setTimeout(() => {
|
||||
// Synapse may fling the user here via UIA fallback,
|
||||
// this is part of the API to signal completion to the calling client
|
||||
// https://spec.matrix.org/v1.11/client-server-api/#fallback
|
||||
if (window.onAuthDone) {
|
||||
window.onAuthDone();
|
||||
} else if (window.opener?.postMessage) {
|
||||
window.opener.postMessage("authDone", "*");
|
||||
}
|
||||
});
|
||||
|
||||
navigate({ to: "/reset-cross-signing/success", replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const onClick = async (): Promise<void> => {
|
||||
await allowReset({ userId });
|
||||
|
||||
setTimeout(() => {
|
||||
// Synapse may fling the user here via UIA fallback,
|
||||
// this is part of the API to signal completion to the calling client
|
||||
// https://spec.matrix.org/v1.11/client-server-api/#fallback
|
||||
if (window.onAuthDone) {
|
||||
window.onAuthDone();
|
||||
} else if (window.opener?.postMessage) {
|
||||
window.opener.postMessage("authDone", "*");
|
||||
}
|
||||
});
|
||||
|
||||
navigate({ to: "/reset-cross-signing/success", replace: true });
|
||||
mutation.mutate(userId);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -129,10 +142,10 @@ function ResetCrossSigning(): React.ReactNode {
|
||||
<Button
|
||||
kind="primary"
|
||||
destructive
|
||||
disabled={result.fetching}
|
||||
disabled={mutation.isPending}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!!result.fetching && <LoadingSpinner inline />}
|
||||
{!!mutation.isPending && <LoadingSpinner inline />}
|
||||
{t("frontend.reset_cross_signing.finish_reset")}
|
||||
</Button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user