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: [],
|
||||
},
|
||||
"./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;
|
||||
|
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@urql/core": "^4.0.6",
|
||||
"@urql/exchange-graphcache": "^6.0.2",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@urql/core": "^4.0.7",
|
||||
"@urql/exchange-graphcache": "^6.0.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"graphql": "^16.6.0",
|
||||
"jotai": "^2.0.4",
|
||||
"jotai-devtools": "^0.4.0",
|
||||
"jotai-location": "^0.5.1",
|
||||
"jotai-urql": "^0.7.1",
|
||||
"react": "^18.2.0",
|
||||
@ -28,38 +30,38 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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-eslint/eslint-plugin": "^3.18.0",
|
||||
"@storybook/addon-actions": "^7.0.6",
|
||||
"@storybook/addon-backgrounds": "^7.0.6",
|
||||
"@storybook/addon-controls": "^7.0.6",
|
||||
"@storybook/addon-docs": "^7.0.6",
|
||||
"@storybook/addon-essentials": "^7.0.6",
|
||||
"@storybook/addon-measure": "^7.0.6",
|
||||
"@storybook/addon-outline": "^7.0.6",
|
||||
"@storybook/addon-toolbars": "^7.0.6",
|
||||
"@storybook/addon-viewport": "^7.0.6",
|
||||
"@storybook/react": "^7.0.6",
|
||||
"@storybook/react-vite": "^7.0.6",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@storybook/addon-actions": "^7.0.7",
|
||||
"@storybook/addon-backgrounds": "^7.0.7",
|
||||
"@storybook/addon-controls": "^7.0.7",
|
||||
"@storybook/addon-docs": "^7.0.7",
|
||||
"@storybook/addon-essentials": "^7.0.7",
|
||||
"@storybook/addon-measure": "^7.0.7",
|
||||
"@storybook/addon-outline": "^7.0.7",
|
||||
"@storybook/addon-toolbars": "^7.0.7",
|
||||
"@storybook/addon-viewport": "^7.0.7",
|
||||
"@storybook/react": "^7.0.7",
|
||||
"@storybook/react-vite": "^7.0.7",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitest/coverage-c8": "^0.30.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"storybook": "^7.0.6",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"storybook": "^7.0.7",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.1",
|
||||
"vite": "^4.3.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-graphql-codegen": "^3.2.0",
|
||||
"vitest": "^0.30.1"
|
||||
|
@ -25,14 +25,14 @@ type Location = {
|
||||
};
|
||||
|
||||
type HomeRoute = { type: "home" };
|
||||
type DumbRoute = { type: "dumb" };
|
||||
type AccountRoute = { type: "account" };
|
||||
type OAuth2ClientRoute = { type: "client"; id: string };
|
||||
type BrowserSessionRoute = { type: "session"; id: string };
|
||||
type UnknownRoute = { type: "unknown"; segments: string[] };
|
||||
|
||||
export type Route =
|
||||
| HomeRoute
|
||||
| DumbRoute
|
||||
| AccountRoute
|
||||
| OAuth2ClientRoute
|
||||
| BrowserSessionRoute
|
||||
| UnknownRoute;
|
||||
@ -41,8 +41,8 @@ const routeToSegments = (route: Route): string[] => {
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return [];
|
||||
case "dumb":
|
||||
return ["dumb"];
|
||||
case "account":
|
||||
return ["account"];
|
||||
case "client":
|
||||
return ["client", route.id];
|
||||
case "session":
|
||||
@ -57,8 +57,8 @@ const segmentsToRoute = (segments: string[]): Route => {
|
||||
return { type: "home" };
|
||||
}
|
||||
|
||||
if (segments.length === 1 && segments[0] === "dumb") {
|
||||
return { type: "dumb" };
|
||||
if (segments.length === 1 && segments[0] === "account") {
|
||||
return { type: "account" };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "client") {
|
||||
@ -105,6 +105,7 @@ export const routeAtom = atom(
|
||||
);
|
||||
|
||||
const Home = lazy(() => import("./pages/Home"));
|
||||
const Account = lazy(() => import("./pages/Account"));
|
||||
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
|
||||
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
|
||||
|
||||
@ -114,12 +115,12 @@ const InnerRouter: React.FC = () => {
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return <Home />;
|
||||
case "account":
|
||||
return <Account />;
|
||||
case "client":
|
||||
return <OAuth2Client id={route.id} />;
|
||||
case "session":
|
||||
return <BrowserSession id={route.id} />;
|
||||
case "dumb":
|
||||
return <>Dumb route.</>;
|
||||
case "unknown":
|
||||
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 */
|
||||
compact?: boolean;
|
||||
|
||||
/** Uses the 'ghotst' (outline) alternative */
|
||||
/** Uses the 'ghost' (outline) alternative */
|
||||
ghost?: boolean;
|
||||
|
||||
/** Disables all interactions with the button */
|
||||
disabled?: boolean;
|
||||
|
||||
/** The type of the button */
|
||||
type?: "button" | "submit" | "reset";
|
||||
} & React.HTMLProps<HTMLButtonElement>;
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
@ -31,12 +34,13 @@ const Button: React.FC<Props> = ({
|
||||
compact,
|
||||
ghost,
|
||||
disabled,
|
||||
type,
|
||||
...props
|
||||
}) => {
|
||||
const sizeClass = compact ? "py-1 px-3" : "py-1 px-5";
|
||||
|
||||
let ghostClass = "";
|
||||
let normalClass = "";
|
||||
let ghostClass;
|
||||
let normalClass;
|
||||
|
||||
if (disabled) {
|
||||
ghostClass = "opacity-30 border border-accent text-accent";
|
||||
@ -52,7 +56,7 @@ const Button: React.FC<Props> = ({
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
type={type || "button"}
|
||||
className={`rounded-lg font-semibold ${colors} ${sizeClass}`}
|
||||
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">
|
||||
<NavBar className="mx-auto px-3 py-4 container">
|
||||
<NavItem route={{ type: "home" }}>Home</NavItem>
|
||||
<NavItem route={{ type: "dumb" }}>Dumb</NavItem>
|
||||
<NavItem route={{ type: "account" }}>My Account</NavItem>
|
||||
</NavBar>
|
||||
|
||||
<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 */
|
||||
import * as types from './graphql';
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
import * as types from "./graphql";
|
||||
import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 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,
|
||||
"\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 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 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,
|
||||
"\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":
|
||||
types.AddEmailDocument,
|
||||
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
|
||||
types.BrowserSession_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":
|
||||
types.BrowserSessionList_UserFragmentDoc,
|
||||
"\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 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
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 ReactDOM from "react-dom/client";
|
||||
import { Provider } from "jotai";
|
||||
import { DevTools } from "jotai-devtools";
|
||||
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import Router from "./Router";
|
||||
@ -23,6 +24,7 @@ import { HydrateAtoms } from "./atoms";
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<Provider>
|
||||
<DevTools />
|
||||
<HydrateAtoms>
|
||||
<React.Suspense fallback={<LoadingScreen />}>
|
||||
<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: [
|
||||
codegen(),
|
||||
react(),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
"jotai/babel/plugin-react-refresh",
|
||||
"jotai/babel/plugin-debug-label",
|
||||
],
|
||||
},
|
||||
}),
|
||||
eslint({
|
||||
// Explicitly set the config file, else storybook gets confused
|
||||
overrideConfigFile: "./.eslintrc.cjs",
|
||||
exclude: ["./src/gql/*"],
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
|
Reference in New Issue
Block a user