diff --git a/frontend/src/component/feature/FeatureToggleList/FavoriteIconHeader/FavoriteIconHeader.tsx b/frontend/src/component/common/Table/FavoriteIconHeader/FavoriteIconHeader.tsx similarity index 100% rename from frontend/src/component/feature/FeatureToggleList/FavoriteIconHeader/FavoriteIconHeader.tsx rename to frontend/src/component/common/Table/FavoriteIconHeader/FavoriteIconHeader.tsx diff --git a/frontend/src/component/feature/FeatureToggleList/FavoriteIconCell/FavoriteIconCell.tsx b/frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx similarity index 100% rename from frontend/src/component/feature/FeatureToggleList/FavoriteIconCell/FavoriteIconCell.tsx rename to frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 5e32adab16..fdeb85b329 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -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'; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ColumnsMenu/ColumnsMenu.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ColumnsMenu/ColumnsMenu.tsx index f705a639d0..b1cdc28953 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ColumnsMenu/ColumnsMenu.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ColumnsMenu/ColumnsMenu.tsx @@ -38,6 +38,10 @@ interface IColumnsMenuProps { ) => void; } +const columnNameMap: Record = { + favorite: 'Favorite', +}; + export const ColumnsMenu: VFC = ({ allColumns, staticColumns = [], @@ -183,7 +187,10 @@ export const ColumnsMenu: VFC = ({ show={() => ( <>{column.Header} )} - elseShow={() => column.id} + elseShow={() => + columnNameMap[column.id] || + column.id + } /> } diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index fb4bae24a7..0631db386e 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 & { 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: ( + + ), + accessor: 'favorite', + Cell: ({ row: { original: feature } }: any) => ( + 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 }; }) => ( ), - 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 ( { diff --git a/frontend/src/hooks/usePinnedFavorites.test.ts b/frontend/src/hooks/usePinnedFavorites.test.ts index 4c29ef245c..adf26c2e70 100644 --- a/frontend/src/hooks/usePinnedFavorites.test.ts +++ b/frontend/src/hooks/usePinnedFavorites.test.ts @@ -22,7 +22,7 @@ const data = [ id: 5, favorite: false, }, -].map(d => ({ values: d })) as unknown as Row[]; +].map(d => ({ values: d, original: d })) as unknown as Row[]; test('puts favorite items first', () => { const output = data.sort((a, b) => diff --git a/frontend/src/hooks/usePinnedFavorites.ts b/frontend/src/hooks/usePinnedFavorites.ts index 0d877d75f3..c964088159 100644 --- a/frontend/src/hooks/usePinnedFavorites.ts +++ b/frontend/src/hooks/usePinnedFavorites.ts @@ -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, diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 79bc3ac490..9d440b6979 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -9,6 +9,7 @@ export interface IFeatureToggleListItem { createdAt: string; environments: IEnvironments[]; tags?: ITag[]; + favorite?: boolean; } export interface IEnvironments {