Resource List

A resource list displays a collection of objects of the same type, like products or customers. The main job of a resource list is to help merchants find an object and navigate to a full-page representation of it.

Resource list anatomy, showing filters, header, and items

Resource lists can also:

Examples

A resource list with simple items and no bulk actions, sorting, or filtering. See the case study below for implementation details.

Drag to resize example

Props

bulkActions
DisableableAction | ActionListSection[]
Actions available on the currently selected items
filterControl
React.ReactNode
hasMoreItems
boolean
If there are more items than currently in the list
itemsRequired
any[]
Item data; each item is passed to renderItem
promotedBulkActions
DisableableAction[]
Up to 2 bulk actions that will be given more prominence
resourceName
{plural: string, singular: string}
Name of the resource, such as customers or products
selectedItems
string[] | "All"
Collection of IDs for the currently selected items
showHeader
boolean
Boolean to show or hide the header
sortOptions
string | {disabled?: boolean, label: string, value: string}[]
Collection of sort options to choose from
sortValue
string
Current value of the sort control
idForItem
(item: any, index: number) => string
Function to customize the unique ID for each item
onSelectionChange
(selectedItems: string[] | "All") => void
Callback when selection is changed
onSortChange
(selected: string, id: string) => void
Callback when sort option is changed
renderItemRequired
(item: any, id: string) => React.ReactNode
Function to render each list item

Build

Using a resource list in a project involves combining the following components and subcomponents:

The resource list component provides the UI elements for list sorting, filtering, and pagination, but doesn’t provide the logic for these operations. When a sort option is changed, filter added, or second page requested, you’ll need to handle that event (including any network requests) and then update the component with new props.

View the case study for a walkthrough of how to use this component to build an index page for customers.


Purpose

Shopify is organized around objects that represent a merchant’s business, like customers, products, and orders. Each individual order, for example, is given a dedicated page that can be linked to. In Shopify, we call these types of objects resources, and we call the object’s dedicated page its show page.

Problem

Take orders as an example. Merchants may have a lot of them. They need a way to scan their orders, see what state they’re in and find out which ones need action first. In other words, they need a way find an individual order, call up more information about it, and take action on it.

Solution

Resource lists function as:

  • A content format, presenting a set of individual resources in a compact form
  • A system for taking action on one or more individual resources
  • A way to navigate to an individual resource’s show page

Because a show page displays all the content and actions for an individual resource, you can think of a resource list as a summary of these show pages. In this way resource lists bridge a middle level in Shopify’s navigation hierarchy.

Schematic showing content from a show page being surfaced on a resource list

A resource list isn’t a data table

On wide screens, a resource list often looks like a table, especially if some content is aligned in columns. Despite this, resource lists and data tables have different purposes.

A data table is a form of data visualization. It works best to present highly structured data for comparison and analysis.

If your use case is more about visualizing or analyzing data, use the data table component. If your use case is more about finding and taking action on objects, use a resource list.


Best practices

Resource lists can live in many places in Shopify. You could include a short resource list in a card summarizing recent marketing activities. You could also dedicate an entire page to a resource list like Shopify’s main products index.

Resource lists should:

  • Have items that perform an action when clicked. The action should navigate to the resource’s show page or otherwise provide more detail about the item.
  • Customize the content and layout of their list items to support merchants’ needs.
  • Support sorting if the list can be long, and especially if different merchant tasks benefit from different sort orders.
  • Support filtering if the list can be long.
  • Paginate when the current list contains more than 50 items.

Resource lists can optionally:

  • Provide bulk actions for tasks that are often applied to many list items at once. For example, a merchant may want to add the same tag to a large number of products.

Content guidelines

Resource lists should:


Resource list item

The content of a resource list consists of resource list items. Each item summarizes an individual resource and should link to its show page.

Because the content of items depends on the type of resource and merchant tasks, resource list items are flexible.

See the case study section for more about customizing and using resource list items.

Resource list item anatomy, showing handle, media and details

Item examples

Simple resource list item

A basic resource list item with its details filled in at the point of use.

Blog post list item

<ResourceList
  resourceName={{singular: 'blog post', plural: 'blog posts'}}
  items={[
    {
      id: 6,
      url: 'posts/6',
      title: 'How To Get Value From Wireframes',
      author: 'Jonathan Mangrove',
    },
  ]}
  renderItem={(item) => {
    const {id, url, title, author} = item;
    const authorMarkup = author ? <div>by {author}</div> : null;

    return (
      <ResourceList.Item
        id={id}
        url={url}
        accessibilityLabel={`View details for ${title}`}
      >
        <h3>
          <TextStyle variation="strong">{title}</TextStyle>
        </h3>
        {authorMarkup}
      </ResourceList.Item>
    );
  }}
/>

Item with media

The media element can hold an avatar, thumbnail or other small-format graphic.

Example customer list item

<ResourceList
  resourceName={{singular: 'customer', plural: 'customers'}}
  items={[
    {
      id: 145,
      url: 'customers/145',
      avatarSource: 'https://avatars.io/twitter/Astro_Soyeon',
      name: 'Yi So-Yeon',
      location: 'Gwangju, South Korea',
    },
  ]}
  renderItem={(item) => {
    const {id, url, avatarSource, name, location} = item;

    return (
      <ResourceList.Item
        id={id}
        url={url}
        media={
          <Avatar customer size="medium" name={name} source={avatarSource} />
        }
        accessibilityLabel={`View details for ${name}`}
      >
        <h3>
          <TextStyle variation="strong">{name}</TextStyle>
        </h3>
        <div>{location}</div>
      </ResourceList.Item>
    );
  }}
/>

Item with shortcut actions

Shortcut actions present popular actions from the resource’s show page for easy access.

Shortcut actions are shown on hover

<ResourceList
  resourceName={{singular: 'customer', plural: 'customers'}}
  items={[
    {
      id: 145,
      url: 'customers/145',
      avatarSource: 'https://avatars.io/twitter/Astro_Soyeon',
      name: 'Yi So-Yeon',
      location: 'Gwangju, South Korea',
      latestOrderUrl: 'orders/1456',
    },
  ]}
  renderItem={(item) => {
    const {id, url, avatarSource, name, location, latestOrderUrl} = item;
    const shortcutActions = latestOrderUrl
      ? [{content: 'View latest order', url: latestOrderUrl}]
      : null;

    return (
      <ResourceList.Item
        id={id}
        url={url}
        media={
          <Avatar customer size="medium" name={name} source={avatarSource} />
        }
        shortcutActions={shortcutActions}
        accessibilityLabel={`View details for ${name}`}
      >
        <h3>
          <TextStyle variation="strong">{name}</TextStyle>
        </h3>
        <div>{location}</div>
      </ResourceList.Item>
    );
  }}
/>

Item properties

