Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reusable and accessible table component #1700

Merged
merged 37 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
aef6ca1
fix: add initial `offset` param for `GET user/uploads` to support pag…
joshJarr May 27, 2022
dc0dceb
fix: Revert default size back to 25, update tests.
joshJarr May 30, 2022
e296d4d
fix: Add tests for pagination to ensure correct link with offset and …
joshJarr Jun 6, 2022
0dfeb70
Merge branch 'main' of github.com:web3-storage/web3.storage into fix/…
joshJarr Jun 6, 2022
8a12885
fix: Add test for sorting by `sortOrder=Asc` in the `GET user/uploads…
joshJarr Jun 6, 2022
f497d20
fix: Fix linting
joshJarr Jun 6, 2022
f807bec
fix: Adds `count`, `size`, `offset`, and `Prev_link` to header respon…
joshJarr Jun 6, 2022
b7ed26b
fix: Address PR feedback, fix linting.
joshJarr Jun 7, 2022
bc62d68
fix: remove .only in test, change `.not` to `.notStrictEqual`
joshJarr Jun 7, 2022
db5e0f7
fix: Fix the db tests to handle a returned count as well as upload.
joshJarr Jun 7, 2022
5deba87
fix: Address PR feedback on tests, use `.ok` over `.notStrictEqual` t…
joshJarr Jun 8, 2022
d0099fe
Merge branch 'main' of github.com:web3-storage/web3.storage into fix/…
joshJarr Jun 8, 2022
e80a473
fix: Add correct cors headers so that upload metadata can be accessed.
joshJarr Jun 14, 2022
5341db7
Merge branch 'main' of github.com:web3-storage/web3.storage into fix/…
joshJarr Jun 14, 2022
4821c0a
fix: Address prfeedback, use `page` over `offset` param.
joshJarr Jun 20, 2022
6bcb182
Merge branch 'main' of github.com:web3-storage/web3.storage into fix/…
joshJarr Jun 20, 2022
7342533
chore: update documentation and headers to use page over offset.
joshJarr Jun 24, 2022
543de10
fix: Address varous PR feedback.
joshJarr Jun 24, 2022
ac04c13
fix: Add indexes on the upload table for `name` and `inserted_at` to …
joshJarr Jun 27, 2022
0dd8579
Merge branch 'main' of github.com:web3-storage/web3.storage into fix/…
joshJarr Jun 27, 2022
d8ac530
fix: Fix broken tests for uploaads Link header
joshJarr Jun 28, 2022
7053bd7
chore: RM generate_uploads.js file
joshJarr Jun 28, 2022
1a4f683
fix: RM index creation for upload sorting.
joshJarr Jun 28, 2022
1777110
fix: Add validation for `sortOrder` in the uploads API. Remove url en…
joshJarr Jun 28, 2022
0e42730
feat: update api
flea89 Jul 25, 2022
e8b6ba2
feat: some initial refactoring
flea89 Jul 28, 2022
bafd161
feat: more work on table
flea89 Jul 29, 2022
f46efb3
feat: refactor renderers
flea89 Jul 29, 2022
050b2db
feat: table row selection
flea89 Jul 29, 2022
8086c46
feat: file selection and pagination
flea89 Jul 29, 2022
fb2e02b
feat: tidy up and lift selected state
flea89 Aug 1, 2022
da8109e
feat: move throw api
flea89 Aug 3, 2022
e8f37bd
Merge branch 'main' into fix/get-upload-pagination
flea89 Aug 3, 2022
e3a424a
chore: get rid of changes to zero components
flea89 Aug 3, 2022
6c309b0
Some tidy up
flea89 Aug 3, 2022
d383c66
chore: remove changes outside table
flea89 Aug 3, 2022
86bd0b6
chore: items per page is required
flea89 Aug 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/website/components/table/pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react';

/**
* @typedef {Object} PaginationProps
* @prop {string} [className]
* @prop {number} totalRowCount
* @prop {number} itemsPerPage
* @prop {number} [visiblePages]
* @prop {number} page
* @prop {function} [onPageChange]
* @prop {string} [scrollTarget]
*/
export default function Pagination({
className,
totalRowCount,
itemsPerPage,
visiblePages,
page,
onPageChange,
scrollTarget,
}) {
const [pageList, setPageList] = useState(/** @type {number[]} */ ([]));

const pageCount = useMemo(() => Math.ceil(totalRowCount / itemsPerPage), [totalRowCount, itemsPerPage]);

const pageChangeHandler = useCallback(
page => {
if (!!scrollTarget) {
const scrollToElement = document.querySelector(scrollTarget);
scrollToElement?.scrollIntoView(true);
}
onPageChange && onPageChange(page);
},
[scrollTarget, onPageChange]
);
useEffect(() => {
setPageList(
Array.from({ length: pageCount }, (_, i) => i).filter(p => p >= page - visiblePages && p <= page + visiblePages)
);
}, [visiblePages, page, pageCount]);

return (
<div className={clsx(className, 'Pagination')}>
<ul className="pageList">
{page > visiblePages + 1 && (
<button type="button" className="firstPage" onClick={() => pageChangeHandler(0)}>
First
</button>
)}
{page > 1 && (
<button type="button" className="prevPage" onClick={() => pageChangeHandler(page - 1)}>
Prev
</button>
)}
{page > visiblePages + 1 && <div className="prevEllipses">...</div>}
{pageCount !== 1 &&
pageList &&
pageList.map(p => (
<button
type="button"
key={`page-${p}`}
className={clsx('page', { current: p === page })}
onClick={() => pageChangeHandler(p)}
>
{p + 1}
</button>
))}
{pageCount != null && page < pageCount - visiblePages && <div className="nextEllipses">...</div>}
{pageCount != null && page < pageCount && (
<button type="button" className="nextPage" onClick={() => pageChangeHandler(page + 1)}>
Next
</button>
)}
{pageCount != null && page < pageCount - visiblePages && (
<button type="button" className="lastPage" onClick={() => pageChangeHandler(pageCount)}>
Last
</button>
)}
</ul>
</div>
);
}

Pagination.defaultProps = {
visiblePages: 1,
};
36 changes: 36 additions & 0 deletions packages/website/components/table/pagination.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.Pagination {
padding-bottom: 1rem;

.pageList {
list-style: none;
display: flex;
gap: 1rem;

.page {
&.current {
cursor: default;
font-weight: 600;
color: #ff0000;
}

&:hover {
opacity: 0.75;
}
}

.page,
.firstPage,
.prevPage,
.nextPage,
.lastPage {
cursor: pointer;
background: none;
border: none;
}

.prevEllipses,
.nextEllipses {
cursor: default;
}
}
}
20 changes: 20 additions & 0 deletions packages/website/components/table/selectCell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import CheckIcon from 'assets/icons/check';

/**
* @type {import('react').FC}
*
* Used to render a checkbox cell within a table component.
*
* @param {Object} props
* @returns
*/
function SelectCell({ selected = false, id, onSelectChange }) {
return (
<span className="file-select">
<input checked={selected} type="checkbox" id={id} onChange={e => onSelectChange(e.target.checked)} />
<CheckIcon className="check" />
</span>
);
}

export default SelectCell;
210 changes: 210 additions & 0 deletions packages/website/components/table/table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useCallback } from 'react';

import Loading from 'components/loading/loading';
import Dropdown from 'ZeroComponents/dropdown/dropdown';
import Pagination from 'components/table/pagination';
import SelectCell from 'components/table/selectCell';

/**
* @typedef {Object} ColumnDefinition
* @property {string | import('react').ReactComponentElement } headerContent
* @property {string} id
* @property {import('react').FC} [cellRenderer]
* @property {function} [getCellProps]
*
*/

/**
* @type {import('react').FC}
*
* @param {Object} props
* @param {ColumnDefinition[]} props.columns
* @param {Array<object>} props.rows
* @param {number} [props.totalRowCount]
* @param {number} [props.page]
* @param {number} [props.rowsPerPage]
* @param {number[]} [props.rowsPerPageOptions]
* @param {boolean} [props.isEmpty]
* @param {boolean} [props.isLoading]
* @param {string} [props.scrollTarget]
* @param {boolean} [props.withRowSelection]
* @param {import('react').ReactComponentElement} [props.emptyState]
* @param {import('react').ReactComponentElement} [props.leftFooterSlot]
* @param {number[]} [props.selectedRows] List of keys of the selected rows
* @param {function} [props.onPageSelect]
* @param {function} [props.onSetItemsPerPage]
* @param {(key: number|string, value: boolean) => void} [props.onRowSelectedChange]
* @param {(value: boolean) => void} [props.onSelectAll]
*/

function Table({
columns,
rows,
totalRowCount,
page = 0,
rowsPerPage,
rowsPerPageOptions,
isEmpty = false,
emptyState,
isLoading = false,
withRowSelection = false,
leftFooterSlot,
selectedRows,
onRowSelectedChange,
onSelectAll,
onPageSelect,
onSetItemsPerPage,
scrollTarget = '.storage-table',
}) {
/**
* @type { ColumnDefinition[]}
*/
let effectiveColumns = [...columns];

const selectAllRowsHandler = useCallback(
value => {
onSelectAll && onSelectAll(value);
},
[onSelectAll]
);

const selectRowHandler = useCallback(
(rowKey, value) => {
onRowSelectedChange && onRowSelectedChange(rowKey, value);
},
[onRowSelectedChange]
);

const pageSelectHandler = useCallback(
page => {
onPageSelect && onPageSelect(page);
},
[onPageSelect]
);

const keysAllEqual = function areEqual(array1, array2) {
if (array1.length === array2.length) {
return array1.every(element => {
if (array2.includes(element)) {
return true;
}

return false;
});
}

return false;
};

// If row selection is enabled add a column with checkboxes at the start.
if (withRowSelection) {
/**
* @type { ColumnDefinition}
*/
const selectionColumn = {
id: 'rowSelection',
headerContent: (
// Select all checkbox in the header.
<SelectCell
selected={
rows.length !== 0 &&
keysAllEqual(
selectedRows,
rows.map(r => r.key)
)
}
id={`table-storage-select-all`}
onSelectChange={selected => selectAllRowsHandler(selected)}
/>
),
cellRenderer: SelectCell,
getCellProps: (cell, index) => {
return {
selected: selectedRows?.includes(index),
onSelectChange: selected => selectRowHandler(index, selected),
id: `table-storage-row-${index}-select`,
};
},
};

effectiveColumns = [selectionColumn, ...effectiveColumns];
}

const renderRowComponent = (row, index, selected) => {
const rowKey = row.key || index;
return (
<div role="rowgroup" key={rowKey}>
<div
className="storage-table-row"
role="row"
aria-rowindex={index}
aria-selected={selectedRows?.includes(index) || false}
>
{effectiveColumns.map(c => (
<span key={`${c.id}-${rowKey}`} role="cell">
{c.cellRenderer ? (
<c.cellRenderer {...(c.getCellProps ? c.getCellProps(row[c.id], rowKey) : {})}></c.cellRenderer>
) : (
row[c.id]
)}
</span>
))}
</div>
</div>
);
};

if (isEmpty && !isLoading && emptyState) {
return (
<div className="storage-table">
<div className="storage-table-table-content">{emptyState}</div>
</div>
);
}

return (
<div className="storage-table" role="table">
<div role="rowgroup">
<div className="storage-table-row storage-table-header" role="row">
{effectiveColumns.map(c => (
<div key={c.id} role="columnheader">
{c.headerContent}
</div>
))}
</div>
</div>
<div className="storage-table-content">
{isLoading ? <Loading className={'files-loading-spinner'} /> : rows.map((row, i) => renderRowComponent(row, i))}

{!isEmpty && (
<div className="storage-table-footer">
<div>{!!leftFooterSlot && leftFooterSlot}</div>

<Pagination
className="storage-table-pagination"
page={page}
totalRowCount={totalRowCount || rows.length}
itemsPerPage={rowsPerPage}
visiblePages={1}
onPageChange={pageSelectHandler}
scrollTarget={scrollTarget}
/>
{rowsPerPageOptions && rowsPerPageOptions.length !== 0 && (
<Dropdown
className="storage-table-result-dropdown"
value={rowsPerPage}
options={rowsPerPageOptions.map(ipp => ({
label: `View ${ipp} results`,
value: ipp.toString(),
}))}
onChange={value => onSetItemsPerPage && onSetItemsPerPage(parseInt(value))}
/>
)}
</div>
)}
</div>
</div>
);
}

export default Table;
Loading