diff --git a/example/components/App.tsx b/example/components/App.tsx index 48a01cd..b0a1aeb 100644 --- a/example/components/App.tsx +++ b/example/components/App.tsx @@ -1,9 +1,11 @@ +import { Option } from "@swan-io/boxed"; import { useState } from "react"; import { useQuery } from "../../src"; import { graphql } from "../gql"; +import { FilmDetails } from "./FilmDetails"; import { FilmList } from "./FilmList"; -const allFilmsQuery = graphql(` +const AllFilmsQuery = graphql(` query allFilmsWithVariablesQuery($first: Int!, $after: String) { allFilms(first: $first, after: $after) { ...FilmsConnection @@ -12,8 +14,15 @@ const allFilmsQuery = graphql(` `); export const App = () => { + const [optimize, setOptimize] = useState(false); const [after, setAfter] = useState(null); - const [data, { isLoading }] = useQuery(allFilmsQuery, { first: 3, after }); + const [activeFilm, setActiveFilm] = useState>(Option.None()); + + const [data, { isLoading }] = useQuery( + AllFilmsQuery, + { first: 3, after }, + { optimize }, + ); return (
@@ -28,11 +37,39 @@ export const App = () => { return
No films
; } return ( - +
+
+ + + setActiveFilm(Option.Some(filmId)) + } + /> +
+
+ {activeFilm.match({ + None: () =>
No film selected
, + Some: (filmId) => ( + + ), + })} +
+
); }, }), diff --git a/example/components/Film.tsx b/example/components/Film.tsx index 9952e13..8b0c638 100644 --- a/example/components/Film.tsx +++ b/example/components/Film.tsx @@ -9,10 +9,20 @@ export const FilmFragment = graphql(` } `); -export const Film = (props: { film: FragmentType }) => { - const film = useFragment(FilmFragment, props.film); +type Props = { + film: FragmentType; + isActive: boolean; + onPress: (filmId: string) => void; +}; + +export const Film = ({ film: data, isActive, onPress }: Props) => { + const film = useFragment(FilmFragment, data); return ( -
+
onPress(film.id)} + >

{film.title}

{film.releaseDate}

diff --git a/example/components/FilmCharacterList.tsx b/example/components/FilmCharacterList.tsx new file mode 100644 index 0000000..b93e2df --- /dev/null +++ b/example/components/FilmCharacterList.tsx @@ -0,0 +1,65 @@ +import { useForwardPagination } from "../../src"; +import { FragmentType, graphql, useFragment } from "../gql"; + +export const FilmCharactersConnectionFragment = graphql(` + fragment FilmCharactersConnection on FilmCharactersConnection { + edges { + node { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } +`); + +type Props = { + characters: FragmentType; + onNextPage: (cursor: string | null) => void; + isLoadingMore: boolean; +}; + +export const FilmCharacterList = ({ + characters, + onNextPage, + isLoadingMore, +}: Props) => { + const connection = useForwardPagination( + useFragment(FilmCharactersConnectionFragment, characters), + ); + + if (connection.edges == null) { + return null; + } + + return ( + <> +
    + {connection.edges.map((edge) => { + if (edge == null) { + return null; + } + const node = edge.node; + if (node == null) { + return null; + } + return
  • {node.name}
  • ; + })} +
+ + {isLoadingMore ?
Loading more
: null} + + {connection.pageInfo.hasNextPage ? ( + + ) : null} + + ); +}; diff --git a/example/components/FilmDetails.tsx b/example/components/FilmDetails.tsx new file mode 100644 index 0000000..5768a84 --- /dev/null +++ b/example/components/FilmDetails.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { useQuery } from "../../src"; +import { graphql } from "../gql"; +import { FilmCharacterList } from "./FilmCharacterList"; + +export const FilmDetailsQuery = graphql(` + query FilmDetails($filmId: ID!, $first: Int!, $after: String) { + film(id: $filmId) { + id + title + director + openingCrawl + characterConnection(first: $first, after: $after) { + ...FilmCharactersConnection + } + releaseDate + } + } +`); + +type Props = { + filmId: string; + optimize: boolean; +}; + +export const FilmDetails = ({ filmId, optimize }: Props) => { + const [after, setAfter] = useState(null); + const [data, { isLoading }] = useQuery( + FilmDetailsQuery, + { + filmId, + first: 5, + after, + }, + { optimize }, + ); + + return ( +
+ {data.match({ + NotAsked: () => null, + Loading: () =>
Loading ...
, + Done: (result) => + result.match({ + Error: () =>
An error occured
, + Ok: ({ film }) => { + if (film == null) { + return
No film
; + } + return ( + <> +

{film.title}

+
Director: {film.director}
+
Release date: {film.releaseDate}
+
+ Opening crawl: +
{film.openingCrawl}
+
+ {film.characterConnection != null ? ( + <> +

Characters

+ + + ) : null} + + ); + }, + }), + })} +
+ ); +}; diff --git a/example/components/FilmList.tsx b/example/components/FilmList.tsx index f34c493..2061000 100644 --- a/example/components/FilmList.tsx +++ b/example/components/FilmList.tsx @@ -1,3 +1,4 @@ +import { Option } from "@swan-io/boxed"; import { useForwardPagination } from "../../src"; import { FragmentType, graphql, useFragment } from "../gql"; import { Film } from "./Film"; @@ -21,9 +22,17 @@ type Props = { films: FragmentType; onNextPage: (cursor: string | null) => void; isLoadingMore: boolean; + activeFilm: Option; + onPressFilm: (filmId: string) => void; }; -export const FilmList = ({ films, onNextPage, isLoadingMore }: Props) => { +export const FilmList = ({ + films, + onNextPage, + activeFilm, + onPressFilm, + isLoadingMore, +}: Props) => { const connection = useForwardPagination( useFragment(FilmsConnectionFragment, films), ); @@ -42,7 +51,16 @@ export const FilmList = ({ films, onNextPage, isLoadingMore }: Props) => { if (node == null) { return null; } - return ; + return ( + node.id === id) + .getWithDefault(false)} + onPress={onPressFilm} + /> + ); })} {isLoadingMore ?
Loading more
: null} diff --git a/example/gql/gql.ts b/example/gql/gql.ts index 7ea0467..f87b498 100644 --- a/example/gql/gql.ts +++ b/example/gql/gql.ts @@ -17,6 +17,10 @@ const documents = { types.AllFilmsWithVariablesQueryDocument, "\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n": types.FilmItemFragmentDoc, + "\n fragment FilmCharactersConnection on FilmCharactersConnection {\n edges {\n node {\n id\n name\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n": + types.FilmCharactersConnectionFragmentDoc, + "\n query FilmDetails($filmId: ID!, $first: Int!, $after: String) {\n film(id: $filmId) {\n id\n title\n director\n openingCrawl\n characterConnection(first: $first, after: $after) {\n ...FilmCharactersConnection\n }\n releaseDate\n }\n }\n": + types.FilmDetailsDocument, "\n fragment FilmsConnection on FilmsConnection {\n edges {\n node {\n id\n ...FilmItem\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n": types.FilmsConnectionFragmentDoc, }; @@ -47,6 +51,18 @@ export function graphql( export function graphql( source: "\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\n }\n", ): (typeof documents)["\n fragment FilmItem on Film {\n id\n title\n releaseDate\n producers\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 FilmCharactersConnection on FilmCharactersConnection {\n edges {\n node {\n id\n name\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n", +): (typeof documents)["\n fragment FilmCharactersConnection on FilmCharactersConnection {\n edges {\n node {\n id\n name\n }\n }\n pageInfo {\n hasNextPage\n endCursor\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 FilmDetails($filmId: ID!, $first: Int!, $after: String) {\n film(id: $filmId) {\n id\n title\n director\n openingCrawl\n characterConnection(first: $first, after: $after) {\n ...FilmCharactersConnection\n }\n releaseDate\n }\n }\n", +): (typeof documents)["\n query FilmDetails($filmId: ID!, $first: Int!, $after: String) {\n film(id: $filmId) {\n id\n title\n director\n openingCrawl\n characterConnection(first: $first, after: $after) {\n ...FilmCharactersConnection\n }\n releaseDate\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/example/gql/graphql.ts b/example/gql/graphql.ts index 21cf4a0..2e7e2cf 100644 --- a/example/gql/graphql.ts +++ b/example/gql/graphql.ts @@ -1310,6 +1310,44 @@ export type FilmItemFragment = { producers?: Array | null; } & { " $fragmentName"?: "FilmItemFragment" }; +export type FilmCharactersConnectionFragment = { + __typename?: "FilmCharactersConnection"; + edges?: Array<{ + __typename?: "FilmCharactersEdge"; + node?: { __typename?: "Person"; id: string; name?: string | null } | null; + } | null> | null; + pageInfo: { + __typename?: "PageInfo"; + hasNextPage: boolean; + endCursor?: string | null; + }; +} & { " $fragmentName"?: "FilmCharactersConnectionFragment" }; + +export type FilmDetailsQueryVariables = Exact<{ + filmId: Scalars["ID"]["input"]; + first: Scalars["Int"]["input"]; + after?: InputMaybe; +}>; + +export type FilmDetailsQuery = { + __typename?: "Root"; + film?: { + __typename?: "Film"; + id: string; + title?: string | null; + director?: string | null; + openingCrawl?: string | null; + releaseDate?: string | null; + characterConnection?: + | ({ __typename?: "FilmCharactersConnection" } & { + " $fragmentRefs"?: { + FilmCharactersConnectionFragment: FilmCharactersConnectionFragment; + }; + }) + | null; + } | null; +}; + export type FilmsConnectionFragment = { __typename?: "FilmsConnection"; edges?: Array<{ @@ -1327,6 +1365,55 @@ export type FilmsConnectionFragment = { }; } & { " $fragmentName"?: "FilmsConnectionFragment" }; +export const FilmCharactersConnectionFragmentDoc = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "FilmCharactersConnection" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "FilmCharactersConnection" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + ], + }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "pageInfo" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "hasNextPage" } }, + { kind: "Field", name: { kind: "Name", value: "endCursor" } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; export const FilmItemFragmentDoc = { kind: "Document", definitions: [ @@ -1552,3 +1639,155 @@ export const AllFilmsWithVariablesQueryDocument = { AllFilmsWithVariablesQueryQuery, AllFilmsWithVariablesQueryQueryVariables >; +export const FilmDetailsDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "FilmDetails" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "filmId" }, + }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }, + }, + }, + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "first" }, + }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "Int" } }, + }, + }, + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "after" }, + }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "film" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "id" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "filmId" }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "title" } }, + { kind: "Field", name: { kind: "Name", value: "director" } }, + { + kind: "Field", + name: { kind: "Name", value: "openingCrawl" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "characterConnection" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "first" }, + }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "after" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "after" }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "FragmentSpread", + name: { + kind: "Name", + value: "FilmCharactersConnection", + }, + }, + ], + }, + }, + { kind: "Field", name: { kind: "Name", value: "releaseDate" } }, + ], + }, + }, + ], + }, + }, + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "FilmCharactersConnection" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "FilmCharactersConnection" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "edges" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "node" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + ], + }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "pageInfo" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "hasNextPage" } }, + { kind: "Field", name: { kind: "Name", value: "endCursor" } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; diff --git a/example/index.html b/example/index.html index d8bc7bf..d858493 100644 --- a/example/index.html +++ b/example/index.html @@ -6,6 +6,15 @@ font-family: sans-serif; } + html, + body { + padding: 0; + margin: 0; + min-height: 100vh; + display: flex; + flex-direction: column; + } + .AccountMembership { padding: 10px; } @@ -36,6 +45,39 @@ button:active { opacity: 0.9; } + + #app, + .App { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .Main { + display: flex; + flex-direction: row; + align-items: stretch; + flex-grow: 1; + } + + .Sidebar { + border-right: 1px solid #eee; + padding: 20px; + } + + .Film { + cursor: pointer; + padding: 10px 20px; + } + + .Film[data-active="true"] { + background-color: #eee; + } + + .Contents { + flex-grow: 1; + padding: 20px; + }