Prop Type Description
id* string Unique identifier for the item within the list
url string URL for the resource’s show page (required unless `onClick` is provided)
accessibilityLabel* string Accessibility label for item link
ariaControls string Id of the element the item onClick controls
ariaExpanded string Tells screen reader the controlled element is expanded
onClick function(id: string): void Callback when clicked (required if `url` is omitted)
media React.reactNode Content for the media area at the left of the item, usually an Avatar or Thumbnail
children React.reactNode Content for the details area
shortcutActions DisableableAction[] 1 or 2 shortcut actions; must be available on the page linked to by `url`
persistActions boolean Makes the shortcut actions always visible

Item best practices

Resource list items should:

  • Perform an action when clicked. The action should navigate to the resource’s show page or otherwise provide more detail about the item.
  • Be tailored to the specific type of resource being displayed.
  • Lay out the content effectively across all screen sizes.

Resource list items can optionally:

Read the case study to see how the best practices are applied.

Item content guidelines

Resource list items should:

  • Present the content merchants need to find the items they’re looking for.
  • Support merchants’ tasks for the particular type of resource.
  • Present content elements concisely. For example, add a label or clarifying phrase only when necessary.
  • Avoid truncating content where possible.
  • Avoid colons.
  • Conditional actions should follow the verb + noun content formula for buttons.
  • If a content value is empty, don’t use an em dash (“—”) like in a table. Instead, use a phrase like “No orders.”
  • Shortcut actions don’t need to follow the full verb + noun formula for buttons.

See the case study for content guidelines in action.


Resource list filter control

Provides a default interface for adding and removing filters. Supports quick filtering using a text field. A more advanced filter builder can be accessed from a popover. Applied filters are represented as removeable tags.

Resource list with filter control

Filter control examples

Resource list with filter control

Filter control showing a state with applied filters and an additional action (optional).

Example filter control

<ResourceList
  resourceName={{singular: 'customer', plural: 'customers'}}
  items={[
    {
      id: 341,
      url: 'customers/341',
      name: 'Mae Jemison',
      location: 'Decatur, USA',
    },
    {
      id: 256,
      url: 'customers/256',
      name: 'Ellen Ochoa',
      location: 'Los Angeles, USA',
    },
  ]}
  renderItem={(item) => {
    const {id, url, name, location} = item;
    const media = <Avatar customer size="medium" name={name} />;

    return (
      <ResourceList.Item
        id={id}
        url={url}
        media={media}
        accessibilityLabel={`View details for ${name}`}
      >
        <h3>
          <TextStyle variation="strong">{name}</TextStyle>
        </h3>
        <div>{location}</div>
      </ResourceList.Item>
    );
  }}
  filterControl={
    <ResourceList.FilterControl
      filters={[
        {
          key: 'orderCountFilter',
          label: 'Number of orders',
          operatorText: 'is greater than',
          type: FilterType.TextField,
        },
        {
          key: 'accountStatusFilter',
          label: 'Account status',
          operatorText: 'is',
          type: FilterType.Select,
          options: ['Enabled', 'Invited', 'Not invited', 'Declined'],
        },
      ]}
      appliedFilters={[
        {
          key: 'orderCountFilter',
          value: '1',
          label: 'Has orders',
        },
        {
          key: 'accountStatusFilter',
          value: 'Enabled',
          label: 'Account enabled',
        },
      ]}
      onFiltersChange={(appliedFilters) => {
        console.log(
          `Applied filters changed to ${appliedFilters}.`,
          'Todo: use setState to apply this change.',
        );
      }}
      searchValue="USA"
      onSearchChange={(searchValue) => {
        console.log(
          `Search value changed to ${searchValue}.`,
          'Todo: use setState to apply this change.',
        );
      }}
      additionalAction={{
        content: 'Save',
        onAction: () => console.log('Todo: handle save filters.'),
      }}
    />
  }
/>

Filter control properties

Prop Type Description
searchValue string Currently entered text in the search term field
appliedFilters AppliedFilter[] Collection of currently applied filters
focused boolean Whether the search term field is focused
filters Filter[] Available filters
onSearchBlur function(): void Callback when the search term field is blurred
onSearchChange function(searchvalue: string, id: string): void Callback when the search term field is changed
onFiltersChange function(appliedFilters: AppliedFilter[]): void Callback when the applied filters are changed

Filter control best practices

A Resource list’s filter control should:

  • Make filters available that make common merchant tasks easy. For example, provide the option for merchants to filter a customer’s list to email subscribers only. Don’t offer arbitrary filters.
  • Show relevant results for a wide range of search inputs, including partial words. For example, if a merchant types “unful” in the search field for an orders list, it should return all unfulfilled orders as a the result (as well as orders with this string elsewhere in Shopify, such as in an order note).

Filter control content guidelines

Content for the filter control appears in two places: the filter builder and the removable tags that represent applied filters.

Filter builder content

The filter builder itself has three parts: the label, the operator text, and the filter input.

Example filter builder in a popover

In this example:

  • “Account status” is the label
  • “is” is the operator text
  • “Enabled” is one of several options that make up the filter input

Here’s another example:

Second filter builder example

In this case, a the filter input is a text field, so you only need to consider copy for the label, “Number of orders” and operator text, “is greater than”.

  • Filter label and filter input should follow the select menu options guidelines
  • Operator text should start with a lowercase letter
  • All three content elements should form a sentence
  • Operator text may be left out if the sentence reads more clearly without it

Applied filter tags

Example of applied filter tags

The content that represents applied filter tags should use short, clear, non-technical labels.

  • Has orders

  • More than 10 orders

  • Number of orders is greater than 0

  • order_count >= 10



Case study

To cover the resource list component in depth, we’ll create a customer list as an example. We’ll start by implementing a basic resoure list step by step. Then we’ll customize the built-in resource list item to better display our customers. Finally, we’ll add features to make the list more useful to merchants.

  1. Development setup (optional)
  2. A basic resource list
  3. Building a reusable custom list item
  4. Adding bulk actions
  5. Adding sorting
  6. Adding filtering
  7. Adding pagination

You can also jump straight to the end result.

Development setup (optional)

If you want to follow along with the code, our setup will be based on Create React App. If you’ve never used Create React App, you can get started by using npx (npm 5.2+):

npx create-react-app my-app
cd my-app
npm start

Your browser will open to localhost:3000 and update as you code. You can learn more about Create React App on GitHub

The main files in our project directory look like this:

my-app/
  README.md
  node_modules/
  package.json
  src/
    App.css
    App.js
    App.test.js
    index.css
    index.js

We’ll open our App.js and replace it with this:

import React, {Component} from 'react';

class App extends Component {
  render() {
    return <p>Hello world</p>;
  }
}

export default App;

You should now see “Hello World” in your browser.

Next, we need to add the React Polaris library to our project. We’ll install it using npm:

The last thing before we start building is to import the Polaris styles and the components we’ll need.

import React, { Component } from 'react';
import {
  Page,
  Card,
  ResourceList,
  TextStyle,
  Avatar,
} from '@shopify/polaris';
import '@shopify/polaris/styles.css';
...

A basic resource list

Let’s start with some sample data. In a real app the customer data would come from an API endpoint or as part of the initial payload from the server.

