Integrate dynamic data into admin menu, update cmdk-menu

This commit is contained in:
Sam Becker 2025-02-26 17:41:17 -06:00
parent 97d8fef130
commit ac19ed2215
17 changed files with 344 additions and 207 deletions

View File

@ -1,7 +1,7 @@
import { useAppState } from '@/state/AppState';
import clsx from 'clsx/lite';
import { FaCircle } from 'react-icons/fa6';
import { LuCog } from 'react-icons/lu';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
export default function AdminAppInfoIcon({
size = 'large',
@ -18,22 +18,15 @@ export default function AdminAppInfoIcon({
className,
)}>
<LuCog
size={size === 'large' ? 20 : 18}
size={size === 'large' ? 20 : 17}
className="inline-flex translate-y-[1px]"
aria-label="App Info"
/>
{insightIndicatorStatus &&
<FaCircle
size={size === 'large' ? 8 : 7}
className={clsx(
'absolute',
size === 'large'
? 'top-[1.5px] right-[0.5px]'
: 'top-[1px] right-[-0.5px]',
insightIndicatorStatus === 'blue'
? 'text-blue-500'
: 'text-amber-500',
)}
<InsightsIndicatorDot
size={size}
top={size === 'large' ? 1.5 : 1.5}
right={size === 'large' ? 0.5 : 1}
/>}
</span>
);

View File

@ -5,6 +5,7 @@ import {
PATH_ADMIN_INSIGHTS,
PATH_ADMIN_PHOTOS,
PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
PATH_GRID_INFERRED,
} from '@/app/paths';
import { useAppState } from '@/state/AppState';
@ -17,6 +18,8 @@ import { BiLockAlt } from 'react-icons/bi';
import AdminAppInfoIcon from './AdminAppInfoIcon';
import { PiSignOutBold } from 'react-icons/pi';
import { signOutAndRedirectAction } from '@/auth/actions';
import { ComponentProps } from 'react';
import { FaRegFolderOpen } from 'react-icons/fa';
export default function AdminAppMenu({
className,
@ -26,12 +29,87 @@ export default function AdminAppMenu({
buttonClassName?: string
}) {
const {
photosCount,
uploadsCount,
tagsCount,
selectedPhotoIds,
setSelectedPhotoIds,
} = useAppState();
const isSelecting = selectedPhotoIds !== undefined;
const items: ComponentProps<typeof MoreMenu>['items'] = [{
label: 'Manage Photos',
...photosCount !== undefined && {
annotation: `${photosCount}`,
},
icon: <TbPhoto
size={15}
className="translate-x-[-0.5px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_PHOTOS,
}];
if (uploadsCount) {
items.push({
label: 'Uploads',
annotation: `${uploadsCount}`,
icon: <FaRegFolderOpen
size={16}
className="translate-y-[0.5px]"
/>,
href: PATH_ADMIN_UPLOADS,
});
}
if (tagsCount) {
items.push({
label: 'Manage Tags',
annotation: `${tagsCount}`,
icon: <FiTag
size={15}
className="translate-y-[0.5px]"
/>,
href: PATH_ADMIN_TAGS,
});
}
items.push({
label: 'App Info',
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[-0.5px] translate-y-[-0.5px]"
/>,
href: PATH_ADMIN_INSIGHTS,
}, {
label: isSelecting
? 'Exit Select'
: 'Edit Multiple …',
icon: isSelecting
? <IoCloseSharp
className="text-[18px] translate-x-[-1px] translate-y-[1px]"
/>
: <ImCheckboxUnchecked
className="translate-x-[-0.5px] text-[0.75rem]"
/>,
href: PATH_GRID_INFERRED,
action: () => {
if (isSelecting) {
setSelectedPhotoIds?.(undefined);
} else {
setSelectedPhotoIds?.([]);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
shouldPreventDefault: false,
}, {
label: 'Sign Out',
icon: <PiSignOutBold size={15} />,
action: signOutAndRedirectAction,
});
return (
<MoreMenu
header="Admin menu"
@ -45,55 +123,7 @@ export default function AdminAppMenu({
'rounded-none focus:outline-none',
buttonClassName,
)}
items={[{
label: 'Manage Photos',
icon: <TbPhoto
size={16}
className="translate-x-[1px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_PHOTOS,
}, {
label: 'Manage Tags',
icon: <FiTag
size={16}
className="translate-x-[1.5px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_TAGS,
}, {
label: 'App Info',
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[1px] translate-y-[-0.5px]"
/>,
href: PATH_ADMIN_INSIGHTS,
}, {
label: isSelecting
? 'Exit Select'
: 'Select Photos',
icon: isSelecting
? <IoCloseSharp
className="text-[18px] translate-y-[-0.5px]"
/>
: <ImCheckboxUnchecked
className="text-[0.75rem] translate-x-[1px]"
/>,
href: PATH_GRID_INFERRED,
action: () => {
if (isSelecting) {
setSelectedPhotoIds?.(undefined);
} else {
setSelectedPhotoIds?.([]);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
shouldPreventDefault: false,
}, {
label: 'Sign Out',
icon: <PiSignOutBold size={15} className="translate-x-[1px]" />,
action: signOutAndRedirectAction,
}]}
items={items}
ariaLabel="Admin Menu"
/>
);

View File

@ -7,7 +7,7 @@ import clsx from 'clsx/lite';
import ClearCacheButton from '@/admin/ClearCacheButton';
import { usePathname } from 'next/navigation';
import { useAppState } from '@/state/AppState';
import { FaCircle } from 'react-icons/fa6';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
const ADMIN_INFO_PAGES = [{
title: 'Insights',
@ -65,14 +65,10 @@ export default function AdminInfoPage({
{title}
</ResponsiveText>
{title === 'Insights' && insightIndicatorStatus &&
<FaCircle
size={6}
className={clsx(
insightIndicatorStatus === 'blue'
? 'text-blue-500'
: 'text-amber-500',
'absolute top-[4px] right-[-2px]',
)}
<InsightsIndicatorDot
size="small"
top={4}
right={-2}
/>}
</LinkWithStatus>)}
</div>

View File

@ -14,20 +14,21 @@ import AdminNavClient from './AdminNavClient';
export default async function AdminNav() {
const [
countPhotos,
countUploads,
countTags,
countUploads,
mostRecentPhotoUpdateTime,
] = await Promise.all([
getPhotosMetaCached({ hidden: 'include' })
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached().then(tags => tags.length)
.catch(() => 0),
getStorageUploadUrlsNoStore()
.then(urls => urls.length)
.catch(e => {
console.error(`Error getting blob upload urls: ${e}`);
return 0;
}),
getUniqueTagsCached().then(tags => tags.length).catch(() => 0),
getPhotosMostRecentUpdateCached().catch(() => undefined),
]);

View File

@ -23,7 +23,7 @@ import { MdOutlineFileDownload } from 'react-icons/md';
import MoreMenuItem from '@/components/more/MoreMenuItem';
import IconGrSync from '@/app/IconGrSync';
import { isPhotoOutdated } from '@/photo/outdated';
import { FaCircle } from 'react-icons/fa6';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
export default function AdminPhotoMenuClient({
photo,
@ -81,12 +81,13 @@ export default function AdminPhotoMenuClient({
hrefDownloadName: downloadFileNameForPhoto(photo),
});
items.push({
label: <span className="inline-flex items-center gap-2">
label: 'Sync',
labelComplex: <span className="inline-flex items-center gap-2">
<span>Sync</span>
{isPhotoOutdated(photo) &&
<FaCircle
size={8}
className="text-amber-500 translate-y-[1.5px]"
<InsightsIndicatorDot
colorOverride="yellow"
className="translate-y-[1.5px]"
/>}
</span>,
icon: <IconGrSync className="translate-x-[-1px]" />,

View File

@ -6,6 +6,45 @@ import { testOpenAiConnection } from '@/platforms/openai';
import { testDatabaseConnection } from '@/platforms/postgres';
import { testStorageConnection } from '@/platforms/storage';
import { APP_CONFIGURATION } from '@/app/config';
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
import { getPhotosMetaCached, getUniqueTagsCached } from '@/photo/cache';
import { getShouldShowInsightsIndicator } from '@/admin/insights/server';
export const getAdminDataAction = async () =>
runAuthenticatedAdminServerAction(async () => {
const [
countPhotos,
countHiddenPhotos,
countTags,
countUploads,
shouldShowInsightsIndicator,
] = await Promise.all([
getPhotosMetaCached()
.then(({ count }) => count)
.catch(() => 0),
getPhotosMetaCached({ hidden: 'only' })
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached()
.then(tags => tags.length)
.catch(() => 0),
getStorageUploadUrlsNoStore()
.then(urls => urls.length)
.catch(e => {
console.error(`Error getting blob upload urls: ${e}`);
return 0;
}),
getShouldShowInsightsIndicator(),
]);
return {
countPhotos,
countHiddenPhotos,
countTags,
countUploads,
shouldShowInsightsIndicator,
};
});
const scanForError = (
shouldCheck: boolean,

View File

@ -0,0 +1,50 @@
import { useAppState } from '@/state/AppState';
import clsx from 'clsx/lite';
import { FaCircle } from 'react-icons/fa6';
export default function InsightsIndicatorDot({
className,
size = 'medium',
colorOverride,
top,
right,
bottom,
left,
}: {
className?: string
size?: 'small' | 'medium' | 'large'
colorOverride?: 'blue' | 'yellow'
top?: number
right?: number
bottom?: number
left?: number
}) {
const { insightIndicatorStatus } = useAppState();
const getSize = () => {
switch (size) {
case 'small': return 6;
case 'medium': return 7;
case 'large': return 8;
}
};
return (
<FaCircle
size={getSize()}
className={clsx(
(
top !== undefined ||
right !== undefined ||
bottom !== undefined ||
left !== undefined
) && 'absolute',
(colorOverride ?? insightIndicatorStatus) === 'blue'
? 'text-blue-500'
: 'text-amber-500',
className,
)}
style={{ top, right, bottom, left }}
/>
);
}

View File

@ -1,36 +0,0 @@
'use server';
import { runAuthenticatedAdminServerAction } from '@/auth';
import {
getGitHubMetaForCurrentApp,
getSignificantInsights,
InsightIndicatorStatus,
} from '.';
import { getOutdatedPhotosCount } from '@/photo/db/query';
export const getShouldShowInsightsIndicatorAction =
async (): Promise<InsightIndicatorStatus> =>
runAuthenticatedAdminServerAction(async () => {
const [
codeMeta,
photosCountOutdated,
] = await Promise.all([
getGitHubMetaForCurrentApp(),
getOutdatedPhotosCount(),
]);
const {
forkBehind,
noAiRateLimiting,
outdatedPhotos,
} = getSignificantInsights({
codeMeta,
photosCountOutdated,
});
if (noAiRateLimiting || outdatedPhotos) {
return 'yellow';
} else if (forkBehind) {
return 'blue';
}
});

View File

@ -0,0 +1,28 @@
import { getOutdatedPhotosCount } from '@/photo/db/query';
import { getSignificantInsights } from '.';
import { getGitHubMetaForCurrentApp } from '.';
export const getShouldShowInsightsIndicator = async () => {
const [
codeMeta,
photosCountOutdated,
] = await Promise.all([
getGitHubMetaForCurrentApp(),
getOutdatedPhotosCount(),
]);
const {
forkBehind,
noAiRateLimiting,
outdatedPhotos,
} = getSignificantInsights({
codeMeta,
photosCountOutdated,
});
if (noAiRateLimiting || outdatedPhotos) {
return 'yellow';
} else if (forkBehind) {
return 'blue';
}
};

View File

@ -44,7 +44,7 @@ import { TbPhoto } from 'react-icons/tb';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
import PhotoDate from '@/photo/PhotoDate';
import PhotoSmall from '@/photo/PhotoSmall';
import { FaCheck, FaCircle } from 'react-icons/fa6';
import { FaCheck } from 'react-icons/fa6';
import { Tags, addHiddenToTags, formatTag } from '@/tag';
import { FaTag } from 'react-icons/fa';
import { formatCount, formatCountDescriptive } from '@/utility/string';
@ -52,6 +52,7 @@ import CommandKItem from './CommandKItem';
import { GRID_HOMEPAGE_ENABLED } from '@/app/config';
import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@ -102,7 +103,9 @@ export default function CommandKClient({
isUserSignedIn,
setUserEmail,
isCommandKOpen: isOpen,
hiddenPhotosCount,
photosCountHidden,
uploadsCount,
tagsCount,
selectedPhotoIds,
setSelectedPhotoIds,
insightIndicatorStatus,
@ -228,8 +231,8 @@ export default function CommandKClient({
}, [isOpen, setShouldRespondToKeyboardCommands]);
const tagsIncludingHidden = useMemo(() =>
addHiddenToTags(tags, hiddenPhotosCount)
, [tags, hiddenPhotosCount]);
addHiddenToTags(tags, photosCountHidden)
, [tags, photosCountHidden]);
const SECTION_TAGS: CommandKSection = {
heading: 'Tags',
@ -336,70 +339,77 @@ export default function CommandKClient({
const adminSection: CommandKSection = {
heading: 'Admin',
accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />,
items: isUserSignedIn
? ([{
label: 'Manage Photos',
annotation: <BiLockAlt />,
path: PATH_ADMIN_PHOTOS,
}, {
items: [],
};
if (isUserSignedIn) {
adminSection.items.push({
label: 'Manage Photos',
annotation: <BiLockAlt />,
path: PATH_ADMIN_PHOTOS,
});
if (uploadsCount) {
adminSection.items.push({
label: 'Manage Uploads',
annotation: <BiLockAlt />,
path: PATH_ADMIN_UPLOADS,
}, {
});
}
if (tagsCount) {
adminSection.items.push({
label: 'Manage Tags',
annotation: <BiLockAlt />,
path: PATH_ADMIN_TAGS,
}, {
label: 'App Config',
});
}
adminSection.items.push({
label: <span className="flex items-center gap-3">
App Insights
{insightIndicatorStatus &&
<InsightsIndicatorDot />}
</span>,
keywords: ['app insights'],
annotation: <BiLockAlt />,
path: PATH_ADMIN_INSIGHTS,
}, {
label: 'App Config',
annotation: <BiLockAlt />,
path: PATH_ADMIN_CONFIGURATION,
}, {
label: selectedPhotoIds === undefined
? 'Select Multiple Photos'
: 'Exit Select Multiple Photos',
annotation: <BiLockAlt />,
path: selectedPhotoIds === undefined
? PATH_GRID_INFERRED
: undefined,
action: selectedPhotoIds === undefined
? () => setSelectedPhotoIds?.([])
: () => setSelectedPhotoIds?.(undefined),
});
if (showDebugTools) {
adminSection.items.push({
label: 'Baseline Overview',
annotation: <BiLockAlt />,
path: PATH_ADMIN_CONFIGURATION,
path: PATH_ADMIN_BASELINE,
}, {
label: <span className="flex items-center gap-3">
App Insights
{insightIndicatorStatus && <FaCircle
size={8}
className={clsx(
insightIndicatorStatus === 'blue'
? 'text-blue-500'
: 'text-amber-500',
)}
/>}
</span>,
keywords: ['app insights'],
label: 'Components Overview',
annotation: <BiLockAlt />,
path: PATH_ADMIN_INSIGHTS,
}, {
label: selectedPhotoIds === undefined
? 'Select Multiple Photos'
: 'Exit Select Multiple Photos',
annotation: <BiLockAlt />,
path: selectedPhotoIds === undefined
? PATH_GRID_INFERRED
: undefined,
action: selectedPhotoIds === undefined
? () => setSelectedPhotoIds?.([])
: () => setSelectedPhotoIds?.(undefined),
}] as CommandKItem[])
.concat(showDebugTools
? [{
label: 'Baseline Overview',
path: PATH_ADMIN_BASELINE,
}, {
label: 'Components Overview',
path: PATH_ADMIN_COMPONENTS,
}]
: [])
.concat({
label: 'Sign Out',
action: () => {
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
},
})
: [{
label: 'Sign In',
path: PATH_SIGN_IN,
}],
};
path: PATH_ADMIN_COMPONENTS,
});
}
adminSection.items.push({
label: 'Sign Out',
action: () => {
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
},
});
} else {
adminSection.items.push({
label: 'Sign In',
path: PATH_SIGN_IN,
});
}
return (
<Command.Dialog

View File

@ -14,7 +14,7 @@ export default function MoreMenu({
align = 'end',
...props
}: {
items: ComponentProps<typeof MoreMenuItem> []
items: ComponentProps<typeof MoreMenuItem>[]
icon?: ReactNode
header?: ReactNode
className?: string

View File

@ -9,6 +9,8 @@ import { downloadFileFromBrowser } from '@/utility/url';
export default function MoreMenuItem({
label,
labelComplex,
annotation,
icon,
href,
hrefDownloadName,
@ -17,7 +19,9 @@ export default function MoreMenuItem({
dismissMenu,
shouldPreventDefault = true,
}: {
label: ReactNode
label: string
labelComplex?: ReactNode
annotation?: string
icon?: ReactNode
href?: string
hrefDownloadName?: string
@ -48,7 +52,7 @@ export default function MoreMenuItem({
disabled={isLoading}
className={clsx(
'flex items-center h-9',
'pl-2 pr-4 py-2 rounded-sm',
'pl-2 pr-3 py-2 rounded-sm',
'select-none hover:outline-hidden',
'hover:bg-gray-100/90 active:bg-gray-200/75',
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
@ -92,7 +96,11 @@ export default function MoreMenuItem({
styleAs="link-without-hover"
className="translate-y-[1px]"
>
{label}
{labelComplex ?? label}
{annotation &&
<span className="text-dim ml-3">
{annotation}
</span>}
</LoaderButton>
</DropdownMenu.Item>
);

View File

@ -37,11 +37,11 @@ export default function PhotoGridSidebar({
}) {
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
const { hiddenPhotosCount } = useAppState();
const { photosCountHidden } = useAppState();
const tagsIncludingHidden = useMemo(() =>
addHiddenToTags(tags, hiddenPhotosCount)
, [tags, hiddenPhotosCount]);
addHiddenToTags(tags, photosCountHidden)
, [tags, photosCountHidden]);
return (
<div className="space-y-4">

View File

@ -22,7 +22,6 @@ import { redirect } from 'next/navigation';
import { deleteFile } from '@/platforms/storage';
import {
getPhotosCached,
getPhotosMetaCached,
revalidateAdminPaths,
revalidateAllKeysAndPaths,
revalidatePhoto,
@ -416,10 +415,6 @@ export const streamAiImageQueryAction = async (
export const getImageBlurAction = async (url: string) =>
runAuthenticatedAdminServerAction(() => blurImageFromUrl(url));
export const getPhotosHiddenMetaCachedAction = async () =>
runAuthenticatedAdminServerAction(() =>
getPhotosMetaCached({ hidden: 'only' }));
// Public/Private actions
export const getPhotosAction = async (

View File

@ -26,7 +26,10 @@ export interface AppStateContext {
isUserSignedIn?: boolean
adminUpdateTimes?: Date[]
registerAdminUpdate?: () => void
hiddenPhotosCount?: number
photosCount?: number
photosCountHidden?: number
uploadsCount?: number
tagsCount?: number
selectedPhotoIds?: string[]
setSelectedPhotoIds?: Dispatch<SetStateAction<string[] | undefined>>
isPerformingSelectEdit?: boolean

View File

@ -12,11 +12,10 @@ import {
MATTE_PHOTOS,
SHOW_ZOOM_CONTROLS,
} from '@/app/config';
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
import { ShareModalProps } from '@/share';
import { storeTimezoneCookie } from '@/utility/timezone';
import { getShouldShowInsightsIndicatorAction } from '@/admin/insights/actions';
import { InsightIndicatorStatus } from '@/admin/insights';
import { getAdminDataAction } from '@/admin/actions';
export default function AppStateProvider({
children,
@ -44,8 +43,14 @@ export default function AppStateProvider({
useState<string>();
const [adminUpdateTimes, setAdminUpdateTimes] =
useState<Date[]>([]);
const [hiddenPhotosCount, setHiddenPhotosCount] =
useState(0);
const [photosCount, setPhotosCount] =
useState<number>();
const [photosCountHidden, setPhotosCountHidden] =
useState<number>();
const [uploadsCount, setUploadsCount] =
useState<number>();
const [tagsCount, setTagsCount] =
useState<number>();
const [selectedPhotoIds, setSelectedPhotoIds] =
useState<string[] | undefined>();
const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
@ -70,26 +75,37 @@ export default function AppStateProvider({
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
const { data, error } = useSWR('getAuth', getAuthAction);
const { data: auth, error: authError } = useSWR('getAuth', getAuthAction);
useEffect(() => {
if (!error) {
setUserEmail(data?.user?.email ?? undefined);
if (!authError) {
setUserEmail(auth?.user?.email ?? undefined);
}
}, [data, error]);
}, [auth, authError]);
const isUserSignedIn = Boolean(userEmail);
const { data: adminData, error: adminError } = useSWR(
isUserSignedIn ? 'getAdminData' : null,
getAdminDataAction, {
refreshInterval: 1000 * 60 * 5,
},
);
useEffect(() => {
if (isUserSignedIn) {
const timeout = setTimeout(() =>{
getPhotosHiddenMetaCachedAction()
.then(({ count }) => setHiddenPhotosCount(count));
getShouldShowInsightsIndicatorAction()
.then(setInsightIndicatorStatus);
}, 100);
return () => clearTimeout(timeout);
if (adminData) {
const timeout = setTimeout(() => {
setPhotosCount(adminData.countPhotos);
setPhotosCountHidden(adminData.countHiddenPhotos);
setUploadsCount(adminData.countUploads);
setTagsCount(adminData.countTags);
setInsightIndicatorStatus(adminData.shouldShowInsightsIndicator);
}, 100);
return () => clearTimeout(timeout);
}
} else {
setHiddenPhotosCount(0);
setPhotosCountHidden(0);
}
}, [isUserSignedIn]);
}, [adminData, adminError, isUserSignedIn]);
const registerAdminUpdate = useCallback(() =>
setAdminUpdateTimes(updates => [...updates, new Date()])
@ -125,7 +141,10 @@ export default function AppStateProvider({
isUserSignedIn,
adminUpdateTimes,
registerAdminUpdate,
hiddenPhotosCount,
photosCount,
photosCountHidden,
uploadsCount,
tagsCount,
selectedPhotoIds,
setSelectedPhotoIds,
isPerformingSelectEdit,

View File

@ -105,11 +105,11 @@ export const isPathFavs = (pathname?: string) =>
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
export const addHiddenToTags = (tags: Tags, hiddenPhotosCount = 0) => {
if (hiddenPhotosCount > 0) {
export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => {
if (photosCountHidden > 0) {
return tags
.filter(({ tag }) => tag === TAG_FAVS)
.concat({ tag: TAG_HIDDEN, count: hiddenPhotosCount })
.concat({ tag: TAG_HIDDEN, count: photosCountHidden })
.concat(tags.filter(({ tag }) => tag !== TAG_FAVS));
} else {
return tags;