Skip to content

Commit

Permalink
Show loading spinner while loading views in List Table (#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
jesstelford committed May 7, 2019
1 parent 0ea974f commit 5637518
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 127 deletions.
1 change: 1 addition & 0 deletions .changeset/f4c4c52c/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "releases": [{ "name": "@keystone-alpha/admin-ui", "type": "patch" }], "dependents": [] }
1 change: 1 addition & 0 deletions .changeset/f4c4c52c/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Show loading spinner while loading views in List Table
182 changes: 127 additions & 55 deletions packages/admin-ui/client/components/ListTable.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import React, { Component } from 'react';
import React, { Component, Suspense, Fragment } from 'react';
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';

import { captureSuspensePromises, noop } from '@keystone-alpha/utils';
import { DiffIcon, KebabHorizontalIcon, LinkIcon, ShieldIcon, TrashcanIcon } from '@arch-ui/icons';
import { colors, gridSize } from '@arch-ui/theme';
import { alpha } from '@arch-ui/color-utils';
Expand All @@ -15,6 +16,10 @@ import { Card } from '@arch-ui/card';
import DeleteItemModal from './DeleteItemModal';
import { copyToClipboard } from '../util';
import { useListSort } from '../pages/List/dataHooks';
import PageLoading from './PageLoading';
import { NoResults } from './NoResults';

const Render = ({ children }) => children();