...
const customers = [
  {
    id: 341,
    url: 'customers/341',
    name: 'Mae Jemison',
    location: 'Decatur, USA',
  },
  {
    id: 256,
    url: 'customers/256',
    name: 'Ellen Ochoa',
    location: 'Los Angeles, USA',
  },
];
...

Notice that we’ve included an id for each customer. This should be a unique identifier from our database.

We also should make sure to sort our items in a way that makes sense to merchants. For this case study, let’s assume we’ve sorted this list by most recent update.

With our sample data in place we can now display a simple resource list:

...
const resourceName = {
  singular: 'customer',
  plural: 'customers',
};

class App extends Component {
  render() {
    return (
      <Page title="Customers">
        <Card>
          <ResourceList
            resourceName={resourceName}
            items={customers}
            renderItem={(item) => {
              const { id, url, name, location } = item;
              const media = <Avatar customer size="medium" name={name} />;

              return (
                <ResourceList.Item id={id} url={url} media={media} accessibilityLabel={`View details for ${name}`}>
                  <h3><TextStyle variation="strong">{name}</TextStyle></h3>
                  <div>{location}</div>
                </ResourceList.Item>
              );
            }}
          />
        </Card>
      </Page>
    );
  }
}
...

Let’s take a closer look this code.

Notice we’re providing a prop for the singular and plural name of our resource (“customers”). The resource list component will use these strings to build various pieces of content in the component, such as “Showing 50 customers”.

We’ve broken out our item renderer as a separate function to keep things clean.

Next, we’ll build is a customized resource list item.

Building a reusable custom list item

A list of orders is different than a list of products and is used by merchants differently. As a result, most resource lists benefit from careful choice of content and a customized layout. The best way to do this is to customize the built-in resource list item.

In this section, we’ll build a custom resource list item for customers:

Preview of customer list item

Defining the content

We’ll start by figuring out what information and actions merchants need when working with customers.

  • What content is useful to describe the customer?
  • What content do merchants need to find a specific customer?
  • What content related to the customer will help merchants fulfill an order or make a sale?

The customer name is essential. Their physical location is helpful too, especially for merchants with retail stores or multiple locations. Since orders and customer loyalty are important, the customer’s total order count and total spent are also useful for customer loyalty purposes. Finally, we’ll include an avatar for demonstration purposes. Since customers may not have avatars, consider leaving this out.

This gives us the following content, ranked roughly by importance:

  1. Customer name
  2. Location
  3. Number of orders
  4. Total spent
  5. Avatar
Crafting the copy

Resource lists don’t have column headings, so care must be taken to avoid ambiguous copy.

  1. Start by listing out typical values for each piece of content. If the value alone speaks for itself we can use it as-is.

    • Adam West

    • Ottawa, Canada

    • 3

    • $492.76

  2. If a value alone is ambiguous, like the number of orders and total spent, add text to make it clear. When possible, use a short phrase rather than a label with a colon.

    • 3 orders

    • 3

    • Total orders: 3

  3. If a content value is empty for a given item, use a phrase to describe the empty state. For a customer with no orders, use “No orders”. If the value is numeric, “0” may be used. Don’t indicated empty values with em dash (“—”).

    When a core content element is empty, show it grayed out using the subdued text style variation.

    • No orders

    • 0 orders

Using badges as content

The badge component calls strong attention to itself. Showing a badge on every item in a list will distract from the rest of the content.

Whenever possible, use badges conditionally, showing them only when there is an issue or something strongly notable about a particular resource.

Example of a badge highlighting open orders on an item

Building it

We’ll start by creating a src/components/CustomerListItem directory and adding three files, CustomerListItem.js, CustomerListItem.css, and index.js:

my-app/
  README.md
  node_modules/
  package.json
  src/
    components/
      CustomerListItem/
        CustomerListItem.js
        CustomerListItem.css
        index.js
    App.css
    App.js
    App.test.js
    index.css
    index.js

In CustomerListItem.js, we’ll add the following:

// CustomerListItem.js
...
import React from 'react';
import {
  ResourceList,
  Avatar,
  TextStyle,
} from '@shopify/polaris';
import '@shopify/polaris/styles.css';

export default function CustomerListItem(props) {
  const { id, url, name, location } = props;
  const media = <Avatar customer size="medium" name={name} />;

  return (
    <div className="CustomerListItem">
      <ResourceList.Item id={id} url={url} media={media} accessibilityLabel={`View details for ${name}`}>
        <h3><TextStyle variation="strong">{name}</TextStyle></h3>
        <div>{location}</div>
      </ResourceList.Item>
    </div>
  );
}
...

This component is also a good example of composition in React. Notice that most of the code here is the same as what we had in our App.js before. However, now we can build a more specific API for our customer list item on top of the basic resource list item we had before.

Notice also that our component is just a regular JavaScript function. This type of component is called a functional component.

Now we’ll add a boilerplate index.js so we can use a more concise import path for it:

// index.js
import CustomerListItem from './CustomerListItem';
export default CustomerListItem;

In our App.js, we can now import the component like so:

// App.js
import CustomerListItem from './components/CustomerListItem';

And we can use it in place of our basic resource list item, replacing the entire function we’d passed to renderItem with a one-liner:

renderItem={(customer) => <CustomerListItem {...customer} />}

Let’s also flesh out our sample data to match the content we decided to show in our customized resource list item:

// App.js
...
const customers = [
  {
    id: 341,
    url: 'customers/341',
    avatarSource: 'https://avatars.io/twitter/maejemison',
    name: 'Mae Jemison',
    location: 'Decatur, USA',
    orderCount: 5,
    totalSpent: '$497.76',
    note: 'This customer is awesome! Make sure to treat them right',
  },
  {
    id: 256,
    url: 'customers/256',
    avatarSource: 'https://avatars.io/twitter/Astro_Ellen',
    name: 'Ellen Ochoa',
    location: 'Los Angeles, USA',
    orderCount: 1,
    totalSpent: '$48.28',
  },
  {
    id: 145,
    url: 'customers/145',
    avatarSource: 'https://avatars.io/twitter/Astro_Soyeon',
    name: 'Yi So-Yeon',
    location: 'Gwangju, South Korea',
    orderCount: 2,
    totalSpent: '$73.98',
  },
];
...

Now let’s come back to our customer list item and include the new content.

// CustomerListItem.js
...
export default function CustomerListItem(props) {
  const {
    id,
    url,
    avatarSource,
    name,
    location,
    orderCount = 0,
    totalSpent = '$0.00',
  } = props;

  const media = (
    <Avatar customer size="medium" name={name} source={avatarSource} />
  );

  return (
    <div className="CustomerListItem">
      <ResourceList.Item id={id} url={url} media={media} accessibilityLabel={`View details for ${name}`}>
        <h3>{name}</h3>
        <div>{location}</div>
        <div>
          {orderCount} {orderCount === 1 ? 'order' : 'orders'}
        </div>
        <div>{totalSpent} spent</div>
      </ResourceList.Item>
    </div>
  );
}

