diff --git a/packages/@liexp/shared/src/endpoints/admin.endpoints.ts b/packages/@liexp/shared/src/endpoints/admin.endpoints.ts index 69f4869fd..5d9784f6c 100644 --- a/packages/@liexp/shared/src/endpoints/admin.endpoints.ts +++ b/packages/@liexp/shared/src/endpoints/admin.endpoints.ts @@ -98,12 +98,15 @@ export const GetMediaStats = Endpoint({ data: t.strict({ orphans: t.array(t.any), match: t.array(t.any), - temp: t.array(t.any) + temp: t.array(t.any), + noThumbnails: t.array(t.any) }), totals: t.strict({ orphans: t.number, match: t.number, - temp: t.number + temp: t.number, + noThumbnails: t.number + }), total: t.number, }), diff --git a/packages/@liexp/shared/src/io/http/Media.ts b/packages/@liexp/shared/src/io/http/Media.ts index 22d6e74d6..3ce0a7fbb 100644 --- a/packages/@liexp/shared/src/io/http/Media.ts +++ b/packages/@liexp/shared/src/io/http/Media.ts @@ -66,6 +66,7 @@ export const GetListMediaQuery = t.type( ids: optionFromNullable(t.array(UUID)), exclude: optionFromNullable(t.array(UUID)), description: optionFromNullable(t.string), + emptyThumbnail: optionFromNullable(BooleanFromString), emptyEvents: optionFromNullable(BooleanFromString), emptyLinks: optionFromNullable(BooleanFromString), emptyAreas: optionFromNullable(BooleanFromString), diff --git a/packages/@liexp/ui/src/components/admin/media/MediaList.tsx b/packages/@liexp/ui/src/components/admin/media/MediaList.tsx index dcf887e56..5286bf618 100644 --- a/packages/@liexp/ui/src/components/admin/media/MediaList.tsx +++ b/packages/@liexp/ui/src/components/admin/media/MediaList.tsx @@ -8,12 +8,12 @@ import { FunctionField, List, LoadingPage, + NumberInput, ReferenceField, TextInput, useGetIdentity, usePermissions, type ListProps, - NumberInput, } from "react-admin"; import { Box, Typography, amber } from "../../mui"; import { toFormattedDuration } from "./DurationField"; @@ -24,6 +24,7 @@ const RESOURCE = "media"; const mediaFilters = [ , + , , , , diff --git a/services/admin-web/src/pages/dashboard/AdminStats.tsx b/services/admin-web/src/pages/dashboard/AdminStats.tsx index f34ea43aa..54a68071e 100644 --- a/services/admin-web/src/pages/dashboard/AdminStats.tsx +++ b/services/admin-web/src/pages/dashboard/AdminStats.tsx @@ -11,8 +11,13 @@ import { } from "@liexp/ui/lib/components/mui"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; +import { useNavigate } from "react-router"; export const AdminStats: React.FC = () => { + const navigate = useNavigate(); + const onNoThumbnailsClick = React.useCallback(() => { + navigate(`/media?filter=${JSON.stringify({ emptyThumbnail: true })}`); + }, []); return ( @@ -34,6 +39,18 @@ export const AdminStats: React.FC = () => { Media + + No Thumbnails: + + {data.noThumbnails.length} + + + = + (ctx) => () => { + return pipe( + ctx.db.find(MediaEntity, { + where: { + thumbnail: IsNull(), + }, + }), + ); + }; + +export const getMediaAdminStatsFlow: TEFlow<[], any> = (ctx) => () => { + return pipe( + sequenceS(fp.TE.ApplicativePar)({ + orphans: getOrphanMediaFlow(ctx)(), + temp: getTempMediaCountFlow(ctx)(), + noThumbnails: getMediaWithoutThumbnailsFlow(ctx)(), + }), + ); +}; diff --git a/services/api/src/queries/media/fetchManyMedia.query.ts b/services/api/src/queries/media/fetchManyMedia.query.ts index cd401760a..f1fc259e2 100644 --- a/services/api/src/queries/media/fetchManyMedia.query.ts +++ b/services/api/src/queries/media/fetchManyMedia.query.ts @@ -14,6 +14,7 @@ const defaultQuery: http.Media.GetListMediaQuery = { description: fp.O.none, events: fp.O.none, keywords: fp.O.none, + emptyThumbnail: fp.O.none, emptyEvents: fp.O.none, emptyLinks: fp.O.none, emptyAreas: fp.O.none, @@ -39,6 +40,7 @@ export const fetchManyMedia: TEFlow< events, emptyEvents, emptyLinks, + emptyThumbnail, emptyAreas, deletedOnly, exclude, @@ -130,6 +132,12 @@ export const fetchManyMedia: TEFlow< } } + if (fp.O.isSome(emptyThumbnail) && emptyThumbnail.value) { + const where = hasWhere ? q.andWhere.bind(q) : q.where.bind(q); + where("media.thumbnail IS NULL"); + hasWhere = true; + } + if (fp.O.isSome(emptyLinks) && emptyLinks.value) { const where = hasWhere ? q.andWhere.bind(q) : q.where.bind(q); where("links.id IS NULL"); diff --git a/services/api/src/routes/admin/media/getMediaStats.controller.ts b/services/api/src/routes/admin/media/getMediaStats.controller.ts index 261b5d4ab..9c5a668fe 100644 --- a/services/api/src/routes/admin/media/getMediaStats.controller.ts +++ b/services/api/src/routes/admin/media/getMediaStats.controller.ts @@ -1,9 +1,7 @@ import { pipe } from "@liexp/core/lib/fp/index.js"; import { AddEndpoint, Endpoints } from "@liexp/shared/lib/endpoints/index.js"; -import { sequenceS } from "fp-ts/lib/Apply.js"; import * as TE from "fp-ts/lib/TaskEither.js"; -import { getOrphanMediaFlow } from "#flows/media/getOrphanMedia.flow.js"; -import { getTempMediaCountFlow } from "#flows/media/getTempMediaCount.flow.js"; +import { getMediaAdminStatsFlow } from "#flows/admin/media/getMediaAdminStats.flow.js"; import { type Route } from "#routes/route.types.js"; import { authenticationHandler } from "#utils/authenticationHandler.js"; @@ -12,18 +10,16 @@ export const MakeAdminGetMediaStatsRoute: Route = (r, ctx) => { Endpoints.Admin.Custom.GetMediaStats, () => { return pipe( - sequenceS(TE.ApplicativePar)({ - orphans: getOrphanMediaFlow(ctx)(), - temp: getTempMediaCountFlow(ctx)(), - }), - TE.map(({ orphans: data, temp }) => ({ + getMediaAdminStatsFlow(ctx)(), + TE.map(({ orphans: data, temp, noThumbnails }) => ({ body: { - data: { ...data, temp }, + data: { ...data, temp, noThumbnails }, total: data.orphans.length + data.match.length, totals: { orphans: data.orphans.length, match: data.match.length, temp: temp.length, + noThumbnails: noThumbnails.length, }, }, statusCode: 201,