mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: filter persisted in url (#5549)
This commit is contained in:
parent
f348acb3b9
commit
2dcf4af7b1
@ -17,7 +17,7 @@
|
|||||||
"start:demo": "UNLEASH_BASE_PATH=/demo/ UNLEASH_API=https://app.unleash-hosted.com/ yarn run start",
|
"start:demo": "UNLEASH_BASE_PATH=/demo/ UNLEASH_API=https://app.unleash-hosted.com/ yarn run start",
|
||||||
"test": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest run",
|
"test": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest run",
|
||||||
"test:snapshot": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn test -u",
|
"test:snapshot": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn test -u",
|
||||||
"test:watch": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest watch usePersistentTable",
|
"test:watch": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" vitest watch",
|
||||||
"lint": "biome lint src --apply",
|
"lint": "biome lint src --apply",
|
||||||
"lint:check": "biome check src",
|
"lint:check": "biome check src",
|
||||||
"fmt": "biome format src --write",
|
"fmt": "biome format src --write",
|
||||||
|
@ -13,24 +13,30 @@ import { FilterItemChip } from './FilterItemChip/FilterItemChip';
|
|||||||
interface IFilterItemProps {
|
interface IFilterItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
options: Array<{ label: string; value: string }>;
|
options: Array<{ label: string; value: string }>;
|
||||||
onChange?: (value: string) => void;
|
onChange: (value: FilterItem) => void;
|
||||||
|
state: FilterItem | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const singularOperators = ['IS', 'IS_NOT'];
|
const singularOperators = ['IS', 'IS_NOT'];
|
||||||
const pluralOperators = ['IS_ANY_OF', 'IS_NOT_ANY_OF'];
|
const pluralOperators = ['IS_ANY_OF', 'IS_NOT_ANY_OF'];
|
||||||
|
|
||||||
|
export type FilterItem = {
|
||||||
|
operator: string;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export const FilterItem: FC<IFilterItemProps> = ({
|
export const FilterItem: FC<IFilterItemProps> = ({
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
|
state,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [selectedOptions, setSelectedOptions] = useState<typeof options>([]);
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
const currentOperators =
|
const currentOperators =
|
||||||
selectedOptions?.length > 1 ? pluralOperators : singularOperators;
|
state && state.values.length > 1 ? pluralOperators : singularOperators;
|
||||||
const [operator, setOperator] = useState(currentOperators[0]);
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
setAnchorEl(ref.current);
|
setAnchorEl(ref.current);
|
||||||
@ -40,72 +46,58 @@ export const FilterItem: FC<IFilterItemProps> = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnChange = (
|
const selectedOptions = state ? state.values : [];
|
||||||
op: typeof operator,
|
const currentOperator = state ? state.operator : currentOperators[0];
|
||||||
values: typeof selectedOptions,
|
|
||||||
) => {
|
|
||||||
const value = values.length
|
|
||||||
? `${op}:${values?.map((option) => option.value).join(', ')}`
|
|
||||||
: '';
|
|
||||||
onChange?.(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOperatorChange = (value: string) => {
|
|
||||||
setOperator(value);
|
|
||||||
handleOnChange(value, selectedOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOptionsChange = (values: typeof selectedOptions) => {
|
|
||||||
setSelectedOptions(values);
|
|
||||||
handleOnChange(operator, values);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
handleOptionsChange([]);
|
onChange({ operator: 'IS', values: [] });
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = (value: string) => () => {
|
const handleToggle = (value: string) => () => {
|
||||||
if (
|
if (
|
||||||
selectedOptions?.some(
|
selectedOptions?.some((selectedOption) => selectedOption === value)
|
||||||
(selectedOption) => selectedOption.value === value,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
const newOptions = selectedOptions?.filter(
|
const newOptions = selectedOptions?.filter(
|
||||||
(selectedOption) => selectedOption.value !== value,
|
(selectedOption) => selectedOption !== value,
|
||||||
);
|
);
|
||||||
handleOptionsChange(newOptions);
|
onChange({ operator: currentOperator, values: newOptions });
|
||||||
} else {
|
} else {
|
||||||
const newOptions = [
|
const newOptions = [
|
||||||
...(selectedOptions ?? []),
|
...(selectedOptions ?? []),
|
||||||
options.find((option) => option.value === value) ?? {
|
(
|
||||||
label: '',
|
options.find((option) => option.value === value) ?? {
|
||||||
value: '',
|
label: '',
|
||||||
},
|
value: '',
|
||||||
|
}
|
||||||
|
).value,
|
||||||
];
|
];
|
||||||
handleOptionsChange(newOptions);
|
onChange({ operator: currentOperator, values: newOptions });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentOperators.includes(operator)) {
|
if (state && !currentOperators.includes(state.operator)) {
|
||||||
setOperator(currentOperators[0]);
|
onChange({
|
||||||
|
operator: currentOperators[0],
|
||||||
|
values: state.values,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentOperators, operator]);
|
}, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box ref={ref}>
|
<Box ref={ref}>
|
||||||
<FilterItemChip
|
<FilterItemChip
|
||||||
label={label}
|
label={label}
|
||||||
selectedOptions={selectedOptions?.map(
|
selectedOptions={selectedOptions}
|
||||||
(option) => option?.label,
|
|
||||||
)}
|
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
operator={operator}
|
operator={currentOperator}
|
||||||
operatorOptions={currentOperators}
|
operatorOptions={currentOperators}
|
||||||
onChangeOperator={handleOperatorChange}
|
onChangeOperator={(operator) => {
|
||||||
|
onChange({ operator, values: selectedOptions ?? [] });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledPopover
|
<StyledPopover
|
||||||
@ -158,7 +150,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
|
|||||||
checked={
|
checked={
|
||||||
selectedOptions?.some(
|
selectedOptions?.some(
|
||||||
(selectedOption) =>
|
(selectedOption) =>
|
||||||
selectedOption.value ===
|
selectedOption ===
|
||||||
option.value,
|
option.value,
|
||||||
) ?? false
|
) ?? false
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
export type FeatureTogglesListFilters = {
|
export type FeatureTogglesListFilters = {
|
||||||
projectId?: string;
|
project: FilterItem | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IFeatureToggleFiltersProps {
|
interface IFeatureToggleFiltersProps {
|
||||||
@ -30,8 +30,9 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
|||||||
show={() => (
|
show={() => (
|
||||||
<FilterItem
|
<FilterItem
|
||||||
label='Project'
|
label='Project'
|
||||||
|
state={state.project}
|
||||||
options={projectsOptions}
|
options={projectsOptions}
|
||||||
onChange={(value) => onChange({ projectId: value })}
|
onChange={(value) => onChange({ project: value })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -8,13 +8,12 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { useReactTable, createColumnHelper } from '@tanstack/react-table';
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
|
import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
@ -22,7 +21,6 @@ import { FeatureSchema } from 'openapi';
|
|||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
@ -33,17 +31,22 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|||||||
import { focusable } from 'themes/themeStyles';
|
import { focusable } from 'themes/themeStyles';
|
||||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import {
|
import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters';
|
||||||
FeatureToggleFilters,
|
|
||||||
FeatureTogglesListFilters,
|
|
||||||
} from './FeatureToggleFilters/FeatureToggleFilters';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_PAGE_LIMIT,
|
DEFAULT_PAGE_LIMIT,
|
||||||
useFeatureSearch,
|
useFeatureSearch,
|
||||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
import mapValues from 'lodash.mapvalues';
|
import mapValues from 'lodash.mapvalues';
|
||||||
import { NumberParam, StringParam, withDefault } from 'use-query-params';
|
import {
|
||||||
import { BooleansStringParam } from 'utils/serializeQueryParams';
|
BooleansStringParam,
|
||||||
|
FilterItemParam,
|
||||||
|
} from 'utils/serializeQueryParams';
|
||||||
|
import {
|
||||||
|
encodeQueryParams,
|
||||||
|
NumberParam,
|
||||||
|
StringParam,
|
||||||
|
withDefault,
|
||||||
|
} from 'use-query-params';
|
||||||
import { withTableState } from 'utils/withTableState';
|
import { withTableState } from 'utils/withTableState';
|
||||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||||
|
|
||||||
@ -70,16 +73,18 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
offset: withDefault(NumberParam, 0),
|
||||||
|
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
|
||||||
|
query: StringParam,
|
||||||
|
favoritesFirst: withDefault(BooleansStringParam, true),
|
||||||
|
sortBy: withDefault(StringParam, 'createdAt'),
|
||||||
|
sortOrder: withDefault(StringParam, 'desc'),
|
||||||
|
project: FilterItemParam,
|
||||||
|
};
|
||||||
const [tableState, setTableState] = usePersistentTableState(
|
const [tableState, setTableState] = usePersistentTableState(
|
||||||
'features-list-table',
|
'features-list-table',
|
||||||
{
|
config,
|
||||||
offset: withDefault(NumberParam, 0),
|
|
||||||
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
|
|
||||||
query: StringParam,
|
|
||||||
favoritesFirst: withDefault(BooleansStringParam, true),
|
|
||||||
sortBy: withDefault(StringParam, 'createdAt'),
|
|
||||||
sortOrder: withDefault(StringParam, 'desc'),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -89,7 +94,9 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
refetch: refetchFeatures,
|
refetch: refetchFeatures,
|
||||||
initialLoad,
|
initialLoad,
|
||||||
} = useFeatureSearch(
|
} = useFeatureSearch(
|
||||||
mapValues(tableState, (value) => (value ? `${value}` : undefined)),
|
mapValues(encodeQueryParams(config, tableState), (value) =>
|
||||||
|
value ? `${value}` : undefined,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||||
const onFavorite = useCallback(
|
const onFavorite = useCallback(
|
||||||
@ -308,7 +315,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* <FeatureToggleFilters state={tableState} onChange={setTableState} /> */}
|
<FeatureToggleFilters onChange={setTableState} state={tableState} />
|
||||||
<SearchHighlightProvider value={tableState.query || ''}>
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
<PaginatedTable tableInstance={table} totalItems={total} />
|
<PaginatedTable tableInstance={table} totalItems={total} />
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
|
@ -5,6 +5,7 @@ import { usePersistentTableState } from './usePersistentTableState';
|
|||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { createLocalStorage } from '../utils/createLocalStorage';
|
import { createLocalStorage } from '../utils/createLocalStorage';
|
||||||
import { NumberParam, StringParam } from 'use-query-params';
|
import { NumberParam, StringParam } from 'use-query-params';
|
||||||
|
import { FilterItemParam } from '../utils/serializeQueryParams';
|
||||||
|
|
||||||
type TestComponentProps = {
|
type TestComponentProps = {
|
||||||
keyName: string;
|
keyName: string;
|
||||||
@ -80,6 +81,31 @@ describe('usePersistentTableState', () => {
|
|||||||
expect(window.location.href).toContain('my-url?query=initialStorage');
|
expect(window.location.href).toContain('my-url?query=initialStorage');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('initializes correctly from localStorage with complex decoder', async () => {
|
||||||
|
createLocalStorage('testKey', {}).setValue({
|
||||||
|
query: 'initialStorage',
|
||||||
|
filterItem: { operator: 'IS', values: ['default'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestComponent
|
||||||
|
keyName='testKey'
|
||||||
|
queryParamsDefinition={{
|
||||||
|
query: StringParam,
|
||||||
|
filterItem: FilterItemParam,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{ route: '/my-url' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('state-value').textContent).toBe(
|
||||||
|
'initialStorage',
|
||||||
|
);
|
||||||
|
expect(window.location.href).toContain(
|
||||||
|
'my-url?query=initialStorage&filterItem=IS%3Adefault',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('initializes correctly from localStorage and URL', async () => {
|
it('initializes correctly from localStorage and URL', async () => {
|
||||||
createLocalStorage('testKey', {}).setValue({ query: 'initialStorage' });
|
createLocalStorage('testKey', {}).setValue({ query: 'initialStorage' });
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { useQueryParams } from 'use-query-params';
|
import { useQueryParams, encodeQueryParams } from 'use-query-params';
|
||||||
|
import { QueryParamConfigMap } from 'serialize-query-params/src/types';
|
||||||
|
|
||||||
const usePersistentSearchParams = (key: string) => {
|
const usePersistentSearchParams = <T extends QueryParamConfigMap>(
|
||||||
|
key: string,
|
||||||
|
queryParamsDefinition: T,
|
||||||
|
) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { value, setValue } = createLocalStorage(key, {});
|
const { value, setValue } = createLocalStorage(key, {});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -15,19 +19,26 @@ const usePersistentSearchParams = (key: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearchParams(value, { replace: true });
|
setSearchParams(
|
||||||
|
encodeQueryParams(queryParamsDefinition, value) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>,
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return setValue;
|
return setValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePersistentTableState = <
|
export const usePersistentTableState = <T extends QueryParamConfigMap>(
|
||||||
T extends Parameters<typeof useQueryParams>[0],
|
|
||||||
>(
|
|
||||||
key: string,
|
key: string,
|
||||||
queryParamsDefinition: T,
|
queryParamsDefinition: T,
|
||||||
) => {
|
) => {
|
||||||
const updateStoredParams = usePersistentSearchParams(key);
|
const updateStoredParams = usePersistentSearchParams(
|
||||||
|
key,
|
||||||
|
queryParamsDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
const [tableState, setTableState] = useQueryParams(queryParamsDefinition);
|
const [tableState, setTableState] = useQueryParams(queryParamsDefinition);
|
||||||
|
|
||||||
|
@ -28,3 +28,37 @@ export const BooleansStringParam = {
|
|||||||
encode: encodeBoolean,
|
encode: encodeBoolean,
|
||||||
decode: decodeBoolean,
|
decode: decodeBoolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FilterItem = {
|
||||||
|
operator: string;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodeFilterItem = (
|
||||||
|
filterItem: FilterItem | null | undefined,
|
||||||
|
): string | undefined => {
|
||||||
|
return filterItem && filterItem.values.length
|
||||||
|
? `${filterItem.operator}:${filterItem.values.join(',')}`
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeFilterItem = (
|
||||||
|
input: string | (string | null)[] | null | undefined,
|
||||||
|
): FilterItem | null | undefined => {
|
||||||
|
if (typeof input !== 'string' || !input) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [operator, values = ''] = input.split(':');
|
||||||
|
if (!operator) return undefined;
|
||||||
|
|
||||||
|
const splitValues = values.split(',');
|
||||||
|
return splitValues.length > 0
|
||||||
|
? { operator, values: splitValues }
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterItemParam = {
|
||||||
|
encode: encodeFilterItem,
|
||||||
|
decode: decodeFilterItem,
|
||||||
|
};
|
||||||
|
@ -39,7 +39,7 @@ process.nextTick(async () => {
|
|||||||
responseTimeWithAppNameKillSwitch: false,
|
responseTimeWithAppNameKillSwitch: false,
|
||||||
privateProjects: true,
|
privateProjects: true,
|
||||||
featureSearchAPI: true,
|
featureSearchAPI: true,
|
||||||
featureSearchFrontend: false,
|
featureSearchFrontend: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user