It’s worth noting that we’re not really doing anything with id and url in this component. We’re just “forwarding” them on to ResourceList.Item. We can use the rest and spread operators to make this more resilient, so that CustomerListItem accepts any prop that ResourceList.Item does:

...
export default function CustomerListItem(props) {
  const {
    avatarSource,
    name,
    location,
    orderCount = 0,
    totalSpent = '$0.00',
    ...rest,
  } = props;

  const media = (
    <Avatar customer size="medium" name={name} source={avatarSource} />
  );

  return (
    <div className="CustomerListItem">
      <ResourceList.Item {...rest} media={media} accessibilityLabel={`View details for ${name}`}>
        ...

We now have our content in place, but it has no layout.

Laying out the content

When laying out details content:

  • Place the most distinctive and relevant piece of content at the top left. Set it in bold using the strong text style variation.
  • Arrange secondary content to the right, and if necessary, below.

To make use of the available space on wide screens, some content can be arranged in columns. Implementing this requires some care, since items aren’t aware of each other like in a data table. Column alignment works best for content that’s short and predictable in length.

Use the following guidelines:

  • Estimate the maximum expected length of the content and add a buffer.
  • Set this as the minimum width of the content element. Using a minimum width ensures that if content occasionally exceeds the expected width, it won’t break the layout.
  • Choose the alignment of text within the container. Numbers should be right-aligned.

Example of column-aligned content

To accommodate smaller screen sizes, follow these guidelines:

  • As screen size is reduced, alter the layout by stacking some content elements. Layout changes should happen at the same point for all items.
  • As the layout stacks, remove column alignment and any minimum widths.
  • On small screens, when multiple pieces of content fit on a single line, use a bullet character to separate them.

Preview of customer list item

When laying out media content:

  • If the resource doesn’t have a visual representation, it can be left out.
  • Alter size of the media content across screen sizes to improve content density and visual alignment.

Example of resizing media based on screen size

Building it

To handle the layout, we’ll need some class names and some wrapping markup.

  ...
    <div className="CustomerListItem">
      <ResourceList.Item {...rest} media={media} accessibilityLabel={`View details for ${name}`}>
        <h3 className="CustomerListItem__Title">{name}</h3>
        <div className="CustomerListItem__Location">{location}</div>
        <div className="CustomerListItem__Orders">
          <div className="CustomerListItem__OrderCount">
            {orderCount} {orderCount === 1 ? 'order' : 'orders'}
          </div>
          <div className="CustomerListItem__TotalSpent">{totalSpent} spent</div>
        </div>
      </ResourceList.Item>
    </div>
  ...

And we’ll need to write some CSS. We can import our (so far empty) CSS file from our component.

// CustomerListItem.js
import './CustomerListItem.css';

In the CSS itself, we’re going to start mobile first. We’ll open up the CSS file we just imported and write some styles for our content elements:

.CustomerListItem {
}

.CustomerListItem__Title {
  font-weight: 600;
}

.CustomerListItem__Orders {
  display: flex;
  align-items: center;
}

.CustomerListItem__Orders > *:not(:first-child)::before {
  content: '•';
  margin: 0 4px;
  color: #919eab; /* ink, lightest */
}

.CustomerListItem__OrderCount {
  white-space: nowrap;
}

.CustomerListItem__TotalSpent {
  display: flex;
  min-width: 0;
  justify-content: flex-end;
}

Note that we’ve annotated the colors here to show that they correspond to the Polaris color palette.

Now that we have our small screen layout, we can layer on the layouts for medium and wide screens. This requires some additional wrappers. With this extra markup, it’s a good time to split out some of this out to clean up the code:

  ...
  const profile = (
    <div className="CustomerListItem__Profile">
      <h3 className="CustomerListItem__Title">{name}</h3>
      <div className="CustomerListItem__Location">{location}</div>
    </div>
  );

  const orders = (
    <div className="CustomerListItem__Orders">
      <div className="CustomerListItem__OrderCount">
        {orderCount} {orderCount === 1 ? 'order' : 'orders'}
      </div>
      <div className="CustomerListItem__TotalSpent">
        {totalSpent} spent
      </div>
    </div>
  );

  return (
    <div className="CustomerListItem">
      <ResourceList.Item {...rest} media={media} accessibilityLabel={`View details for ${name}`}>
        <div className="CustomerListItem__Main">
          {profile}
          {orders}
        </div>
      </ResourceList.Item>
    </div>
  );
}

Now we can write our styles:

... @media (min-width: 640px) {
  .CustomerListItem__Main {
    display: flex;
  }

  .CustomerListItem__Main > *:not(:last-child) {
    margin-right: 20px;
  }

  .CustomerListItem__Orders {
    flex: 1;
    justify-content: flex-end;
  }

  .CustomerListItem__Orders > *:not(:first-child)::before {
    display: none;
  }

  .CustomerListItem__OrderCount {
    min-width: 80px;
  }

  .CustomerListItem__TotalSpent {
    min-width: 168px;
  }
}

@media (min-width: 800px) {
  .CustomerListItem__Profile {
    display: flex;
    flex-wrap: wrap;
  }

  .CustomerListItem__Profile > *:not(:last-child) {
    margin-right: 12px;
  }
}

Adding conditional content

Example of conditionally showing customer notes

Usually each list item contains the same content elements. When a particular resource is in a noteworthy state, additional content can be shown even though it’s not displayed with other items. For example, merchants can add a customer note on the customer’s show page. This is information the merchant took time to write down, and it’s worth surfacing in the list.

Unlike a customer’s name, we want to show this customer note only if it’s present. A good way to display conditional content in a resource list item is to use the exception list component (coming soon).

Actions can also be presented conditionally, based on the state of the item. For example, for customers that have an open order, we can highlight this and provide a link to those orders.

Example of conditionally showing a link to open orders

Building it

To build this, we’ll add a couple new values to our data and accept them as props:

// App.js
...
const customers = [
  {
    id: 341,
    url: 'customers/341',
    avatarSource: 'https://avatars.io/twitter/maejemison',
    name: 'Mae Jemison',
    location: 'Decatur, USA',
    orderCount: 5,
    totalSpent: '$497.76',
    note: 'This customer is awesome! Make sure to treat them right',
    openOrderCount: 2,
    openOrdersUrl: 'orders/1456',
  },
  ...

We’ll need to import some additional components from Polaris…

  ...
  ExceptionList,
  Button,
} from '@shopify/polaris';
...

…and use them, along with our new data, to render an exception list and a button under the right conditions:

...
export default function CustomerListItem(props) {
  const {
    avatarSource,
    name,
    location,
    orderCount = 0,
    totalSpent = '$0.00',
    note,
    openOrderCount,
    openOrdersUrl,
    ...rest,
  } = props;

  ...

  let exceptions = [];
  let conditionalAction = null;

  if (note) {
    exceptions.push({ icon: 'notes', summary: note });
  }

  if (openOrderCount !== undefined) {
    const label = openOrderCount === 1 ? 'order' : 'orders';
    const summary = `${openOrderCount} open ${label}`;
    exceptions.push({ status: 'warning', icon: 'alert', summary });
    conditionalAction = (
      <Button plain url={openOrdersUrl} external>
        View open orders
      </Button>
    );
  }

  const exceptionList = exceptions.length
    ? (
      <div className="CustomerListItem__Exceptions">
        <ExceptionList items={exceptions} />
      </div>
    )
    : null;

  const conditionalActions = conditionalAction
    ? (
      <div className="CustomerListItem__ConditionalActions">
        {conditionalAction}
      </div>
    )
    : null;

  return (
    <div className="CustomerListItem">
      <ResourceList.Item {...rest} media={media} accessibilityLabel={`View details for ${name}`}>
        <div className="CustomerListItem__Main">
          {profile}
          {orders}
        </div>
        {exceptionList}
        {conditionalActions}
      </ResourceList.Item>
    </div>
  );
}

We can finish this off with a couple of simple styles:

/* CustomerListItem.css */
...
.CustomerListItem__TotalSpent {
  display: flex;
  min-width: 0;
  justify-content: flex-end;
}

.CustomerListItem__Exceptions {
  margin-top: 4px;
}

.CustomerListItem__ConditionalActions {
  margin-top: 4px;
}
...

Adding shortcut actions to resource list items

Example of a shortcut to a customer’s latest order

Occasionally a resource has an action that merchants use a lot. Fulfilling orders is a good example. This action is not only popular, it’s the most important action for open orders.

It makes sense to surface this key action from the show page on each list item, but adding this action to each item would be visually repetitive.

Shortcut actions resolve this. They provide a way to promote popular actions by showing them when the merchant hovers their mouse over a list item. As long as the shortcut action remains available on the resource’s show page, merchants using devices without a mouse can still access them.

Our customer list item can benefit from a shortcut action that lets merchants jump to a customer’s most recent order.

Best practices

Shortcut actions on resource list items must:

  • Be present on the resource’s show page so they’re accessible without a mouse.

Shortcut actions should:

  • Only be provided for actions that are part of a critical, common merchant task.
  • Be available on every item in the list. If the state of a particular resource doesn’t permit the action, it can be left out.
Content guidelines

Shortcut actions should:

  • Not include the noun from their label if the noun refers to the resource itself. For example, for a list of orders:

    • Start fulfilling

    • Start fulfilling order

  • Use the full verb + noun formula for actions that refer to another object.

    • View latest order

    • Latest order

Building it

Shortcut actions can be defined as part of our custom list item, or we can leave it up to the developer using our component to define them for each list, just as they would using ResourceList.Item.

If we were to build a shortcut action into the custom item, we could offer the merchant a link to the customer’s most recent order instead of conditional actions. We could add a prop to allow this:

    ...
    openOrderCount,
    openOrdersUrl,
    latestOrderUrl,
    ...rest,
  } = props;

  ...

  const shortcutActions = openOrdersUrl
    ? [{ content: 'View latest order', url: openOrdersUrl }]
    : null;

  return (
    <div className="CustomerListItem">
      <ResourceList.Item
        {...rest}
        media={media}
        shortcutActions={shortcutActions}
        accessibilityLabel={`View details for ${name}`}
      >
      ...

With that, our custom list item is done.

Now let’s save our merchants some time by using more features of the resource list component.

Adding bulk actions to a resource list

Image showing bulk selection and actions

Resource lists support optional bulk actions. These allow merchants to select items and perform an action on the selection.

Taking action on many items at once can save merchants a lot of time. However, it can also be difficult to undo. Merchants need to have a high degree of confidence that they aren’t making mistakes in bulk.

  • Be deliberate about content elements shown on each list item. Make sure merchants have the content and context they need to be confident about taking action on many resources at once.
  • Provide conditional content to make merchants aware when a resource is in a notable or exceptional state.

Because resource lists prioritize acting on individual items, selection checkboxes are hidden by default on small screens to save space for content. A bulk actions mode can be toggled on or off using a button that is made visible at these screen sizes.

Sequence showing bulk actions on a small device

Up to two frequently-used bulk actions may be visually promoted outside of the actions menu to improve ease of access and discoverability. On narrower screens, promoted actions move back into the actions menu, but always appear at the top of the list.

Promoted actions on wide and narrow screens

Best practices

Bulk actions are optional. If a resource list is always very short, or if there’s no action that makes sense for merchants to perform in bulk, don’t offer bulk actions.

When offering bulk actions, they should:

  • Save merchants time (it makes sense to take the action on many resources at once)
  • Warn merchants when a bulk action is irreversible using a confirmation modal
  • Be shown as promoted bulk actions if they are frequently used
  • Be shown in the in order they are most often used

Content guidelines

Be strategic about what type of buik actions you provide to merchants—ask yourself if they’ll save merchants time.

Button and menu item copy

Bulk action buttons and menu items should use the full verb + noun pattern.

Applying the guidelines

For our customers list, we’ve decided to offer the following bulk actions:

Bulk action copy Notes
Edit customers Opens the bulk editor to allow mass edits. This will be a primary bulk action.
Add tags
Remove tags
Delete customers Should present a confirmation modal to ensure merchants really intend a bulk delete action.

Building it

We’ll start where we left off previously, with our items being rendered.

Now we’ll add the bulk actions. We’ll need to do several things to get this wired up:

  1. Define our bulk actions and pass them to the resource list
  2. Add a handler to respond when the merchant begins making a bulk selection
  3. Add a way to keep track of which items have been selected and make sure our component knows, so it can display the change

The way we keep track of the current selection is with state.

...
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      selectedItems: [],
    }

    this.handleSelectionChange = this.handleSelectionChange.bind(this);
  }

  render() {
    const { selectedItems } = this.state;

    return (
      <ResourceList
        resourceName={resourceName}
        items={customers}
        renderItem={(customer) => <CustomerListItem {...customer} />}
        selectedItems={selectedItems}
        onSelectionChange={this.handleSelectionChange}
        promotedBulkActions={[
          { content: 'Edit customers' },
        ]}
        bulkActions={[
          { content: 'Add tags' },
          { content: 'Remove tags' },
          { content: 'Delete customers' },
        ]}
      />
    );
  }

  handleSelectionChange(selectedItems) {
    this.setState({ selectedItems });
  }
}

