mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
Favorite features on project (#2580)
This commit is contained in:
parent
0a3823e188
commit
ef6ec4a83b
@ -22,8 +22,8 @@ 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 { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker';
|
||||
|
||||
|
@ -38,6 +38,10 @@ interface IColumnsMenuProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const columnNameMap: Record<string, string> = {
|
||||
favorite: 'Favorite',
|
||||
};
|
||||
|
||||
export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
allColumns,
|
||||
staticColumns = [],
|
||||
@ -183,7 +187,10 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
show={() => (
|
||||
<>{column.Header}</>
|
||||
)}
|
||||
elseShow={() => column.id}
|
||||
elseShow={() =>
|
||||
columnNameMap[column.id] ||
|
||||
column.id
|
||||
}
|
||||
/>
|
||||
</Typography>
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTheme } from '@mui/system';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useFlexLayout, useSortBy, useTable, SortingRule } from 'react-table';
|
||||
@ -15,7 +15,6 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
@ -25,23 +24,26 @@ import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import { useEnvironmentsRef } from './hooks/useEnvironmentsRef';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
import { UpdateEnabledMessage } from '../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
|
||||
interface IProjectFeatureTogglesProps {
|
||||
features: IProject['features'];
|
||||
@ -51,7 +53,7 @@ interface IProjectFeatureTogglesProps {
|
||||
|
||||
type ListItemType = Pick<
|
||||
IProject['features'][number],
|
||||
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale'
|
||||
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
|
||||
> & {
|
||||
environments: {
|
||||
[key in string]: {
|
||||
@ -65,6 +67,7 @@ const staticColumns = ['Actions', 'name'];
|
||||
|
||||
const defaultSort: SortingRule<string> & {
|
||||
columns?: string[];
|
||||
favorites?: boolean;
|
||||
} = { id: 'createdAt' };
|
||||
|
||||
export const ProjectFeatureToggles = ({
|
||||
@ -103,9 +106,15 @@ export const ProjectFeatureToggles = ({
|
||||
);
|
||||
const { refetch } = useProject(projectId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
||||
usePinnedFavorites(
|
||||
searchParams.has('favorites')
|
||||
? searchParams.get('favorites') === 'true'
|
||||
: storedParams.favorites
|
||||
);
|
||||
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||
useFeatureApi();
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const {
|
||||
onChangeRequestToggle,
|
||||
onChangeRequestToggleClose,
|
||||
@ -167,8 +176,42 @@ export const ProjectFeatureToggles = ({
|
||||
]
|
||||
);
|
||||
|
||||
const onFavorite = useCallback(
|
||||
async (feature: IFeatureToggleListItem) => {
|
||||
if (feature?.favorite) {
|
||||
await unfavorite(projectId, feature.name);
|
||||
} else {
|
||||
await favorite(projectId, feature.name);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
[projectId, refetch]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
...(uiConfig?.flags?.favorites
|
||||
? [
|
||||
{
|
||||
id: 'favorite',
|
||||
Header: (
|
||||
<FavoriteIconHeader
|
||||
isActive={isFavoritesPinned}
|
||||
onClick={onChangeIsFavoritePinned}
|
||||
/>
|
||||
),
|
||||
accessor: 'favorite',
|
||||
Cell: ({ row: { original: feature } }: any) => (
|
||||
<FavoriteIconCell
|
||||
value={feature?.favorite}
|
||||
onClick={() => onFavorite(feature)}
|
||||
/>
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
@ -197,18 +240,20 @@ export const ProjectFeatureToggles = ({
|
||||
sortType: 'alphanumeric',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: IFeatureToggleListItem) =>
|
||||
row.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
hideInMenu: true,
|
||||
searchable: true,
|
||||
},
|
||||
// FIXME: no tags on project feature toggles from backend
|
||||
// {
|
||||
// id: 'tags',
|
||||
// Header: 'Tags',
|
||||
// accessor: (row: IFeatureToggleListItem) =>
|
||||
// row.tags
|
||||
// ?.map(({ type, value }) => `${type}:${value}`)
|
||||
// .join('\n') || '',
|
||||
// Cell: FeatureTagCell,
|
||||
// width: 80,
|
||||
// hideInMenu: true,
|
||||
// searchable: true,
|
||||
// isVisible: false,
|
||||
// },
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
@ -219,28 +264,25 @@ export const ProjectFeatureToggles = ({
|
||||
...environments.map(name => ({
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
accessor: `environments.${name}`,
|
||||
id: `environments.${name}`,
|
||||
accessor: `environments.${name}.enabled`,
|
||||
align: 'center',
|
||||
Cell: ({
|
||||
value,
|
||||
row: { original: feature },
|
||||
}: {
|
||||
value: { name: string; enabled: boolean };
|
||||
value: boolean;
|
||||
row: { original: ListItemType };
|
||||
}) => (
|
||||
<FeatureToggleSwitch
|
||||
value={value?.enabled || false}
|
||||
value={value}
|
||||
projectId={projectId}
|
||||
featureName={feature?.name}
|
||||
environmentName={value?.name}
|
||||
environmentName={name}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
),
|
||||
sortType: (v1: any, v2: any, id: string) => {
|
||||
const a = v1?.values?.[id]?.enabled;
|
||||
const b = v2?.values?.[id]?.enabled;
|
||||
return a === b ? 0 : a ? -1 : 1;
|
||||
},
|
||||
sortType: 'boolean',
|
||||
filterName: name,
|
||||
filterParsing: (value: any) =>
|
||||
value.enabled ? 'enabled' : 'disabled',
|
||||
@ -260,7 +302,14 @@ export const ProjectFeatureToggles = ({
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[projectId, environments, loading, onToggle]
|
||||
[
|
||||
projectId,
|
||||
environments,
|
||||
loading,
|
||||
onToggle,
|
||||
isFavoritesPinned,
|
||||
uiConfig?.flags?.favorites,
|
||||
]
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
@ -277,6 +326,7 @@ export const ProjectFeatureToggles = ({
|
||||
type,
|
||||
stale,
|
||||
tags,
|
||||
favorite,
|
||||
environments: featureEnvironments,
|
||||
}) => ({
|
||||
name,
|
||||
@ -285,6 +335,7 @@ export const ProjectFeatureToggles = ({
|
||||
type,
|
||||
stale,
|
||||
tags,
|
||||
favorite,
|
||||
environments: Object.fromEntries(
|
||||
environments.map(env => [
|
||||
env,
|
||||
@ -324,7 +375,6 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const initialState = useMemo(
|
||||
() => {
|
||||
const searchParams = new URLSearchParams();
|
||||
const allColumnIds = columns.map(
|
||||
(column: any) => column?.accessor || column?.id
|
||||
);
|
||||
@ -364,9 +414,7 @@ export const ProjectFeatureToggles = ({
|
||||
[environments] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const getRowId = useCallback((row: any) => {
|
||||
return row.name;
|
||||
}, []);
|
||||
const getRowId = useCallback((row: any) => row.name, []);
|
||||
|
||||
const {
|
||||
allColumns,
|
||||
@ -389,15 +437,16 @@ export const ProjectFeatureToggles = ({
|
||||
useSortBy
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!features.some(({ tags }) => tags?.length)) {
|
||||
setHiddenColumns(hiddenColumns => [...hiddenColumns, 'tags']);
|
||||
} else {
|
||||
setHiddenColumns(hiddenColumns =>
|
||||
hiddenColumns.filter(column => column !== 'tags')
|
||||
);
|
||||
}
|
||||
}, [setHiddenColumns, features]);
|
||||
// TODO: update after tags are added, move to other useEffect
|
||||
// useEffect(() => {
|
||||
// if (!features.some(({ tags }) => tags?.length)) {
|
||||
// setHiddenColumns(hiddenColumns => [...hiddenColumns, 'tags']);
|
||||
// } else {
|
||||
// setHiddenColumns(hiddenColumns =>
|
||||
// hiddenColumns.filter(column => column !== 'tags')
|
||||
// );
|
||||
// }
|
||||
// }, [setHiddenColumns, features]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
@ -411,6 +460,9 @@ export const ProjectFeatureToggles = ({
|
||||
if (searchValue) {
|
||||
tableState.search = searchValue;
|
||||
}
|
||||
if (isFavoritesPinned) {
|
||||
tableState.favorites = 'true';
|
||||
}
|
||||
tableState.columns = allColumns
|
||||
.map(({ id }) => id)
|
||||
.filter(
|
||||
@ -427,9 +479,17 @@ export const ProjectFeatureToggles = ({
|
||||
id: sortBy[0].id,
|
||||
desc: sortBy[0].desc || false,
|
||||
columns: tableState.columns.split(','),
|
||||
favorites: isFavoritesPinned || false,
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
|
||||
}, [
|
||||
loading,
|
||||
sortBy,
|
||||
hiddenColumns,
|
||||
searchValue,
|
||||
setSearchParams,
|
||||
isFavoritesPinned,
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
|
@ -3,6 +3,7 @@ import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
||||
import useAPI from '../useApi/useApi';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { usePlausibleTracker } from '../../../usePlausibleTracker';
|
||||
|
||||
export const useFavoriteFeaturesApi = () => {
|
||||
|
@ -22,7 +22,7 @@ const data = [
|
||||
id: 5,
|
||||
favorite: false,
|
||||
},
|
||||
].map(d => ({ values: d })) as unknown as Row<object>[];
|
||||
].map(d => ({ values: d, original: d })) as unknown as Row<object>[];
|
||||
|
||||
test('puts favorite items first', () => {
|
||||
const output = data.sort((a, b) =>
|
||||
|
@ -20,9 +20,9 @@ export const sortTypesWithFavorites: Record<
|
||||
id: string,
|
||||
desc?: boolean
|
||||
) => {
|
||||
if (v1?.values?.favorite && !v2?.values?.favorite)
|
||||
if (v1?.original?.favorite && !v2?.original?.favorite)
|
||||
return desc ? 1 : -1;
|
||||
if (!v1?.values?.favorite && v2?.values?.favorite)
|
||||
if (!v1?.original?.favorite && v2?.original?.favorite)
|
||||
return desc ? -1 : 1;
|
||||
return value(v1, v2, id, desc);
|
||||
},
|
||||
@ -45,10 +45,9 @@ export const usePinnedFavorites = (initialState = false) => {
|
||||
setIsFavoritesPinned(!isFavoritesPinned);
|
||||
};
|
||||
|
||||
const enhancedSortTypes = useMemo(
|
||||
() => (isFavoritesPinned ? sortTypesWithFavorites : sortTypes),
|
||||
[isFavoritesPinned]
|
||||
);
|
||||
const enhancedSortTypes = useMemo(() => {
|
||||
return isFavoritesPinned ? sortTypesWithFavorites : sortTypes;
|
||||
}, [isFavoritesPinned]);
|
||||
|
||||
return {
|
||||
isFavoritesPinned,
|
||||
|
@ -9,6 +9,7 @@ export interface IFeatureToggleListItem {
|
||||
createdAt: string;
|
||||
environments: IEnvironments[];
|
||||
tags?: ITag[];
|
||||
favorite?: boolean;
|
||||
}
|
||||
|
||||
export interface IEnvironments {
|
||||
|
Loading…
Reference in New Issue
Block a user