Skip to content

Commit

Permalink
✨ add image usage button
Browse files Browse the repository at this point in the history
  • Loading branch information
ikesau committed Dec 11, 2024
1 parent d7ee8d7 commit 64fb22c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 19 deletions.
115 changes: 96 additions & 19 deletions adminSiteClient/ImagesIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import React, {
useMemo,
useState,
} from "react"
import { Button, Flex, Input, Mentions, Popconfirm, Table, Upload } from "antd"
import {
Button,
Flex,
Input,
Mentions,
Popconfirm,
Popover,
Table,
Upload,
} from "antd"
import { AdminLayout } from "./AdminLayout.js"
import { AdminAppContext } from "./AdminAppContext.js"
import { DbEnrichedImageWithUserId, DbPlainUser } from "@ourworldindata/types"
Expand All @@ -20,14 +29,20 @@ import {
} from "@fortawesome/free-solid-svg-icons"
import { RcFile } from "antd/es/upload/interface.js"
import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js"
import { keyBy } from "lodash"
import { Dictionary, keyBy } from "lodash"
import cx from "classnames"

type ImageMap = Record<string, DbEnrichedImageWithUserId>

type UserMap = Record<string, DbPlainUser>

type UsageInfo = {
title: string
id: string
}

type ImageEditorApi = {
getUsage: () => void
getAltText: (id: number) => Promise<{ altText: string; success: boolean }>
patchImage: (
image: DbEnrichedImageWithUserId,
Expand Down Expand Up @@ -183,12 +198,49 @@ function UserSelect({
)
}

function UsageViewer({ usage }: { usage: UsageInfo[] | undefined }) {
const content = (
<div>
{usage ? (
<ul className="ImageIndexPage__usage-list">
{usage.map((use) => (
<li key={use.id}>
<a href={`/admin/gdocs/${use.id}/preview`}>
{use.title}
</a>
</li>
))}
</ul>
) : null}
</div>
)

return (
<Popover
content={content}
title="Published posts that reference this image"
trigger="click"
>
<Button type="text" disabled={!usage || !usage.length}>
See usage
{usage ? (
<span className="ImageIndexPage__usage-chip">
{usage.length}
</span>
) : null}
</Button>
</Popover>
)
}

function createColumns({
api,
users,
usage,
}: {
api: ImageEditorApi
users: UserMap
usage: Dictionary<UsageInfo[]>
}): ColumnsType<DbEnrichedImageWithUserId> {
return [
{
Expand Down Expand Up @@ -319,22 +371,35 @@ function createColumns({
title: "Action",
key: "action",
width: 100,
render: (_, image) => (
<Flex vertical>
<PutImageButton putImage={api.putImage} id={image.id} />
<Popconfirm
title="Are you sure?"
description="This will delete the image being used in production."
onConfirm={() => api.deleteImage(image)}
okText="Yes"
cancelText="No"
>
<Button type="text" danger>
Delete
</Button>
</Popconfirm>
</Flex>
),
render: (_, image) => {
const isDeleteDisabled = !!(usage && usage[image.id]?.length)
return (
<Flex vertical>
<UsageViewer usage={usage && usage[image.id]} />
<PutImageButton putImage={api.putImage} id={image.id} />
<Popconfirm
title="Are you sure?"
description="This will delete the image being used in production."
onConfirm={() => api.deleteImage(image)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
disabled={isDeleteDisabled}
title={
isDeleteDisabled
? "This image is being used in production"
: undefined
}
>
Delete
</Button>
</Popconfirm>
</Flex>
)
},
},
]
}
Expand Down Expand Up @@ -421,10 +486,18 @@ export function ImageIndexPage() {
const { admin } = useContext(AdminAppContext)
const [images, setImages] = useState<ImageMap>({})
const [users, setUsers] = useState<UserMap>({})
const [usage, setUsage] = useState<Dictionary<UsageInfo[]>>({})
const [filenameSearchValue, setFilenameSearchValue] = useState("")

const api = useMemo(
(): ImageEditorApi => ({
getUsage: async () => {
const usage = await admin.requestJSON<{
success: true
usage: Dictionary<UsageInfo[]>
}>(`/api/images/usage`, {}, "GET")
setUsage(usage.usage)
},
getAltText: (id) => {
return admin.requestJSON<{
success: true
Expand Down Expand Up @@ -527,11 +600,15 @@ export function ImageIndexPage() {
[images, filenameSearchValue]
)

const columns = useMemo(() => createColumns({ api, users }), [api, users])
const columns = useMemo(
() => createColumns({ api, users, usage }),
[api, users, usage]
)

useEffect(() => {
void api.getImages()
void api.getUsers()
void api.getUsage()
}, [api])

return (
Expand Down
18 changes: 18 additions & 0 deletions adminSiteClient/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
}
}
}

.ImageIndexPage__unsaved-chip {
color: gray;
background-color: lightgray;
Expand All @@ -1248,3 +1249,20 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
position: relative;
}
}

.ImageIndexPage__usage-chip {
background-color: #ddd;
padding: 2px 4px;
display: inline-block;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
line-height: 12px;
}

.ImageIndexPage__usage-list {
max-width: 300px;
padding-left: 16px;
margin-top: -8px;
margin-bottom: 0;
}
9 changes: 9 additions & 0 deletions adminSiteServer/apiRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,15 @@ deleteRouteWithRWTransaction(
}
)

getRouteWithROTransaction(apiRouter, "/images/usage", async (_, __, trx) => {
const usage = await db.getImageUsage(trx)

return {
success: true,
usage,
}
})

getRouteWithROTransaction(
apiRouter,
`/gpt/suggest-topics/${TaggableType.Charts}/:chartId.json`,
Expand Down
37 changes: 37 additions & 0 deletions db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,3 +741,40 @@ export function getCloudflareImage(
[filename]
)
}

/**
* Get the title, slug, and googleId of all gdocs that reference each image
*/
export function getImageUsage(trx: KnexReadonlyTransaction): Promise<
Record<
number,
{
title: string
id: string
}[]
>
> {
return knexRaw<{
imageId: number
posts: string
}>(
trx,
`-- sql
SELECT
i.id as imageId,
JSON_ARRAYAGG(
JSON_OBJECT(
'title', p.content->>'$.title',
'id', p.id
)
) as posts
FROM posts_gdocs p
JOIN posts_gdocs_x_images pi ON p.id = pi.gdocId
JOIN images i ON pi.imageId = i.id
GROUP BY i.id`
).then((results) =>
Object.fromEntries(
results.map((result) => [result.imageId, JSON.parse(result.posts)])
)
)
}

0 comments on commit 64fb22c

Please sign in to comment.