This allows merchants to make a selection and see the change. Next, we’ll wire up the bulk action buttons.

...
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      selectedItems: [],
    }

    this.handleSelectionChange = this.handleSelectionChange.bind(this);
    this.handleBulkEdit = this.handleBulkEdit.bind(this);
    this.handleBulkAddTags = this.handleBulkAddTags.bind(this);
    this.handleBulkRemoveTags = this.handleBulkRemoveTags.bind(this);
    this.handleBulkDelete = this.handleBulkDelete.bind(this);
  }

  render() {
    const { selectedItems } = this.state;

    return (
      <ResourceList
        ...
        promotedBulkActions={[
          { content: 'Edit customers', onAction: this.handleBulkEdit },
        ]}
        bulkActions={[
          { content: 'Add tags', onAction: this.handleBulkAddTags },
          { content: 'Remove tags', onAction: this.handleBulkRemoveTags },
          { content: 'Delete customers', onAction: this.handleBulkDelete },
        ]}
      />
    );
  }

  handleSelectionChange(selectedItems) {
    this.setState({ selectedItems });
  }

  handleBulkEdit() {
    console.log('Opening bulk editor…');
  }

  handleBulkAddTags() {
    console.log('Asynchronously adding tags to customers…');
    // A Flash message should be displayed to confirm that async process
    // has started.
  }

  handleBulkRemoveTags() {
    console.log('Removing tags from customers…');
  }

  handleBulkDelete() {
    console.log('Handling bulk customer deletion…');
    // Since this action destroys resources in bulk, show a
    // confirmation modal before completing the action.
  }
}

