mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: project overview table state (#5530)
Use new table state management on project overview and on project/features
This commit is contained in:
		
							parent
							
								
									5c889df9be
								
							
						
					
					
						commit
						ddca82213a
					
				@ -9,15 +9,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
				
			|||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
 | 
					import { useLastViewedProject } from 'hooks/useLastViewedProject';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
					import { useUiFlag } from 'hooks/useUiFlag';
 | 
				
			||||||
import {
 | 
					import { PaginatedProjectFeatureToggles } from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
 | 
				
			||||||
    DEFAULT_PAGE_LIMIT,
 | 
					 | 
				
			||||||
    useFeatureSearch,
 | 
					 | 
				
			||||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    PaginatedProjectFeatureToggles,
 | 
					 | 
				
			||||||
    ProjectTableState,
 | 
					 | 
				
			||||||
} from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
 | 
					 | 
				
			||||||
import { useTableState } from 'hooks/useTableState';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const refreshInterval = 15 * 1000;
 | 
					const refreshInterval = 15 * 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,37 +38,6 @@ const PaginatedProjectOverview = () => {
 | 
				
			|||||||
        refreshInterval,
 | 
					        refreshInterval,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [tableState, setTableState] = useTableState<ProjectTableState>(
 | 
					 | 
				
			||||||
        {},
 | 
					 | 
				
			||||||
        `project-features-${projectId}`,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const page = parseInt(tableState.page || '1', 10);
 | 
					 | 
				
			||||||
    const pageSize = tableState?.pageSize
 | 
					 | 
				
			||||||
        ? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
 | 
					 | 
				
			||||||
        : DEFAULT_PAGE_LIMIT;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const {
 | 
					 | 
				
			||||||
        features: searchFeatures,
 | 
					 | 
				
			||||||
        total,
 | 
					 | 
				
			||||||
        refetch,
 | 
					 | 
				
			||||||
        loading,
 | 
					 | 
				
			||||||
        initialLoad,
 | 
					 | 
				
			||||||
    } = useFeatureSearch(
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            offset: `${(page - 1) * pageSize}`,
 | 
					 | 
				
			||||||
            limit: `${pageSize}`,
 | 
					 | 
				
			||||||
            sortBy: tableState.sortBy || 'createdAt',
 | 
					 | 
				
			||||||
            sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
 | 
					 | 
				
			||||||
            favoritesFirst: tableState.favorites,
 | 
					 | 
				
			||||||
            project: projectId ? `IS:${projectId}` : '',
 | 
					 | 
				
			||||||
            query: tableState.search,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            refreshInterval,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { environments } = project;
 | 
					    const { environments } = project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
@ -84,21 +45,9 @@ const PaginatedProjectOverview = () => {
 | 
				
			|||||||
            <StyledContentContainer>
 | 
					            <StyledContentContainer>
 | 
				
			||||||
                <StyledProjectToggles>
 | 
					                <StyledProjectToggles>
 | 
				
			||||||
                    <PaginatedProjectFeatureToggles
 | 
					                    <PaginatedProjectFeatureToggles
 | 
				
			||||||
                        key={
 | 
					 | 
				
			||||||
                            (loading || projectLoading) &&
 | 
					 | 
				
			||||||
                            searchFeatures.length === 0
 | 
					 | 
				
			||||||
                                ? 'loading'
 | 
					 | 
				
			||||||
                                : 'ready'
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        style={{ width: '100%', margin: 0 }}
 | 
					                        style={{ width: '100%', margin: 0 }}
 | 
				
			||||||
                        features={searchFeatures || []}
 | 
					 | 
				
			||||||
                        environments={environments}
 | 
					                        environments={environments}
 | 
				
			||||||
                        initialLoad={initialLoad && searchFeatures.length === 0}
 | 
					                        storageKey='project-features'
 | 
				
			||||||
                        loading={loading && searchFeatures.length === 0}
 | 
					 | 
				
			||||||
                        onChange={refetch}
 | 
					 | 
				
			||||||
                        total={total}
 | 
					 | 
				
			||||||
                        tableState={tableState}
 | 
					 | 
				
			||||||
                        setTableState={setTableState}
 | 
					 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                </StyledProjectToggles>
 | 
					                </StyledProjectToggles>
 | 
				
			||||||
            </StyledContentContainer>
 | 
					            </StyledContentContainer>
 | 
				
			||||||
 | 
				
			|||||||
@ -64,47 +64,63 @@ import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureTogg
 | 
				
			|||||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
 | 
					import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
 | 
				
			||||||
import useLoading from 'hooks/useLoading';
 | 
					import useLoading from 'hooks/useLoading';
 | 
				
			||||||
import { StickyPaginationBar } from '../../../common/Table/StickyPaginationBar/StickyPaginationBar';
 | 
					import { StickyPaginationBar } from '../../../common/Table/StickyPaginationBar/StickyPaginationBar';
 | 
				
			||||||
import { DEFAULT_PAGE_LIMIT } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
 | 
					import {
 | 
				
			||||||
 | 
					    DEFAULT_PAGE_LIMIT,
 | 
				
			||||||
 | 
					    useFeatureSearch,
 | 
				
			||||||
 | 
					} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
 | 
				
			||||||
 | 
					import mapValues from 'lodash.mapvalues';
 | 
				
			||||||
 | 
					import { usePersistentTableState } from 'hooks/usePersistentTableState';
 | 
				
			||||||
 | 
					import { BooleansStringParam } from 'utils/serializeQueryParams';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    NumberParam,
 | 
				
			||||||
 | 
					    StringParam,
 | 
				
			||||||
 | 
					    ArrayParam,
 | 
				
			||||||
 | 
					    withDefault,
 | 
				
			||||||
 | 
					} from 'use-query-params';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
 | 
					const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
 | 
				
			||||||
    whiteSpace: 'nowrap',
 | 
					    whiteSpace: 'nowrap',
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ProjectTableState = {
 | 
					 | 
				
			||||||
    page?: string;
 | 
					 | 
				
			||||||
    sortBy?: string;
 | 
					 | 
				
			||||||
    pageSize?: string;
 | 
					 | 
				
			||||||
    sortOrder?: 'asc' | 'desc';
 | 
					 | 
				
			||||||
    favorites?: 'true' | 'false';
 | 
					 | 
				
			||||||
    columns?: string;
 | 
					 | 
				
			||||||
    search?: string;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface IPaginatedProjectFeatureTogglesProps {
 | 
					interface IPaginatedProjectFeatureTogglesProps {
 | 
				
			||||||
    features: SearchFeaturesSchema['features'];
 | 
					 | 
				
			||||||
    environments: IProject['environments'];
 | 
					    environments: IProject['environments'];
 | 
				
			||||||
    loading: boolean;
 | 
					 | 
				
			||||||
    onChange: () => void;
 | 
					 | 
				
			||||||
    total?: number;
 | 
					 | 
				
			||||||
    initialLoad: boolean;
 | 
					 | 
				
			||||||
    tableState: ProjectTableState;
 | 
					 | 
				
			||||||
    setTableState: (state: Partial<ProjectTableState>, quiet?: boolean) => void;
 | 
					 | 
				
			||||||
    style?: CSSProperties;
 | 
					    style?: CSSProperties;
 | 
				
			||||||
 | 
					    refreshInterval?: number;
 | 
				
			||||||
 | 
					    storageKey?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
 | 
					const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PaginatedProjectFeatureToggles = ({
 | 
					export const PaginatedProjectFeatureToggles = ({
 | 
				
			||||||
    features,
 | 
					 | 
				
			||||||
    loading,
 | 
					 | 
				
			||||||
    initialLoad,
 | 
					 | 
				
			||||||
    environments,
 | 
					    environments,
 | 
				
			||||||
    onChange,
 | 
					 | 
				
			||||||
    total,
 | 
					 | 
				
			||||||
    tableState,
 | 
					 | 
				
			||||||
    setTableState,
 | 
					 | 
				
			||||||
    style,
 | 
					    style,
 | 
				
			||||||
 | 
					    refreshInterval = 15 * 1000,
 | 
				
			||||||
 | 
					    storageKey = 'project-feature-toggles',
 | 
				
			||||||
}: IPaginatedProjectFeatureTogglesProps) => {
 | 
					}: IPaginatedProjectFeatureTogglesProps) => {
 | 
				
			||||||
 | 
					    const projectId = useRequiredPathParam('projectId');
 | 
				
			||||||
 | 
					    const [tableState, setTableState] = usePersistentTableState(
 | 
				
			||||||
 | 
					        `${storageKey}-${projectId}`,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            offset: withDefault(NumberParam, 0),
 | 
				
			||||||
 | 
					            limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
 | 
				
			||||||
 | 
					            query: StringParam,
 | 
				
			||||||
 | 
					            favoritesFirst: withDefault(BooleansStringParam, true),
 | 
				
			||||||
 | 
					            sortBy: withDefault(StringParam, 'createdAt'),
 | 
				
			||||||
 | 
					            sortOrder: withDefault(StringParam, 'desc'),
 | 
				
			||||||
 | 
					            columns: ArrayParam,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { features, total, refetch, loading, initialLoad } = useFeatureSearch(
 | 
				
			||||||
 | 
					        mapValues({ ...tableState, projectId }, (value) =>
 | 
				
			||||||
 | 
					            value ? `${value}` : undefined,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            refreshInterval,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const onChange = refetch;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { classes: styles } = useStyles();
 | 
					    const { classes: styles } = useStyles();
 | 
				
			||||||
    const bodyLoadingRef = useLoading(loading);
 | 
					    const bodyLoadingRef = useLoading(loading);
 | 
				
			||||||
    const headerLoadingRef = useLoading(initialLoad);
 | 
					    const headerLoadingRef = useLoading(initialLoad);
 | 
				
			||||||
@ -120,7 +136,6 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
    const [isCustomColumns, setIsCustomColumns] = useState(
 | 
					    const [isCustomColumns, setIsCustomColumns] = useState(
 | 
				
			||||||
        Boolean(tableState.columns),
 | 
					        Boolean(tableState.columns),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const projectId = useRequiredPathParam('projectId');
 | 
					 | 
				
			||||||
    const { onToggle: onFeatureToggle, modals: featureToggleModals } =
 | 
					    const { onToggle: onFeatureToggle, modals: featureToggleModals } =
 | 
				
			||||||
        useFeatureToggleSwitch(projectId);
 | 
					        useFeatureToggleSwitch(projectId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -170,13 +185,10 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                id: 'favorite',
 | 
					                id: 'favorite',
 | 
				
			||||||
                Header: (
 | 
					                Header: (
 | 
				
			||||||
                    <FavoriteIconHeader
 | 
					                    <FavoriteIconHeader
 | 
				
			||||||
                        isActive={tableState.favorites === 'true'}
 | 
					                        isActive={tableState.favoritesFirst}
 | 
				
			||||||
                        onClick={() =>
 | 
					                        onClick={() =>
 | 
				
			||||||
                            setTableState({
 | 
					                            setTableState({
 | 
				
			||||||
                                favorites:
 | 
					                                favoritesFirst: !tableState.favoritesFirst,
 | 
				
			||||||
                                    tableState.favorites === 'true'
 | 
					 | 
				
			||||||
                                        ? undefined
 | 
					 | 
				
			||||||
                                        : 'true',
 | 
					 | 
				
			||||||
                            })
 | 
					                            })
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
@ -323,7 +335,7 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        [projectId, environments, loading, tableState.favorites, onChange],
 | 
					        [projectId, environments, loading, tableState.favoritesFirst, onChange],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [showTitle, setShowTitle] = useState(true);
 | 
					    const [showTitle, setShowTitle] = useState(true);
 | 
				
			||||||
@ -366,14 +378,10 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const { getSearchText, getSearchContext } = useSearch(
 | 
					    const { getSearchText, getSearchContext } = useSearch(
 | 
				
			||||||
        columns,
 | 
					        columns,
 | 
				
			||||||
        tableState.search || '',
 | 
					        tableState.query || '',
 | 
				
			||||||
        featuresData,
 | 
					        featuresData,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const initialPageSize = tableState.pageSize
 | 
					 | 
				
			||||||
        ? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
 | 
					 | 
				
			||||||
        : DEFAULT_PAGE_LIMIT;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const allColumnIds = columns
 | 
					    const allColumnIds = columns
 | 
				
			||||||
        .map(
 | 
					        .map(
 | 
				
			||||||
            (column: any) =>
 | 
					            (column: any) =>
 | 
				
			||||||
@ -396,14 +404,13 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                ? {
 | 
					                ? {
 | 
				
			||||||
                      hiddenColumns: allColumnIds.filter(
 | 
					                      hiddenColumns: allColumnIds.filter(
 | 
				
			||||||
                          (id) =>
 | 
					                          (id) =>
 | 
				
			||||||
                              !(tableState.columns?.split(',') || [])?.includes(
 | 
					                              !tableState.columns?.includes(id) &&
 | 
				
			||||||
                                  id,
 | 
					                              !staticColumns.includes(id),
 | 
				
			||||||
                              ) && !staticColumns.includes(id),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                : {}),
 | 
					                : {}),
 | 
				
			||||||
            pageSize: initialPageSize,
 | 
					            pageSize: tableState.limit,
 | 
				
			||||||
            pageIndex: tableState.page ? parseInt(tableState.page, 10) - 1 : 0,
 | 
					            pageIndex: tableState.offset * tableState.limit,
 | 
				
			||||||
            selectedRowIds: {},
 | 
					            selectedRowIds: {},
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        [initialLoad],
 | 
					        [initialLoad],
 | 
				
			||||||
@ -411,9 +418,7 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const data = useMemo(() => {
 | 
					    const data = useMemo(() => {
 | 
				
			||||||
        if (initialLoad || loading) {
 | 
					        if (initialLoad || loading) {
 | 
				
			||||||
            const loadingData = Array(
 | 
					            const loadingData = Array(tableState.limit)
 | 
				
			||||||
                parseInt(tableState.pageSize || `${initialPageSize}`, 10),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
                .fill(null)
 | 
					                .fill(null)
 | 
				
			||||||
                .map((_, index) => ({
 | 
					                .map((_, index) => ({
 | 
				
			||||||
                    id: index, // Assuming `id` is a required property
 | 
					                    id: index, // Assuming `id` is a required property
 | 
				
			||||||
@ -434,11 +439,8 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
    }, [loading, featuresData]);
 | 
					    }, [loading, featuresData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const pageCount = useMemo(
 | 
					    const pageCount = useMemo(
 | 
				
			||||||
        () =>
 | 
					        () => Math.ceil((total || 0) / tableState.limit),
 | 
				
			||||||
            tableState.pageSize
 | 
					        [total, tableState.limit],
 | 
				
			||||||
                ? Math.ceil((total || 0) / parseInt(tableState.pageSize))
 | 
					 | 
				
			||||||
                : 0,
 | 
					 | 
				
			||||||
        [total, tableState.pageSize],
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const getRowId = useCallback((row: any) => row.name, []);
 | 
					    const getRowId = useCallback((row: any) => row.name, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -478,30 +480,26 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
    // Refetching - https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/faq.md#how-can-i-use-the-table-state-to-fetch-new-data
 | 
					    // Refetching - https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/faq.md#how-can-i-use-the-table-state-to-fetch-new-data
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        setTableState({
 | 
					        setTableState({
 | 
				
			||||||
            page: `${pageIndex + 1}`,
 | 
					            offset: pageIndex * pageSize,
 | 
				
			||||||
            pageSize: `${pageSize}`,
 | 
					            limit: pageSize,
 | 
				
			||||||
            sortBy: sortBy[0]?.id || 'createdAt',
 | 
					            sortBy: sortBy[0]?.id || 'createdAt',
 | 
				
			||||||
            sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
 | 
					            sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }, [pageIndex, pageSize, sortBy]);
 | 
					    }, [pageIndex, pageSize, sortBy]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        // FIXME: refactor column visibility logic when switching to react-table v8
 | 
				
			||||||
        if (!loading && isCustomColumns) {
 | 
					        if (!loading && isCustomColumns) {
 | 
				
			||||||
            setTableState(
 | 
					            setTableState({
 | 
				
			||||||
                {
 | 
					                columns:
 | 
				
			||||||
                    columns:
 | 
					                    hiddenColumns !== undefined
 | 
				
			||||||
                        hiddenColumns !== undefined
 | 
					                        ? allColumnIds.filter(
 | 
				
			||||||
                            ? allColumnIds
 | 
					                              (id) =>
 | 
				
			||||||
                                  .filter(
 | 
					                                  !hiddenColumns.includes(id) &&
 | 
				
			||||||
                                      (id) =>
 | 
					                                  !staticColumns.includes(id),
 | 
				
			||||||
                                          !hiddenColumns.includes(id) &&
 | 
					                          )
 | 
				
			||||||
                                          !staticColumns.includes(id),
 | 
					                        : undefined,
 | 
				
			||||||
                                  )
 | 
					            });
 | 
				
			||||||
                                  .join(',')
 | 
					 | 
				
			||||||
                            : undefined,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                true, // Columns state is controllable by react-table - update only URL and storage, not state
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }, [loading, isCustomColumns, hiddenColumns]);
 | 
					    }, [loading, isCustomColumns, hiddenColumns]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -548,10 +546,12 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                                                data-loading
 | 
					                                                data-loading
 | 
				
			||||||
                                                placeholder='Search and Filter'
 | 
					                                                placeholder='Search and Filter'
 | 
				
			||||||
                                                expandable
 | 
					                                                expandable
 | 
				
			||||||
                                                initialValue={tableState.search}
 | 
					                                                initialValue={
 | 
				
			||||||
 | 
					                                                    tableState.query || ''
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
                                                onChange={(value) => {
 | 
					                                                onChange={(value) => {
 | 
				
			||||||
                                                    setTableState({
 | 
					                                                    setTableState({
 | 
				
			||||||
                                                        search: value,
 | 
					                                                        query: value,
 | 
				
			||||||
                                                    });
 | 
					                                                    });
 | 
				
			||||||
                                                }}
 | 
					                                                }}
 | 
				
			||||||
                                                onFocus={() =>
 | 
					                                                onFocus={() =>
 | 
				
			||||||
@ -630,11 +630,9 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                                condition={isSmallScreen}
 | 
					                                condition={isSmallScreen}
 | 
				
			||||||
                                show={
 | 
					                                show={
 | 
				
			||||||
                                    <Search
 | 
					                                    <Search
 | 
				
			||||||
                                        initialValue={tableState.search}
 | 
					                                        initialValue={tableState.query || ''}
 | 
				
			||||||
                                        onChange={(value) => {
 | 
					                                        onChange={(value) => {
 | 
				
			||||||
                                            setTableState({
 | 
					                                            setTableState({ query: value });
 | 
				
			||||||
                                                search: value,
 | 
					 | 
				
			||||||
                                            });
 | 
					 | 
				
			||||||
                                        }}
 | 
					                                        }}
 | 
				
			||||||
                                        hasFilters
 | 
					                                        hasFilters
 | 
				
			||||||
                                        getSearchContext={getSearchContext}
 | 
					                                        getSearchContext={getSearchContext}
 | 
				
			||||||
@ -652,7 +650,7 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                    aria-live='polite'
 | 
					                    aria-live='polite'
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    <SearchHighlightProvider
 | 
					                    <SearchHighlightProvider
 | 
				
			||||||
                        value={getSearchText(tableState.search || '')}
 | 
					                        value={getSearchText(tableState.query || '')}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                        <VirtualizedTable
 | 
					                        <VirtualizedTable
 | 
				
			||||||
                            rows={rows}
 | 
					                            rows={rows}
 | 
				
			||||||
@ -665,15 +663,13 @@ export const PaginatedProjectFeatureToggles = ({
 | 
				
			|||||||
                        condition={rows.length === 0}
 | 
					                        condition={rows.length === 0}
 | 
				
			||||||
                        show={
 | 
					                        show={
 | 
				
			||||||
                            <ConditionallyRender
 | 
					                            <ConditionallyRender
 | 
				
			||||||
                                condition={
 | 
					                                condition={(tableState.query || '')?.length > 0}
 | 
				
			||||||
                                    (tableState.search || '')?.length > 0
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                show={
 | 
					                                show={
 | 
				
			||||||
                                    <Box sx={{ padding: theme.spacing(3) }}>
 | 
					                                    <Box sx={{ padding: theme.spacing(3) }}>
 | 
				
			||||||
                                        <TablePlaceholder>
 | 
					                                        <TablePlaceholder>
 | 
				
			||||||
                                            No feature toggles found matching
 | 
					                                            No feature toggles found matching
 | 
				
			||||||
                                            “
 | 
					                                            “
 | 
				
			||||||
                                            {tableState.search}
 | 
					                                            {tableState.query}
 | 
				
			||||||
                                            ”
 | 
					                                            ”
 | 
				
			||||||
                                        </TablePlaceholder>
 | 
					                                        </TablePlaceholder>
 | 
				
			||||||
                                    </Box>
 | 
					                                    </Box>
 | 
				
			||||||
 | 
				
			|||||||
@ -15,11 +15,10 @@ import {
 | 
				
			|||||||
    useFeatureSearch,
 | 
					    useFeatureSearch,
 | 
				
			||||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
 | 
					} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    ProjectTableState,
 | 
					    // ProjectTableState,
 | 
				
			||||||
    PaginatedProjectFeatureToggles,
 | 
					    PaginatedProjectFeatureToggles,
 | 
				
			||||||
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
 | 
					} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useTableState } from 'hooks/useTableState';
 | 
					 | 
				
			||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
 | 
					import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
 | 
				
			||||||
import { FeatureTypeCount } from '../../../interfaces/project';
 | 
					import { FeatureTypeCount } from '../../../interfaces/project';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,37 +54,6 @@ const PaginatedProjectOverview: FC<{
 | 
				
			|||||||
        refreshInterval,
 | 
					        refreshInterval,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [tableState, setTableState] = useTableState<ProjectTableState>(
 | 
					 | 
				
			||||||
        {},
 | 
					 | 
				
			||||||
        `${storageKey}-${projectId}`,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const page = parseInt(tableState.page || '1', 10);
 | 
					 | 
				
			||||||
    const pageSize = tableState?.pageSize
 | 
					 | 
				
			||||||
        ? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
 | 
					 | 
				
			||||||
        : DEFAULT_PAGE_LIMIT;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const {
 | 
					 | 
				
			||||||
        features: searchFeatures,
 | 
					 | 
				
			||||||
        total,
 | 
					 | 
				
			||||||
        refetch,
 | 
					 | 
				
			||||||
        loading,
 | 
					 | 
				
			||||||
        initialLoad,
 | 
					 | 
				
			||||||
    } = useFeatureSearch(
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            offset: `${(page - 1) * pageSize}`,
 | 
					 | 
				
			||||||
            limit: `${pageSize}`,
 | 
					 | 
				
			||||||
            sortBy: tableState.sortBy || 'createdAt',
 | 
					 | 
				
			||||||
            sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
 | 
					 | 
				
			||||||
            favoritesFirst: tableState.favorites,
 | 
					 | 
				
			||||||
            project: projectId ? `IS:${projectId}` : '',
 | 
					 | 
				
			||||||
            query: tableState.search,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            refreshInterval,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const {
 | 
					    const {
 | 
				
			||||||
        members,
 | 
					        members,
 | 
				
			||||||
        featureTypeCounts,
 | 
					        featureTypeCounts,
 | 
				
			||||||
@ -112,14 +80,9 @@ const PaginatedProjectOverview: FC<{
 | 
				
			|||||||
                        style={
 | 
					                        style={
 | 
				
			||||||
                            fullWidth ? { width: '100%', margin: 0 } : undefined
 | 
					                            fullWidth ? { width: '100%', margin: 0 } : undefined
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        features={searchFeatures || []}
 | 
					 | 
				
			||||||
                        environments={environments}
 | 
					                        environments={environments}
 | 
				
			||||||
                        initialLoad={initialLoad && searchFeatures.length === 0}
 | 
					                        refreshInterval={refreshInterval}
 | 
				
			||||||
                        loading={loading && searchFeatures.length === 0}
 | 
					                        storageKey={storageKey}
 | 
				
			||||||
                        onChange={refetch}
 | 
					 | 
				
			||||||
                        total={total}
 | 
					 | 
				
			||||||
                        tableState={tableState}
 | 
					 | 
				
			||||||
                        setTableState={setTableState}
 | 
					 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                </StyledProjectToggles>
 | 
					                </StyledProjectToggles>
 | 
				
			||||||
            </StyledContentContainer>
 | 
					            </StyledContentContainer>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,268 +0,0 @@
 | 
				
			|||||||
import { vi, type Mock } from 'vitest';
 | 
					 | 
				
			||||||
import { renderHook } from '@testing-library/react-hooks';
 | 
					 | 
				
			||||||
import { useTableState } from './useTableState';
 | 
					 | 
				
			||||||
import { createLocalStorage } from 'utils/createLocalStorage';
 | 
					 | 
				
			||||||
import { useSearchParams } from 'react-router-dom';
 | 
					 | 
				
			||||||
import { act } from 'react-test-renderer';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
vi.mock('react-router-dom', () => ({
 | 
					 | 
				
			||||||
    useSearchParams: vi.fn(() => [new URLSearchParams(), vi.fn()]),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
vi.mock('../utils/createLocalStorage', () => ({
 | 
					 | 
				
			||||||
    createLocalStorage: vi.fn(() => ({
 | 
					 | 
				
			||||||
        value: {},
 | 
					 | 
				
			||||||
        setValue: vi.fn(),
 | 
					 | 
				
			||||||
    })),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mockStorage = createLocalStorage as Mock;
 | 
					 | 
				
			||||||
const mockQuery = useSearchParams as Mock;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('useTableState', () => {
 | 
					 | 
				
			||||||
    beforeEach(() => {
 | 
					 | 
				
			||||||
        mockStorage.mockRestore();
 | 
					 | 
				
			||||||
        mockQuery.mockRestore();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should return default params', () => {
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState<{
 | 
					 | 
				
			||||||
                page: '0';
 | 
					 | 
				
			||||||
                pageSize: '10';
 | 
					 | 
				
			||||||
            }>({ page: '0', pageSize: '10' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '0', pageSize: '10' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should return params from local storage', () => {
 | 
					 | 
				
			||||||
        mockStorage.mockReturnValue({
 | 
					 | 
				
			||||||
            value: { pageSize: 25 },
 | 
					 | 
				
			||||||
            setValue: vi.fn(),
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ pageSize: '10' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ pageSize: 25 });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should return params from url', () => {
 | 
					 | 
				
			||||||
        mockQuery.mockReturnValue([new URLSearchParams('page=1'), vi.fn()]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '0' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '1' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should use params from url over local storage', () => {
 | 
					 | 
				
			||||||
        mockQuery.mockReturnValue([
 | 
					 | 
				
			||||||
            new URLSearchParams('page=2&pageSize=25'),
 | 
					 | 
				
			||||||
            vi.fn(),
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
        mockStorage.mockReturnValue({
 | 
					 | 
				
			||||||
            value: { pageSize: '10', sortOrder: 'desc' },
 | 
					 | 
				
			||||||
            setValue: vi.fn(),
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '1', pageSize: '5' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({
 | 
					 | 
				
			||||||
            page: '2',
 | 
					 | 
				
			||||||
            pageSize: '25',
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('sets local state', () => {
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '1' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({ page: '2' });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '2' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('keeps previous state', () => {
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '1', pageSize: '10' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({ page: '2' });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '2', pageSize: '10' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('removes params from previous state', () => {
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '1', pageSize: '10' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({ pageSize: undefined });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '1' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // ensure that there are no keys with undefined values
 | 
					 | 
				
			||||||
        expect(Object.keys(result.current[0])).toHaveLength(1);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it.skip('removes params from url', () => {
 | 
					 | 
				
			||||||
        const querySetter = vi.fn();
 | 
					 | 
				
			||||||
        mockQuery.mockReturnValue([new URLSearchParams('page=2'), querySetter]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState(
 | 
					 | 
				
			||||||
                { page: '1', pageSize: '10' },
 | 
					 | 
				
			||||||
                'test',
 | 
					 | 
				
			||||||
                ['page', 'pageSize'],
 | 
					 | 
				
			||||||
                [],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '2', pageSize: '10' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({ page: '10', pageSize: undefined });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '10' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(querySetter).toHaveBeenCalledWith({
 | 
					 | 
				
			||||||
            page: '10',
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('removes params from local storage', () => {
 | 
					 | 
				
			||||||
        const storageSetter = vi.fn();
 | 
					 | 
				
			||||||
        mockStorage.mockReturnValue({
 | 
					 | 
				
			||||||
            value: { sortBy: 'type' },
 | 
					 | 
				
			||||||
            setValue: storageSetter,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState(
 | 
					 | 
				
			||||||
                { sortBy: 'createdAt', pageSize: '10' },
 | 
					 | 
				
			||||||
                'test',
 | 
					 | 
				
			||||||
                [],
 | 
					 | 
				
			||||||
                ['sortBy', 'pageSize'],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ sortBy: 'type', pageSize: '10' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            result.current[1]({ pageSize: undefined });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ sortBy: 'type' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(storageSetter).toHaveBeenCalledWith({
 | 
					 | 
				
			||||||
            sortBy: 'type',
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    test.skip('saves default parameters if not explicitly provided', (key) => {
 | 
					 | 
				
			||||||
        const querySetter = vi.fn();
 | 
					 | 
				
			||||||
        const storageSetter = vi.fn();
 | 
					 | 
				
			||||||
        mockQuery.mockReturnValue([new URLSearchParams(), querySetter]);
 | 
					 | 
				
			||||||
        mockStorage.mockReturnValue({
 | 
					 | 
				
			||||||
            value: {},
 | 
					 | 
				
			||||||
            setValue: storageSetter,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() => useTableState({}, 'test'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            result.current[1]({
 | 
					 | 
				
			||||||
                unspecified: 'test',
 | 
					 | 
				
			||||||
                page: '2',
 | 
					 | 
				
			||||||
                pageSize: '10',
 | 
					 | 
				
			||||||
                search: 'test',
 | 
					 | 
				
			||||||
                sortBy: 'type',
 | 
					 | 
				
			||||||
                sortOrder: 'favorites',
 | 
					 | 
				
			||||||
                favorites: 'false',
 | 
					 | 
				
			||||||
                columns: ['test', 'id'],
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(storageSetter).toHaveBeenCalledTimes(1);
 | 
					 | 
				
			||||||
        expect(storageSetter).toHaveBeenCalledWith({
 | 
					 | 
				
			||||||
            pageSize: '10',
 | 
					 | 
				
			||||||
            search: 'test',
 | 
					 | 
				
			||||||
            sortBy: 'type',
 | 
					 | 
				
			||||||
            sortOrder: 'favorites',
 | 
					 | 
				
			||||||
            favorites: 'false',
 | 
					 | 
				
			||||||
            columns: ['test', 'id'],
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        expect(querySetter).toHaveBeenCalledTimes(1);
 | 
					 | 
				
			||||||
        expect(querySetter).toHaveBeenCalledWith({
 | 
					 | 
				
			||||||
            page: '2',
 | 
					 | 
				
			||||||
            pageSize: '10',
 | 
					 | 
				
			||||||
            search: 'test',
 | 
					 | 
				
			||||||
            sortBy: 'type',
 | 
					 | 
				
			||||||
            sortOrder: 'favorites',
 | 
					 | 
				
			||||||
            favorites: 'false',
 | 
					 | 
				
			||||||
            columns: ['test', 'id'],
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it("doesn't save default params if explicitly specified", () => {
 | 
					 | 
				
			||||||
        const storageSetter = vi.fn();
 | 
					 | 
				
			||||||
        mockStorage.mockReturnValue({
 | 
					 | 
				
			||||||
            value: {},
 | 
					 | 
				
			||||||
            setValue: storageSetter,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        const querySetter = vi.fn();
 | 
					 | 
				
			||||||
        mockQuery.mockReturnValue([new URLSearchParams(), querySetter]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState<{
 | 
					 | 
				
			||||||
                [key: string]: string;
 | 
					 | 
				
			||||||
            }>({}, 'test', ['saveOnlyThisToUrl'], ['page']),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({
 | 
					 | 
				
			||||||
                saveOnlyThisToUrl: 'test',
 | 
					 | 
				
			||||||
                page: '2',
 | 
					 | 
				
			||||||
                pageSize: '10',
 | 
					 | 
				
			||||||
                search: 'test',
 | 
					 | 
				
			||||||
                sortBy: 'type',
 | 
					 | 
				
			||||||
                sortOrder: 'favorites',
 | 
					 | 
				
			||||||
                favorites: 'false',
 | 
					 | 
				
			||||||
                columns: 'test,id',
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(querySetter).toHaveBeenCalledWith({ saveOnlyThisToUrl: 'test' });
 | 
					 | 
				
			||||||
        expect(storageSetter).toHaveBeenCalledWith({ page: '2' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('can update query and storage without triggering a rerender', () => {
 | 
					 | 
				
			||||||
        const { result } = renderHook(() =>
 | 
					 | 
				
			||||||
            useTableState({ page: '1' }, 'test', [], []),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const setParams = result.current[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        act(() => {
 | 
					 | 
				
			||||||
            setParams({ page: '2' }, true);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(result.current[0]).toEqual({ page: '1' });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -1,109 +0,0 @@
 | 
				
			|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
 | 
					 | 
				
			||||||
import { useSearchParams } from 'react-router-dom';
 | 
					 | 
				
			||||||
import { createLocalStorage } from '../utils/createLocalStorage';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const filterObjectKeys = <T extends Record<string, unknown>>(
 | 
					 | 
				
			||||||
    obj: T,
 | 
					 | 
				
			||||||
    keys: Array<keyof T>,
 | 
					 | 
				
			||||||
) =>
 | 
					 | 
				
			||||||
    Object.fromEntries(
 | 
					 | 
				
			||||||
        Object.entries(obj).filter(([key]) => keys.includes(key as keyof T)),
 | 
					 | 
				
			||||||
    ) as T;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const defaultStoredKeys = [
 | 
					 | 
				
			||||||
    'pageSize',
 | 
					 | 
				
			||||||
    'sortBy',
 | 
					 | 
				
			||||||
    'sortOrder',
 | 
					 | 
				
			||||||
    'favorites',
 | 
					 | 
				
			||||||
    'columns',
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
export const defaultQueryKeys = [
 | 
					 | 
				
			||||||
    ...defaultStoredKeys,
 | 
					 | 
				
			||||||
    'search',
 | 
					 | 
				
			||||||
    'query',
 | 
					 | 
				
			||||||
    'page',
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * There are 3 sources of params, in order of priority:
 | 
					 | 
				
			||||||
 *   1. local state
 | 
					 | 
				
			||||||
 *   2. search params from the url
 | 
					 | 
				
			||||||
 *   3. stored params in local storage
 | 
					 | 
				
			||||||
 *   4. default parameters
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * `queryKeys` will be saved in the url
 | 
					 | 
				
			||||||
 * `storedKeys` will be saved in local storage
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @deprecated
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @param defaultParams initial state
 | 
					 | 
				
			||||||
 * @param storageId identifier for the local storage
 | 
					 | 
				
			||||||
 * @param queryKeys array of elements to be saved in the url
 | 
					 | 
				
			||||||
 * @param storageKeys array of elements to be saved in local storage
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const useTableState = <Params extends Record<string, string>>(
 | 
					 | 
				
			||||||
    defaultParams: Params,
 | 
					 | 
				
			||||||
    storageId: string,
 | 
					 | 
				
			||||||
    queryKeys?: Array<keyof Params | string>,
 | 
					 | 
				
			||||||
    storageKeys?: Array<keyof Params | string>,
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
    const [searchParams, setSearchParams] = useSearchParams();
 | 
					 | 
				
			||||||
    const { value: storedParams, setValue: setStoredParams } =
 | 
					 | 
				
			||||||
        createLocalStorage(`${storageId}:tableQuery`, defaultParams);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const searchQuery = Object.fromEntries(searchParams.entries());
 | 
					 | 
				
			||||||
    const hasQuery = Object.keys(searchQuery).length > 0;
 | 
					 | 
				
			||||||
    const [state, setState] = useState({
 | 
					 | 
				
			||||||
        ...defaultParams,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const params = useMemo(
 | 
					 | 
				
			||||||
        () =>
 | 
					 | 
				
			||||||
            ({
 | 
					 | 
				
			||||||
                ...state,
 | 
					 | 
				
			||||||
                ...(hasQuery ? {} : storedParams),
 | 
					 | 
				
			||||||
                ...searchQuery,
 | 
					 | 
				
			||||||
            }) as Params,
 | 
					 | 
				
			||||||
        [hasQuery, storedParams, searchQuery],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(() => {
 | 
					 | 
				
			||||||
        const urlParams = filterObjectKeys(
 | 
					 | 
				
			||||||
            params,
 | 
					 | 
				
			||||||
            queryKeys || defaultQueryKeys,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if (!hasQuery && Object.keys(urlParams).length > 0) {
 | 
					 | 
				
			||||||
            setSearchParams(urlParams, { replace: true });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }, [params, hasQuery, setSearchParams, queryKeys]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateParams = useCallback(
 | 
					 | 
				
			||||||
        (value: Partial<Params>, quiet = false) => {
 | 
					 | 
				
			||||||
            const newState: Params = {
 | 
					 | 
				
			||||||
                ...params,
 | 
					 | 
				
			||||||
                ...value,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // remove keys with undefined values
 | 
					 | 
				
			||||||
            Object.keys(newState).forEach((key) => {
 | 
					 | 
				
			||||||
                if (newState[key] === undefined) {
 | 
					 | 
				
			||||||
                    delete newState[key];
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!quiet) {
 | 
					 | 
				
			||||||
                setState(newState);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            setSearchParams(
 | 
					 | 
				
			||||||
                filterObjectKeys(newState, queryKeys || defaultQueryKeys),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            setStoredParams(
 | 
					 | 
				
			||||||
                filterObjectKeys(newState, storageKeys || defaultStoredKeys),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return params;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        [setState, setSearchParams, setStoredParams],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [params, updateParams] as const;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user