From 17c9bd6393a82738c685649e08009d93404c0e4d Mon Sep 17 00:00:00 2001 From: ALC Consulting Date: Fri, 7 Jul 2023 15:55:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=B0=EF=B8=8F(frontend)=20remove=20Sortabl?= =?UTF-8?q?eTable=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SortableTable component has been replaced by the DataGrid component from Cunningham. --- .../components/SortableTable/index.spec.tsx | 288 ---------------- .../src/components/SortableTable/index.tsx | 325 ------------------ 2 files changed, 613 deletions(-) delete mode 100644 src/frontend/apps/standalone_site/src/components/SortableTable/index.spec.tsx delete mode 100644 src/frontend/apps/standalone_site/src/components/SortableTable/index.tsx diff --git a/src/frontend/apps/standalone_site/src/components/SortableTable/index.spec.tsx b/src/frontend/apps/standalone_site/src/components/SortableTable/index.spec.tsx deleted file mode 100644 index e8a965841f..0000000000 --- a/src/frontend/apps/standalone_site/src/components/SortableTable/index.spec.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { - screen, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { Box, Text } from 'grommet'; -import { Deferred, render } from 'lib-tests'; - -import { SortableTable } from '.'; - -describe('', () => { - it('renders rows directly', async () => { - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - expect(await screen.findByText('my table')).toBeInTheDocument(); - expect(screen.getByText('label 1')).toBeInTheDocument(); - expect(screen.getByText('label 2')).toBeInTheDocument(); - }); - - it('renders rows, sorting, select box and pagination', async () => { - const items = [{ label: 'label 1' }, { label: 'label 2' }]; - const sorts = [{ label: 'by name' }, { label: 'by url' }]; - const onSortChange = jest.fn().mockReturnValue([items[0]]); - const onSelectionChange = jest.fn(); - const onPageChange = jest.fn().mockReturnValue([items[1]]); - - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - await screen.findByText('my table'); - screen.getByText('label 1'); - expect(screen.queryByText('label 2')).not.toBeInTheDocument(); - - // sort menu tests - expect( - screen.getByRole('button', { name: 'Sort item in the table' }), - ).toBeInTheDocument(); - expect(screen.getByText('by name')).toBeInTheDocument(); - - userEvent.click( - screen.getByRole('button', { name: 'Sort item in the table' }), - ); - - await screen.findByRole('menuitem', { name: 'by url' }); - expect(onSortChange).not.toHaveBeenCalled(); - - userEvent.click(screen.getByRole('menuitem', { name: 'by url' })); - - await waitFor(() => expect(onSortChange).toHaveBeenCalled()); - expect(onSortChange).toHaveBeenCalledWith(sorts[1]); - - // selection tests - expect( - screen.getByRole('checkbox', { name: 'Select all lines' }), - ).not.toBeChecked(); - expect( - screen.getByRole('checkbox', { name: 'Select line 1' }), - ).not.toBeChecked(); - - userEvent.click(screen.getByRole('checkbox', { name: 'Select all lines' })); - await waitFor(() => - expect( - screen.getByRole('checkbox', { name: 'Deselect all lines' }), - ).toBeChecked(), - ); - expect( - screen.getByRole('checkbox', { name: 'Deselect line 1' }), - ).toBeChecked(); - expect(onSelectionChange).toHaveBeenCalled(); - expect(onSelectionChange).toHaveBeenCalledWith([items[0]]); - - userEvent.click(screen.getByRole('checkbox', { name: 'Deselect line 1' })); - await waitFor(() => - expect( - screen.getByRole('checkbox', { name: 'Select line 1' }), - ).not.toBeChecked(), - ); - expect( - screen.getByRole('checkbox', { name: 'Select all lines' }), - ).not.toBeChecked(); - expect(onSelectionChange).toHaveBeenCalledTimes(2); - expect(onSelectionChange).toHaveBeenNthCalledWith(2, []); - - userEvent.click(screen.getByRole('checkbox', { name: 'Select line 1' })); - - // pagination tests - expect( - screen.getByRole('navigation', { name: 'Pagination Navigation' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Go to previous page' }), - ).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Go to page 1' }), - ).not.toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Go to page 2' }), - ).not.toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Go to next page' }), - ).not.toBeDisabled(); - - userEvent.click(screen.getByRole('button', { name: 'Go to page 2' })); - - await screen.findByText('label 2'); - expect(onPageChange).toHaveBeenCalled(); - expect(onPageChange).toHaveBeenCalledWith(2); - - expect( - screen.getByRole('button', { name: 'Go to previous page' }), - ).not.toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Go to next page' }), - ).toBeDisabled(); - - // selection is reset on change - expect( - screen.getByRole('checkbox', { name: 'Select line 1' }), - ).toBeInTheDocument(); - }); - - it('renders the loading without content', async () => { - const items: { label: string }[] = []; - - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - expect(await screen.findByText('my table')).toBeInTheDocument(); - expect(screen.getByRole('status')).toBeInTheDocument(); - }); - - it('renders the loading on top of current content', async () => { - const items: { label: string }[] = [{ label: 'some label' }]; - - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - expect(await screen.findByText('my table')).toBeInTheDocument(); - expect(screen.getByText('some label')).toBeInTheDocument(); - expect(screen.getByRole('status')).toBeInTheDocument(); - }); - - it('renders a loader while fetching data to render', async () => { - const deferred = new Deferred<{ label: string }[]>(); - - render( - await deferred.promise} - > - {(item) => ( - - {item.label} - - )} - , - ); - - await screen.findByText('my table'); - screen.getByRole('status'); - - deferred.resolve([{ label: 'some label' }, { label: 'some other label' }]); - - await waitForElementToBeRemoved(() => screen.queryByRole('status')); - - expect(screen.getByText('some label')).toBeInTheDocument(); - expect(screen.getByText('some other label')).toBeInTheDocument(); - }); - - it('renders a loader while loading new page context', async () => { - const deferred = new Deferred<{ label: string }[]>(); - const onPageChange = jest.fn().mockReturnValue(deferred.promise); - - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - await screen.findByText('my table'); - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - screen.getByText('some label'); - screen.getByText('some other label'); - - userEvent.click(screen.getByRole('button', { name: 'Go to page 2' })); - - await waitFor(() => expect(onPageChange).toHaveBeenCalled()); - - await screen.findByRole('status'); - screen.getByText('some label'); - screen.getByText('some other label'); - - deferred.resolve([ - { label: 'a new label' }, - { label: 'and an other one just for fun' }, - ]); - - await waitForElementToBeRemoved(() => screen.queryByRole('status')); - - screen.getByText('a new label'); - screen.getByText('and an other one just for fun'); - }); - - it('renders with single selection row', () => { - const items = [{ label: 'label 1' }, { label: 'label 2' }]; - const onSelectionChange = jest.fn(); - - render( - - {(item) => ( - - {item.label} - - )} - , - ); - - expect( - screen.getByRole('radio', { name: 'Select line 1' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('radio', { name: 'Select line 2' }), - ).toBeInTheDocument(); - }); -}); diff --git a/src/frontend/apps/standalone_site/src/components/SortableTable/index.tsx b/src/frontend/apps/standalone_site/src/components/SortableTable/index.tsx deleted file mode 100644 index 6bc2dc1724..0000000000 --- a/src/frontend/apps/standalone_site/src/components/SortableTable/index.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { - Box, - CheckBox, - Menu, - Pagination, - RadioButton, - Stack, - Text, -} from 'grommet'; -import { Maybe } from 'lib-common'; -import { Spinner } from 'lib-components'; -import { Fragment, ReactNode, useEffect, useState } from 'react'; -import { defineMessages } from 'react-intl'; - -import { ITEM_PER_PAGE } from 'conf/global'; - -interface SortByBase { - label: string; -} - -interface SortableTableBase { - title: React.ReactNode; - loading?: boolean; - items: (() => Promise) | ItemType[]; - children: (item: ItemType) => ReactNode; -} - -interface UnSortable { - sortable?: false; -} -interface Sortable { - sortable: true; - currentSort: SortByType; - sortBy: SortByType[]; - onSortChange: (newSort: SortByType) => ItemType[] | Promise; -} - -interface UnSelectable { - selectable?: false; -} -type SelectionType = 'single' | 'multiple'; -interface Selectable { - selectable: true; - type?: SelectionType; - onSelectionChange: (newSelection: ItemType[]) => void; -} - -interface UnPaginable { - paginable?: false; -} -interface Paginable { - paginable: true; - currentPage?: number; - numberOfItems: number; - pageSize?: number; - onPageChange: (newPage: number) => ItemType[] | Promise; -} - -export const commonSortMessages = defineMessages({ - sortByAscendingCreationDate: { - defaultMessage: 'Creation date', - description: 'Sort table objects by ascending creation date', - id: 'components.SortableTable.sortByAscendingCreationDate', - }, - sortByDescendingCreationDate: { - defaultMessage: 'Creation date (reversed)', - description: 'Sort table objects by descending creation date', - id: 'components.SortableTable.sortByDescendingCreationDate', - }, - sortByAscendingTitle: { - defaultMessage: 'Title', - description: 'Sort table objects by ascending title.', - id: 'components.SortableTable.sortByAscendingTitle', - }, - sortByDescendingTitle: { - defaultMessage: 'Title (reversed)', - description: 'Sort table objects by descending title.', - id: 'components.SortableTable.sortByDescendingTitle', - }, -}); - -type SortableTableProps< - ItemType, - SortByType extends SortByBase = SortByBase, -> = SortableTableBase & - (UnSortable | Sortable) & - (Selectable | UnSelectable) & - (Paginable | UnPaginable); - -export const SortableTable = < - ItemType, - SortByType extends SortByBase = SortByBase, ->({ - title, - loading: initialLoading, - items: initialItems, - children, - ...props -}: SortableTableProps) => { - const [isLoading, setIsLoading] = useState(true); - const [items, setItems] = useState([]); - const [previousSelection, setPreviousSelection] = useState([]); - const [currentSelection, setCurrentSelection] = useState([]); - const [targetSort, setTargetSort] = useState>(undefined); - const [asyncLoader, setAsyncLoader] = - useState Promise>>(undefined); - - let selectionCallback: Maybe['onSelectionChange']> = - undefined; - let selectionType: Maybe = undefined; - if (props.selectable) { - selectionCallback = props.onSelectionChange; - selectionType = props.type || 'multiple'; - } - - let sortCallback: Maybe['onSortChange']> = - undefined; - let SortMenu: Maybe; - if (props.sortable) { - sortCallback = props.onSortChange; - - const sortItems = props.sortBy; - const sortCurrentItem = props.currentSort; - const availableItems = sortItems.filter((item) => { - return item.label !== sortCurrentItem.label; - }); - - SortMenu = ( - ({ - label: item.label, - onClick: () => { - setTargetSort(item); - }, - }))} - /> - ); - } - - useEffect(() => { - if (Array.isArray(initialItems)) { - setCurrentSelection([]); - setIsLoading(false); - setItems(initialItems); - setAsyncLoader(undefined); - } else { - setAsyncLoader(() => initialItems); - } - }, [initialItems]); - - useEffect(() => { - setIsLoading((current) => initialLoading || current); - }, [initialLoading]); - - // on selection change, call the callback if set - useEffect(() => { - const isChangesInSelection = !( - currentSelection.length === previousSelection.length && - currentSelection.every((item) => previousSelection.includes(item)) - ); - if (!isChangesInSelection) { - return; - } - - setPreviousSelection(currentSelection); - selectionCallback?.(currentSelection); - }, [currentSelection, selectionCallback, previousSelection]); - - // on sort change, call the callback and updates rows - useEffect(() => { - if (!targetSort || !sortCallback) { - return; - } - const loader = sortCallback; - - setAsyncLoader(() => async () => await loader(targetSort)); - }, [targetSort, sortCallback]); - - // async load item in the table - useEffect(() => { - if (!asyncLoader) { - return; - } - - let canceled = false; - - const loadItems = async () => { - setCurrentSelection([]); - setIsLoading(true); - - const newItems = await asyncLoader(); - - if (canceled) { - return; - } - setItems(newItems); - setIsLoading(false); - }; - - loadItems(); - return () => { - canceled = true; - }; - }, [asyncLoader]); - - const itemPerPage = (props.paginable && props.pageSize) || ITEM_PER_PAGE; - - return ( - - - {props.selectable && selectionType === 'multiple' && ( - - { - if (currentSelection.length === items.length) { - setCurrentSelection([]); - } else { - setCurrentSelection(items); - } - }} - /> - - )} - - {title} - - {SortMenu} - - 0 ? 'first' : 'last'}> - - {items.map((item, index) => ( - 0 ? { top: 'xsmall' } : undefined} - > - {props.selectable && ( - - {selectionType === 'multiple' && ( - { - if (currentSelection.includes(item)) { - setCurrentSelection( - currentSelection.filter((t) => t !== item), - ); - } else { - setCurrentSelection([...currentSelection, item]); - } - }} - /> - )} - {selectionType === 'single' && ( - { - setCurrentSelection([item]); - }} - /> - )} - - )} - {children(item)} - - ))} - - - {isLoading && ( - - - - - - )} - - - {props.paginable && props.numberOfItems > itemPerPage && ( - - - { - setAsyncLoader( - () => async () => await props.onPageChange(newPage), - ); - }} - page={props.currentPage} - step={itemPerPage} - /> - - - )} - - ); -};