If you’re new to React or ES2015 you might be wondering about the lines in our constructor that re-assign our handlers. This is a pattern that ensures this in our callbacks is correctly bound to the instance. Doing so in the class constructor helps ensure good performance.

Adding sorting to the list

Detail of the resource list header showing the sort control

When a merchant sorts a list of resources they’re changing the order of the entire set. This is different from filtering, which is when the list of resources is narrowed down to a subset of the original list.

Whether or not you provide sort options, resource lists should have a default sort order that makes sense to merchants and supports their most common tasks.

When you provide sort options to merchants, they’re presented using a select component placed in a standard position in the list header. Each option represents the content element to sort by and a sort direction (ascending or descending).

Best practices

Sort options should:

  • Be offered for long lists, especially paginated lists.
  • Usually correspond to visible content elements in the list, but don’t have to.
  • Avoid offering more than about 8 sort options. Use research to determine the most common ways merchants want to sort a particular list.

Content guidelines

A sort order is always based on a content element, like the customer name or the number of orders. For now, let’s refer to this content element as the “sort basis”.

  1. The basic content formula for sort options is {sort direction} + {sort basis}.

    The sort direction should consist of words like “Most”/“Least”, “High”/“Low”, or “Newest”/“Oldest”.

    Do

    Sort by
    Most spent
    Least spent

    Don’t

    Sort by
    High spend
    Low spend

    Do

    Sort by
    High conversion
    Low conversion

    Don’t

    Sort by
    Largest conversion
    Smallest conversion

    The sort basis can consist of multiple words to avoid ambiguity.

    Do

    Sort by
    Most online store visits

    Don’t

    Sort by
    Most visits

    Avoid using multiple words for the sort direction. Adding “-est” may help.

    Do

    Sort by
    Newest update
    Oldest update

    Don’t

    Sort by
    Most recent update
    Least recent update

  2. If sorting alphabetically, the formula is slightly different. Indicate the sort direction with “A–Z” or “Z–A” at the end of the text, without parentheses. Note the use of an en dash without spaces on either side.

    Do

    Sort by
    Product title A–Z
    Product title Z–A

    Don’t

    Sort by
    Product title (A - Z)
    Product title (Z - A)

  3. Sometimes it doesn’t make sense to offer both sort directions, such as when sorting by overall relevance. It’s not a requirement to offer both directions. When offering a single sort direction, the sort direction text can be omitted from the formula.

    Do

    Sort by
    Relevance

    Don’t

    Sort by
    Most relevant
    Least relevant

Applying the guidelines

Based on merchant research and following the best practices and content guidelines, we’ve decided to offer the following options for our customer list:

Sort option Copy
Date updated (newest first) Default Newest update
Date updated (oldest first) Oldest update
Lifetime spent (highest first) Most spent
Order count (highest first) Most orders
Customer last name (A–Z) Last name A–Z
Customer last name (Z–A) Last name Z–A

Building it

We’ll start where we left off, with bulk actions in place. Remember that even before adding sort options, our customer data has already been sorted by most recent update, since this is most helpful to merchants.

As with bulk actions, there are broadly three parts to the implementation:

  1. Defining the sort options and passing them to our list
  2. Tracking the currently selected option in state and making sure our list receives the value in render
  3. Setting up a handler to respond to and update the state when the merchant changes the sort option
...
const sortOptions = [
  { label: 'Newest update', value: 'DATE_MODIFIED_DESC' },
  { label: 'Oldest update', value: 'DATE_MODIFIED_ASC' },
  { label: 'Most spent', value: 'TOTAL_SPENT_DESC' },
  { label: 'Most orders', value: 'ORDER_COUNT_DESC' },
  { label: 'Last name A–Z', value: 'ALPHABETICAL_ASC' },
  { label: 'Last name Z–A', value: 'ALPHABETICAL_DESC' },
];

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      selectedItems: [],
      sortValue: 'DATE_MODIFIED_DESC',
    }
    ...
    this.handleSortChange = this.handleSortChange.bind(this);
  }

  render() {
    const {selectedItems, sortValue} = this.state;

    return (
      <ResourceList
        ...
        sortOptions={sortOptions}
        sortValue={sortValue}
        onSortChange={this.handleSortChange}
      />
    );
  }

  handleSortChange(sortValue) {
    this.setState({ sortValue });
  }

  handleSelectionChange(selectedItems) {
    this.setState({ selectedItems });
  }
  ...

We still have one issue though: our items haven’t been re-sorted. To do this, we’ll need to move the items into state. When our sort change handler is called, we’ll build a new array of options and update the items in state.

The actual logic used to build the new items array is dependent on your app, and so the implementation here has been left as a stub. However, it will likely involve fetching new item data from the server.

...
const sortOptions = [
  { label: 'Newest update', value: 'DATE_MODIFIED_DESC' },
  { label: 'Oldest update', value: 'DATE_MODIFIED_ASC' },
  { label: 'Most spent', value: 'TOTAL_SPENT_DESC' },
  { label: 'Most orders', value: 'ORDER_COUNT_DESC' },
  { label: 'Last name A–Z', value: 'ALPHABETICAL_ASC' },
  { label: 'Last name Z–A', value: 'ALPHABETICAL_DESC' },
];

// Not implemented
function fetchCustomers() {
  return customers;
}

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: customers,
      selectedItems: [],
      sortValue: 'DATE_MODIFIED_DESC',
    }

    ...
  }

  render() {
    const { items, selectedItems, sortValue } = this.state;

    return (
      <ResourceList
        ...
        items={items}
        ...
        sortOptions={sortOptions}
        sortValue={sortValue}
        onSortChange={this.handleSortChange}
      />
    );
  }

  handleSortChange(sortValue) {
    const items = fetchCustomers();
    this.setState({ items, sortValue });
  }
  …

Adding filtering to the list

Filter control example

Filtering allows a resource list to be narrowed based on one or more criteria. The resource list component provides a standard filter control.

Best practices and content guidelines

For filtering guidelines, see the corresponding section under the resource list filter control subcomponent:

Applying the guidelines