// Styled Components
const Table = styled('table')({
Expand Down Expand Up @@ -214,12 +219,6 @@ class ListRow extends Component {
{fields.map(field => {
const { path } = field;

const isLoading = !item.hasOwnProperty(path);

if (isLoading) {
return <BodyCell key={path} />; // TODO: Better loading state?
}

if (itemErrors[path] instanceof Error && itemErrors[path].name === 'AccessDeniedError') {
return (
<BodyCell key={path}>
Expand Down Expand Up @@ -292,6 +291,12 @@ class ListRow extends Component {
}
}

const SingleCell = ({ columns, children }) => (
<tr>
<td colSpan={columns}>{children}</td>
</tr>
);

export default function ListTable(props) {
const {
adminPath,
Expand All @@ -304,68 +309,135 @@ export default function ListTable(props) {
onChange,
onSelectChange,
selectedItems,
currentPage,
filters,
search,
} = props;

const [sortBy, onSortChange] = useListSort(list.key);

const handleSelectAll = () => {
const allSelected = items.length === selectedItems.length;
const allSelected = items && items.length === selectedItems.length;
const value = allSelected ? [] : items.map(i => i.id);
onSelectChange(value);
};

const cypressId = 'ks-list-table';

return (
<Card css={{ marginBottom: '3em' }}>
<Table id={cypressId} style={{ tableLayout: isFullWidth ? null : 'fixed' }}>
<colgroup>
<col width="32" />
{fields.map(f => (
<col key={f.path} />
))}
<col width="32" />
</colgroup>
<thead>
<tr>
<HeaderCell>
<div css={{ position: 'relative', top: 3 }}>
<CheckboxPrimitive
checked={items.length === selectedItems.length}
onChange={handleSelectAll}
tabIndex="0"
/>
</div>
</HeaderCell>
{fields.map(field => (
<SortLink
data-field={field.path}
key={field.path}
sortable={field.path !== '_label_'}
field={field}
handleSortChange={onSortChange}
active={sortBy.field.path === field.path}
sortAscending={sortBy.direction === 'ASC'}
// +2 because of check-boxes on left, and overflow menu on right
const columns = fields.length + 2;

const TableContents = ({ isLoading, children }) => (
<Fragment>
<colgroup>
<col width="32" />
{fields.map(f => (
<col key={f.path} />
))}
<col width="32" />
</colgroup>
<thead>
<tr>
<HeaderCell>
<div css={{ position: 'relative', top: 3 }}>
<CheckboxPrimitive
checked={items && items.length === selectedItems.length}
onChange={isLoading ? noop : handleSelectAll}
tabIndex="0"
isDisabled={isLoading}
/>
))}
<HeaderCell css={{ padding: 0 }}>{columnControl}</HeaderCell>
</tr>
</thead>
<tbody>
{items.map((item, itemIndex) => (
<ListRow
fields={fields}
isSelected={selectedItems.includes(item.id)}
item={item}
itemErrors={itemsErrors[itemIndex] || {}}
key={item.id}
link={({ path, id }) => `${adminPath}/${path}/${id}`}
list={list}
onDelete={onChange}
onSelectChange={onSelectChange}
</div>
</HeaderCell>
{fields.map(field => (
<SortLink
data-field={field.path}
key={field.path}
sortable={field.path !== '_label_'}
field={field}
handleSortChange={onSortChange}
active={sortBy.field.path === field.path}
sortAscending={sortBy.direction === 'ASC'}
/>
))}
</tbody>
<HeaderCell css={{ padding: 0 }}>{columnControl}</HeaderCell>
</tr>
</thead>
<tbody data-test-table-loaded={!isLoading}>{children}</tbody>
</Fragment>
);

return (
<Card css={{ marginBottom: '3em' }}>
<Table id={cypressId} style={{ tableLayout: isFullWidth ? null : 'fixed' }}>
<Suspense
fallback={
<TableContents isLoading>
<SingleCell columns={columns}>
<PageLoading />
</SingleCell>
</TableContents>
}
>
<Render>
{() => {
// Now that the network request for data has been triggered, we
// try to initialise the fields. They are Suspense capable, so may
// throw Promises which will be caught by the above <Suspense>
captureSuspensePromises(
fields
.filter(field => field.path !== '_label_')
.map(field => () => field.initCellView())
);

// NOTE: We don't check for isLoading here because we want to
// avoid showing the <PageLoading /> component when we already
// have (possibly stale) data to show.
// Instead, we show the loader when there's _no data at all_.
if (!items) {
return (
<TableContents isLoading>
<SingleCell columns={columns}>
<PageLoading />
</SingleCell>
</TableContents>
);
}

if (!items.length) {
return (
<TableContents>
<SingleCell columns={columns}>
<NoResults
currentPage={currentPage}
filters={filters}
list={list}
search={search}
/>
</SingleCell>
</TableContents>
);
}

return (
<TableContents>
{items.map((item, itemIndex) => (
<ListRow
fields={fields}
isSelected={selectedItems.includes(item.id)}
item={item}
itemErrors={itemsErrors[itemIndex] || {}}
key={item.id}
link={({ path, id }) => `${adminPath}/${path}/${id}`}
list={list}
onDelete={onChange}
onSelectChange={onSelectChange}
/>
))}
</TableContents>
);
}}
</Render>
</Suspense>
</Table>
</Card>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Button } from '@arch-ui/button';
import { InfoIcon } from '@arch-ui/icons';
import { colors } from '@arch-ui/theme';

import { useListPagination } from './dataHooks';
import { useListPagination } from '../pages/List/dataHooks';

const NoResultsWrapper = ({ children, ...props }) => (
<div
Expand All @@ -27,7 +27,7 @@ const NoResultsWrapper = ({ children, ...props }) => (
</div>
);

export const NoResults = ({ currentPage, filters, itemCount, list, search }) => {
export const NoResults = ({ currentPage, filters, list, search }) => {
const { onChange } = useListPagination(list.key);
const onResetPage = () => onChange(1);

Expand Down Expand Up @@ -64,9 +64,5 @@ export const NoResults = ({ currentPage, filters, itemCount, list, search }) =>
);
}

if (itemCount === 0) {
return <NoResultsWrapper>No {list.plural.toLowerCase()} to display yet...</NoResultsWrapper>;
}

return null;
return <NoResultsWrapper>No {list.plural.toLowerCase()} to display yet...</NoResultsWrapper>;
};
Loading

0 comments on commit 5637518

Please sign in to comment.