diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index c53d46a418..6dd7944a37 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -1,5 +1,12 @@ -import { useCallback, useContext, useEffect, useMemo, useState } from "react" -import * as React from "react" +import { + useContext, + useEffect, + useMemo, + useState, + useCallback, + createContext, + Fragment, +} from "react" import { Button, Card, @@ -35,10 +42,11 @@ import { Admin } from "./Admin.js" import { ALL_GRAPHER_CHART_TYPES, DbEnrichedImageWithUserId, - DbPlainTag, GRAPHER_MAP_TYPE, GrapherChartOrMapType, OwidGdocDataInsightIndexItem, + MinimalTag, + MinimalTagWithIsTopic, } from "@ourworldindata/types" import { copyToClipboard, @@ -58,6 +66,7 @@ import { } from "./imagesHelpers.js" import { ReuploadImageForDataInsightModal } from "./ReuploadImageForDataInsightModal.js" import { CreateDataInsightModal } from "./CreateDataInsightModal.js" +import { EditableTags } from "./EditableTags.js" type NarrativeDataInsightIndexItem = RequiredBy< OwidGdocDataInsightIndexItem, @@ -72,12 +81,10 @@ type DataInsightIndexItemThatCanBeUploaded = | NarrativeDataInsightIndexItem | FigmaDataInsightIndexItem -type ChartTypeFilter = GrapherChartOrMapType | "all" -type PublicationFilter = "all" | "published" | "scheduled" | "draft" +type ChartTypeFilter = GrapherChartOrMapType +type PublicationFilter = "published" | "scheduled" | "draft" type Layout = "list" | "gallery" -const DEFAULT_CHART_TYPE_FILTER: ChartTypeFilter = "all" -const DEFAULT_PUBLICATION_FILTER: PublicationFilter = "all" const DEFAULT_LAYOUT: Layout = "list" const editIcon = @@ -88,9 +95,11 @@ const copyIcon = const panoramaIcon = const plusIcon = -const NotificationContext = React.createContext(null) +const NotificationContext = createContext(null) function createColumns(ctx: { + availableTopicTags: MinimalTag[] + updateTags: (gdocId: string, tags: MinimalTag[]) => Promise highlightFn: ( text: string | null | undefined ) => React.ReactElement | string @@ -102,6 +111,7 @@ function createColumns(ctx: { { title: "Preview", key: "preview", + width: 200, render: (_, dataInsight) => hasImage(dataInsight) ? ( <> @@ -152,10 +162,10 @@ function createColumns(ctx: { render: (authors: string[], dataInsight) => ( <> {authors.map((author, index) => ( - + {ctx.highlightFn(author)} {index < authors.length - 1 ? ", " : ""} - + ))} {dataInsight.approvedBy && ` (approved by ${dataInsight.approvedBy})`} @@ -166,18 +176,15 @@ function createColumns(ctx: { title: "Topic tags", dataIndex: "tags", key: "tags", - render: (tags: DbPlainTag[]) => - tags.map((tag) => ( - - {ctx.highlightFn(tag.name)} - - )), + render: (tags, dataInsight) => ( + + ctx.updateTags(dataInsight.id, tags as MinimalTag[]) + } + suggestions={ctx.availableTopicTags} + /> + ), }, { title: "Published", @@ -289,12 +296,18 @@ export function DataInsightIndexPage() { const [dataInsights, setDataInsights, refreshDataInsights] = useDataInsights(admin) + const [availableTopicTags, setAvailableTopicTags] = useState( + [] + ) + const [searchValue, setSearchValue] = useState("") + const [topicTagFilter, setTopicTagFilter] = useState() const [chartTypeFilter, setChartTypeFilter] = useState< - GrapherChartOrMapType | "all" - >(DEFAULT_CHART_TYPE_FILTER) - const [publicationFilter, setPublicationFilter] = - useState(DEFAULT_PUBLICATION_FILTER) + GrapherChartOrMapType | undefined + >() + const [publicationFilter, setPublicationFilter] = useState< + PublicationFilter | undefined + >() const [layout, setLayout] = useState(DEFAULT_LAYOUT) const [dataInsightForImageUpload, setDataInsightForImageUpload] = @@ -311,16 +324,24 @@ export function DataInsightIndexPage() { ) const filteredDataInsights = useMemo(() => { + const topicTagFilterFn = ( + dataInsight: OwidGdocDataInsightIndexItem + ) => { + if (!topicTagFilter) return true + return dataInsight.tags?.some((tag) => tag.name === topicTagFilter) + } + const chartTypeFilterFn = ( dataInsight: OwidGdocDataInsightIndexItem ) => { - if (chartTypeFilter === "all") return true + if (!chartTypeFilter) return true return dataInsight.chartType === chartTypeFilter } const publicationFilterFn = ( dataInsight: OwidGdocDataInsightIndexItem ) => { + if (!publicationFilter) return true switch (publicationFilter) { case "draft": return !dataInsight.published @@ -334,8 +355,6 @@ export function DataInsightIndexPage() { dataInsight.published && dayjs(dataInsight.publishedAt).isBefore(dayjs()) ) - case "all": - return true } } @@ -353,11 +372,35 @@ export function DataInsightIndexPage() { return dataInsights.filter( (di) => + topicTagFilterFn(di) && chartTypeFilterFn(di) && publicationFilterFn(di) && searchFilterFn(di) ) - }, [dataInsights, chartTypeFilter, publicationFilter, searchWords]) + }, [ + dataInsights, + topicTagFilter, + chartTypeFilter, + publicationFilter, + searchWords, + ]) + + const updateTags = useCallback( + async (gdocId: string, tags: MinimalTag[]) => { + const json = await admin.requestJSON( + `/api/gdocs/${gdocId}/setTags`, + { tagIds: tags.map((t) => t.id) }, + "POST" + ) + if (json.success) { + const dataInsight = dataInsights.find( + (gdoc) => gdoc.id === gdocId + ) + if (dataInsight) dataInsight.tags = tags + } + }, + [admin, dataInsights] + ) const columns = useMemo(() => { const highlightFn = highlightFunctionForSearchWords(searchWords) @@ -367,10 +410,12 @@ export function DataInsightIndexPage() { ) => setDataInsightForImageUpload(dataInsight) return createColumns({ + availableTopicTags, + updateTags, highlightFn, triggerImageUploadFlow, }) - }, [searchWords]) + }, [searchWords, availableTopicTags, updateTags]) const updateDataInsightPreview = ( dataInsightId: string, @@ -417,13 +462,26 @@ export function DataInsightIndexPage() { } } + useEffect(() => { + const fetchTags = () => + admin.getJSON<{ tags: MinimalTagWithIsTopic[] }>("/api/tags.json") + + void fetchTags().then((result) => + setAvailableTopicTags(result.tags.filter((tag) => tag.isTopic)) + ) + }, [admin]) + return ( {notificationContextHolder}
- - + + { if (e.key === "Escape") setSearchValue("") }} - style={{ width: 500, marginBottom: 20 }} + style={{ width: 350 }} + /> + ({ value: type, label: startCase(type), @@ -452,15 +520,13 @@ export function DataInsightIndexPage() { onChange={(value: ChartTypeFilter) => setChartTypeFilter(value) } + allowClear popupMatchSelectWidth={false} />