Skip to content

Commit

Permalink
feat(Table): add page size select (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
etienneburdet authored May 21, 2024
1 parent 9aeee09 commit 4ea6838
Show file tree
Hide file tree
Showing 26 changed files with 3,333 additions and 6,158 deletions.
8,982 changes: 3,003 additions & 5,979 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/visualizations-react/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
testEnvironment: 'jsdom',
moduleNameMapper: {
src: '<rootDir>/src',
'stories/(.*)': '<rootDir>/stories/$1',
reactify: '<rootDir>/src/reactify',
// https://jestjs.io/docs/webpack#handling-static-assets
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
Expand Down
1 change: 1 addition & 0 deletions packages/visualizations-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@storybook/react-vite": "^7.6.17",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.5.2",
"@turf/turf": "^6.5.0",
"@types/geojson": "^7946.0.10",
"@types/jest": "^29.0.2",
Expand Down
101 changes: 101 additions & 0 deletions packages/visualizations-react/stories/Table/PaginatedTemplates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState, useEffect } from 'react';
import type { DataFrame, Pagination } from '@opendatasoft/visualizations';
import { Table } from '../../src';
import data from './data';
import options from './options';

const fetchData = async ({ size, page }: { size: number; page: number }) => {
const startIndex = (page - 1) * size;
const endIndex = startIndex + size;
await setTimeout(() => {}, 300);
const dataFrame: DataFrame = data?.slice(startIndex, endIndex);
return dataFrame;
};

// eslint-disable-next-line import/prefer-default-export
export const usePaginatedData = ({
current,
recordsPerPage,
}: {
current: number;
recordsPerPage: number;
}) => {
const [records, setRecords] = useState<DataFrame>();
const [pageSize, setPageSize] = useState(recordsPerPage);
const [page, setPage] = useState(current);

useEffect(() => {
(async () => {
const newRecords = await fetchData({
size: pageSize,
page,
});
setRecords(newRecords);
})();
}, [recordsPerPage, page, pageSize, setRecords]);

const paginatedData = { value: records, isLoading: false };
const totalRecords = data.values.length;

return {
records,
setRecords,
pageSize,
setPageSize,
page,
setPage,
totalRecords,
paginatedData,
};
};

export const PaginatedTemplate = (pagination: Pagination) => {
const { current = 1, recordsPerPage = 5 } = pagination;
const { paginatedData, page, pageSize, setPage } = usePaginatedData({
current,
recordsPerPage,
});

const stateFulOptions = {
...options,
pagination: {
current: page,
recordsPerPage: pageSize,
totalRecords: data.length,
onPageChange: setPage,
},
};

return <Table data={paginatedData} options={stateFulOptions} />;
};

export const PageSizeTemplate = (pagination: Pagination) => {
const { current = 1, recordsPerPage = 5 } = pagination;
const { paginatedData, page, pageSize, setPage, setPageSize } = usePaginatedData({
current,
recordsPerPage,
});

const stateFulOptions = {
...options,
pagination: {
current: page,
recordsPerPage: pageSize,
totalRecords: data.length,
onPageChange: setPage, //
pageSizeSelect: {
options: [
{ label: '2 / pages', value: 2 },
{ label: '5 / pages', value: 5 },
{ label: '10 / pages', value: 10 },
],
onChange: (newSize: number) => {
setPageSize(newSize);
setPage(1);
}, // stateful, defined in template
},
},
};

return <Table data={paginatedData} options={stateFulOptions} />;
};
112 changes: 28 additions & 84 deletions packages/visualizations-react/stories/Table/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,52 @@
import React, { useState } from 'react';
import React from 'react';
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import type { TableProps } from '@opendatasoft/visualizations';
import { Table } from '../../src';
import value from './data';
import options from './options';
import './pagination.css';
import { PaginatedTemplate, PageSizeTemplate } from './PaginatedTemplates';

const meta: ComponentMeta<typeof Table> = {
title: 'Table/Pagination',
component: Table,
};
export default meta;

const data: TableProps['data'] = {
value,
loading: false,
};

const sliceDataX = (numberPerPage: number) => (fullData: TableProps['data'], page: number) => {
const startIndex = (page - 1) * numberPerPage;
return fullData.value?.slice(startIndex, startIndex + numberPerPage);
};

const makeTemplate = (numberPerPage: number) => {
const sliceData = sliceDataX(numberPerPage);
const PaginatedTemplate: ComponentStory<typeof Table> = args => {
const { data: rawData, options: paginatedOptions } = args;
const { pagination } = paginatedOptions;
const { initial = 0 } = pagination || {};
const [records, setRecords] = useState(sliceData(rawData, initial || 1));
if (paginatedOptions.pagination && rawData.value) {
paginatedOptions.pagination.onChangePage = async (page: number) => {
await setTimeout(() => {
setRecords(sliceData(rawData, page));
}, 300);
};
}

const paginatedData = { value: records, isLoading: false };
return <Table data={paginatedData} options={paginatedOptions} />;
};
return PaginatedTemplate;
};

const paginatedOptions = {
...options,
pagination: {
initial: 2,
recordsPerPage: 3,
totalRecords: value.length,
onChangePage: () => {}, // set in template
},
};
const PaginatedTemplate = makeTemplate(3);
export const Paginated = PaginatedTemplate.bind({});
const PaginatedTable: ComponentStory<typeof PaginatedTemplate> = args => (
<PaginatedTemplate {...args} />
);
export const Paginated = PaginatedTable.bind({});
Paginated.args = {
data,
options: paginatedOptions,
};

const longPagesOptions = {
...options,
pagination: {
initial: 1,
recordsPerPage: 10,
totalRecords: value.length,
onChangePage: () => {}, // set in template
},
};
const LongPaginatedTemplate = makeTemplate(10);
export const LongPages = LongPaginatedTemplate.bind({});
LongPages.args = {
data,
options: longPagesOptions,
current: 2,
recordsPerPage: 3,
};

const shortPagesOptions = {
...options,
pagination: {
initial: 1,
recordsPerPage: 2,
totalRecords: value.length,
onChangePage: () => {}, // set in template
},
export const Longpagination = PaginatedTable.bind({});
Longpagination.args = {
current: 1,
recordsPerPage: 10,
};
const ShortPaginatedTemplate = makeTemplate(2);
export const ShortPages = ShortPaginatedTemplate.bind({});
ShortPages.args = {
data,
options: shortPagesOptions,
export const Shortpagination = PaginatedTable.bind({});
Shortpagination.args = {
current: 1,
recordsPerPage: 2,
};


const StyledPaginated: ComponentStory<typeof Table> = args => (
const StyledPaginated: ComponentStory<typeof PaginatedTemplate> = args => (
<div className="custom-pagination">
<PaginatedTemplate {...args} />
</div>
);

export const CustomStyle = StyledPaginated.bind({});
CustomStyle.args = {
data,
options: {
...paginatedOptions,
unstyled: true,
},
current: 2,
recordsPerPage: 3,
};

const PageSizeTable: ComponentStory<typeof PageSizeTemplate> = args => (
<PageSizeTemplate {...args} />
);
export const PageSize = PageSizeTable.bind({});
PageSize.args = {
current: 2,
recordsPerPage: 5,
};
50 changes: 50 additions & 0 deletions packages/visualizations-react/test/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { Table } from 'src';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import data from 'stories/Table/data';
import options from 'stories/Table/options';
import { usePaginatedData } from 'stories/Table/PaginatedTemplates';

/* This template will fail to catch a new page and returns previous data: {
value,
loading: false,
}, page and pageSize
simulating e.g. an API call fail.
The select should stay on it's previous value, not the clicked one.
*/
const PageSizeFail = () => {
const { paginatedData, setPage, setPageSize } = usePaginatedData({
current: 2,
recordsPerPage: 5,
});

const stateFulOptions = {
...options,
pagination: {
current: 2,
recordsPerPage: 5,
totalRecords: data.length,
onPageChange: () => setPage(2), //
pageSizeSelect: {
options: [
{ label: '2 / pages', value: 2 },
{ label: '5 / pages', value: 5 },
{ label: '10 / pages', value: 10 },
],
onChange: () => setPageSize(5), // stateful, defined in template
},
},
};
return <Table data={paginatedData} options={stateFulOptions} />;
};

test('Page size select stays on the correct component if page change fails', async () => {
const user = userEvent.setup();
render(<PageSizeFail />);

expect(screen.getByRole('combobox')).toHaveValue('5');

await user.selectOptions(screen.getByRole('combobox'), '10');
expect(screen.getByRole('combobox')).toHaveValue('5');
});
33 changes: 33 additions & 0 deletions packages/visualizations/src/components/Pagination/PageSize.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import { toNumber } from 'lodash';
import type { PageSizeOption } from './types';
export let options: PageSizeOption[];
export let onChange: (size: number) => void;
export let selected: number;
let value: number;
/* This ensure a "controlled" behavior
e.g. if the onChange fails it will take the value of selected, not the one the user clicked.
I tried $: value = selected and bind:value={selected}, but they don't override user selection
*/
$: if (value !== selected) {
value = selected;
}
</script>

<select bind:value on:change={(e) => onChange(toNumber(e.currentTarget.value))}>
{#each options as option}
<option label={option.label} value={option.value} />
{/each}
</select>

<style>
:global(.ods-dataviz--default) select {
background-color: white;
padding: var(--spacing-50);
border: solid 1px var(--border-color);
border-radius: var(--border-radius-2);
}
</style>
Loading

2 comments on commit 4ea6838

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

94.64%

Coverage Report
FileBranchesFuncsLinesUncovered Lines
src
   index.ts100%100%100%
src/client
   error.ts100%100%100%
   index.ts74.03%100%95.31%102–103, 124, 13, 146, 148, 148–149, 15, 15, 151, 162, 169, 169, 17, 17, 171, 176, 179, 182, 184, 52, 82
   types.ts100%100%100%
src/odsql
   clauses.ts71.43%80%90.91%14, 32, 42
   index.ts83.72%95.74%94.19%111, 146, 25, 28, 56–57, 57, 57–58, 68, 78–79

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

94.64%

Coverage Report
FileBranchesFuncsLinesUncovered Lines
src
   index.ts100%100%100%
src/client
   error.ts100%100%100%
   index.ts74.03%100%95.31%102–103, 124, 13, 146, 148, 148–149, 15, 15, 151, 162, 169, 169, 17, 17, 171, 176, 179, 182, 184, 52, 82
   types.ts100%100%100%
src/odsql
   clauses.ts71.43%80%90.91%14, 32, 42
   index.ts83.72%95.74%94.19%111, 146, 25, 28, 56–57, 57, 57–58, 68, 78–79

Please sign in to comment.