DEV Community

Mohammed Shaheem P
Mohammed Shaheem P

Posted on

Shopify Orders, Setup Pagination with shopify-api and GraphQL

I took more time than I should have taken while trying to build an orders page with pagination using a React and NestJS with typescript.

Between very few references and average documentations, I'm sure this article could be helpful to someone else who endup in this situation (or better I might endup losing this code and would need a place to find this), so why not write an article about it!

I was using NestJS for backend, but anyone using any other node framework could easily follow this, and I'm using Shopify Polaris for frontend components.

For the sake of simplicity, I'm only adding important Code Blocks here

Backend Function

  pageSize = 25;

  async fetchOrders(
    gqlClient: GraphqlClient,
    cursor: NullableString = null,
    direction: NullableString = FetchDirection.FORWARD,
  ) {
    const [first, last, after, before] =
      direction === FetchDirection.FORWARD
        ? [this.pageSize, null, cursor, null]
        : [null, this.pageSize, null, cursor];

    try {
      const response = await gqlClient.query<OrdersResponse>({
        data: {
          query: `query GetOrders($first: Int, $last: Int, $before: String, $after: String) {
            orders(first: $first, last: $last, before: $before, after: $after) {
              edges {
                cursor
                node {
                  id
                  name
                  customer {
                    firstName
                    lastName
                  }
                  displayFinancialStatus
                  displayFulfillmentStatus
                  totalPriceSet {
                    presentmentMoney {
                      amount
                    }
                  }
                  createdAt
                }
              }
              pageInfo {
                hasNextPage
                hasPreviousPage
              }
            }
          }`,
          variables: {
            first,
            last,
            before,
            after,
          },
        },
      });
      return response.body.data.orders;
    } catch (error) {
      throw new InternalServerErrorException(error);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Frontend Components

export default function OrdersPage() {
  const fetch = useAuthenticatedFetch();
  const [searchParams] = useSearchParams({});

  const [hasNextPage, sethasNextPage] = useState<boolean>(false);
  const [hasPreviousPage, setHasPreviousPage] = useState<boolean>(false);
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [orderEdges, setOrderEdges] = useState<OrderEdge[]>([]);

  const fetchOrders = useCallback(
    async (
      direction: NullableString = "forward",
      cursor: NullableString = "",
    ) => {
      setIsLoading(true);
      let fetchUrl;
      if (direction && cursor) {
        fetchUrl = `/api/orders/?cursor=${cursor}&direction=${direction}`;
      } else {
        fetchUrl = "/api/orders";
      }

      try {
        const { edges, pageInfo }: OrdersResponse = await fetch(fetchUrl, {
          method: "GET",
        }).then((res) => res.json());

        setOrderEdges(edges);

        sethasNextPage(pageInfo.hasNextPage);
        setHasPreviousPage(pageInfo.hasPreviousPage);
        setIsLoading(false);

        return;
      } catch (error) {
        console.error(error);
        setIsLoading(false);
        return;
      }
    },
    [],
  );

  useEffect(() => {
    const cursor = searchParams.get("cursor");
    const direction = searchParams.get("direction");

    fetchOrders(direction, cursor);
  }, [searchParams]);

  return (
    <Page>
      <TitleBar title="Orders" />
      <Layout>
        <Layout.Section>
          <OrdersTable
            orderEdges={orderEdges}
            hasNextPage={hasNextPage}
            hasPreviousPage={hasPreviousPage}
            loading={isLoading}
          />
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Enter fullscreen mode Exit fullscreen mode
const resourceName = {
  singular: "order",
  plural: "orders",
};

interface OrdersTableProps {
  orderEdges: OrderEdge[];
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  loading: boolean;
}

export function OrdersTable({
  orderEdges,
  hasNextPage,
  hasPreviousPage,
  loading,
}: OrdersTableProps) {
  const [_, setSearchParams] = useSearchParams({});

  const resourceIDResolver = (order: OrderEdge) => {
    return order.node.id;
  };

  const { selectedResources, allResourcesSelected, handleSelectionChange } = useIndexResourceState(orderEdges, {
      resourceIDResolver,
    });

  const indexTableRowMarkup = useMemo(() => {
    return orderEdges.map(
      (
        {
          node: {
            id,
            name,
            createdAt,
            customer,
            displayFinancialStatus,
            displayFulfillmentStatus,
            totalPriceSet: {
              presentmentMoney: { amount },
            },
          },
        },
        index,
      ) => {
        const customerFullName =
          customer &&
          `${customer?.firstName || ""} ${customer?.lastName || ""}`.trim();
        const financialStatusForDisplay =displayFinancialStatus
                .split("_")
                .map((piece) => capitalize(piece))
                .join(" ");

        const financialStatusBadgeType = displayFinancialStatus === FinancialStatus.PAID
              ? "success"
              : undefined;
        const fulfillmentStatusBadgeType = displayFulfillmentStatus === FulfillmentStatus.FULFILLED
              ? "success"
              : undefined;

        return (
          <IndexTable.Row
            id={id}
            key={id}
            selected={selectedResources.includes(id)}
            position={index}
          >
            <IndexTable.Cell>
              <TextStyle variation="strong">{name}</TextStyle>
            </IndexTable.Cell>
            <IndexTable.Cell>
              {new Date(createdAt).toLocaleDateString()}
            </IndexTable.Cell>
            <IndexTable.Cell>{customerFullName}</IndexTable.Cell>
            <IndexTable.Cell>
                <Badge status={financialStatusBadgeType}>
                    {financialStatusForDisplay}
                </Badge>
            </IndexTable.Cell>
            <IndexTable.Cell>
              <Badge status={fulfillmentStatusBadgeType}>
                  {capitalize(
                      (displayFulfillmentStatus as FulfillmentStatus) ||
                        "unfulfilled",
                    )}
              </Badge>
            </IndexTable.Cell>
            <IndexTable.Cell>{amount}</IndexTable.Cell>
          </IndexTable.Row>
        );
      },
    );
  }, [orderEdges, selectedResources]);

  return (
    <Card sectioned>
      <IndexTable
        resourceName={resourceName}
        itemCount={orderEdges.length}
        selectedItemsCount={
          allResourcesSelected ? "All" : selectedResources.length
        }
        onSelectionChange={handleSelectionChange}
        headings={[
          { title: "Order" },
          { title: "Date" },
          { title: "Customer" },
          { title: "Payment Status" },
          { title: "Fulfillment Status" },
          { title: "Total" },
        ]}
        loading={loading}
      >
        {indexTableRowMarkup}
      </IndexTable>
      <div className={styles.paginationWrapper}>
        <Pagination
          hasPrevious={hasPreviousPage}
          hasNext={hasNextPage}
          onPrevious={() => {
            const cursor = orderEdges[0].cursor;
            setSearchParams({ cursor, direction: "backward" });
          }}
          onNext={() => {
            const cursor = orderEdges[orderEdges.length - 1].cursor;
            setSearchParams({ cursor, direction: "forward" });
          }}
        />
      </div>
    </Card>
  );
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)