Based on merchant research and following the best practices and content guidelines, we’ve decided to offer the following filtering options:

Filter label Operator text Filter input
Money spent is greater than TextField
Number of orders is greater than TextField
Order date is In the last week
In the last month
In the last three months
In the last year
Is an email subscriber   Yes
No
Tagged with   Textfield
Located in country Textfield

Building it

We’ll start with the bulk actions and sorting we added previously and create an object representing the available filters.

// App.js
...
const availableFilters = [
  {
    key: 'spentFilter',
    label: 'Money spent',
    operatorText:'is greater than',
    type: FilterType.TextField,
  },
  {
    key: 'orderCountFilter',
    label: 'Number of orders',
    operatorText:'is greater than',
    type: FilterType.TextField,
  },
  {
    key: 'orderDateFilter',
    label: 'Order date',
    operatorText:'is',
    type: FilterType.Select,
    options: [
      'In the last week',
      'In the last month',
      'In the last three months',
      'In the last year',
    ],
  },
  {
    key: 'emailSubscriberFilter',
    label: 'Is an email subscriber',
    type: FilterType.Select,
    options: [
      'Yes',
      'No',
    ],
  },
  {
    key: 'tagsFilter',
    label: 'Tagged with',
    type: FilterType.TextField,
  },
  {
    key: 'locationFilter',
    label: 'Located in',
    operatorText: 'country',
    type: FilterType.Select,
  },
];
...

Resource list doesn’t accept these available filters directly. Instead, it delegates rendering of the filter control to a separate component. Here we’ll use the built-in resource list filter control.

  ...
  render() {
    return (
      <ResourceList
        ...
        onSortChange={this.handleSortChange}
        filterControl={
          <ResourceList.FilterControl
            filters={availableFilters}
          />
        }
      />
    );
  }
  ...

Next, we need to deal with state. We’ll add 2 new properties to our state object. One will handle the text input in the filter control’s search field. The other property will handle the rest of the filters. As we did in our sorting implementation, we’ll add handler methods that call setState to update the UI when the merchant changes the filters.

...
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: customers,
      selectedItems: [],
      sortValue: 'DATE_MODIFIED_DESC',
      appliedFilters: [],
      searchValue: '',
    }
    ...
    this.handleFiltersChange = this.handleFiltersChange.bind(this);
    this.handleSearchChange = this.handleSearchChange.bind(this);
  }

  render() {
    return (
      const {
        items,
        selectedItems,
        sortValue,
        appliedFilters,
        searchValue
      } = this.state;

      <ResourceList
        ...
        sortOptions={sortOptions}
        sortValue={sortValue}
        onSortChange={this.handleSortChange}
        filterControl={
          <ResourceList.FilterControl
            filters={availableFilters}
            appliedFilters={appliedFilters}
            onFiltersChange={this.handleFiltersChange}
            searchValue={searchValue}
            onSearchChange={this.handleSearchChange}
          />
        }
      />
    );
  }

  handleFiltersChange(appliedFilters) {
    const items = fetchCustomers();
    this.setState({ items, appliedFilters });
  }

  handleSearchChange(searchValue) {
    const items = fetchCustomers();
    this.setState({ items, searchValue });
  }
  ...
}

As with sorting, exactly how the new items array is generated depends on your application, and so the implementation here is left as a stub. However, it will likely involve fetching new item data from the server.

Adding pagination to the list

A resource list with pagination

Resource lists can be long. To make the list digestible, it should be split into pages at 50 items or fewer. Use the pagination component to allow navigation between pages.

Place the pagination immediate below the resource list.

Pagination interacts with bulk actions. When a resource list is paginated, the Select all control selects only the visible items. You can offer the option to select everything in the entire list.

Selecting across pages in a paginated list

Best practices for pagination

Resource lists should:

  • Have a URL for each page.
  • Be paginated when they have more than 50 items.
  • Disable the pagination component’s previous (or next) button on the first (or last) page in the list.

Align the pagination controls to the left, or centered. The exact layout is flexible.

Building it

The first thing we’ll need to do is build a simple component to handle our pagination layout. Like our custom list item, we’ll add a component to our local components directory:

my-app/
  README.md
  node_modules/
  package.json
  src/
    components/
      CustomerListItem/
        CustomerListItem.jsx
        CustomerListItem.css
        index.js
      CustomerListFooter/
        CustomerListFooter.jsx
        CustomerListFooter.css
        index.js
    App.css
    App.js
    App.test.js
    index.css
    index.js
/* CustomerListFooter.css */
.CustomerListFooter {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  border-top: 1px solid #dfe4e8; /* sky */
}
// CustomerListFooter.js
import React from 'react';

import './CustomerListFooter.css';

export default function CustomerListFooter(props) {
  return <div className="CustomerListFooter">{props.children}</div>;
}
// index.js
import CustomerListFooter from './CustomerListFooter';
export default CustomerListFooter;

Now we can use this component to add our pagination.

import React from 'react';
import {
  Page,
  Card,
  ResourceList,
  Pagination,
} from '@shopify/polaris';
import '@shopify/polaris/styles.css';

import CustomerListItem from './components/CustomerListItem';
import CustomerListFooter from './components/CustomerListFooter';

...

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: customers,
      selectedItems: [],
      sortValue: 'DATE_MODIFIED_DESC',
      appliedFilters: [],
      searchValue: '',
      isFirstPage: true,
      isLastPage: false,
    }
    ...
    this.handlePreviousPage = this.handlePreviousPage.bind(this);
    this.handleNextPage = this.handleNextPage.bind(this);
  }

  render() {
    const {
      items,
      selectedItems,
      sortValue,
      appliedFilters,
      searchValue,
      isFirstPage,
      isLastPage,
    } = this.state;

    const paginationMarkup = items.length > 0
      ? (
        <CustomerListFooter>
          <Pagination
            hasPrevious={!isFirstPage}
            hasNext={!isLastPage}
            onPrevious={this.handlePreviousPage}
            onNext={this.handleNextPage}
          />
        </CustomerListFooter>
      )
      : null;

    return (
      <ResourceList
        ...
      />

      {paginationMarkup}
    );
  }

  handlePreviousPage() {
    const items = fetchCustomers();
    // Todo: figure out how to determine if items represent
    // first or last page.
    this.setState({ items, isFirstPage: true, isLastPage: false });
  }

  handleNextPage() {
    const items = fetchCustomers();
    // Todo: figure out how to determine if items represent
    // first or last page.
    this.setState({ items, isFirstPage: false, isLastPage: true });
  }
  ...

If we want to allow selecting all items across all pages in our paginated resource list, we can enable this interaction with the hasMoreItems boolean prop.

      ...
      <ResourceList
        ...
        filterControl={
          <ResourceList.FilterControl
            ...
          />
        }
        hasMoreItems
      />

End result

And with that, our resource list UI is complete. Here is our finished code:

// App.js
import React, {Component} from 'react';
import {
  Page,
  Card,
  ResourceList,
  FilterType,
  Pagination,
} from '@shopify/polaris';
import '@shopify/polaris/styles.css';

