From 0726887bb8c95f44f0df62ac8f5558ccaa865de1 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 15 Dec 2023 10:20:55 +0100 Subject: [PATCH] feat: reset persistent table offset on change (#5650) --- .../FeatureToggleListTable.tsx | 12 ++--- .../src/component/filter/AddFilterButton.tsx | 4 +- .../src/component/filter/Filters/Filters.tsx | 2 +- .../ExperimentalProjectTable.tsx | 50 ++---------------- .../hooks/usePersistentTableState.test.tsx | 52 ++++++++++++++++++- frontend/src/hooks/usePersistentTableState.ts | 40 ++++++++++++-- 6 files changed, 98 insertions(+), 62 deletions(-) diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 9a1c905d77..c5d4549475 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -92,9 +92,6 @@ export const FeatureToggleListTable: VFC = () => { 'features-list-table', stateConfig, ); - // offset needs to be first so we can override it - const setTableStateWithOffsetReset: typeof setTableState = (data) => - setTableState({ offset: 0, ...data }); const filterState = { project: tableState.project, @@ -141,7 +138,7 @@ export const FeatureToggleListTable: VFC = () => { - setTableStateWithOffsetReset({ + setTableState({ favoritesFirst: !tableState.favoritesFirst, }) } @@ -230,7 +227,7 @@ export const FeatureToggleListTable: VFC = () => { ); const table = useReactTable( - withTableState(tableState, setTableStateWithOffsetReset, { + withTableState(tableState, setTableState, { columns, data, }), @@ -255,8 +252,7 @@ export const FeatureToggleListTable: VFC = () => { } }, [isSmallScreen, isMediumScreen]); - const setSearchValue = (query = '') => - setTableStateWithOffsetReset({ query }); + const setSearchValue = (query = '') => setTableState({ query }); const rows = table.getRowModel().rows; @@ -340,7 +336,7 @@ export const FeatureToggleListTable: VFC = () => { } > diff --git a/frontend/src/component/filter/AddFilterButton.tsx b/frontend/src/component/filter/AddFilterButton.tsx index 0011d0fffe..90d467dad7 100644 --- a/frontend/src/component/filter/AddFilterButton.tsx +++ b/frontend/src/component/filter/AddFilterButton.tsx @@ -33,7 +33,7 @@ interface IAddFilterButtonProps { availableFilters: IFilterItem[]; } -const AddFilterButton = ({ +export const AddFilterButton = ({ visibleOptions, setVisibleOptions, hiddenOptions, @@ -87,5 +87,3 @@ const AddFilterButton = ({ ); }; - -export default AddFilterButton; diff --git a/frontend/src/component/filter/Filters/Filters.tsx b/frontend/src/component/filter/Filters/Filters.tsx index 3aeebd22d4..d05bb76267 100644 --- a/frontend/src/component/filter/Filters/Filters.tsx +++ b/frontend/src/component/filter/Filters/Filters.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, VFC } from 'react'; import { Box, styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import AddFilterButton from '../AddFilterButton'; +import { AddFilterButton } from '../AddFilterButton'; import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem'; import { FilterItem, FilterItemParams } from '../FilterItem/FilterItem'; diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx index cb9f3c9d7b..832ccd1060 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx +++ b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx @@ -1,68 +1,26 @@ -import React, { - type CSSProperties, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import { - Checkbox, - IconButton, - styled, - Tooltip, - useMediaQuery, - Box, - useTheme, -} from '@mui/material'; -import { Add } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; -import { - useFlexLayout, - usePagination, - useRowSelect, - useSortBy, - useTable, -} from 'react-table'; +import React, { useCallback, useMemo, useState } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageContent } from 'component/common/PageContent/PageContent'; -import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; -import { getCreateTogglePath } from 'utils/routePathHelpers'; -import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; -import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { IProject } from 'interfaces/project'; -import { PaginatedTable, VirtualizedTable } from 'component/common/Table'; +import { PaginatedTable } from 'component/common/Table'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; -import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; -import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; -import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch'; -import { Search } from 'component/common/Search/Search'; -import { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; import { ProjectEnvironmentType } from '../../ProjectFeatureToggles/hooks/useEnvironmentsRef'; import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell'; import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu'; -import { useStyles } from '../../ProjectFeatureToggles/ProjectFeatureToggles.styles'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; -import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; -import FileDownload from '@mui/icons-material/FileDownload'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; import { MemoizedRowSelectCell } from '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell'; import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; -import { ListItemType } from '../../ProjectFeatureToggles/ProjectFeatureToggles.types'; -import { createFeatureToggleCell } from '../../ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell'; import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; import useLoading from 'hooks/useLoading'; -import { StickyPaginationBar } from '../../../../common/Table/StickyPaginationBar/StickyPaginationBar'; import { DEFAULT_PAGE_LIMIT, useFeatureSearch, @@ -74,11 +32,11 @@ import { FilterItemParam, } from 'utils/serializeQueryParams'; import { + ArrayParam, + encodeQueryParams, NumberParam, StringParam, - ArrayParam, withDefault, - encodeQueryParams, } from 'use-query-params'; import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; diff --git a/frontend/src/hooks/usePersistentTableState.test.tsx b/frontend/src/hooks/usePersistentTableState.test.tsx index 4e18ffc1e9..16aac060ad 100644 --- a/frontend/src/hooks/usePersistentTableState.test.tsx +++ b/frontend/src/hooks/usePersistentTableState.test.tsx @@ -172,11 +172,61 @@ describe('usePersistentTableState', () => { screen.getByText('Update Offset').click(); screen.getByText('Update State').click(); - expect(window.location.href).toContain('my-url?query=after&offset=20'); + expect(window.location.href).toContain('my-url?query=after&offset=0'); await waitFor(() => { const { value } = createLocalStorage('testKey', {}); expect(value).toStrictEqual({ query: 'after' }); }); }); + + it('resets offset to 0 on state update', async () => { + createLocalStorage('testKey', {}).setValue({ query: 'before' }); + + render( + , + { route: '/my-url?query=before&offset=10' }, + ); + + expect(window.location.href).toContain('my-url?query=before&offset=10'); + + screen.getByText('Update State').click(); + + await waitFor(() => { + expect(window.location.href).toContain( + 'my-url?query=after&offset=0', + ); + expect(window.location.href).not.toContain('offset=10'); + }); + }); + + it('does not reset offset to 0 without offset decoder', async () => { + createLocalStorage('testKey', {}).setValue({ query: 'before' }); + + render( + , + { route: '/my-url?query=before&offset=10' }, + ); + + expect(window.location.href).toContain('my-url?query=before&offset=10'); + + screen.getByText('Update State').click(); + + await waitFor(() => { + expect(window.location.href).toContain( + 'my-url?query=after&offset=10', + ); + }); + }); }); diff --git a/frontend/src/hooks/usePersistentTableState.ts b/frontend/src/hooks/usePersistentTableState.ts index a336a6d199..7cdfdd2bda 100644 --- a/frontend/src/hooks/usePersistentTableState.ts +++ b/frontend/src/hooks/usePersistentTableState.ts @@ -1,7 +1,7 @@ -import { useEffect } from 'react'; +import { useEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; import { createLocalStorage } from 'utils/createLocalStorage'; -import { useQueryParams, encodeQueryParams } from 'use-query-params'; +import { encodeQueryParams, useQueryParams } from 'use-query-params'; import { QueryParamConfigMap } from 'serialize-query-params/src/types'; const usePersistentSearchParams = ( @@ -39,7 +39,41 @@ export const usePersistentTableState = ( queryParamsDefinition, ); - const [tableState, setTableState] = useQueryParams(queryParamsDefinition); + const [tableState, setTableStateInternal] = useQueryParams( + queryParamsDefinition, + ); + + type SetTableStateInternalParam = Parameters< + typeof setTableStateInternal + >[0]; + + const setTableState = useCallback( + (newState: SetTableStateInternalParam) => { + if (!queryParamsDefinition.offset) { + return setTableStateInternal(newState); + } + if (typeof newState === 'function') { + setTableStateInternal((prevState) => { + const updatedState = (newState as Function)(prevState); + return queryParamsDefinition.offset + ? { + offset: queryParamsDefinition.offset.decode('0'), + ...updatedState, + } + : updatedState; + }); + } else { + const updatedState = queryParamsDefinition.offset + ? { + offset: queryParamsDefinition.offset.decode('0'), + ...newState, + } + : newState; + setTableStateInternal(updatedState); + } + }, + [setTableStateInternal, queryParamsDefinition.offset], + ); useEffect(() => { const { offset, ...rest } = tableState;