Lab - Add Dynamic Data

Plan: B2B Developer

Lesson 17 of 25 · 20 min

Introduction

The “Recent Orders” section on our custom Overview page is currently populated with mock data. In this exercise, we’ll replace this with real data fetched via the B2B GraphQL API.

In this lab, you will:

  • Write a GraphQL query to fetch recent orders
  • Replace mock data with real data
  • Add several more data-driven sections to the Overview page

Setup

This exercise continues where you left off previously. Make sure the dev server is started in your Buyer Portal project:

yarn dev

If you need a fresh start, you can follow the instructions below to set up a new project complete with previous exercise code.

  1. Copy the project with the appropriate tag.
degit https://github.com/bigcommerce-edu/lab-b2b-buyer-portal.git#e-gql-pre <directory-name>
  1. Install dependencies.
cd <directory-name>
yarn install
  1. Run the dev server.
yarn dev

Remember!

Script Manager in your store control panel must contain appropriate header/footer scripts to load the Buyer Portal from your local environment on the appropriate port.

Revisit the initial setup lab for full details on setting up your environment.

Exercise Reference Copy

If it’s helpful to compare your own code as you go, you can clone or download the completed version of this labs as a reference.

GraphQL Lab Snapshot

Step 1: Query for Recent Orders

Remember, all file paths referenced in these exercises are relative to apps/storefront/src in your Buyer Portal project.

The data.ts file in our page component’s directory already contains GraphQL logic for several sections we’ll be adding in this exercise. This file doesn’t yet contain the proper GraphQL for recent orders.

  1. Open pages/Overview/data.ts and add a response interface and GraphQL query string.
Overview/data.ts
interface RecentOrdersResponse {
allOrders: {
edges: {
node: OverviewOrder;
}[]
}
}
interface RecentInvoicesResponse {
...
}
...
const RecentOrdersQuery = `
query GetRecentOrders(
$limit: Int,
$sort: String
) {
allOrders(
first: $limit,
orderBy: $sort
){
edges {
node {
orderId
createdAt
totalIncTax
poNumber
}
}
}
}
`;
const RecentInvoicesQuery = `
...
`;

It’s not strictly important where in the file this code is added. RecentOrdersResponse defines what we expect the response to our GraphQL query to look like. Note that each node is expected to be an OverviewOrder, which is another interface already defined with fields such as orderId, createdAt, and poNumber.

The fields of OverviewOrder should be familiar, as our “Recent Orders” table is already configured with columns matching these keys. The mock data currently populating the table matches this schema.

RecentOrdersQuery is the GraphQL query itself, using the allOrders field to fetch a list of records. Note the query is written to support sorting and paging variables, making it flexible for reuse.

  1. Add the getRecentOrders function.
Overview/data.ts
export const getRecentOrders = async () => {
const resp = await B3Request.graphqlB2B({
query: RecentOrdersQuery,
variables: {
limit: 5,
sort: "-createdAt",
},
}) as RecentOrdersResponse;
return resp.allOrders?.edges.map((edge) => edge.node) ?? [];
};
export const getRecentInvoices = async () => {
...
};

Our code doesn’t need to concern itself with how the user token used to authenticate the GraphQL request is managed or retrieved, with B3Request handling this for us.

Note the use of map to form a flattened array of orders from each node in allOrders.edges in the response. getRecentOrders will return an array where each item has an orderId, poNumber, etc.

Our use of the RecentOrdersResponse type assertion tells TypeScript what to expect but doesn’t actually validate that the response data matches this. Our code currently lacks any actual error handling for the GraphQL fetch. Zod, which was used in the previous “mini-app” exercises, can be used in the same way within the Buyer Portal as well.

  1. Open pages/Overview/components/RecentOrders.tsx, update the import from ../data to include the new function, and remove the mock data array.
RecentOrders.tsx
import { getRecentOrders, OverviewOrder } from "../data";
...
/* Remove this
const mockOrders = [
...
];
*/
  1. In the useEffect callback, replace the current setOrders call with a call to getRecentOrders.
RecentOrders.tsx
export default function RecentOrders({
...
}: OrdersProps) {
...
const [orders, setOrders] = useState<OverviewOrder[]>([]);
useEffect(() => {
if (!startLoad) return;
getRecentOrders().then((b2bOrders) => {
setOrders(b2bOrders);
});
}, [startLoad]);
const orderColumns = [
...
];
}

The orders state will be updated when the response is received from the GraphQL request.

Now that we’re loading real data, let’s complete the “spinner” implementation by adding a piece of React state to track when the data is loading.

  1. Add a state variable and update the useEffect callback and JSX accordingly.
