mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
parent
f00eac0881
commit
dbd897e3bd
@ -9,7 +9,7 @@ interface IBatchSelectionActionsBarProps {
|
||||
const StyledStickyContainer = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
bottom: 50,
|
||||
zIndex: theme.zIndex.mobileStepper,
|
||||
zIndex: theme.zIndex.fab,
|
||||
pointerEvents: 'none',
|
||||
}));
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import useProject, {
|
||||
useProjectNameOrId,
|
||||
} from 'hooks/api/getters/useProject/useProject';
|
||||
@ -9,15 +9,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import {
|
||||
ISortingRules,
|
||||
DEFAULT_PAGE_LIMIT,
|
||||
useFeatureSearch,
|
||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import {
|
||||
PaginatedProjectFeatureToggles,
|
||||
ProjectTableState,
|
||||
} from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
||||
import { SortingRule } from 'react-table';
|
||||
import { useTableState } from 'hooks/useTableState';
|
||||
|
||||
const refreshInterval = 15 * 1000;
|
||||
|
||||
@ -40,26 +40,21 @@ const StyledContentContainer = styled(Box)(() => ({
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
export const DEFAULT_PAGE_LIMIT = 25;
|
||||
|
||||
const PaginatedProjectOverview = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { project, loading: projectLoading } = useProject(projectId, {
|
||||
refreshInterval,
|
||||
});
|
||||
const [pageLimit, setPageLimit] = useState(DEFAULT_PAGE_LIMIT);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
searchParams.get('search') || '',
|
||||
const [tableState, setTableState] = useTableState<ProjectTableState>(
|
||||
{},
|
||||
`project-features-${projectId}`,
|
||||
);
|
||||
|
||||
const [sortingRules, setSortingRules] = useState<ISortingRules>({
|
||||
sortBy: 'createdBy',
|
||||
sortOrder: 'desc',
|
||||
isFavoritesPinned: false,
|
||||
});
|
||||
const page = parseInt(tableState.page || '1', 10);
|
||||
const pageSize = tableState?.pageSize
|
||||
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||
: DEFAULT_PAGE_LIMIT;
|
||||
|
||||
const {
|
||||
features: searchFeatures,
|
||||
@ -68,28 +63,21 @@ const PaginatedProjectOverview = () => {
|
||||
loading,
|
||||
initialLoad,
|
||||
} = useFeatureSearch(
|
||||
currentOffset,
|
||||
pageLimit,
|
||||
sortingRules,
|
||||
(page - 1) * pageSize,
|
||||
pageSize,
|
||||
{
|
||||
sortBy: tableState.sortBy || 'createdAt',
|
||||
sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
|
||||
favoritesFirst: tableState.favorites === 'true',
|
||||
},
|
||||
projectId,
|
||||
searchValue,
|
||||
tableState.search,
|
||||
{
|
||||
refreshInterval,
|
||||
},
|
||||
);
|
||||
|
||||
const { environments } = project;
|
||||
const fetchNextPage = () => {
|
||||
if (!loading) {
|
||||
setCurrentOffset(Math.min(total, currentOffset + pageLimit));
|
||||
}
|
||||
};
|
||||
const fetchPrevPage = () => {
|
||||
setCurrentOffset(Math.max(0, currentOffset - pageLimit));
|
||||
};
|
||||
|
||||
const hasPreviousPage = currentOffset > 0;
|
||||
const hasNextPage = currentOffset + pageLimit < total;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
@ -97,35 +85,20 @@ const PaginatedProjectOverview = () => {
|
||||
<StyledProjectToggles>
|
||||
<PaginatedProjectFeatureToggles
|
||||
key={
|
||||
loading && searchFeatures.length === 0
|
||||
(loading || projectLoading) &&
|
||||
searchFeatures.length === 0
|
||||
? 'loading'
|
||||
: 'ready'
|
||||
}
|
||||
features={searchFeatures}
|
||||
style={{ width: '100%', margin: 0 }}
|
||||
features={searchFeatures || []}
|
||||
environments={environments}
|
||||
initialLoad={initialLoad && searchFeatures.length === 0}
|
||||
loading={loading && searchFeatures.length === 0}
|
||||
onChange={refetch}
|
||||
total={total}
|
||||
searchValue={searchValue}
|
||||
setSearchValue={setSearchValue}
|
||||
sortingRules={sortingRules}
|
||||
setSortingRules={setSortingRules}
|
||||
paginationBar={
|
||||
<StickyPaginationBar>
|
||||
<PaginationBar
|
||||
total={total}
|
||||
hasNextPage={hasNextPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
fetchPrevPage={fetchPrevPage}
|
||||
currentOffset={currentOffset}
|
||||
pageLimit={pageLimit}
|
||||
setPageLimit={setPageLimit}
|
||||
/>
|
||||
</StickyPaginationBar>
|
||||
}
|
||||
tableState={tableState}
|
||||
setTableState={setTableState}
|
||||
/>
|
||||
</StyledProjectToggles>
|
||||
</StyledContentContainer>
|
||||
@ -133,36 +106,6 @@ const PaginatedProjectOverview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const StyledStickyBar = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
zIndex: 9999,
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
||||
height: '52px',
|
||||
}));
|
||||
|
||||
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
const StickyPaginationBar: React.FC = ({ children }) => {
|
||||
return (
|
||||
<StyledStickyBar>
|
||||
<StyledStickyBarContentContainer>
|
||||
{children}
|
||||
</StyledStickyBarContentContainer>
|
||||
</StyledStickyBar>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
||||
*/
|
||||
|
@ -34,11 +34,8 @@ interface IColumnsMenuProps {
|
||||
dividerBefore?: string[];
|
||||
dividerAfter?: string[];
|
||||
isCustomized?: boolean;
|
||||
setHiddenColumns: (
|
||||
hiddenColumns:
|
||||
| string[]
|
||||
| ((previousHiddenColumns: string[]) => string[]),
|
||||
) => void;
|
||||
setHiddenColumns: (hiddenColumns: string[]) => void;
|
||||
onCustomize?: () => void;
|
||||
}
|
||||
|
||||
const columnNameMap: Record<string, string> = {
|
||||
@ -51,6 +48,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
dividerBefore = [],
|
||||
dividerAfter = [],
|
||||
isCustomized = false,
|
||||
onCustomize,
|
||||
setHiddenColumns,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@ -69,7 +67,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
environmentsToShow: number = 0,
|
||||
) => {
|
||||
const visibleEnvColumns = allColumns
|
||||
.filter(({ id }) => id.startsWith('environments.') !== false)
|
||||
.filter(({ id }) => id.startsWith('environment:') !== false)
|
||||
.map(({ id }) => id)
|
||||
.slice(0, environmentsToShow);
|
||||
const hiddenColumns = allColumns
|
||||
@ -160,9 +158,10 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
show={<StyledDivider />}
|
||||
/>,
|
||||
<StyledMenuItem
|
||||
onClick={() =>
|
||||
column.toggleHidden(column.isVisible)
|
||||
}
|
||||
onClick={() => {
|
||||
column.toggleHidden(column.isVisible);
|
||||
onCustomize?.();
|
||||
}}
|
||||
disabled={staticColumns.includes(column.id)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
|
@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
type CSSProperties,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
IconButton,
|
||||
@ -9,10 +15,10 @@ import {
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
SortingRule,
|
||||
useFlexLayout,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
@ -32,7 +38,6 @@ import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/Fe
|
||||
import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||
@ -40,17 +45,12 @@ 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,
|
||||
useEnvironmentsRef,
|
||||
} from './hooks/useEnvironmentsRef';
|
||||
import { ProjectEnvironmentType } from './hooks/useEnvironmentsRef';
|
||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
||||
import FileDownload from '@mui/icons-material/FileDownload';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||
@ -63,17 +63,22 @@ import { ListItemType } from './ProjectFeatureToggles.types';
|
||||
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import useLoading from 'hooks/useLoading';
|
||||
import { DEFAULT_PAGE_LIMIT } from '../ProjectOverview';
|
||||
import { StickyPaginationBar } from '../StickyPaginationBar/StickyPaginationBar';
|
||||
import { DEFAULT_PAGE_LIMIT } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
|
||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
export interface ISortingRules {
|
||||
sortBy: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
isFavoritesPinned: boolean;
|
||||
}
|
||||
export type ProjectTableState = {
|
||||
page?: string;
|
||||
sortBy?: string;
|
||||
pageSize?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
favorites?: 'true' | 'false';
|
||||
columns?: string;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
features: IProject['features'];
|
||||
@ -82,33 +87,23 @@ interface IPaginatedProjectFeatureTogglesProps {
|
||||
onChange: () => void;
|
||||
total?: number;
|
||||
initialLoad: boolean;
|
||||
searchValue: string;
|
||||
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
paginationBar: JSX.Element;
|
||||
sortingRules: ISortingRules;
|
||||
setSortingRules: (sortingRules: ISortingRules) => void;
|
||||
style?: React.CSSProperties;
|
||||
tableState: ProjectTableState;
|
||||
setTableState: (state: Partial<ProjectTableState>, quiet?: boolean) => void;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||
|
||||
const defaultSort: SortingRule<string> & {
|
||||
columns?: string[];
|
||||
} = { id: 'createdAt', desc: true };
|
||||
|
||||
export const PaginatedProjectFeatureToggles = ({
|
||||
features,
|
||||
loading,
|
||||
initialLoad,
|
||||
environments: newEnvironments = [],
|
||||
environments,
|
||||
onChange,
|
||||
total,
|
||||
searchValue,
|
||||
setSearchValue,
|
||||
paginationBar,
|
||||
sortingRules,
|
||||
setSortingRules,
|
||||
style = {},
|
||||
tableState,
|
||||
setTableState,
|
||||
style,
|
||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const bodyLoadingRef = useLoading(loading);
|
||||
@ -122,30 +117,14 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
const [featureArchiveState, setFeatureArchiveState] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [isCustomColumns, setIsCustomColumns] = useState(
|
||||
Boolean(tableState.columns),
|
||||
);
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||
useFeatureToggleSwitch(projectId);
|
||||
|
||||
const { value: storedParams, setValue: setStoredParams } =
|
||||
createLocalStorage(
|
||||
`${projectId}:FeatureToggleListTable:v1`,
|
||||
defaultSort,
|
||||
);
|
||||
const { value: globalStore, setValue: setGlobalStore } =
|
||||
useGlobalLocalStorage();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const environments = useEnvironmentsRef(
|
||||
loading
|
||||
? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
|
||||
: newEnvironments,
|
||||
);
|
||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
||||
usePinnedFavorites(
|
||||
searchParams.has('favorites')
|
||||
? searchParams.get('favorites') === 'true'
|
||||
: globalStore.favorites,
|
||||
);
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||
@ -191,8 +170,15 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
id: 'favorite',
|
||||
Header: (
|
||||
<FavoriteIconHeader
|
||||
isActive={isFavoritesPinned}
|
||||
onClick={onChangeIsFavoritePinned}
|
||||
isActive={tableState.favorites === 'true'}
|
||||
onClick={() =>
|
||||
setTableState({
|
||||
favorites:
|
||||
tableState.favorites === 'true'
|
||||
? undefined
|
||||
: 'true',
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
accessor: 'favorite',
|
||||
@ -280,35 +266,40 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
Cell: DateCell,
|
||||
minWidth: 120,
|
||||
},
|
||||
...environments.map((value: ProjectEnvironmentType | string) => {
|
||||
const name =
|
||||
typeof value === 'string'
|
||||
? value
|
||||
: (value as ProjectEnvironmentType).environment;
|
||||
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
||||
const FeatureToggleCell = createFeatureToggleCell(
|
||||
projectId,
|
||||
name,
|
||||
isChangeRequestEnabled,
|
||||
onChange,
|
||||
onFeatureToggle,
|
||||
);
|
||||
|
||||
return {
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environment:${name}`,
|
||||
accessor: (row: ListItemType) =>
|
||||
row.environments[name]?.enabled,
|
||||
align: 'center',
|
||||
Cell: FeatureToggleCell,
|
||||
sortType: 'boolean',
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
};
|
||||
}),
|
||||
...environments.map(
|
||||
(projectEnvironment: ProjectEnvironmentType | string) => {
|
||||
const name =
|
||||
typeof projectEnvironment === 'string'
|
||||
? projectEnvironment
|
||||
: (projectEnvironment as ProjectEnvironmentType)
|
||||
.environment;
|
||||
const isChangeRequestEnabled =
|
||||
isChangeRequestConfigured(name);
|
||||
const FeatureToggleCell = createFeatureToggleCell(
|
||||
projectId,
|
||||
name,
|
||||
isChangeRequestEnabled,
|
||||
onChange,
|
||||
onFeatureToggle,
|
||||
);
|
||||
|
||||
return {
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environment:${name}`,
|
||||
accessor: (row: ListItemType) => {
|
||||
return row.environments?.[name]?.enabled;
|
||||
},
|
||||
align: 'center',
|
||||
Cell: FeatureToggleCell,
|
||||
sortType: 'boolean',
|
||||
sortDescFirst: true,
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
};
|
||||
},
|
||||
),
|
||||
{
|
||||
id: 'Actions',
|
||||
maxWidth: 56,
|
||||
@ -332,7 +323,7 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
[projectId, environments, loading],
|
||||
[projectId, environments, loading, tableState.favorites, onChange],
|
||||
);
|
||||
|
||||
const [showTitle, setShowTitle] = useState(true);
|
||||
@ -345,10 +336,10 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
environments.map((env) => {
|
||||
const thisEnv = feature?.environments.find(
|
||||
(featureEnvironment) =>
|
||||
featureEnvironment?.name === env,
|
||||
featureEnvironment?.name === env.environment,
|
||||
);
|
||||
return [
|
||||
env,
|
||||
typeof env === 'string' ? env : env.environment,
|
||||
{
|
||||
name: env,
|
||||
enabled: thisEnv?.enabled || false,
|
||||
@ -374,89 +365,92 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
|
||||
const { getSearchText, getSearchContext } = useSearch(
|
||||
columns,
|
||||
searchValue,
|
||||
tableState.search || '',
|
||||
featuresData,
|
||||
);
|
||||
|
||||
const initialPageSize = tableState.pageSize
|
||||
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||
: DEFAULT_PAGE_LIMIT;
|
||||
|
||||
const allColumnIds = columns
|
||||
.map(
|
||||
(column: any) =>
|
||||
(column?.id as string) ||
|
||||
(typeof column?.accessor === 'string'
|
||||
? (column?.accessor as string)
|
||||
: ''),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
sortBy: [
|
||||
{
|
||||
id: tableState.sortBy || 'createdAt',
|
||||
desc: tableState.sortOrder === 'desc',
|
||||
},
|
||||
],
|
||||
...(tableState.columns
|
||||
? {
|
||||
hiddenColumns: allColumnIds.filter(
|
||||
(id) =>
|
||||
!(tableState.columns?.split(',') || [])?.includes(
|
||||
id,
|
||||
) && !staticColumns.includes(id),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
pageSize: initialPageSize,
|
||||
pageIndex: tableState.page ? parseInt(tableState.page, 10) - 1 : 0,
|
||||
selectedRowIds: {},
|
||||
}),
|
||||
[initialLoad],
|
||||
);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (initialLoad || loading) {
|
||||
const loadingData = Array(15)
|
||||
const loadingData = Array(
|
||||
parseInt(tableState.pageSize || `${initialPageSize}`, 10),
|
||||
)
|
||||
.fill(null)
|
||||
.map((_, index) => ({
|
||||
id: index, // Assuming `id` is a required property
|
||||
type: '-',
|
||||
name: `Feature name ${index}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
environments: {
|
||||
production: {
|
||||
environments: [
|
||||
{
|
||||
name: 'production',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
// Coerce loading data to FeatureSchema[]
|
||||
return loadingData as unknown as FeatureSchema[];
|
||||
return loadingData as unknown as typeof featuresData;
|
||||
}
|
||||
return featuresData;
|
||||
}, [loading, featuresData]);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => {
|
||||
const allColumnIds = columns
|
||||
.map(
|
||||
(column: any) =>
|
||||
(column?.id as string) ||
|
||||
(typeof column?.accessor === 'string'
|
||||
? (column?.accessor as string)
|
||||
: ''),
|
||||
)
|
||||
.filter(Boolean);
|
||||
let hiddenColumns = environments
|
||||
.filter((_, index) => index >= 3)
|
||||
.map((environment) => `environments.${environment}`);
|
||||
|
||||
if (searchParams.has('columns')) {
|
||||
const columnsInParams =
|
||||
searchParams.get('columns')?.split(',') || [];
|
||||
const visibleColumns = [...staticColumns, ...columnsInParams];
|
||||
hiddenColumns = allColumnIds.filter(
|
||||
(columnId) => !visibleColumns.includes(columnId),
|
||||
);
|
||||
} else if (storedParams.columns) {
|
||||
const visibleColumns = [
|
||||
...staticColumns,
|
||||
...storedParams.columns,
|
||||
];
|
||||
hiddenColumns = allColumnIds.filter(
|
||||
(columnId) => !visibleColumns.includes(columnId),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sortBy: [
|
||||
{
|
||||
id:
|
||||
searchParams.get('sort') ||
|
||||
storedParams.id ||
|
||||
sortingRules.sortBy,
|
||||
desc: searchParams.has('order')
|
||||
? searchParams.get('order') === 'desc'
|
||||
: storedParams.desc,
|
||||
},
|
||||
],
|
||||
hiddenColumns,
|
||||
selectedRowIds: {},
|
||||
};
|
||||
},
|
||||
[environments], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const pageCount = useMemo(
|
||||
() =>
|
||||
tableState.pageSize
|
||||
? Math.ceil((total || 0) / parseInt(tableState.pageSize))
|
||||
: 0,
|
||||
[total, tableState.pageSize],
|
||||
);
|
||||
|
||||
const getRowId = useCallback((row: any) => row.name, []);
|
||||
|
||||
const {
|
||||
allColumns,
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { selectedRowIds, sortBy, hiddenColumns },
|
||||
state: { pageIndex, pageSize, hiddenColumns, selectedRowIds, sortBy },
|
||||
canNextPage,
|
||||
canPreviousPage,
|
||||
previousPage,
|
||||
nextPage,
|
||||
setPageSize,
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
toggleAllRowsSelected,
|
||||
@ -465,74 +459,52 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||
data,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSelectedRows: false,
|
||||
disableSortRemove: true,
|
||||
autoResetSortBy: false,
|
||||
manualSortBy: true,
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
getRowId,
|
||||
},
|
||||
useFlexLayout,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
);
|
||||
|
||||
// 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(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
const sortedByColumn = sortBy[0].id;
|
||||
const sortOrder = sortBy[0].desc ? 'desc' : 'asc';
|
||||
const tableState: Record<string, string> = {};
|
||||
tableState.sort = sortedByColumn;
|
||||
if (sortBy[0].desc) {
|
||||
tableState.order = sortOrder;
|
||||
}
|
||||
if (searchValue) {
|
||||
tableState.search = searchValue;
|
||||
}
|
||||
if (isFavoritesPinned) {
|
||||
tableState.favorites = 'true';
|
||||
}
|
||||
tableState.columns = allColumns
|
||||
.map(({ id }) => id)
|
||||
.filter(
|
||||
(id) =>
|
||||
!staticColumns.includes(id) && !hiddenColumns?.includes(id),
|
||||
)
|
||||
.join(',');
|
||||
|
||||
setSearchParams(tableState, {
|
||||
replace: true,
|
||||
});
|
||||
setStoredParams((params) => ({
|
||||
...params,
|
||||
id: sortBy[0].id,
|
||||
desc: sortBy[0].desc || false,
|
||||
columns: tableState.columns.split(','),
|
||||
}));
|
||||
const favoritesPinned = Boolean(isFavoritesPinned);
|
||||
setGlobalStore((params) => ({
|
||||
...params,
|
||||
favorites: favoritesPinned,
|
||||
}));
|
||||
setSortingRules({
|
||||
sortBy: sortedByColumn,
|
||||
sortOrder,
|
||||
isFavoritesPinned: favoritesPinned,
|
||||
setTableState({
|
||||
page: `${pageIndex + 1}`,
|
||||
pageSize: `${pageSize}`,
|
||||
sortBy: sortBy[0]?.id || 'createdAt',
|
||||
sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
|
||||
});
|
||||
}, [pageIndex, pageSize, sortBy]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
loading,
|
||||
sortBy,
|
||||
hiddenColumns,
|
||||
searchValue,
|
||||
setSearchParams,
|
||||
isFavoritesPinned,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!loading && isCustomColumns) {
|
||||
setTableState(
|
||||
{
|
||||
columns:
|
||||
hiddenColumns !== undefined
|
||||
? allColumnIds
|
||||
.filter(
|
||||
(id) =>
|
||||
!hiddenColumns.includes(id) &&
|
||||
!staticColumns.includes(id),
|
||||
)
|
||||
.join(',')
|
||||
: undefined,
|
||||
},
|
||||
true, // Columns state is controllable by react-table - update only URL and storage, not state
|
||||
);
|
||||
}
|
||||
}, [loading, isCustomColumns, hiddenColumns]);
|
||||
|
||||
const showPaginationBar = Boolean(total && total > DEFAULT_PAGE_LIMIT);
|
||||
const showPaginationBar = Boolean(total && total > pageSize);
|
||||
const paginatedStyles = showPaginationBar
|
||||
? {
|
||||
borderBottomLeftRadius: 0,
|
||||
@ -546,7 +518,7 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
disableLoading
|
||||
disablePadding
|
||||
className={styles.container}
|
||||
style={{ ...style, ...paginatedStyles }}
|
||||
style={{ ...paginatedStyles, ...style }}
|
||||
header={
|
||||
<Box
|
||||
ref={headerLoadingRef}
|
||||
@ -575,8 +547,12 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
data-loading
|
||||
placeholder='Search and Filter'
|
||||
expandable
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
initialValue={tableState.search}
|
||||
onChange={(value) => {
|
||||
setTableState({
|
||||
search: value,
|
||||
});
|
||||
}}
|
||||
onFocus={() =>
|
||||
setShowTitle(false)
|
||||
}
|
||||
@ -596,10 +572,11 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
staticColumns={staticColumns}
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={Boolean(
|
||||
storedParams.columns,
|
||||
)}
|
||||
isCustomized={isCustomColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
onCustomize={() =>
|
||||
setIsCustomColumns(true)
|
||||
}
|
||||
/>
|
||||
<PageHeader.Divider
|
||||
sx={{ marginLeft: 0 }}
|
||||
@ -652,8 +629,12 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
condition={isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
initialValue={tableState.search}
|
||||
onChange={(value) => {
|
||||
setTableState({
|
||||
search: value,
|
||||
});
|
||||
}}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
id='projectFeatureToggles'
|
||||
@ -669,7 +650,9 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
aria-busy={loading}
|
||||
aria-live='polite'
|
||||
>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<SearchHighlightProvider
|
||||
value={getSearchText(tableState.search || '')}
|
||||
>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
@ -677,31 +660,35 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
|
||||
<Box sx={{ padding: theme.spacing(3) }}>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchValue?.length > 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
(tableState.search || '')?.length > 0
|
||||
}
|
||||
show={
|
||||
<Box sx={{ padding: theme.spacing(3) }}>
|
||||
<TablePlaceholder>
|
||||
No feature toggles found matching
|
||||
“
|
||||
{searchValue}
|
||||
{tableState.search}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
</Box>
|
||||
}
|
||||
elseShow={
|
||||
<Box sx={{ padding: theme.spacing(3) }}>
|
||||
<TablePlaceholder>
|
||||
No feature toggles available. Get
|
||||
started by adding a new feature
|
||||
toggle.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
@ -731,7 +718,9 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
showExportDialog={showExportDialog}
|
||||
data={data}
|
||||
onClose={() => setShowExportDialog(false)}
|
||||
environments={environments}
|
||||
environments={environments.map(
|
||||
({ environment }) => environment,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -740,7 +729,18 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
</PageContent>
|
||||
<ConditionallyRender
|
||||
condition={showPaginationBar}
|
||||
show={paginationBar}
|
||||
show={
|
||||
<StickyPaginationBar
|
||||
total={total || 0}
|
||||
hasNextPage={canNextPage}
|
||||
hasPreviousPage={canPreviousPage}
|
||||
fetchNextPage={nextPage}
|
||||
fetchPrevPage={previousPage}
|
||||
currentOffset={pageIndex * pageSize}
|
||||
pageLimit={pageSize}
|
||||
setPageLimit={setPageSize}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<BatchSelectionActionsBar
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
|
@ -387,7 +387,7 @@ export const ProjectFeatureToggles = ({
|
||||
.filter(Boolean);
|
||||
let hiddenColumns = environments
|
||||
.filter((_, index) => index >= 3)
|
||||
.map((environment) => `environments.${environment}`);
|
||||
.map((environment) => `environment:${environment}`);
|
||||
|
||||
if (searchParams.has('columns')) {
|
||||
const columnsInParams =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import useProject, {
|
||||
useProjectNameOrId,
|
||||
} from 'hooks/api/getters/useProject/useProject';
|
||||
@ -9,17 +9,17 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||
import { ProjectStats } from './ProjectStats/ProjectStats';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import {
|
||||
ISortingRules,
|
||||
DEFAULT_PAGE_LIMIT,
|
||||
useFeatureSearch,
|
||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import {
|
||||
ProjectTableState,
|
||||
PaginatedProjectFeatureToggles,
|
||||
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
||||
import { SortingRule } from 'react-table';
|
||||
import { useTableState } from 'hooks/useTableState';
|
||||
|
||||
const refreshInterval = 15 * 1000;
|
||||
|
||||
@ -42,26 +42,24 @@ const StyledContentContainer = styled(Box)(() => ({
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
export const DEFAULT_PAGE_LIMIT = 25;
|
||||
|
||||
const PaginatedProjectOverview = () => {
|
||||
const PaginatedProjectOverview: FC<{
|
||||
fullWidth?: boolean;
|
||||
storageKey?: string;
|
||||
}> = ({ fullWidth, storageKey = 'project-overview' }) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { project, loading: projectLoading } = useProject(projectId, {
|
||||
refreshInterval,
|
||||
});
|
||||
const [pageLimit, setPageLimit] = useState(DEFAULT_PAGE_LIMIT);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
searchParams.get('search') || '',
|
||||
const [tableState, setTableState] = useTableState<ProjectTableState>(
|
||||
{},
|
||||
`${storageKey}-${projectId}`,
|
||||
);
|
||||
|
||||
const [sortingRules, setSortingRules] = useState<ISortingRules>({
|
||||
sortBy: 'createdBy',
|
||||
sortOrder: 'desc',
|
||||
isFavoritesPinned: false,
|
||||
});
|
||||
const page = parseInt(tableState.page || '1', 10);
|
||||
const pageSize = tableState?.pageSize
|
||||
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||
: DEFAULT_PAGE_LIMIT;
|
||||
|
||||
const {
|
||||
features: searchFeatures,
|
||||
@ -70,11 +68,15 @@ const PaginatedProjectOverview = () => {
|
||||
loading,
|
||||
initialLoad,
|
||||
} = useFeatureSearch(
|
||||
currentOffset,
|
||||
pageLimit,
|
||||
sortingRules,
|
||||
(page - 1) * pageSize,
|
||||
pageSize,
|
||||
{
|
||||
sortBy: tableState.sortBy || 'createdAt',
|
||||
sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
|
||||
favoritesFirst: tableState.favorites === 'true',
|
||||
},
|
||||
projectId,
|
||||
searchValue,
|
||||
tableState.search,
|
||||
{
|
||||
refreshInterval,
|
||||
},
|
||||
@ -82,20 +84,9 @@ const PaginatedProjectOverview = () => {
|
||||
|
||||
const { members, features, health, description, environments, stats } =
|
||||
project;
|
||||
const fetchNextPage = () => {
|
||||
if (!loading) {
|
||||
setCurrentOffset(Math.min(total, currentOffset + pageLimit));
|
||||
}
|
||||
};
|
||||
const fetchPrevPage = () => {
|
||||
setCurrentOffset(Math.max(0, currentOffset - pageLimit));
|
||||
};
|
||||
|
||||
const hasPreviousPage = currentOffset > 0;
|
||||
const hasNextPage = currentOffset + pageLimit < total;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer key={projectId}>
|
||||
<ProjectInfo
|
||||
id={projectId}
|
||||
description={description}
|
||||
@ -108,35 +99,17 @@ const PaginatedProjectOverview = () => {
|
||||
<ProjectStats stats={project.stats} />
|
||||
<StyledProjectToggles>
|
||||
<PaginatedProjectFeatureToggles
|
||||
key={
|
||||
loading && searchFeatures.length === 0
|
||||
? 'loading'
|
||||
: 'ready'
|
||||
style={
|
||||
fullWidth ? { width: '100%', margin: 0 } : undefined
|
||||
}
|
||||
features={searchFeatures}
|
||||
features={searchFeatures || []}
|
||||
environments={environments}
|
||||
initialLoad={initialLoad && searchFeatures.length === 0}
|
||||
loading={loading && searchFeatures.length === 0}
|
||||
onChange={refetch}
|
||||
total={total}
|
||||
searchValue={searchValue}
|
||||
setSearchValue={setSearchValue}
|
||||
sortingRules={sortingRules}
|
||||
setSortingRules={setSortingRules}
|
||||
paginationBar={
|
||||
<StickyPaginationBar>
|
||||
<PaginationBar
|
||||
total={total}
|
||||
hasNextPage={hasNextPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
fetchPrevPage={fetchPrevPage}
|
||||
currentOffset={currentOffset}
|
||||
pageLimit={pageLimit}
|
||||
setPageLimit={setPageLimit}
|
||||
/>
|
||||
</StickyPaginationBar>
|
||||
}
|
||||
tableState={tableState}
|
||||
setTableState={setTableState}
|
||||
/>
|
||||
</StyledProjectToggles>
|
||||
</StyledContentContainer>
|
||||
@ -144,37 +117,6 @@ const PaginatedProjectOverview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const StyledStickyBar = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
marginLeft: theme.spacing(2),
|
||||
zIndex: 9999,
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
||||
height: '52px',
|
||||
}));
|
||||
|
||||
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
const StickyPaginationBar: React.FC = ({ children }) => {
|
||||
return (
|
||||
<StyledStickyBar>
|
||||
<StyledStickyBarContentContainer>
|
||||
{children}
|
||||
</StyledStickyBarContentContainer>
|
||||
</StyledStickyBar>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
||||
*/
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
||||
import { ComponentProps, FC } from 'react';
|
||||
|
||||
const StyledStickyBar = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
marginLeft: theme.spacing(2),
|
||||
zIndex: theme.zIndex.fab,
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
||||
height: '52px',
|
||||
}));
|
||||
|
||||
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
export const StickyPaginationBar: FC<ComponentProps<typeof PaginationBar>> = ({
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<StyledStickyBar>
|
||||
<StyledStickyBarContentContainer>
|
||||
<PaginationBar {...props} />
|
||||
</StyledStickyBarContentContainer>
|
||||
</StyledStickyBar>
|
||||
);
|
||||
};
|
@ -4,7 +4,12 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { translateToQueryParams } from './searchToQueryParams';
|
||||
import { ISortingRules } from 'component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||
|
||||
type ISortingRules = {
|
||||
sortBy: string;
|
||||
sortOrder: string;
|
||||
favoritesFirst: boolean;
|
||||
};
|
||||
|
||||
type IFeatureSearchResponse = {
|
||||
features: IFeatureToggleListItem[];
|
||||
@ -109,6 +114,8 @@ const createFeatureSearch = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_PAGE_LIMIT = 25;
|
||||
|
||||
export const useFeatureSearch = createFeatureSearch();
|
||||
|
||||
const getFeatureSearchFetcher = (
|
||||
@ -137,7 +144,7 @@ const getFeatureSearchFetcher = (
|
||||
};
|
||||
|
||||
const translateToSortQueryParams = (sortingRules: ISortingRules) => {
|
||||
const { sortBy, sortOrder, isFavoritesPinned } = sortingRules;
|
||||
const sortQueryParams = `sortBy=${sortBy}&sortOrder=${sortOrder}&favoritesFirst=${isFavoritesPinned}`;
|
||||
const { sortBy, sortOrder, favoritesFirst } = sortingRules;
|
||||
const sortQueryParams = `sortBy=${sortBy}&sortOrder=${sortOrder}&favoritesFirst=${favoritesFirst}`;
|
||||
return sortQueryParams;
|
||||
};
|
||||
|
@ -231,7 +231,7 @@ describe('useTableState', () => {
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTableState<{
|
||||
[key: string]: string | string[];
|
||||
[key: string]: string;
|
||||
}>({}, 'test', ['saveOnlyThisToUrl'], ['page']),
|
||||
);
|
||||
const setParams = result.current[1];
|
||||
@ -245,7 +245,7 @@ describe('useTableState', () => {
|
||||
sortBy: 'type',
|
||||
sortOrder: 'favorites',
|
||||
favorites: 'false',
|
||||
columns: ['test', 'id'],
|
||||
columns: 'test,id',
|
||||
});
|
||||
});
|
||||
|
||||
@ -253,39 +253,16 @@ describe('useTableState', () => {
|
||||
expect(storageSetter).toHaveBeenCalledWith({ page: '2' });
|
||||
});
|
||||
|
||||
it('can reset state to the default instead of overwriting', () => {
|
||||
mockStorage.mockReturnValue({
|
||||
value: { pageSize: 25 },
|
||||
setValue: vi.fn(),
|
||||
});
|
||||
mockQuery.mockReturnValue([new URLSearchParams('page=4'), vi.fn()]);
|
||||
|
||||
it('can update query and storage without triggering a rerender', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTableState<{
|
||||
page: string;
|
||||
pageSize?: string;
|
||||
sortBy?: string;
|
||||
}>({ page: '1', pageSize: '10' }, 'test'),
|
||||
useTableState({ page: '1' }, 'test', [], []),
|
||||
);
|
||||
|
||||
const setParams = result.current[1];
|
||||
|
||||
act(() => {
|
||||
setParams({ sortBy: 'type' });
|
||||
});
|
||||
expect(result.current[0]).toEqual({
|
||||
page: '4',
|
||||
pageSize: '10',
|
||||
sortBy: 'type',
|
||||
setParams({ page: '2' }, true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
setParams({ pageSize: '50' }, true);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual({
|
||||
page: '1',
|
||||
pageSize: '50',
|
||||
});
|
||||
expect(result.current[0]).toEqual({ page: '1' });
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { createLocalStorage } from '../utils/createLocalStorage';
|
||||
|
||||
@ -35,7 +35,7 @@ const defaultQueryKeys = [...defaultStoredKeys, 'page'];
|
||||
* @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 | string[]>>(
|
||||
export const useTableState = <Params extends Record<string, string>>(
|
||||
defaultParams: Params,
|
||||
storageId: string,
|
||||
queryKeys?: Array<keyof Params>,
|
||||
@ -52,31 +52,34 @@ export const useTableState = <Params extends Record<string, string | string[]>>(
|
||||
...searchQuery,
|
||||
} as Params);
|
||||
|
||||
const updateParams = (value: Partial<Params>, reset = false) => {
|
||||
const newState: Params = reset
|
||||
? { ...defaultParams, ...value }
|
||||
: {
|
||||
...params,
|
||||
...value,
|
||||
};
|
||||
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];
|
||||
// remove keys with undefined values
|
||||
Object.keys(newState).forEach((key) => {
|
||||
if (newState[key] === undefined) {
|
||||
delete newState[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (!quiet) {
|
||||
setParams(newState);
|
||||
}
|
||||
});
|
||||
setSearchParams(
|
||||
filterObjectKeys(newState, queryKeys || defaultQueryKeys),
|
||||
);
|
||||
setStoredParams(
|
||||
filterObjectKeys(newState, storageKeys || defaultStoredKeys),
|
||||
);
|
||||
|
||||
setParams(newState);
|
||||
setSearchParams(
|
||||
filterObjectKeys(newState, queryKeys || defaultQueryKeys),
|
||||
);
|
||||
setStoredParams(
|
||||
filterObjectKeys(newState, storageKeys || defaultStoredKeys),
|
||||
);
|
||||
|
||||
return params;
|
||||
};
|
||||
return params;
|
||||
},
|
||||
[setParams, setSearchParams, setStoredParams],
|
||||
);
|
||||
|
||||
return [params, updateParams] as const;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user