You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
WIP my account page
This commit is contained in:
@ -10,10 +10,17 @@ const config: CodegenConfig = {
|
|||||||
plugins: [],
|
plugins: [],
|
||||||
},
|
},
|
||||||
"./src/gql/schema.ts": {
|
"./src/gql/schema.ts": {
|
||||||
plugins: ["urql-introspection"],
|
plugins: [
|
||||||
|
{
|
||||||
|
add: {
|
||||||
|
content: "/* eslint-disable */",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"urql-introspection",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hooks: { afterAllFileWrite: ["eslint --fix"] },
|
hooks: { afterOneFileWrite: ["prettier --write"] },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
1677
frontend/package-lock.json
generated
1677
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,11 +16,13 @@
|
|||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urql/core": "^4.0.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@urql/exchange-graphcache": "^6.0.2",
|
"@urql/core": "^4.0.7",
|
||||||
|
"@urql/exchange-graphcache": "^6.0.3",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"jotai": "^2.0.4",
|
"jotai": "^2.0.4",
|
||||||
|
"jotai-devtools": "^0.4.0",
|
||||||
"jotai-location": "^0.5.1",
|
"jotai-location": "^0.5.1",
|
||||||
"jotai-urql": "^0.7.1",
|
"jotai-urql": "^0.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -28,38 +30,38 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^3.3.1",
|
"@graphql-codegen/cli": "^3.3.1",
|
||||||
"@graphql-codegen/client-preset": "^3.0.0",
|
"@graphql-codegen/client-preset": "^3.0.1",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||||
"@graphql-eslint/eslint-plugin": "^3.18.0",
|
"@graphql-eslint/eslint-plugin": "^3.18.0",
|
||||||
"@storybook/addon-actions": "^7.0.6",
|
"@storybook/addon-actions": "^7.0.7",
|
||||||
"@storybook/addon-backgrounds": "^7.0.6",
|
"@storybook/addon-backgrounds": "^7.0.7",
|
||||||
"@storybook/addon-controls": "^7.0.6",
|
"@storybook/addon-controls": "^7.0.7",
|
||||||
"@storybook/addon-docs": "^7.0.6",
|
"@storybook/addon-docs": "^7.0.7",
|
||||||
"@storybook/addon-essentials": "^7.0.6",
|
"@storybook/addon-essentials": "^7.0.7",
|
||||||
"@storybook/addon-measure": "^7.0.6",
|
"@storybook/addon-measure": "^7.0.7",
|
||||||
"@storybook/addon-outline": "^7.0.6",
|
"@storybook/addon-outline": "^7.0.7",
|
||||||
"@storybook/addon-toolbars": "^7.0.6",
|
"@storybook/addon-toolbars": "^7.0.7",
|
||||||
"@storybook/addon-viewport": "^7.0.6",
|
"@storybook/addon-viewport": "^7.0.7",
|
||||||
"@storybook/react": "^7.0.6",
|
"@storybook/react": "^7.0.7",
|
||||||
"@storybook/react-vite": "^7.0.6",
|
"@storybook/react-vite": "^7.0.7",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.1",
|
||||||
"@types/react": "^18.0.37",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.2.1",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"@vitest/coverage-c8": "^0.30.1",
|
"@vitest/coverage-c8": "^0.30.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"react-test-renderer": "^18.2.0",
|
"react-test-renderer": "^18.2.0",
|
||||||
"storybook": "^7.0.6",
|
"storybook": "^7.0.7",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vite": "^4.3.1",
|
"vite": "^4.3.2",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-graphql-codegen": "^3.2.0",
|
"vite-plugin-graphql-codegen": "^3.2.0",
|
||||||
"vitest": "^0.30.1"
|
"vitest": "^0.30.1"
|
||||||
|
@ -25,14 +25,14 @@ type Location = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type HomeRoute = { type: "home" };
|
type HomeRoute = { type: "home" };
|
||||||
type DumbRoute = { type: "dumb" };
|
type AccountRoute = { type: "account" };
|
||||||
type OAuth2ClientRoute = { type: "client"; id: string };
|
type OAuth2ClientRoute = { type: "client"; id: string };
|
||||||
type BrowserSessionRoute = { type: "session"; id: string };
|
type BrowserSessionRoute = { type: "session"; id: string };
|
||||||
type UnknownRoute = { type: "unknown"; segments: string[] };
|
type UnknownRoute = { type: "unknown"; segments: string[] };
|
||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
| HomeRoute
|
| HomeRoute
|
||||||
| DumbRoute
|
| AccountRoute
|
||||||
| OAuth2ClientRoute
|
| OAuth2ClientRoute
|
||||||
| BrowserSessionRoute
|
| BrowserSessionRoute
|
||||||
| UnknownRoute;
|
| UnknownRoute;
|
||||||
@ -41,8 +41,8 @@ const routeToSegments = (route: Route): string[] => {
|
|||||||
switch (route.type) {
|
switch (route.type) {
|
||||||
case "home":
|
case "home":
|
||||||
return [];
|
return [];
|
||||||
case "dumb":
|
case "account":
|
||||||
return ["dumb"];
|
return ["account"];
|
||||||
case "client":
|
case "client":
|
||||||
return ["client", route.id];
|
return ["client", route.id];
|
||||||
case "session":
|
case "session":
|
||||||
@ -57,8 +57,8 @@ const segmentsToRoute = (segments: string[]): Route => {
|
|||||||
return { type: "home" };
|
return { type: "home" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segments.length === 1 && segments[0] === "dumb") {
|
if (segments.length === 1 && segments[0] === "account") {
|
||||||
return { type: "dumb" };
|
return { type: "account" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segments.length === 2 && segments[0] === "client") {
|
if (segments.length === 2 && segments[0] === "client") {
|
||||||
@ -105,6 +105,7 @@ export const routeAtom = atom(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Home = lazy(() => import("./pages/Home"));
|
const Home = lazy(() => import("./pages/Home"));
|
||||||
|
const Account = lazy(() => import("./pages/Account"));
|
||||||
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
|
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
|
||||||
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
|
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
|
||||||
|
|
||||||
@ -114,12 +115,12 @@ const InnerRouter: React.FC = () => {
|
|||||||
switch (route.type) {
|
switch (route.type) {
|
||||||
case "home":
|
case "home":
|
||||||
return <Home />;
|
return <Home />;
|
||||||
|
case "account":
|
||||||
|
return <Account />;
|
||||||
case "client":
|
case "client":
|
||||||
return <OAuth2Client id={route.id} />;
|
return <OAuth2Client id={route.id} />;
|
||||||
case "session":
|
case "session":
|
||||||
return <BrowserSession id={route.id} />;
|
return <BrowserSession id={route.id} />;
|
||||||
case "dumb":
|
|
||||||
return <>Dumb route.</>;
|
|
||||||
case "unknown":
|
case "unknown":
|
||||||
return <>Unknown route {JSON.stringify(route.segments)}</>;
|
return <>Unknown route {JSON.stringify(route.segments)}</>;
|
||||||
}
|
}
|
||||||
|
88
frontend/src/components/AddEmailForm.tsx
Normal file
88
frontend/src/components/AddEmailForm.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React, { useRef, useTransition } from "react";
|
||||||
|
import { atomWithMutation } from "jotai-urql";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { graphql } from "../gql";
|
||||||
|
import Button from "./Button";
|
||||||
|
import UserEmail from "./UserEmail";
|
||||||
|
import Input from "./Input";
|
||||||
|
import Typography from "./Typography";
|
||||||
|
|
||||||
|
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||||
|
mutation AddEmail($userId: ID!, $email: String!) {
|
||||||
|
addEmail(input: { userId: $userId, email: $email }) {
|
||||||
|
status
|
||||||
|
email {
|
||||||
|
id
|
||||||
|
...UserEmail_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const addUserEmailAtom = atomWithMutation(ADD_EMAIL_MUTATION);
|
||||||
|
|
||||||
|
const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const [addEmailResult, addEmail] = useAtom(addUserEmailAtom);
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const email = e.currentTarget.email.value;
|
||||||
|
startTransition(() => {
|
||||||
|
addEmail({ userId, email }).then(() => {
|
||||||
|
if (formRef.current) {
|
||||||
|
formRef.current.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{addEmailResult.data?.addEmail.status === "ADDED" && (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
<Typography variant="subtitle">Email added!</Typography>
|
||||||
|
</div>
|
||||||
|
<UserEmail email={addEmailResult.data?.addEmail.email} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{addEmailResult.data?.addEmail.status === "EXISTS" && (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
<Typography variant="subtitle">Email already exists!</Typography>
|
||||||
|
</div>
|
||||||
|
<UserEmail email={addEmailResult.data?.addEmail.email} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<form className="flex" onSubmit={handleSubmit} ref={formRef}>
|
||||||
|
<Input
|
||||||
|
className="flex-1 mr-2"
|
||||||
|
disabled={pending}
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
/>
|
||||||
|
<Button disabled={pending} type="submit">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddEmailForm;
|
@ -19,11 +19,14 @@ type Props = {
|
|||||||
/** Makes the button more compact */
|
/** Makes the button more compact */
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
|
||||||
/** Uses the 'ghotst' (outline) alternative */
|
/** Uses the 'ghost' (outline) alternative */
|
||||||
ghost?: boolean;
|
ghost?: boolean;
|
||||||
|
|
||||||
/** Disables all interactions with the button */
|
/** Disables all interactions with the button */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/** The type of the button */
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
} & React.HTMLProps<HTMLButtonElement>;
|
} & React.HTMLProps<HTMLButtonElement>;
|
||||||
|
|
||||||
const Button: React.FC<Props> = ({
|
const Button: React.FC<Props> = ({
|
||||||
@ -31,12 +34,13 @@ const Button: React.FC<Props> = ({
|
|||||||
compact,
|
compact,
|
||||||
ghost,
|
ghost,
|
||||||
disabled,
|
disabled,
|
||||||
|
type,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const sizeClass = compact ? "py-1 px-3" : "py-1 px-5";
|
const sizeClass = compact ? "py-1 px-3" : "py-1 px-5";
|
||||||
|
|
||||||
let ghostClass = "";
|
let ghostClass;
|
||||||
let normalClass = "";
|
let normalClass;
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
ghostClass = "opacity-30 border border-accent text-accent";
|
ghostClass = "opacity-30 border border-accent text-accent";
|
||||||
@ -52,7 +56,7 @@ const Button: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type="button"
|
type={type || "button"}
|
||||||
className={`rounded-lg font-semibold ${colors} ${sizeClass}`}
|
className={`rounded-lg font-semibold ${colors} ${sizeClass}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
50
frontend/src/components/Input.stories.ts
Normal file
50
frontend/src/components/Input.stories.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
import Input from "./Input";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "UI/Input",
|
||||||
|
component: Input,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
argTypes: {
|
||||||
|
value: {
|
||||||
|
control: "text",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: "text",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
value: "",
|
||||||
|
placeholder: "Placeholder",
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Input>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Input>;
|
||||||
|
|
||||||
|
export const Basic: Story = {};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
28
frontend/src/components/Input.tsx
Normal file
28
frontend/src/components/Input.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLProps<HTMLInputElement>;
|
||||||
|
|
||||||
|
const Input: React.FC<Props> = ({ disabled, className, ...props }) => {
|
||||||
|
const disabledClass = disabled
|
||||||
|
? "bg-grey-50 dark:bg-grey-400"
|
||||||
|
: "bg-white dark:bg-grey-450";
|
||||||
|
const fullClassName = `${className} px-2 py-1 border-2 border-grey-50 dark:border-grey-400 dark:text-white placeholder-grey-100 dark:placeholder-grey-150 rounded-lg ${disabledClass}`;
|
||||||
|
return <input disabled={disabled} className={fullClassName} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
@ -20,7 +20,7 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
|||||||
<div className="bg-grey-25 text-black-900 dark:bg-black-800 dark:text-white flex flex-col min-h-screen">
|
<div className="bg-grey-25 text-black-900 dark:bg-black-800 dark:text-white flex flex-col min-h-screen">
|
||||||
<NavBar className="mx-auto px-3 py-4 container">
|
<NavBar className="mx-auto px-3 py-4 container">
|
||||||
<NavItem route={{ type: "home" }}>Home</NavItem>
|
<NavItem route={{ type: "home" }}>Home</NavItem>
|
||||||
<NavItem route={{ type: "dumb" }}>Dumb</NavItem>
|
<NavItem route={{ type: "account" }}>My Account</NavItem>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
|
||||||
<main className="mx-auto p-4 container">{children}</main>
|
<main className="mx-auto p-4 container">{children}</main>
|
||||||
|
53
frontend/src/components/UserEmail.tsx
Normal file
53
frontend/src/components/UserEmail.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
|
import React from "react";
|
||||||
|
import Block from "./Block";
|
||||||
|
import Typography, { Bold } from "./Typography";
|
||||||
|
import DateTime from "./DateTime";
|
||||||
|
|
||||||
|
const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
|
fragment UserEmail_email on UserEmail {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
confirmedAt
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const UserEmail: React.FC<{ email: FragmentType<typeof FRAGMENT> }> = ({
|
||||||
|
email,
|
||||||
|
}) => {
|
||||||
|
const data = useFragment(FRAGMENT, email);
|
||||||
|
return (
|
||||||
|
<Block>
|
||||||
|
<Typography variant="caption">
|
||||||
|
<Bold>{data.email}</Bold>
|
||||||
|
{data.confirmedAt ? "" : " (not verified)"}
|
||||||
|
</Typography>
|
||||||
|
{data.confirmedAt ? (
|
||||||
|
<Typography variant="micro">
|
||||||
|
Verified <DateTime datetime={data.confirmedAt} />
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="micro">
|
||||||
|
Added <DateTime datetime={data.createdAt} />
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserEmail;
|
108
frontend/src/components/UserEmailList.tsx
Normal file
108
frontend/src/components/UserEmailList.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { atomWithQuery } from "jotai-urql";
|
||||||
|
import { atomFamily } from "jotai/utils";
|
||||||
|
import deepEqual from "fast-deep-equal";
|
||||||
|
|
||||||
|
import { graphql } from "../gql";
|
||||||
|
import { useTransition } from "react";
|
||||||
|
import Button from "./Button";
|
||||||
|
import UserEmail from "./UserEmail";
|
||||||
|
import BlockList from "./BlockList";
|
||||||
|
|
||||||
|
const QUERY = graphql(/* GraphQL */ `
|
||||||
|
query UserEmailListQuery($userId: ID!, $first: Int!, $after: String) {
|
||||||
|
user(id: $userId) {
|
||||||
|
id
|
||||||
|
emails(first: $first, after: $after) {
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
...UserEmail_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const emailPageResultFamily = atomFamily(
|
||||||
|
({ userId, after }: { userId: string; after: string | null }) =>
|
||||||
|
atomWithQuery({
|
||||||
|
query: QUERY,
|
||||||
|
getVariables: () => ({ userId, first: 5, after }),
|
||||||
|
}),
|
||||||
|
deepEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailPageListFamily = atomFamily((_userId: string) =>
|
||||||
|
atom([null as string | null])
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailNextPageFamily = atomFamily((userId: string) =>
|
||||||
|
atom(null, (get, set, after: string) => {
|
||||||
|
const currentList = get(emailPageListFamily(userId));
|
||||||
|
set(emailPageListFamily(userId), [...currentList, after]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailPageFamily = atomFamily((userId: string) =>
|
||||||
|
atom(async (get) => {
|
||||||
|
const list = get(emailPageListFamily(userId));
|
||||||
|
return await Promise.all(
|
||||||
|
list.map((after) => get(emailPageResultFamily({ userId, after })))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserEmailList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const result = useAtomValue(emailPageFamily(userId));
|
||||||
|
const setLoadNextPage = useSetAtom(emailNextPageFamily(userId));
|
||||||
|
const endPageInfo = result[result.length - 1]?.data?.user?.emails?.pageInfo;
|
||||||
|
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (endPageInfo?.hasNextPage && endPageInfo.endCursor) {
|
||||||
|
const cursor = endPageInfo.endCursor;
|
||||||
|
startTransition(() => {
|
||||||
|
setLoadNextPage(cursor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockList>
|
||||||
|
{result.flatMap(
|
||||||
|
(page) =>
|
||||||
|
page.data?.user?.emails?.edges?.map((edge) => (
|
||||||
|
<UserEmail email={edge.node} key={edge.cursor} />
|
||||||
|
)) || []
|
||||||
|
)}
|
||||||
|
{endPageInfo?.hasNextPage && (
|
||||||
|
<Button compact ghost onClick={loadNextPage}>
|
||||||
|
{pending ? "Loading..." : "Load more"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</BlockList>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserEmailList;
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import * as types from './graphql';
|
import * as types from "./graphql";
|
||||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of all GraphQL operations in the project.
|
* Map of all GraphQL operations in the project.
|
||||||
@ -13,15 +13,34 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
|
|||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||||
*/
|
*/
|
||||||
const documents = {
|
const documents = {
|
||||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": types.BrowserSession_SessionFragmentDoc,
|
"\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
|
||||||
"\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n": types.BrowserSessionList_UserFragmentDoc,
|
types.AddEmailDocument,
|
||||||
"\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n": types.CompatSsoLogin_LoginFragmentDoc,
|
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
||||||
"\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n": types.CompatSsoLoginList_UserFragmentDoc,
|
types.BrowserSession_SessionFragmentDoc,
|
||||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
|
"\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n":
|
||||||
"\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n": types.OAuth2SessionList_UserFragmentDoc,
|
types.BrowserSessionList_UserFragmentDoc,
|
||||||
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n": types.BrowserSessionQueryDocument,
|
"\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n":
|
||||||
"\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n": types.HomeQueryDocument,
|
types.CompatSsoLogin_LoginFragmentDoc,
|
||||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n": types.OAuth2ClientQueryDocument,
|
"\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n":
|
||||||
|
types.CompatSsoLoginList_UserFragmentDoc,
|
||||||
|
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n":
|
||||||
|
types.OAuth2Session_SessionFragmentDoc,
|
||||||
|
"\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n":
|
||||||
|
types.OAuth2SessionList_UserFragmentDoc,
|
||||||
|
"\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n":
|
||||||
|
types.UserEmail_EmailFragmentDoc,
|
||||||
|
"\n query UserEmailListQuery($userId: ID!, $first: Int!, $after: String) {\n user(id: $userId) {\n id\n emails(first: $first, after: $after) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n":
|
||||||
|
types.UserEmailListQueryDocument,
|
||||||
|
"\n query CurrentUserQuery {\n viewer {\n ... on User {\n __typename\n id\n }\n }\n }\n":
|
||||||
|
types.CurrentUserQueryDocument,
|
||||||
|
"\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n":
|
||||||
|
types.AccountQueryDocument,
|
||||||
|
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
|
||||||
|
types.BrowserSessionQueryDocument,
|
||||||
|
"\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n":
|
||||||
|
types.HomeQueryDocument,
|
||||||
|
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n":
|
||||||
|
types.OAuth2ClientQueryDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,42 +60,91 @@ export function graphql(source: string): unknown;
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n"): (typeof documents)["\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n"): (typeof documents)["\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment BrowserSessionList_user on User {\n browserSessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n"): (typeof documents)["\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n"): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n"): (typeof documents)["\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: $count, after: $cursor) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"): (typeof documents)["\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n"
|
||||||
|
): (typeof documents)["\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n"): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n"];
|
export function graphql(
|
||||||
|
source: "\n query UserEmailListQuery($userId: ID!, $first: Int!, $after: String) {\n user(id: $userId) {\n id\n emails(first: $first, after: $after) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query UserEmailListQuery($userId: ID!, $first: Int!, $after: String) {\n user(id: $userId) {\n id\n emails(first: $first, after: $after) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: "\n query CurrentUserQuery {\n viewer {\n ... on User {\n __typename\n id\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query CurrentUserQuery {\n viewer {\n ... on User {\n __typename\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.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: "\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: "\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query HomeQuery($count: Int!, $cursor: String) {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(
|
||||||
|
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n"
|
||||||
|
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n"];
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
|
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
|
||||||
|
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { Provider } from "jotai";
|
import { Provider } from "jotai";
|
||||||
|
import { DevTools } from "jotai-devtools";
|
||||||
|
|
||||||
import LoadingScreen from "./components/LoadingScreen";
|
import LoadingScreen from "./components/LoadingScreen";
|
||||||
import Router from "./Router";
|
import Router from "./Router";
|
||||||
@ -23,6 +24,7 @@ import { HydrateAtoms } from "./atoms";
|
|||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider>
|
<Provider>
|
||||||
|
<DevTools />
|
||||||
<HydrateAtoms>
|
<HydrateAtoms>
|
||||||
<React.Suspense fallback={<LoadingScreen />}>
|
<React.Suspense fallback={<LoadingScreen />}>
|
||||||
<Router />
|
<Router />
|
||||||
|
76
frontend/src/pages/Account.tsx
Normal file
76
frontend/src/pages/Account.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { atomFamily } from "jotai/utils";
|
||||||
|
import { atomWithQuery } from "jotai-urql";
|
||||||
|
|
||||||
|
import { graphql } from "../gql";
|
||||||
|
import UserEmailList from "../components/UserEmailList";
|
||||||
|
import { Title } from "../components/Typography";
|
||||||
|
import AddEmailForm from "../components/AddEmailForm";
|
||||||
|
|
||||||
|
const CURRENT_USER_QUERY = graphql(/* GraphQL */ `
|
||||||
|
query CurrentUserQuery {
|
||||||
|
viewer {
|
||||||
|
... on User {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const currentUserAtom = atomWithQuery({ query: CURRENT_USER_QUERY });
|
||||||
|
|
||||||
|
const QUERY = graphql(/* GraphQL */ `
|
||||||
|
query AccountQuery($id: ID!) {
|
||||||
|
user(id: $id) {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const accountAtomFamily = atomFamily((id: string) =>
|
||||||
|
atomWithQuery({ query: QUERY, getVariables: () => ({ id }) })
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserAccount: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
const result = useAtomValue(accountAtomFamily(id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Title>Hello {result.data?.user?.username}</Title>
|
||||||
|
<UserEmailList userId={id} />
|
||||||
|
<AddEmailForm userId={id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentUserAccount: React.FC = () => {
|
||||||
|
const result = useAtomValue(currentUserAtom);
|
||||||
|
if (result.data?.viewer?.__typename === "User") {
|
||||||
|
return (
|
||||||
|
<div className="w-96 mx-auto">
|
||||||
|
<UserAccount id={result.data.viewer.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="w-96 mx-auto">Not logged in.</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CurrentUserAccount;
|
@ -27,10 +27,18 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
codegen(),
|
codegen(),
|
||||||
react(),
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
"jotai/babel/plugin-react-refresh",
|
||||||
|
"jotai/babel/plugin-debug-label",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
eslint({
|
eslint({
|
||||||
// Explicitly set the config file, else storybook gets confused
|
// Explicitly set the config file, else storybook gets confused
|
||||||
overrideConfigFile: "./.eslintrc.cjs",
|
overrideConfigFile: "./.eslintrc.cjs",
|
||||||
|
exclude: ["./src/gql/*"],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
|
Reference in New Issue
Block a user