mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-01 01:18:10 +02:00
feat: favorite feature table icons (#2525)
This commit is contained in:
parent
dacaaa51b7
commit
5f88269744
@ -0,0 +1,40 @@
|
||||
import { VFC } from 'react';
|
||||
import { Box, IconButton, styled } from '@mui/material';
|
||||
import {
|
||||
Star as StarIcon,
|
||||
StarBorder as StarBorderIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IFavoriteIconCellProps {
|
||||
value?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const InactiveIconButton = styled(IconButton)(({ theme }) => ({
|
||||
color: 'transparent',
|
||||
'&:hover, &:focus': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
export const FavoriteIconCell: VFC<IFavoriteIconCellProps> = ({
|
||||
value = false,
|
||||
onClick,
|
||||
}) => (
|
||||
<Box sx={{ pl: 1.25 }}>
|
||||
<ConditionallyRender
|
||||
condition={value}
|
||||
show={
|
||||
<IconButton onClick={onClick} color="primary" size="small">
|
||||
<StarIcon fontSize="small" />
|
||||
</IconButton>
|
||||
}
|
||||
elseShow={
|
||||
<InactiveIconButton onClick={onClick} size="small">
|
||||
<StarBorderIcon fontSize="small" />
|
||||
</InactiveIconButton>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
@ -0,0 +1,45 @@
|
||||
import { VFC } from 'react';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import {
|
||||
Star as StarIcon,
|
||||
StarBorder as StarBorderIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IFavoriteIconHeaderProps {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const FavoriteIconHeader: VFC<IFavoriteIconHeaderProps> = ({
|
||||
isActive = false,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
isActive
|
||||
? 'Unpin favorite features from the top'
|
||||
: 'Pin favorite features to the top'
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
mx: -0.75,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={onClick}
|
||||
size="small"
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={isActive}
|
||||
show={<StarIcon fontSize="small" />}
|
||||
elseShow={<StarBorderIcon fontSize="small" />}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -13,7 +13,6 @@ import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/Fe
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||
@ -21,6 +20,11 @@ import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FavoriteIconCell } from './FavoriteIconCell/FavoriteIconCell';
|
||||
import { FavoriteIconHeader } from './FavoriteIconHeader/FavoriteIconHeader';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||
name: 'Name of the feature',
|
||||
@ -31,81 +35,14 @@ export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||
});
|
||||
|
||||
export type PageQueryType = Partial<
|
||||
Record<'sort' | 'order' | 'search', string>
|
||||
Record<'sort' | 'order' | 'search' | 'favorites', string>
|
||||
>;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: FeatureSeenCell,
|
||||
sortType: 'date',
|
||||
align: 'center',
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
Cell: FeatureTypeCell,
|
||||
align: 'center',
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
minWidth: 150,
|
||||
Cell: FeatureNameCell,
|
||||
sortType: 'alphanumeric',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: FeatureSchema) =>
|
||||
row.tags?.map(({ type, value }) => `${type}:${value}`).join('\n') ||
|
||||
'',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Project ID',
|
||||
accessor: 'project',
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<LinkCell title={value} to={`/projects/${value}`} />
|
||||
),
|
||||
sortType: 'alphanumeric',
|
||||
maxWidth: 150,
|
||||
filterName: 'project',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'State',
|
||||
accessor: 'stale',
|
||||
Cell: FeatureStaleCell,
|
||||
sortType: 'boolean',
|
||||
maxWidth: 120,
|
||||
filterName: 'state',
|
||||
filterParsing: (value: any) => (value ? 'stale' : 'active'),
|
||||
},
|
||||
// Always hidden -- for search
|
||||
{
|
||||
accessor: 'description',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'createdAt' };
|
||||
|
||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||
'FeatureToggleListTable:v1',
|
||||
defaultSort
|
||||
{ ...defaultSort, favorites: false }
|
||||
);
|
||||
|
||||
export const FeatureToggleListTable: VFC = () => {
|
||||
@ -126,7 +63,117 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
hiddenColumns: ['description'],
|
||||
globalFilter: searchParams.get('search') || '',
|
||||
}));
|
||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
||||
usePinnedFavorites(
|
||||
searchParams.has('favorites')
|
||||
? searchParams.get('favorites') === 'true'
|
||||
: storedParams.favorites
|
||||
);
|
||||
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
...(uiConfig?.flags?.favorites
|
||||
? [
|
||||
{
|
||||
Header: (
|
||||
<FavoriteIconHeader
|
||||
isActive={isFavoritesPinned}
|
||||
onClick={onChangeIsFavoritePinned}
|
||||
/>
|
||||
),
|
||||
accessor: 'favorite',
|
||||
Cell: ({ row: { original: feature } }: any) => (
|
||||
<FavoriteIconCell
|
||||
value={feature?.favorite}
|
||||
onClick={() =>
|
||||
feature?.favorite
|
||||
? unfavorite(
|
||||
feature.project,
|
||||
feature.name
|
||||
)
|
||||
: favorite(
|
||||
feature.project,
|
||||
feature.name
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: FeatureSeenCell,
|
||||
sortType: 'date',
|
||||
align: 'center',
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
Cell: FeatureTypeCell,
|
||||
align: 'center',
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
minWidth: 150,
|
||||
Cell: FeatureNameCell,
|
||||
sortType: 'alphanumeric',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: FeatureSchema) =>
|
||||
row.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Project ID',
|
||||
accessor: 'project',
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<LinkCell title={value} to={`/projects/${value}`} />
|
||||
),
|
||||
sortType: 'alphanumeric',
|
||||
maxWidth: 150,
|
||||
filterName: 'project',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'State',
|
||||
accessor: 'stale',
|
||||
Cell: FeatureStaleCell,
|
||||
sortType: 'boolean',
|
||||
maxWidth: 120,
|
||||
filterName: 'state',
|
||||
filterParsing: (value: any) => (value ? 'stale' : 'active'),
|
||||
},
|
||||
// Always hidden -- for search
|
||||
{
|
||||
accessor: 'description',
|
||||
},
|
||||
],
|
||||
[isFavoritesPinned]
|
||||
);
|
||||
|
||||
const {
|
||||
data: searchedData,
|
||||
@ -174,7 +221,7 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
hiddenColumns.push('type', 'createdAt', 'tags');
|
||||
}
|
||||
setHiddenColumns(hiddenColumns);
|
||||
}, [setHiddenColumns, isSmallScreen, isMediumScreen, features]);
|
||||
}, [setHiddenColumns, isSmallScreen, isMediumScreen, features, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const tableState: PageQueryType = {};
|
||||
@ -185,12 +232,19 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
if (searchValue) {
|
||||
tableState.search = searchValue;
|
||||
}
|
||||
if (isFavoritesPinned) {
|
||||
tableState.favorites = 'true';
|
||||
}
|
||||
|
||||
setSearchParams(tableState, {
|
||||
replace: true,
|
||||
});
|
||||
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||
}, [sortBy, searchValue, setSearchParams]);
|
||||
setStoredParams({
|
||||
id: sortBy[0].id,
|
||||
desc: sortBy[0].desc || false,
|
||||
favorites: isFavoritesPinned || false,
|
||||
});
|
||||
}, [sortBy, searchValue, setSearchParams, isFavoritesPinned]);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
|
@ -0,0 +1,68 @@
|
||||
import { useCallback } from 'react';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
export const useFavoriteFeaturesApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeatures } = useFeatures();
|
||||
|
||||
const favorite = useCallback(
|
||||
async (projectId: string, featureName: string) => {
|
||||
const path = `api/admin/projects/${projectId}/features/${featureName}/favorites`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'POST' },
|
||||
'addFavoriteFeature'
|
||||
);
|
||||
|
||||
try {
|
||||
await makeRequest(req.caller, req.id);
|
||||
|
||||
setToastData({
|
||||
title: 'Toggle added to favorites',
|
||||
type: 'success',
|
||||
});
|
||||
refetchFeatures();
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
},
|
||||
[createRequest, makeRequest]
|
||||
);
|
||||
|
||||
const unfavorite = useCallback(
|
||||
async (projectId: string, featureName: string) => {
|
||||
const path = `api/admin/projects/${projectId}/features/${featureName}/favorites`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'DELETE' },
|
||||
'removeFavoriteFeature'
|
||||
);
|
||||
|
||||
try {
|
||||
await makeRequest(req.caller, req.id);
|
||||
|
||||
setToastData({
|
||||
title: 'Toggle removed from favorites',
|
||||
type: 'success',
|
||||
});
|
||||
refetchFeatures();
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
},
|
||||
[createRequest, makeRequest]
|
||||
);
|
||||
|
||||
return {
|
||||
favorite,
|
||||
unfavorite,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
@ -1,18 +1,17 @@
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { openApiAdmin } from 'utils/openapiClient';
|
||||
import { useApiGetter } from 'hooks/api/getters/useApiGetter/useApiGetter';
|
||||
import { FeaturesSchema } from 'openapi';
|
||||
import useSWR from 'swr';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
export interface IUseFeaturesOutput {
|
||||
features?: FeatureSchema[];
|
||||
refetchFeatures: () => void;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Feature toggle'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
export const useFeatures = (): IUseFeaturesOutput => {
|
||||
const { data, refetch, loading, error } = useApiGetter(
|
||||
'apiAdminFeaturesGet',
|
||||
() => openApiAdmin.getAllToggles(),
|
||||
export const useFeatures = () => {
|
||||
const { data, error, mutate } = useSWR<FeaturesSchema>(
|
||||
'api/admin/features',
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 15 * 1000, // ms
|
||||
}
|
||||
@ -20,8 +19,8 @@ export const useFeatures = (): IUseFeaturesOutput => {
|
||||
|
||||
return {
|
||||
features: data?.features,
|
||||
refetchFeatures: refetch,
|
||||
loading,
|
||||
loading: !error && !data,
|
||||
refetchFeatures: mutate,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
47
frontend/src/hooks/usePinnedFavorites.test.ts
Normal file
47
frontend/src/hooks/usePinnedFavorites.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Row } from 'react-table';
|
||||
import { sortTypesWithFavorites } from './usePinnedFavorites';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
favorite: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
favorite: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
favorite: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
favorite: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
favorite: false,
|
||||
},
|
||||
].map(d => ({ values: d })) as unknown as Row<object>[];
|
||||
|
||||
test('puts favorite items first', () => {
|
||||
const output = data.sort((a, b) =>
|
||||
sortTypesWithFavorites.alphanumeric(a, b, 'id')
|
||||
);
|
||||
const ids = output.map(({ values: { id } }) => id);
|
||||
const favorites = output.map(({ values: { favorite } }) => favorite);
|
||||
|
||||
expect(ids).toEqual([1, 3, 2, 4, 5]);
|
||||
expect(favorites).toEqual([true, true, false, false, false]);
|
||||
});
|
||||
|
||||
test('in descending order put favorites last (react-table will reverse order)', () => {
|
||||
const output = data.sort((a, b) =>
|
||||
sortTypesWithFavorites.alphanumeric(a, b, 'id', true)
|
||||
);
|
||||
const ids = output.map(({ values: { id } }) => id);
|
||||
const favorites = output.map(({ values: { favorite } }) => favorite);
|
||||
|
||||
expect(ids).toEqual([2, 4, 5, 1, 3]);
|
||||
expect(favorites).toEqual([false, false, false, true, true]);
|
||||
});
|
51
frontend/src/hooks/usePinnedFavorites.ts
Normal file
51
frontend/src/hooks/usePinnedFavorites.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import type { Row, SortByFn } from 'react-table';
|
||||
|
||||
type WithFavorite = {
|
||||
favorite: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export const sortTypesWithFavorites: Record<
|
||||
keyof typeof sortTypes,
|
||||
SortByFn<object> // TODO: possible type improvement in react-table v8
|
||||
> = Object.assign(
|
||||
{},
|
||||
...Object.entries(sortTypes).map(([key, value]) => ({
|
||||
[key]: (
|
||||
v1: Row<WithFavorite>,
|
||||
v2: Row<WithFavorite>,
|
||||
id: string,
|
||||
desc?: boolean
|
||||
) => {
|
||||
if (v1?.values?.favorite && !v2?.values?.favorite)
|
||||
return desc ? 1 : -1;
|
||||
if (!v1?.values?.favorite && v2?.values?.favorite)
|
||||
return desc ? -1 : 1;
|
||||
return value(v1, v2, id, desc);
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
/**
|
||||
* Move favorites to the top of the list.
|
||||
*/
|
||||
export const usePinnedFavorites = (initialState = false) => {
|
||||
const [isFavoritesPinned, setIsFavoritesPinned] = useState(initialState);
|
||||
|
||||
const onChangeIsFavoritePinned = () => {
|
||||
setIsFavoritesPinned(!isFavoritesPinned);
|
||||
};
|
||||
|
||||
const enhancedSortTypes = useMemo(
|
||||
() => (isFavoritesPinned ? sortTypesWithFavorites : sortTypes),
|
||||
[isFavoritesPinned]
|
||||
);
|
||||
|
||||
return {
|
||||
isFavoritesPinned,
|
||||
onChangeIsFavoritePinned,
|
||||
sortTypes: enhancedSortTypes,
|
||||
};
|
||||
};
|
@ -41,11 +41,11 @@ export interface IFlags {
|
||||
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
||||
embedProxyFrontend?: boolean;
|
||||
syncSSOGroups?: boolean;
|
||||
favorites?: boolean;
|
||||
changeRequests?: boolean;
|
||||
cloneEnvironment?: boolean;
|
||||
variantsPerEnvironment?: boolean;
|
||||
tokensLastSeen?: boolean;
|
||||
favorites?: boolean;
|
||||
networkView?: boolean;
|
||||
}
|
||||
|
||||
|
48
frontend/src/utils/sortTypes.test.ts
Normal file
48
frontend/src/utils/sortTypes.test.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Row } from 'react-table';
|
||||
import { sortTypes } from './sortTypes';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
age: 42,
|
||||
bool: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
age: 35,
|
||||
bool: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
age: 25,
|
||||
bool: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
age: 32,
|
||||
bool: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
age: 18,
|
||||
bool: true,
|
||||
},
|
||||
].map(d => ({ values: d })) as unknown as Row<{
|
||||
id: number;
|
||||
age: number;
|
||||
bool: boolean;
|
||||
}>[];
|
||||
|
||||
test('sortTypes', () => {
|
||||
expect(
|
||||
data
|
||||
.sort((a, b) => sortTypes.boolean(a, b, 'bool'))
|
||||
.map(({ values: { id } }) => id)
|
||||
).toEqual([2, 4, 1, 3, 5]);
|
||||
|
||||
expect(
|
||||
data
|
||||
.sort((a, b) => sortTypes.alphanumeric(a, b, 'age'))
|
||||
.map(({ values: { age } }) => age)
|
||||
).toEqual([18, 25, 32, 35, 42]);
|
||||
});
|
@ -1,24 +1,47 @@
|
||||
import { IdType, Row } from 'react-table';
|
||||
|
||||
/**
|
||||
* For `react-table`.
|
||||
*
|
||||
* @see https://react-table.tanstack.com/docs/api/useSortBy#table-options
|
||||
*/
|
||||
export const sortTypes = {
|
||||
date: (v1: any, v2: any, id: string) => {
|
||||
date: <D extends object>(
|
||||
v1: Row<D>,
|
||||
v2: Row<D>,
|
||||
id: IdType<D>,
|
||||
_desc?: boolean
|
||||
) => {
|
||||
const a = new Date(v1?.values?.[id] || 0);
|
||||
const b = new Date(v2?.values?.[id] || 0);
|
||||
return b?.getTime() - a?.getTime(); // newest first by default
|
||||
},
|
||||
boolean: (v1: any, v2: any, id: string) => {
|
||||
boolean: <D extends object>(
|
||||
v1: Row<D>,
|
||||
v2: Row<D>,
|
||||
id: IdType<D>,
|
||||
_desc?: boolean
|
||||
) => {
|
||||
const a = v1?.values?.[id];
|
||||
const b = v2?.values?.[id];
|
||||
return a === b ? 0 : a ? 1 : -1;
|
||||
},
|
||||
alphanumeric: (a: any, b: any, id: string) =>
|
||||
(a?.values?.[id] || '')
|
||||
?.toLowerCase()
|
||||
.localeCompare(b?.values?.[id]?.toLowerCase() || ''),
|
||||
playgroundResultState: (v1: any, v2: any, id: string) => {
|
||||
alphanumeric: <D extends object>(
|
||||
a: Row<D>,
|
||||
b: Row<D>,
|
||||
id: IdType<D>,
|
||||
_desc?: boolean
|
||||
) => {
|
||||
const aVal = `${a?.values?.[id] || ''}`.toLowerCase();
|
||||
const bVal = `${b?.values?.[id] || ''}`.toLowerCase();
|
||||
return aVal?.localeCompare(bVal);
|
||||
},
|
||||
playgroundResultState: <D extends object>(
|
||||
v1: Row<D>,
|
||||
v2: Row<D>,
|
||||
id: IdType<D>,
|
||||
_desc?: boolean
|
||||
) => {
|
||||
const a = v1?.values?.[id];
|
||||
const b = v2?.values?.[id];
|
||||
if (a === b) return 0;
|
||||
|
@ -345,8 +345,11 @@ class FeatureController extends Controller {
|
||||
res.status(200).json(feature);
|
||||
}
|
||||
|
||||
// TODO: remove?
|
||||
// Kept to keep backward compatibility
|
||||
/**
|
||||
* @deprecated TODO: remove?
|
||||
*
|
||||
* Kept to keep backward compatibility
|
||||
*/
|
||||
async toggle(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const userName = extractUsername(req);
|
||||
const { featureName } = req.params;
|
||||
|
@ -675,9 +675,7 @@ class FeatureToggleService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Warn: Legacy!
|
||||
*
|
||||
* @deprecated Legacy!
|
||||
*
|
||||
* Used to retrieve metadata of all feature toggles defined in Unleash.
|
||||
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
|
||||
|
Loading…
Reference in New Issue
Block a user