Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Commit

Permalink
Finish replacing jotai-urql with bare urql
Browse files Browse the repository at this point in the history
Mostly remaining was pagination related stuff & fixing tests
  • Loading branch information
sandhose committed Feb 15, 2024
1 parent 3f90839 commit f0f7497
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 454 deletions.
1 change: 0 additions & 1 deletion frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
}
},
"app_sessions_list": {
"error": "Failed to load app sessions",
"heading": "Apps"
},
"browser_session_details": {
Expand Down
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"jotai": "^2.6.4",
"jotai-devtools": "^0.7.1",
"jotai-location": "^0.5.2",
"jotai-urql": "^0.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.5",
Expand Down
88 changes: 0 additions & 88 deletions frontend/src/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,88 +0,0 @@
// 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 { AnyVariables, CombinedError, OperationContext } from "@urql/core";
import { atom, WritableAtom } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { AtomWithQuery, clientAtom } from "jotai-urql";
import type { ReactElement } from "react";
import { useQuery } from "urql";

import { graphql } from "./gql";
import { client } from "./graphql";
import { err, ok, Result } from "./result";

export type GqlResult<T> = Result<T, CombinedError>;
export type GqlAtom<T> = WritableAtom<
Promise<GqlResult<T>>,
[context?: Partial<OperationContext>],
void
>;

/**
* Map the result of a query atom to a new value, making it a GqlResult
*
* @param queryAtom: An atom got from atomWithQuery
* @param mapper: A function that takes the data from the query and returns a new value
*/
export const mapQueryAtom = <Data, Variables extends AnyVariables, NewData>(
queryAtom: AtomWithQuery<Data, Variables>,
mapper: (data: Data) => NewData,
): GqlAtom<NewData> => {
return atom(
async (get): Promise<GqlResult<NewData>> => {
const result = await get(queryAtom);
if (result.error) {
return err(result.error);
}

if (result.data === undefined) {
throw new Error("Query result is undefined");
}

return ok(mapper(result.data));
},

(_get, set, context) => {
set(queryAtom, context);
},
);
};

export const HydrateAtoms: React.FC<{ children: ReactElement }> = ({
children,
}) => {
useHydrateAtoms([[clientAtom, client]]);
return children;
};

const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
query CurrentViewerQuery {
viewer {
__typename
... on User {
id
}
}
}
`);

export const useCurrentUserId = (): string | null => {
const [result] = useQuery({ query: CURRENT_VIEWER_QUERY });
if (result.error) throw result.error;
if (!result.data) throw new Error(); // Suspense mode is enabled
return result.data.viewer.__typename === "User"
? result.data.viewer.id
: null;
};
72 changes: 17 additions & 55 deletions frontend/src/components/BrowserSessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
import { useState, useTransition } from "react";
import { useQuery } from "urql";

import { mapQueryAtom } from "../atoms";
import { graphql } from "../gql";
import { SessionState, PageInfo } from "../gql/graphql";
import {
atomForCurrentPagination,
atomWithPagination,
FIRST_PAGE,
Pagination,
} from "../pagination";
import { isOk, unwrap, unwrapOk } from "../result";
import { SessionState } from "../gql/graphql";
import { FIRST_PAGE, Pagination, usePages, usePagination } from "../pagination";

import BlockList from "./BlockList";
import BrowserSession from "./BrowserSession";
Expand Down Expand Up @@ -72,52 +63,22 @@ const QUERY = graphql(/* GraphQL */ `
}
`);

const filterAtom = atom<SessionState | null>(SessionState.Active);
const currentPaginationAtom = atomForCurrentPagination();

const browserSessionListFamily = atomFamily((userId: string) => {
const browserSessionListQuery = atomWithQuery({
query: QUERY,
getVariables: (get) => ({
userId,
state: get(filterAtom),
...get(currentPaginationAtom),
}),
});

const browserSessionList = mapQueryAtom(
browserSessionListQuery,
(data) => data.user?.browserSessions || null,
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
const [pagination, setPagination] = usePagination();
const [pending, startTransition] = useTransition();
const [filter, setFilter] = useState<SessionState | null>(
SessionState.Active,
);

return browserSessionList;
});

const pageInfoFamily = atomFamily((userId: string) => {
const pageInfoAtom = atom(async (get): Promise<PageInfo | null> => {
const result = await get(browserSessionListFamily(userId));
return (isOk(result) && unwrapOk(result)?.pageInfo) || null;
const [result] = useQuery({
query: QUERY,
variables: { userId, state: filter, ...pagination },
});
return pageInfoAtom;
});

const paginationFamily = atomFamily((userId: string) => {
const paginationAtom = atomWithPagination(
currentPaginationAtom,
pageInfoFamily(userId),
);
if (result.error) throw result.error;
const browserSessions = result.data?.user?.browserSessions;
if (!browserSessions) throw new Error(); // Suspense mode is enabled

return paginationAtom;
});

const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
const [pending, startTransition] = useTransition();
const result = useAtomValue(browserSessionListFamily(userId));
const setPagination = useSetAtom(currentPaginationAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
const [filter, setFilter] = useAtom(filterAtom);
const [prevPage, nextPage] = usePages(pagination, browserSessions.pageInfo);

const browserSessions = unwrap(result);
if (browserSessions === null) return <>Failed to load browser sessions</>;

const paginate = (pagination: Pagination): void => {
Expand Down Expand Up @@ -145,6 +106,7 @@ const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
<label>
<input
type="checkbox"
disabled={pending}
checked={filter === SessionState.Active}
onChange={toggleFilter}
/>{" "}
Expand Down
22 changes: 16 additions & 6 deletions frontend/src/components/CompatSession.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
// @vitest-environment happy-dom

import { create } from "react-test-renderer";
import { Provider } from "urql";
import { describe, expect, it, beforeAll } from "vitest";
import { never } from "wonka";

import { makeFragmentData } from "../gql";
import { WithLocation } from "../test-utils/WithLocation";
Expand All @@ -24,6 +26,10 @@ import { mockLocale } from "../test-utils/mockLocale";
import CompatSession, { FRAGMENT } from "./CompatSession";

describe("<CompatSession />", () => {
const mockClient = {
executeQuery: (): typeof never => never,
};

const baseSession = {
id: "session-id",
deviceId: "abcd1234",
Expand All @@ -42,9 +48,11 @@ describe("<CompatSession />", () => {
it("renders an active session", () => {
const session = makeFragmentData(baseSession, FRAGMENT);
const component = create(
<WithLocation>
<CompatSession session={session} />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation>
<CompatSession session={session} />
</WithLocation>
</Provider>,
);
expect(component.toJSON()).toMatchSnapshot();
});
Expand All @@ -58,9 +66,11 @@ describe("<CompatSession />", () => {
FRAGMENT,
);
const component = create(
<WithLocation>
<CompatSession session={session} />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation>
<CompatSession session={session} />
</WithLocation>
</Provider>,
);
expect(component.toJSON()).toMatchSnapshot();
});
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/components/Layout/Layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@
// @vitest-environment happy-dom

import { render } from "@testing-library/react";
import { Provider } from "urql";
import { describe, expect, it } from "vitest";
import { never } from "wonka";

import { WithLocation } from "../../test-utils/WithLocation";

import Layout from "./Layout";

describe("<Layout />", () => {
it("renders app navigation correctly", async () => {
const mockClient = {
executeQuery: (): typeof never => never,
};

const component = render(
<WithLocation path="/">
<Layout userId="abc123" />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation path="/">
<Layout userId="abc123" />
</WithLocation>
</Provider>,
);

expect(await component.findByText("Profile")).toMatchSnapshot();
Expand Down
30 changes: 21 additions & 9 deletions frontend/src/components/OAuth2Session.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
// @vitest-environment happy-dom

import { create } from "react-test-renderer";
import { Provider } from "urql";
import { describe, expect, it, beforeAll } from "vitest";
import { never } from "wonka";

import { makeFragmentData } from "../gql";
import { Oauth2ApplicationType } from "../gql/graphql";
Expand All @@ -25,6 +27,10 @@ import { mockLocale } from "../test-utils/mockLocale";
import OAuth2Session, { FRAGMENT } from "./OAuth2Session";

describe("<OAuth2Session />", () => {
const mockClient = {
executeQuery: (): typeof never => never,
};

const defaultSession = {
id: "session-id",
scope:
Expand All @@ -48,9 +54,11 @@ describe("<OAuth2Session />", () => {
const session = makeFragmentData(defaultSession, FRAGMENT);

const component = create(
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>
</Provider>,
);
expect(component.toJSON()).toMatchSnapshot();
});
Expand All @@ -64,9 +72,11 @@ describe("<OAuth2Session />", () => {
FRAGMENT,
);
const component = create(
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>
</Provider>,
);
expect(component.toJSON()).toMatchSnapshot();
});
Expand All @@ -84,9 +94,11 @@ describe("<OAuth2Session />", () => {
FRAGMENT,
);
const component = create(
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>,
<Provider value={mockClient}>
<WithLocation>
<OAuth2Session session={session} />
</WithLocation>
</Provider>,
);
expect(component.toJSON()).toMatchSnapshot();
});
Expand Down
Loading

0 comments on commit f0f7497

Please sign in to comment.