radar/twist-v2

View on GitHub
frontend/src/Book/Invitations/InviteForm.tsx

Summary

Maintainability
A
25 mins
Test Coverage
import React, { useState } from "react";
import AsyncSelect from "react-select/async";
import { OptionTypeBase, ValueType } from "react-select";
import {
  BookIdTitleAndReadersQuery,
  useBookIdTitleAndReadersQuery,
  useInviteUserMutation,
  useUsersLazyQuery,
} from "../../graphql/types";
import PermissionDenied from "../../PermissionDenied";
import QueryWrapper from "../../QueryWrapper";
import { gql } from "@apollo/client";
import { Link } from "@reach/router";

gql`
  query bookIDTitleAndReaders($permalink: String!) {
    book(permalink: $permalink) {
      ... on PermissionDenied {
        error
      }

      ... on Book {
        id
        title

        readers {
          githubLogin
          name
        }
      }
    }
  }
`;

gql`
  query users($githubLogin: String!) {
    users(githubLogin: $githubLogin) {
      id
      githubLogin
      name
    }
  }
`;

gql`
  mutation inviteUser($bookId: ID!, $userId: ID!) {
    inviteUser(bookId: $bookId, userId: $userId) {
      bookId
      userId
    }
  }
`;

type Book = Extract<BookIdTitleAndReadersQuery["book"], { __typename: "Book" }>;
type Readers = Book["readers"];

type InviteFormProps = {
  bookPermalink: string;
  bookId: string;
  bookTitle: string;
  bookReaders: Readers;
};

type Selection = ValueType<OptionTypeBase, false>;

const InviteForm: React.FC<InviteFormProps> = ({
  bookPermalink,
  bookTitle,
  bookId,
  bookReaders,
}) => {
  const [userID, setUserID] = useState<string | null>(null);
  const [message, setMessage] = useState<string>("");
  const [usersQuery, { data }] = useUsersLazyQuery();

  const [inviteUserMutation] = useInviteUserMutation({ errorPolicy: "all" });

  const loadOptions = async (githubLogin: string, callback: Function) => {
    await usersQuery({ variables: { githubLogin } });

    if (data) {
      const readerLogins = bookReaders.map((reader) => reader.githubLogin);
      const uninvitedUsers = data.users.filter(
        (user) => !readerLogins.includes(user.githubLogin)
      );
      callback(
        uninvitedUsers.map((user) => {
          return {
            value: user.id,
            label: `${user.githubLogin} (${user.name})`,
          };
        })
      );
    }
  };

  const selectUser = (selection: Selection) => {
    if (!selection) return;

    setUserID(selection.value);
  };

  const inviteUser = () => {
    if (!userID) {
      return;
    }
    inviteUserMutation({
      variables: { userId: userID, bookId: bookId },
      refetchQueries: ["readers"],
    }).then((response) => {
      setMessage("Invite sent!");
    });
  };

  return (
    <>
      <Link to={`/books/${bookPermalink}`}>
        <h1>{bookTitle}</h1>
      </Link>
      <h2>Invite a reader</h2>

      <AsyncSelect
        loadOptions={loadOptions}
        onChange={selectUser}
        noOptionsMessage={({ inputValue }) =>
          `Could not find '${inputValue}' -- have they already been invited?`
        }
      />

      <button
        type="button"
        className="btn btn-blue mt-2"
        onClick={inviteUser}
        disabled={!userID}
      >
        Invite
      </button>
      <span className="ml-2 text-green-600">{message}</span>
    </>
  );
};

type WrappedInviteProps = {
  bookPermalink: string;
};

const WrappedInviteForm: React.FC<WrappedInviteProps> = ({ bookPermalink }) => {
  const { data, loading, error } = useBookIdTitleAndReadersQuery({
    variables: {
      permalink: bookPermalink as string,
    },
  });

  const renderInviteOrPermissionDenied = (data: BookIdTitleAndReadersQuery) => {
    if (data.book.__typename === "PermissionDenied") {
      return <PermissionDenied />;
    }

    return (
      <InviteForm
        bookPermalink={bookPermalink}
        bookId={data.book.id}
        bookTitle={data.book.title}
        bookReaders={data.book.readers}
      />
    );
  };

  return (
    <div className="main md:w-1/2">
      <QueryWrapper loading={loading} error={error}>
        {data && renderInviteOrPermissionDenied(data)}
      </QueryWrapper>
    </div>
  );
};

export default WrappedInviteForm;