RecentOrders.tsx
export default function RecentOrders({
...
}: OrdersProps) {
...
const [orders, setOrders] = useState<OverviewOrder[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!startLoad || !loading) return;
getRecentOrders().then((b2bOrders) => {
setOrders(b2bOrders);
setLoading(false);
});
}, [startLoad, loading]);
...
return (
<B3Spin isSpinning={loading}>
...
</B3Spin>
);
}

Remember that we’ve already configured our component to defer triggering this data fetching until the accordion is expanded.

Revisit your Overview page and see the Recent Orders fetch in action. Try using the Network tab in your browser’s developer tools to inspect the GraphQL request and response.

Step 2: Add Other Data-Driven Sections

As this is an Overview page, we want to show more than just recent orders. There are already components and GraphQL queries in place in our boilerplate code for invoices, shopping lists, and quotes. Let’s add these remaining sections.

  1. Open pages/Overview/index.tsx and add imports for the other components.
Overview/index.tsx
import RecentInvoices from "./components/RecentInvoices";
import RecentShoppingLists from "./components/RecentShoppingLists";
import RecentQuotes from "./components/RecentQuotes";
  1. Similar to the ordersOpen state we’re using to track when the “Recent Orders” accordion is open, add state values for the other sections.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
const [ordersOpen, setOrdersOpen] = useState<boolean>(false);
const [invoicesOpen, setInvoicesOpen] = useState<boolean>(false);
const [shoppingListsOpen, setShoppingListsOpen] = useState<boolean>(false);
const [quotesOpen, setQuotesOpen] = useState<boolean>(false);
const allowOrders = validatePermissionWithComparisonType({
...
});
...
}

Remember that we’ve used selective permission checking to control whether “Recent Orders” is displayed. We’ll need to add similar logic for the other sections.

  1. Add permission checks for the new sections.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
const allowOrders = validatePermissionWithComparisonType({
...
});
const allowInvoices = validatePermissionWithComparisonType({
code: newPermissions.invoicePermissionCodes,
level: permissionLevels.COMPANY,
containOrEqual: 'contain',
});
const allowShoppingLists = validatePermissionWithComparisonType({
code: newPermissions.shoppingListsPermissionCodes,
level: permissionLevels.USER,
containOrEqual: 'contain',
});
const allowQuotes = validatePermissionWithComparisonType({
code: newPermissions.quotesPermissionCodes,
level: permissionLevels.COMPANY,
containOrEqual: 'contain',
});
return (
...
);
}
  1. Add the appropriate JSX to render the new sections. The following code is verbose, but this is simply applying the same component structure we already used for “Recent Orders.”
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
...
{allowOrders && (
...
)}
{allowInvoices && (
<Grid
item
key="recent-invoices"
xs={12}
>
<Accordion
onChange={(_e, isExpanded) => setInvoicesOpen(isExpanded)}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography variant="h3">{b3Lang('overview.recentInvoices')}</Typography>
</AccordionSummary>
<AccordionDetails>
<RecentInvoices
startLoad={invoicesOpen}
setOpenPage={setOpenPage}
/>
</AccordionDetails>
</Accordion>
</Grid>
)}
{allowShoppingLists && (
<Grid
item
key="recent-shopping-lists"
xs={12}
>
<Accordion
onChange={(_e, isExpanded) => setShoppingListsOpen(isExpanded)}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography variant="h3">{b3Lang('overview.recentShoppingLists')}</Typography>
</AccordionSummary>
<AccordionDetails>
<RecentShoppingLists
startLoad={shoppingListsOpen}
setOpenPage={setOpenPage}
/>
</AccordionDetails>
</Accordion>
</Grid>
)}
{allowQuotes && (
<Grid
item
key="recent-quotes"
xs={12}
>
<Accordion
onChange={(_e, isExpanded) => setQuotesOpen(isExpanded)}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography variant="h3">{b3Lang('overview.recentQuotes')}</Typography>
</AccordionSummary>
<AccordionDetails>
<RecentQuotes
startLoad={quotesOpen}
setOpenPage={setOpenPage}
/>
</AccordionDetails>
</Accordion>
</Grid>
)}
</Grid>
</>
);
}

Your Overview page should now have fully functioning sections for all data types. Observe the GraphQL requests that are triggered by expanding each accordion.

Full Overview content

Full Exercise Code

Taking It Further

  • Review the B2B GraphQL API or the native Buyer Portal code to explore the queries related to company users and addresses. Add summary information about these data types to the Overview page.

Resources