1
0
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:
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 { 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';

View File

@ -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>
}

View File

@ -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

View File

@ -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 = () => {

View File

@ -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) =>

View File

@ -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,

View File

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