import CustomerListItem from './components/CustomerListItem';
import CustomerListFooter from './components/CustomerListFooter';

const resourceName = {
  singular: 'customer',
  plural: 'customers',
};

// This would normally come from an API request
const customers = [
  {
    id: 341,
    url: 'customers/341',
    avatarSource: 'https://avatars.io/twitter/maejemison',
    name: 'Mae Jemison',
    location: 'Decatur, USA',
    orderCount: 5,
    totalSpent: '$497.76',
    note: 'This customer is awesome! Make sure to treat them right',
    openOrderCount: 2,
    openOrdersUrl: 'http://google.com',
  },
  {
    id: 256,
    url: 'customers/256',
    avatarSource: 'https://avatars.io/twitter/Astro_Ellen',
    name: 'Ellen Ochoa',
    location: 'Los Angeles, USA',
    orderCount: 1,
    totalSpent: '$48.28',
  },
  {
    id: 145,
    url: 'customers/145',
    avatarSource: 'https://avatars.io/twitter/Astro_Soyeon',
    name: 'Yi So-Yeon',
    location: 'Gwangju, South Korea',
    orderCount: 2,
    totalSpent: '$73.98',
  },
];

const sortOptions = [
  {label: 'Newest update', value: 'DATE_MODIFIED_DESC'},
  {label: 'Oldest update', value: 'DATE_MODIFIED_ASC'},
  {label: 'Most spent', value: 'TOTAL_SPENT_DESC'},
  {label: 'Most orders', value: 'ORDER_COUNT_DESC'},
  {label: 'Last name A–Z', value: 'ALPHABETICAL_ASC'},
  {label: 'Last name Z–A', value: 'ALPHABETICAL_DESC'},
];

const availableFilters = [
  {
    key: 'spentFilter',
    label: 'Money spent',
    operatorText: 'is greater than',
    type: FilterType.TextField,
  },
  {
    key: 'orderCountFilter',
    label: 'Number of orders',
    operatorText: 'is greater than',
    type: FilterType.TextField,
  },
  {
    key: 'orderDateFilter',
    label: 'Order date',
    operatorText: 'is',
    type: FilterType.Select,
    options: [
      'In the last week',
      'In the last month',
      'In the last three months',
      'In the last year',
    ],
  },
  {
    key: 'emailSubscriberFilter',
    label: 'Is an email subscriber',
    type: FilterType.Select,
    options: ['Yes', 'No'],
  },
  {
    key: 'tagsFilter',
    label: 'Tagged with',
    type: FilterType.TextField,
  },
  {
    key: 'locationFilter',
    label: 'Located in',
    operatorText: 'country',
    type: FilterType.Select,
  },
];

// Not implemented
function fetchCustomers(options) {
  return customers;
}

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: customers,
      selectedItems: [],
      sortValue: 'DATE_MODIFIED_DESC',
      appliedFilters: [],
      searchValue: '',
      isFirstPage: true,
      isLastPage: false,
    };

    this.handleSelectionChange = this.handleSelectionChange.bind(this);
    this.handleBulkEdit = this.handleBulkEdit.bind(this);
    this.handleBulkAddTags = this.handleBulkAddTags.bind(this);
    this.handleBulkRemoveTags = this.handleBulkRemoveTags.bind(this);
    this.handleBulkDelete = this.handleBulkDelete.bind(this);
    this.handleSortChange = this.handleSortChange.bind(this);
    this.handleFiltersChange = this.handleFiltersChange.bind(this);
    this.handleSearchChange = this.handleSearchChange.bind(this);
    this.handlePreviousPage = this.handlePreviousPage.bind(this);
    this.handleSaveFilters = this.handleSaveFilters.bind(this);
    this.handlePreviousPage = this.handlePreviousPage.bind(this);
    this.handleNextPage = this.handleNextPage.bind(this);
  }

  render() {
    const {
      items,
      selectedItems,
      sortValue,
      appliedFilters,
      searchValue,
      isFirstPage,
      isLastPage,
    } = this.state;

    const paginationMarkup =
      items.length > 0 ? (
        <CustomerListFooter>
          <Pagination
            hasPrevious={!isFirstPage}
            hasNext={!isLastPage}
            onPrevious={this.handlePreviousPage}
            onNext={this.handleNextPage}
          />
        </CustomerListFooter>
      ) : null;

    return (
      <Page title="Customers">
        <Card>
          <ResourceList
            resourceName={resourceName}
            items={items}
            renderItem={(customer) => <CustomerListItem {...customer} />}
            selectedItems={selectedItems}
            onSelectionChange={this.handleSelectionChange}
            promotedBulkActions={[
              {content: 'Edit customers', onAction: this.handleBulkEdit},
            ]}
            bulkActions={[
              {content: 'Add tags', onAction: this.handleBulkAddTags},
              {content: 'Remove tags', onAction: this.handleBulkRemoveTags},
              {content: 'Delete customers', onAction: this.handleBulkDelete},
            ]}
            sortOptions={sortOptions}
            sortValue={sortValue}
            onSortChange={this.handleSortChange}
            filterControl={
              <ResourceList.FilterControl
                filters={availableFilters}
                appliedFilters={appliedFilters}
                onFiltersChange={this.handleFiltersChange}
                searchValue={searchValue}
                onSearchChange={this.handleSearchChange}
                additionalAction={{
                  content: 'Save',
                  onAction: this.handleSaveFilters,
                }}
              />
            }
            hasMoreItems
          />

          {paginationMarkup}
        </Card>
      </Page>
    );
  }

  handlePreviousPage() {
    const items = fetchCustomers();
    // Todo: figure out how to determine if items represent
    // first or last page.
    this.setState({items, isFirstPage: true, isLastPage: false});
  }

  handleNextPage() {
    const items = fetchCustomers();
    // Todo: figure out how to determine if items represent
    // first or last page.
    this.setState({items, isFirstPage: false, isLastPage: true});
  }

  handleFiltersChange(appliedFilters) {
    const items = fetchCustomers();
    this.setState({items, appliedFilters});
  }

  handleSearchChange(searchValue) {
    const items = fetchCustomers();
    this.setState({items, searchValue});
  }

  handleSortChange(sortValue) {
    const items = fetchCustomers();
    this.setState({items, sortValue});
  }

  handleSelectionChange(selectedItems) {
    this.setState({selectedItems});
  }

  handleBulkEdit() {
    console.log('Opening bulk editor…');
  }

  handleBulkAddTags() {
    console.log('Asynchronously adding tags to customers…');
    // A Flash message should be displayed to confirm that async process
    // has started.
  }

  handleBulkRemoveTags() {
    console.log('Removing tags from customers…');
  }

  handleBulkDelete() {
    console.log('Handling bulk customer deletion…');
    // Since this action destroys resources in bulk, show a
    // confirmation modal (“Are you sure you want to delete {n}
    // customers”) before completing the action.
  }

  handleSaveFilters() {
    console.log('Saving current filters…');
  }
}

export default App;