1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

Favorite features on project (#2580)

This commit is contained in:
Tymoteusz Czech 2022-12-01 13:10:42 +01:00 committed by GitHub
parent 0a3823e188
commit ef6ec4a83b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 61 deletions

View File

@ -22,8 +22,8 @@ import { Search } from 'component/common/Search/Search';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { usePinnedFavorites } from 'hooks/usePinnedFavorites'; import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FavoriteIconCell } from './FavoriteIconCell/FavoriteIconCell'; import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
import { FavoriteIconHeader } from './FavoriteIconHeader/FavoriteIconHeader'; import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker'; import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker';

View File

@ -38,6 +38,10 @@ interface IColumnsMenuProps {
) => void; ) => void;
} }
const columnNameMap: Record<string, string> = {
favorite: 'Favorite',
};
export const ColumnsMenu: VFC<IColumnsMenuProps> = ({ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
allColumns, allColumns,
staticColumns = [], staticColumns = [],
@ -183,7 +187,10 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
show={() => ( show={() => (
<>{column.Header}</> <>{column.Header}</>
)} )}
elseShow={() => column.id} elseShow={() =>
columnNameMap[column.id] ||
column.id
}
/> />
</Typography> </Typography>
} }

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; 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 { Add } from '@mui/icons-material';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useFlexLayout, useSortBy, useTable, SortingRule } from 'react-table'; 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 { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { sortTypes } from 'utils/sortTypes';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { IProject } from 'interfaces/project'; import { IProject } from 'interfaces/project';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
@ -25,23 +24,26 @@ import { createLocalStorage } from 'utils/createLocalStorage';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors'; import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog'; 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 { useEnvironmentsRef } from './hooks/useEnvironmentsRef';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch'; import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
import { ActionsCell } from './ActionsCell/ActionsCell'; import { ActionsCell } from './ActionsCell/ActionsCell';
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu'; import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
import { useStyles } from './ProjectFeatureToggles.styles'; import { useStyles } from './ProjectFeatureToggles.styles';
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
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';
interface IProjectFeatureTogglesProps { interface IProjectFeatureTogglesProps {
features: IProject['features']; features: IProject['features'];
@ -51,7 +53,7 @@ interface IProjectFeatureTogglesProps {
type ListItemType = Pick< type ListItemType = Pick<
IProject['features'][number], IProject['features'][number],
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' 'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
> & { > & {
environments: { environments: {
[key in string]: { [key in string]: {
@ -65,6 +67,7 @@ const staticColumns = ['Actions', 'name'];
const defaultSort: SortingRule<string> & { const defaultSort: SortingRule<string> & {
columns?: string[]; columns?: string[];
favorites?: boolean;
} = { id: 'createdAt' }; } = { id: 'createdAt' };
export const ProjectFeatureToggles = ({ export const ProjectFeatureToggles = ({
@ -103,9 +106,15 @@ export const ProjectFeatureToggles = ({
); );
const { refetch } = useProject(projectId); const { refetch } = useProject(projectId);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
usePinnedFavorites(
searchParams.has('favorites')
? searchParams.get('favorites') === 'true'
: storedParams.favorites
);
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi(); useFeatureApi();
const { favorite, unfavorite } = useFavoriteFeaturesApi();
const { const {
onChangeRequestToggle, onChangeRequestToggle,
onChangeRequestToggleClose, 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( 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', Header: 'Seen',
accessor: 'lastSeenAt', accessor: 'lastSeenAt',
@ -197,18 +240,20 @@ export const ProjectFeatureToggles = ({
sortType: 'alphanumeric', sortType: 'alphanumeric',
searchable: true, searchable: true,
}, },
{ // FIXME: no tags on project feature toggles from backend
id: 'tags', // {
Header: 'Tags', // id: 'tags',
accessor: (row: IFeatureToggleListItem) => // Header: 'Tags',
row.tags // accessor: (row: IFeatureToggleListItem) =>
?.map(({ type, value }) => `${type}:${value}`) // row.tags
.join('\n') || '', // ?.map(({ type, value }) => `${type}:${value}`)
Cell: FeatureTagCell, // .join('\n') || '',
width: 80, // Cell: FeatureTagCell,
hideInMenu: true, // width: 80,
searchable: true, // hideInMenu: true,
}, // searchable: true,
// isVisible: false,
// },
{ {
Header: 'Created', Header: 'Created',
accessor: 'createdAt', accessor: 'createdAt',
@ -219,28 +264,25 @@ export const ProjectFeatureToggles = ({
...environments.map(name => ({ ...environments.map(name => ({
Header: loading ? () => '' : name, Header: loading ? () => '' : name,
maxWidth: 90, maxWidth: 90,
accessor: `environments.${name}`, id: `environments.${name}`,
accessor: `environments.${name}.enabled`,
align: 'center', align: 'center',
Cell: ({ Cell: ({
value, value,
row: { original: feature }, row: { original: feature },
}: { }: {
value: { name: string; enabled: boolean }; value: boolean;
row: { original: ListItemType }; row: { original: ListItemType };
}) => ( }) => (
<FeatureToggleSwitch <FeatureToggleSwitch
value={value?.enabled || false} value={value}
projectId={projectId} projectId={projectId}
featureName={feature?.name} featureName={feature?.name}
environmentName={value?.name} environmentName={name}
onToggle={onToggle} onToggle={onToggle}
/> />
), ),
sortType: (v1: any, v2: any, id: string) => { sortType: 'boolean',
const a = v1?.values?.[id]?.enabled;
const b = v2?.values?.[id]?.enabled;
return a === b ? 0 : a ? -1 : 1;
},
filterName: name, filterName: name,
filterParsing: (value: any) => filterParsing: (value: any) =>
value.enabled ? 'enabled' : 'disabled', value.enabled ? 'enabled' : 'disabled',
@ -260,7 +302,14 @@ export const ProjectFeatureToggles = ({
disableSortBy: true, disableSortBy: true,
}, },
], ],
[projectId, environments, loading, onToggle] [
projectId,
environments,
loading,
onToggle,
isFavoritesPinned,
uiConfig?.flags?.favorites,
]
); );
const [searchValue, setSearchValue] = useState( const [searchValue, setSearchValue] = useState(
@ -277,6 +326,7 @@ export const ProjectFeatureToggles = ({
type, type,
stale, stale,
tags, tags,
favorite,
environments: featureEnvironments, environments: featureEnvironments,
}) => ({ }) => ({
name, name,
@ -285,6 +335,7 @@ export const ProjectFeatureToggles = ({
type, type,
stale, stale,
tags, tags,
favorite,
environments: Object.fromEntries( environments: Object.fromEntries(
environments.map(env => [ environments.map(env => [
env, env,
@ -324,7 +375,6 @@ export const ProjectFeatureToggles = ({
const initialState = useMemo( const initialState = useMemo(
() => { () => {
const searchParams = new URLSearchParams();
const allColumnIds = columns.map( const allColumnIds = columns.map(
(column: any) => column?.accessor || column?.id (column: any) => column?.accessor || column?.id
); );
@ -364,9 +414,7 @@ export const ProjectFeatureToggles = ({
[environments] // eslint-disable-line react-hooks/exhaustive-deps [environments] // eslint-disable-line react-hooks/exhaustive-deps
); );
const getRowId = useCallback((row: any) => { const getRowId = useCallback((row: any) => row.name, []);
return row.name;
}, []);
const { const {
allColumns, allColumns,
@ -389,15 +437,16 @@ export const ProjectFeatureToggles = ({
useSortBy useSortBy
); );
useEffect(() => { // TODO: update after tags are added, move to other useEffect
if (!features.some(({ tags }) => tags?.length)) { // useEffect(() => {
setHiddenColumns(hiddenColumns => [...hiddenColumns, 'tags']); // if (!features.some(({ tags }) => tags?.length)) {
} else { // setHiddenColumns(hiddenColumns => [...hiddenColumns, 'tags']);
setHiddenColumns(hiddenColumns => // } else {
hiddenColumns.filter(column => column !== 'tags') // setHiddenColumns(hiddenColumns =>
); // hiddenColumns.filter(column => column !== 'tags')
} // );
}, [setHiddenColumns, features]); // }
// }, [setHiddenColumns, features]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
@ -411,6 +460,9 @@ export const ProjectFeatureToggles = ({
if (searchValue) { if (searchValue) {
tableState.search = searchValue; tableState.search = searchValue;
} }
if (isFavoritesPinned) {
tableState.favorites = 'true';
}
tableState.columns = allColumns tableState.columns = allColumns
.map(({ id }) => id) .map(({ id }) => id)
.filter( .filter(
@ -427,9 +479,17 @@ export const ProjectFeatureToggles = ({
id: sortBy[0].id, id: sortBy[0].id,
desc: sortBy[0].desc || false, desc: sortBy[0].desc || false,
columns: tableState.columns.split(','), columns: tableState.columns.split(','),
favorites: isFavoritesPinned || false,
})); }));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]); }, [
loading,
sortBy,
hiddenColumns,
searchValue,
setSearchParams,
isFavoritesPinned,
]);
return ( return (
<PageContent <PageContent

View File

@ -3,6 +3,7 @@ import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
import useProject from 'hooks/api/getters/useProject/useProject';
import { usePlausibleTracker } from '../../../usePlausibleTracker'; import { usePlausibleTracker } from '../../../usePlausibleTracker';
export const useFavoriteFeaturesApi = () => { export const useFavoriteFeaturesApi = () => {

View File

@ -22,7 +22,7 @@ const data = [
id: 5, id: 5,
favorite: false, 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', () => { test('puts favorite items first', () => {
const output = data.sort((a, b) => const output = data.sort((a, b) =>

View File

@ -20,9 +20,9 @@ export const sortTypesWithFavorites: Record<
id: string, id: string,
desc?: boolean desc?: boolean
) => { ) => {
if (v1?.values?.favorite && !v2?.values?.favorite) if (v1?.original?.favorite && !v2?.original?.favorite)
return desc ? 1 : -1; return desc ? 1 : -1;
if (!v1?.values?.favorite && v2?.values?.favorite) if (!v1?.original?.favorite && v2?.original?.favorite)
return desc ? -1 : 1; return desc ? -1 : 1;
return value(v1, v2, id, desc); return value(v1, v2, id, desc);
}, },
@ -45,10 +45,9 @@ export const usePinnedFavorites = (initialState = false) => {
setIsFavoritesPinned(!isFavoritesPinned); setIsFavoritesPinned(!isFavoritesPinned);
}; };
const enhancedSortTypes = useMemo( const enhancedSortTypes = useMemo(() => {
() => (isFavoritesPinned ? sortTypesWithFavorites : sortTypes), return isFavoritesPinned ? sortTypesWithFavorites : sortTypes;
[isFavoritesPinned] }, [isFavoritesPinned]);
);
return { return {
isFavoritesPinned, isFavoritesPinned,

View File

@ -9,6 +9,7 @@ export interface IFeatureToggleListItem {
createdAt: string; createdAt: string;
environments: IEnvironments[]; environments: IEnvironments[];
tags?: ITag[]; tags?: ITag[];
favorite?: boolean;
} }
export interface IEnvironments { export interface IEnvironments {