From 838fdf5441db9185a6e1ec117e573e7b3df924e0 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Wed, 12 Feb 2025 18:40:12 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=8E=89=20make=20tags=20editable=20on?= =?UTF-8?q?=20the=20DI=20index=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 75 +++++++++++++++++------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index f876b6a80a..28d84c05ac 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,10 @@ import { Admin } from "./Admin.js" import { ALL_GRAPHER_CHART_TYPES, DbEnrichedImageWithUserId, - DbPlainTag, GRAPHER_MAP_TYPE, GrapherChartOrMapType, OwidGdocDataInsightIndexItem, + MinimalTag, } from "@ourworldindata/types" import { copyToClipboard, @@ -58,6 +65,7 @@ import { } from "./imagesHelpers.js" import { ReuploadImageForDataInsightModal } from "./ReuploadImageForDataInsightModal.js" import { CreateDataInsightModal } from "./CreateDataInsightModal.js" +import { EditableTags } from "./EditableTags.js" type NarrativeDataInsightIndexItem = RequiredBy< OwidGdocDataInsightIndexItem, @@ -88,9 +96,11 @@ const copyIcon = const panoramaIcon = const plusIcon = -const NotificationContext = React.createContext(null) +const NotificationContext = createContext(null) function createColumns(ctx: { + availableTags: MinimalTag[] + updateTags: (gdocId: string, tags: MinimalTag[]) => Promise highlightFn: ( text: string | null | undefined ) => React.ReactElement | string @@ -154,10 +164,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})`} @@ -165,21 +175,18 @@ function createColumns(ctx: { ), }, { - title: "Topic tags", + title: "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.availableTags} + /> + ), }, { title: "Published", @@ -291,6 +298,8 @@ export function DataInsightIndexPage() { const [dataInsights, setDataInsights, refreshDataInsights] = useDataInsights(admin) + const [availableTags, setAvailableTags] = useState([]) + const [searchValue, setSearchValue] = useState("") const [chartTypeFilter, setChartTypeFilter] = useState< GrapherChartOrMapType | "all" @@ -361,6 +370,23 @@ export function DataInsightIndexPage() { ) }, [dataInsights, 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) @@ -369,10 +395,12 @@ export function DataInsightIndexPage() { ) => setDataInsightForImageUpload(dataInsight) return createColumns({ + availableTags, + updateTags, highlightFn, triggerImageUploadFlow, }) - }, [searchWords]) + }, [searchWords, availableTags, updateTags]) const updateDataInsightPreview = ( dataInsightId: string, @@ -419,6 +447,13 @@ export function DataInsightIndexPage() { } } + useEffect(() => { + const fetchTags = async () => + (await admin.getJSON("/api/tags.json")) as { tags: MinimalTag[] } + + void fetchTags().then((result) => setAvailableTags(result.tags)) + }, [admin]) + return ( From d514d1cce8c673e4ff8774d0d1598cef6829a881 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 17 Feb 2025 10:48:35 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20add=20topic=20tag?= =?UTF-8?q?=20filter=20on=20di=20index=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 46 ++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 28d84c05ac..086dd08856 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -175,7 +175,7 @@ function createColumns(ctx: { ), }, { - title: "Tags", + title: "Topic tags", dataIndex: "tags", key: "tags", render: (tags, dataInsight) => ( @@ -301,6 +301,7 @@ export function DataInsightIndexPage() { const [availableTags, setAvailableTags] = useState([]) const [searchValue, setSearchValue] = useState("") + const [topicTagFilter, setTopicTagFilter] = useState() const [chartTypeFilter, setChartTypeFilter] = useState< GrapherChartOrMapType | "all" >(DEFAULT_CHART_TYPE_FILTER) @@ -322,6 +323,13 @@ 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 ) => { @@ -364,11 +372,18 @@ 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[]) => { @@ -459,8 +474,12 @@ export function DataInsightIndexPage() { {notificationContextHolder}
- - + + { if (e.key === "Escape") setSearchValue("") }} - style={{ width: 500, marginBottom: 20 }} + style={{ width: 350 }} + /> + ({ value: type, @@ -517,6 +549,7 @@ export function DataInsightIndexPage() { type="dashed" onClick={() => { setSearchValue("") + setTopicTagFilter(undefined) setChartTypeFilter( DEFAULT_CHART_TYPE_FILTER ) @@ -532,6 +565,7 @@ export function DataInsightIndexPage() { setLayout(e.target.value)} + block > List From 324fd4b2af959a52714500b83a117235e3dd6140 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 17 Feb 2025 10:55:22 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20(admin)=20only=20offer=20topic?= =?UTF-8?q?=20tags=20for=20DIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 086dd08856..e50a2226b8 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -46,6 +46,7 @@ import { GrapherChartOrMapType, OwidGdocDataInsightIndexItem, MinimalTag, + MinimalTagWithIsTopic, } from "@ourworldindata/types" import { copyToClipboard, @@ -99,7 +100,7 @@ const plusIcon = const NotificationContext = createContext(null) function createColumns(ctx: { - availableTags: MinimalTag[] + availableTopicTags: MinimalTag[] updateTags: (gdocId: string, tags: MinimalTag[]) => Promise highlightFn: ( text: string | null | undefined @@ -112,6 +113,7 @@ function createColumns(ctx: { { title: "Preview", key: "preview", + width: 200, render: (_, dataInsight) => hasImage(dataInsight) ? ( <> @@ -184,7 +186,7 @@ function createColumns(ctx: { onSave={(tags) => ctx.updateTags(dataInsight.id, tags as MinimalTag[]) } - suggestions={ctx.availableTags} + suggestions={ctx.availableTopicTags} /> ), }, @@ -298,7 +300,9 @@ export function DataInsightIndexPage() { const [dataInsights, setDataInsights, refreshDataInsights] = useDataInsights(admin) - const [availableTags, setAvailableTags] = useState([]) + const [availableTopicTags, setAvailableTopicTags] = useState( + [] + ) const [searchValue, setSearchValue] = useState("") const [topicTagFilter, setTopicTagFilter] = useState() @@ -410,12 +414,12 @@ export function DataInsightIndexPage() { ) => setDataInsightForImageUpload(dataInsight) return createColumns({ - availableTags, + availableTopicTags, updateTags, highlightFn, triggerImageUploadFlow, }) - }, [searchWords, availableTags, updateTags]) + }, [searchWords, availableTopicTags, updateTags]) const updateDataInsightPreview = ( dataInsightId: string, @@ -463,10 +467,12 @@ export function DataInsightIndexPage() { } useEffect(() => { - const fetchTags = async () => - (await admin.getJSON("/api/tags.json")) as { tags: MinimalTag[] } + const fetchTags = () => + admin.getJSON<{ tags: MinimalTagWithIsTopic[] }>("/api/tags.json") - void fetchTags().then((result) => setAvailableTags(result.tags)) + void fetchTags().then((result) => + setAvailableTopicTags(result.tags.filter((tag) => tag.isTopic)) + ) }, [admin]) return ( @@ -493,7 +499,7 @@ export function DataInsightIndexPage() { value={topicTagFilter} placeholder="Select a topic tag..." allowClear - options={availableTags.map((tag) => ({ + options={availableTopicTags.map((tag) => ({ value: tag.name, label: tag.name, }))} From 6be295bcf22c95782b507e6656a8404094d8c448 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 17 Feb 2025 10:59:57 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A8=20use=20undefind=20instead=20o?= =?UTF-8?q?f=20'all'=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 42 +++++++++--------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index e50a2226b8..0bffc43d0a 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -81,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 = @@ -307,10 +305,11 @@ export function DataInsightIndexPage() { 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] = @@ -337,13 +336,14 @@ export function DataInsightIndexPage() { 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 @@ -357,8 +357,6 @@ export function DataInsightIndexPage() { dataInsight.published && dayjs(dataInsight.publishedAt).isBefore(dayjs()) ) - case "all": - return true } } @@ -498,7 +496,6 @@ export function DataInsightIndexPage() { ({ value: type, label: startCase(type), @@ -527,15 +522,13 @@ export function DataInsightIndexPage() { onChange={(value: ChartTypeFilter) => setChartTypeFilter(value) } + allowClear popupMatchSelectWidth={false} />