1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

feat: reset persistent table offset on change (#5650)

This commit is contained in:
Mateusz Kwasniewski 2023-12-15 10:20:55 +01:00 committed by GitHub
parent 53b32db278
commit 0726887bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 62 deletions

View File

@ -92,9 +92,6 @@ export const FeatureToggleListTable: VFC = () => {
'features-list-table', 'features-list-table',
stateConfig, stateConfig,
); );
// offset needs to be first so we can override it
const setTableStateWithOffsetReset: typeof setTableState = (data) =>
setTableState({ offset: 0, ...data });
const filterState = { const filterState = {
project: tableState.project, project: tableState.project,
@ -141,7 +138,7 @@ export const FeatureToggleListTable: VFC = () => {
<FavoriteIconHeader <FavoriteIconHeader
isActive={tableState.favoritesFirst} isActive={tableState.favoritesFirst}
onClick={() => onClick={() =>
setTableStateWithOffsetReset({ setTableState({
favoritesFirst: !tableState.favoritesFirst, favoritesFirst: !tableState.favoritesFirst,
}) })
} }
@ -230,7 +227,7 @@ export const FeatureToggleListTable: VFC = () => {
); );
const table = useReactTable( const table = useReactTable(
withTableState(tableState, setTableStateWithOffsetReset, { withTableState(tableState, setTableState, {
columns, columns,
data, data,
}), }),
@ -255,8 +252,7 @@ export const FeatureToggleListTable: VFC = () => {
} }
}, [isSmallScreen, isMediumScreen]); }, [isSmallScreen, isMediumScreen]);
const setSearchValue = (query = '') => const setSearchValue = (query = '') => setTableState({ query });
setTableStateWithOffsetReset({ query });
const rows = table.getRowModel().rows; const rows = table.getRowModel().rows;
@ -340,7 +336,7 @@ export const FeatureToggleListTable: VFC = () => {
} }
> >
<FeatureToggleFilters <FeatureToggleFilters
onChange={setTableStateWithOffsetReset} onChange={setTableState}
state={filterState} state={filterState}
/> />
<SearchHighlightProvider value={tableState.query || ''}> <SearchHighlightProvider value={tableState.query || ''}>

View File

@ -33,7 +33,7 @@ interface IAddFilterButtonProps {
availableFilters: IFilterItem[]; availableFilters: IFilterItem[];
} }
const AddFilterButton = ({ export const AddFilterButton = ({
visibleOptions, visibleOptions,
setVisibleOptions, setVisibleOptions,
hiddenOptions, hiddenOptions,
@ -87,5 +87,3 @@ const AddFilterButton = ({
</div> </div>
); );
}; };
export default AddFilterButton;

View File

@ -1,7 +1,7 @@
import { useEffect, useState, VFC } from 'react'; import { useEffect, useState, VFC } from 'react';
import { Box, styled } from '@mui/material'; import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AddFilterButton from '../AddFilterButton'; import { AddFilterButton } from '../AddFilterButton';
import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem'; import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
import { FilterItem, FilterItemParams } from '../FilterItem/FilterItem'; import { FilterItem, FilterItemParams } from '../FilterItem/FilterItem';

View File

@ -1,68 +1,26 @@
import React, { import React, { useCallback, useMemo, useState } from '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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PageContent } from 'component/common/PageContent/PageContent'; 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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; 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 { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { IProject } from 'interfaces/project'; 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 { 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 { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
import { ProjectEnvironmentType } from '../../ProjectFeatureToggles/hooks/useEnvironmentsRef'; import { ProjectEnvironmentType } from '../../ProjectFeatureToggles/hooks/useEnvironmentsRef';
import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell'; import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell';
import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu'; import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu';
import { useStyles } from '../../ProjectFeatureToggles/ProjectFeatureToggles.styles';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; 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 { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { MemoizedRowSelectCell } from '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell'; import { MemoizedRowSelectCell } from '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { ListItemType } from '../../ProjectFeatureToggles/ProjectFeatureToggles.types';
import { createFeatureToggleCell } from '../../ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell';
import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { StickyPaginationBar } from '../../../../common/Table/StickyPaginationBar/StickyPaginationBar';
import { import {
DEFAULT_PAGE_LIMIT, DEFAULT_PAGE_LIMIT,
useFeatureSearch, useFeatureSearch,
@ -74,11 +32,11 @@ import {
FilterItemParam, FilterItemParam,
} from 'utils/serializeQueryParams'; } from 'utils/serializeQueryParams';
import { import {
ArrayParam,
encodeQueryParams,
NumberParam, NumberParam,
StringParam, StringParam,
ArrayParam,
withDefault, withDefault,
encodeQueryParams,
} from 'use-query-params'; } from 'use-query-params';
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { createColumnHelper, useReactTable } from '@tanstack/react-table';

View File

@ -172,11 +172,61 @@ describe('usePersistentTableState', () => {
screen.getByText('Update Offset').click(); screen.getByText('Update Offset').click();
screen.getByText('Update State').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(() => { await waitFor(() => {
const { value } = createLocalStorage('testKey', {}); const { value } = createLocalStorage('testKey', {});
expect(value).toStrictEqual({ query: 'after' }); expect(value).toStrictEqual({ query: 'after' });
}); });
}); });
it('resets offset to 0 on state update', async () => {
createLocalStorage('testKey', {}).setValue({ query: 'before' });
render(
<TestComponent
keyName='testKey'
queryParamsDefinition={{
query: StringParam,
offset: NumberParam,
}}
/>,
{ 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(
<TestComponent
keyName='testKey'
queryParamsDefinition={{
query: StringParam,
}}
/>,
{ 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',
);
});
});
}); });

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react'; import { useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { createLocalStorage } from 'utils/createLocalStorage'; 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'; import { QueryParamConfigMap } from 'serialize-query-params/src/types';
const usePersistentSearchParams = <T extends QueryParamConfigMap>( const usePersistentSearchParams = <T extends QueryParamConfigMap>(
@ -39,7 +39,41 @@ export const usePersistentTableState = <T extends QueryParamConfigMap>(
queryParamsDefinition, 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(() => { useEffect(() => {
const { offset, ...rest } = tableState; const { offset, ...rest } = tableState;