From b2d1549c9960b1f3862d142117028244d3d5cecd Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Fri, 24 Jan 2025 16:07:29 +0100 Subject: [PATCH 01/26] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20add=20data=20ins?= =?UTF-8?q?ight=20index=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/AdminApp.tsx | 9 + adminSiteClient/AdminSidebar.tsx | 6 + adminSiteClient/DataInsightIndexPage.tsx | 211 ++++++++++++++++++ adminSiteServer/apiRouter.ts | 8 + adminSiteServer/apiRoutes/dataInsights.ts | 66 ++++++ db/model/Gdoc/GdocFactory.ts | 26 ++- .../types/src/gdocTypes/Gdoc.ts | 13 ++ packages/@ourworldindata/types/src/index.ts | 1 + 8 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 adminSiteClient/DataInsightIndexPage.tsx create mode 100644 adminSiteServer/apiRoutes/dataInsights.ts diff --git a/adminSiteClient/AdminApp.tsx b/adminSiteClient/AdminApp.tsx index 4f61bf023c..2837b5fc92 100644 --- a/adminSiteClient/AdminApp.tsx +++ b/adminSiteClient/AdminApp.tsx @@ -45,6 +45,7 @@ import { IndicatorChartEditorPage } from "./IndicatorChartEditorPage.js" import { ChartViewEditorPage } from "./ChartViewEditorPage.js" import { ChartViewIndexPage } from "./ChartViewIndexPage.js" import { ImageIndexPage } from "./ImagesIndexPage.js" +import { DataInsightIndexPage } from "./DataInsightIndexPage.js" @observer class AdminErrorMessage extends React.Component<{ admin: Admin }> { @@ -337,6 +338,14 @@ export class AdminApp extends React.Component<{ )} /> + ( + + + + )} + /> ( Narrative charts +
  • + + Data insights + +
  • Posts diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx new file mode 100644 index 0000000000..ac20755ad9 --- /dev/null +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -0,0 +1,211 @@ +import { useContext, useEffect, useMemo, useState } from "react" +import * as React from "react" +import { Button, Flex, Input, Space, Table, Tag } from "antd" + +import { AdminLayout } from "./AdminLayout.js" +import { Timeago } from "./Forms.js" +import { ColumnsType } from "antd/es/table/InternalTable.js" +import { + buildSearchWordsFromSearchString, + filterFunctionForSearchWords, + highlightFunctionForSearchWords, +} from "../adminShared/search.js" +import { GdocsStoreContext } from "./GdocsStoreContext.js" +import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" +import { dayjs } from "@ourworldindata/utils" + +function createColumns(ctx: { + highlightFn: ( + text: string | null | undefined + ) => React.ReactElement | string +}): ColumnsType { + return [ + { + title: "Title", + dataIndex: "title", + key: "title", + width: 300, + render: (title) => ctx.highlightFn(title), + }, + { + title: "Authors", + dataIndex: "authors", + key: "authors", + width: 150, + render: (authors: string[], dataInsight) => ( + <> + {authors.map((author, index) => ( + + {ctx.highlightFn(author)} + {index < authors.length - 1 ? ", " : ""} + + ))} + {dataInsight["approved-by"] && + ` (approved by ${dataInsight["approved-by"]})`} + + ), + }, + { + title: "Tags", + dataIndex: "tags", + key: "tags", + render: (tags: DbPlainTag[]) => + tags.map((tag) => ( + + + + {ctx.highlightFn(tag.name)} + + + + )), + }, + { + title: "Published", + dataIndex: "publishedAt", + key: "publishedAt", + render: (publishedAt) => { + if (!publishedAt) return undefined + const publicationDate = dayjs(publishedAt) + const isScheduledForPublication = + publicationDate.isAfter(dayjs()) + if (isScheduledForPublication) + return ( + <> + Scheduled for publication{" "} + + + ) + return ( + <> + Published + + ) + }, + }, + { + title: "Links", + key: "links", + render: (_, dataInsight) => ( + + + {dataInsight["narrative-view"] && ( + + )} + {dataInsight["grapher-url"] && ( + + )} + {dataInsight["explorer-url"] && ( + + )} + {dataInsight["figma-url"] && ( + + )} + + ), + }, + { + title: "Actions", + key: "actions", + render: (_, dataInsight) => ( + + ), + }, + ] +} + +export function DataInsightIndexPage() { + const context = useContext(GdocsStoreContext) + const [dataInsights, setDataInsights] = useState< + OwidGdocDataInsightIndexItem[] + >([]) + const [searchValue, setSearchValue] = useState("") + + const searchWords = useMemo( + () => buildSearchWordsFromSearchString(searchValue), + [searchValue] + ) + + const filteredDataInsights = useMemo(() => { + const filterFn = filterFunctionForSearchWords( + searchWords, + (dataInsight: OwidGdocDataInsightIndexItem) => [ + dataInsight.title, + dataInsight.slug, + ...(dataInsight.tags ?? []).map((tag) => tag.name), + ...dataInsight.authors, + dataInsight.markdown ?? "", + ] + ) + + return dataInsights.filter(filterFn) + }, [dataInsights, searchWords]) + + const highlightFn = useMemo( + () => highlightFunctionForSearchWords(searchWords), + [searchWords] + ) + + const columns = useMemo(() => createColumns({ highlightFn }), [highlightFn]) + + useEffect(() => { + const fetchAllDataInsights = async () => + (await context?.admin.getJSON( + "/api/dataInsights" + )) as OwidGdocDataInsightIndexItem[] + + void fetchAllDataInsights().then((dataInsights) => + setDataInsights(dataInsights) + ) + }, [context?.admin]) + + return ( + +
    + + setSearchValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") setSearchValue("") + }} + style={{ width: 500, marginBottom: 20 }} + /> + + + + + ) +} diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index 67daf92055..0f897bc758 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -125,6 +125,7 @@ import { deleteChart, getChartTagsJson, } from "./apiRoutes/charts.js" +import { getAllDataInsightIndexItems } from "./apiRoutes/dataInsights.js" const apiRouter = new FunctionalRouter() @@ -250,6 +251,13 @@ putRouteWithRWTransaction(apiRouter, "/gdocs/:id", createOrUpdateGdoc) deleteRouteWithRWTransaction(apiRouter, "/gdocs/:id", deleteGdoc) postRouteWithRWTransaction(apiRouter, "/gdocs/:gdocId/setTags", setGdocTags) +// Data insight routes +getRouteWithROTransaction( + apiRouter, + "/dataInsights", + getAllDataInsightIndexItems +) + // Images routes getRouteNonIdempotentWithRWTransaction( apiRouter, diff --git a/adminSiteServer/apiRoutes/dataInsights.ts b/adminSiteServer/apiRoutes/dataInsights.ts new file mode 100644 index 0000000000..3462acbbe9 --- /dev/null +++ b/adminSiteServer/apiRoutes/dataInsights.ts @@ -0,0 +1,66 @@ +import e from "express" +import { Request } from "../authentication.js" +import { + DbRawPostGdoc, + OwidGdocDataInsightIndexItem, + OwidGdocDataInsightInterface, + parsePostsGdocsRow, + PostsGdocsTableName, +} from "@ourworldindata/types" +import * as db from "../../db/db.js" +import { getTagsGroupedByGdocId } from "../../db/model/Gdoc/GdocFactory.js" + +export async function getAllDataInsightIndexItems( + req: Request, + res: e.Response>, + trx: db.KnexReadonlyTransaction +) { + return getAllDataInsightIndexItemsOrderedByUpdatedAt(trx) +} + +async function getAllDataInsightIndexItemsOrderedByUpdatedAt( + knex: db.KnexReadonlyTransaction +): Promise { + const dataInsights: DbRawPostGdoc[] = await knex + .table(PostsGdocsTableName) + .where("type", "data-insight") + .orderBy("updatedAt", "desc") + const groupedTags = await getTagsGroupedByGdocId( + knex, + dataInsights.map((gdoc) => gdoc.id) + ) + return dataInsights.map((gdoc) => + extractDataInsightIndexItem({ + ...(parsePostsGdocsRow(gdoc) as OwidGdocDataInsightInterface), + tags: groupedTags[gdoc.id] ? groupedTags[gdoc.id] : null, + }) + ) +} + +function extractDataInsightIndexItem( + gdoc: OwidGdocDataInsightInterface +): OwidGdocDataInsightIndexItem { + const grapherUrl = gdoc.content["grapher-url"]?.trim() + const isGrapherUrl = grapherUrl?.startsWith( + "https://ourworldindata.org/grapher/" + ) + const isExplorerUrl = grapherUrl?.startsWith( + "https://ourworldindata.org/explorers/" + ) + + return { + id: gdoc.id, + slug: gdoc.slug, + tags: gdoc.tags ?? [], + published: gdoc.published, + publishedAt: gdoc.publishedAt, + title: gdoc.content.title ?? "", + authors: gdoc.content.authors, + markdown: gdoc.markdown, + "approved-by": gdoc.content["approved-by"], + "narrative-view": gdoc.content["narrative-view"], + "grapher-url": isGrapherUrl ? grapherUrl : undefined, + "explorer-url": isExplorerUrl ? grapherUrl : undefined, + "figma-url": gdoc.content["figma-url"], + } +} diff --git a/db/model/Gdoc/GdocFactory.ts b/db/model/Gdoc/GdocFactory.ts index baa58daa4e..132003bb4d 100644 --- a/db/model/Gdoc/GdocFactory.ts +++ b/db/model/Gdoc/GdocFactory.ts @@ -634,6 +634,22 @@ export async function upsertGdoc( } } +export async function getTagsGroupedByGdocId( + knex: KnexReadonlyTransaction, + gdocIds: string[] +): Promise> { + const tags = await knexRaw>( + knex, + `-- sql + SELECT gt.gdocId as gdocId, tags.* + FROM tags + JOIN posts_gdocs_x_tags gt ON gt.tagId = tags.id + WHERE gt.gdocId in (:ids)`, + { ids: gdocIds } + ) + return groupBy(tags, "gdocId") +} + export async function getAllGdocIndexItemsOrderedByUpdatedAt( knex: KnexReadonlyTransaction ): Promise { @@ -643,16 +659,10 @@ export async function getAllGdocIndexItemsOrderedByUpdatedAt( const gdocs: DbRawPostGdoc[] = await knex .table(PostsGdocsTableName) .orderBy("updatedAt", "desc") - const tagsForGdocs = await knexRaw>( + const groupedTags = await getTagsGroupedByGdocId( knex, - `-- sql - SELECT gt.gdocId as gdocId, tags.* - FROM tags - JOIN posts_gdocs_x_tags gt ON gt.tagId = tags.id - WHERE gt.gdocId in (:ids)`, - { ids: gdocs.map((gdoc) => gdoc.id) } + gdocs.map((gdoc) => gdoc.id) ) - const groupedTags = groupBy(tagsForGdocs, "gdocId") return gdocs.map((gdoc) => extractGdocIndexItem({ ...parsePostsGdocsRow(gdoc), diff --git a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts index 7c58f42dcd..a87a4fc14d 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts @@ -160,6 +160,19 @@ export interface OwidGdocDataInsightContent { type: OwidGdocType.DataInsight } +export type OwidGdocDataInsightIndexItem = Pick< + OwidGdocBaseInterface, + "id" | "slug" | "tags" | "published" | "publishedAt" | "markdown" +> & { "explorer-url"?: string } & Pick< + OwidGdocDataInsightContent, + | "title" + | "authors" + | "narrative-view" + | "grapher-url" + | "figma-url" + | "approved-by" + > & { "explorer-url"?: string } + export const DATA_INSIGHTS_INDEX_PAGE_SIZE = 20 export interface OwidGdocDataInsightInterface extends OwidGdocBaseInterface { diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index e3a4d69a12..f8b026f109 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -338,6 +338,7 @@ export { type OwidGdocIndexItem, extractGdocIndexItem, type ChartViewInfo, + type OwidGdocDataInsightIndexItem, } from "./gdocTypes/Gdoc.js" export { From aab82b207092fce59d75c0ce778cdce75fc0834c Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Fri, 24 Jan 2025 17:35:14 +0100 Subject: [PATCH 02/26] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20add=20chart=20ty?= =?UTF-8?q?pe=20to=20data=20insight=20index=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 10 +- adminSiteServer/apiRoutes/dataInsights.ts | 161 ++++++++++++++++-- packages/@ourworldindata/grapher/src/index.ts | 5 +- .../types/src/gdocTypes/Gdoc.ts | 8 +- 4 files changed, 170 insertions(+), 14 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index ac20755ad9..7e2ece0942 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -12,7 +12,7 @@ import { } from "../adminShared/search.js" import { GdocsStoreContext } from "./GdocsStoreContext.js" import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" -import { dayjs } from "@ourworldindata/utils" +import { dayjs, startCase } from "@ourworldindata/utils" function createColumns(ctx: { highlightFn: ( @@ -64,6 +64,13 @@ function createColumns(ctx: { )), }, + { + title: "Chart type", + dataIndex: "chartType", + key: "chartType", + width: 150, + render: (chartType) => ctx.highlightFn(startCase(chartType)), + }, { title: "Published", dataIndex: "publishedAt", @@ -163,6 +170,7 @@ export function DataInsightIndexPage() { (dataInsight: OwidGdocDataInsightIndexItem) => [ dataInsight.title, dataInsight.slug, + startCase(dataInsight.chartType), ...(dataInsight.tags ?? []).map((tag) => tag.name), ...dataInsight.authors, dataInsight.markdown ?? "", diff --git a/adminSiteServer/apiRoutes/dataInsights.ts b/adminSiteServer/apiRoutes/dataInsights.ts index 3462acbbe9..542fe90cd4 100644 --- a/adminSiteServer/apiRoutes/dataInsights.ts +++ b/adminSiteServer/apiRoutes/dataInsights.ts @@ -1,14 +1,23 @@ import e from "express" import { Request } from "../authentication.js" import { + DbRawChartConfig, DbRawPostGdoc, + GRAPHER_CHART_TYPES, + GRAPHER_MAP_TYPE, + GRAPHER_TAB_QUERY_PARAMS, + GrapherChartOrMapType, + GrapherChartType, + GrapherInterface, OwidGdocDataInsightIndexItem, OwidGdocDataInsightInterface, + parseChartConfig, parsePostsGdocsRow, - PostsGdocsTableName, } from "@ourworldindata/types" import * as db from "../../db/db.js" import { getTagsGroupedByGdocId } from "../../db/model/Gdoc/GdocFactory.js" +import { mapQueryParamToChartTypeName } from "@ourworldindata/grapher" +import { getTimeDomainFromQueryString } from "@ourworldindata/utils" export async function getAllDataInsightIndexItems( req: Request, @@ -21,24 +30,43 @@ export async function getAllDataInsightIndexItems( async function getAllDataInsightIndexItemsOrderedByUpdatedAt( knex: db.KnexReadonlyTransaction ): Promise { - const dataInsights: DbRawPostGdoc[] = await knex - .table(PostsGdocsTableName) - .where("type", "data-insight") - .orderBy("updatedAt", "desc") + const dataInsights = await db.knexRaw< + DbRawPostGdoc & { chartConfig?: DbRawChartConfig["full"] } + >( + knex, + `-- sql + SELECT + pg.*, + COALESCE(cc_narrativeView.full, cc_grapherUrl.full) AS chartConfig + FROM posts_gdocs pg + -- extract slugs from URLs of the format /grapher/slug + LEFT JOIN chart_configs cc_grapherUrl + ON cc_grapherUrl.slug = SUBSTRING_INDEX(SUBSTRING_INDEX(content ->> '$."grapher-url"', '/grapher/', -1), '\\?', 1) + LEFT JOIN chart_views cw + ON cw.name = content ->> '$."narrative-view"' + LEFT JOIN chart_configs cc_narrativeView + ON cc_narrativeView.id = cw.chartConfigId + WHERE type = 'data-insight' + ORDER BY pg.updatedAt DESC` + ) const groupedTags = await getTagsGroupedByGdocId( knex, dataInsights.map((gdoc) => gdoc.id) ) return dataInsights.map((gdoc) => - extractDataInsightIndexItem({ - ...(parsePostsGdocsRow(gdoc) as OwidGdocDataInsightInterface), - tags: groupedTags[gdoc.id] ? groupedTags[gdoc.id] : null, - }) + extractDataInsightIndexItem( + { + ...(parsePostsGdocsRow(gdoc) as OwidGdocDataInsightInterface), + tags: groupedTags[gdoc.id] ? groupedTags[gdoc.id] : null, + }, + gdoc.chartConfig ? parseChartConfig(gdoc.chartConfig) : undefined + ) ) } function extractDataInsightIndexItem( - gdoc: OwidGdocDataInsightInterface + gdoc: OwidGdocDataInsightInterface, + chartConfig?: GrapherInterface ): OwidGdocDataInsightIndexItem { const grapherUrl = gdoc.content["grapher-url"]?.trim() const isGrapherUrl = grapherUrl?.startsWith( @@ -62,5 +90,118 @@ function extractDataInsightIndexItem( "grapher-url": isGrapherUrl ? grapherUrl : undefined, "explorer-url": isExplorerUrl ? grapherUrl : undefined, "figma-url": gdoc.content["figma-url"], + chartType: detectChartType(gdoc, chartConfig), + } +} + +function detectChartType( + gdoc: OwidGdocDataInsightInterface, + chartConfig?: GrapherInterface +): GrapherChartOrMapType | undefined { + if (!chartConfig) return undefined + + if (gdoc.content["narrative-view"]) + return getChartTypeFromConfig(chartConfig) + + if (gdoc.content["grapher-url"]) { + try { + const url = new URL(gdoc.content["grapher-url"]) + return getChartTypeFromConfigAndQueryParams( + chartConfig, + url.searchParams + ) + } catch { + return getChartTypeFromConfig(chartConfig) + } + } + + return undefined +} + +function getChartTypeFromConfig( + chartConfig: GrapherInterface +): GrapherChartOrMapType | undefined { + return getChartTypeFromConfigAndQueryParams(chartConfig) +} + +function getChartTypeFromConfigAndQueryParams( + chartConfig: GrapherInterface, + queryParams?: URLSearchParams +): GrapherChartOrMapType | undefined { + // If the tab query parameter is set, use it to determine the chart type + const tab = queryParams?.get("tab") + if (tab) { + // Handle cases where tab is set to 'line' or 'slope' + const chartType = mapQueryParamToChartTypeName(tab) + if (chartType) + return maybeLineChartThatTurnedIntoDiscreteBar( + chartType, + chartConfig, + queryParams + ) + + // Handle cases where tab is set to 'chart', 'map' or 'table' + if (tab === GRAPHER_TAB_QUERY_PARAMS.table) return undefined + if (tab === GRAPHER_TAB_QUERY_PARAMS.map) return GRAPHER_MAP_TYPE + if (tab === GRAPHER_TAB_QUERY_PARAMS.chart) { + const chartType = getChartTypeFromConfigField( + chartConfig.chartTypes + ) + if (chartType) + return maybeLineChartThatTurnedIntoDiscreteBar( + chartType, + chartConfig, + queryParams + ) + } } + + // If the chart has a map tab and it's the default tab, use the map type + if ( + chartConfig.hasMapTab && + chartConfig.tab === GRAPHER_TAB_QUERY_PARAMS.map + ) + return GRAPHER_MAP_TYPE + + // Otherwise, rely on the config's chartTypes field + const chartType = getChartTypeFromConfigField(chartConfig.chartTypes) + if (chartType) { + return maybeLineChartThatTurnedIntoDiscreteBar( + chartType, + chartConfig, + queryParams + ) + } + + return undefined +} + +function getChartTypeFromConfigField( + chartTypes?: GrapherChartType[] +): GrapherChartType | undefined { + if (!chartTypes) return GRAPHER_CHART_TYPES.LineChart + if (chartTypes.length === 0) return undefined + return chartTypes[0] +} + +function maybeLineChartThatTurnedIntoDiscreteBar( + chartType: GrapherChartType, + chartConfig: GrapherInterface, + queryParams?: URLSearchParams +): GrapherChartType { + if (chartType !== GRAPHER_CHART_TYPES.LineChart) return chartType + + const time = queryParams?.get("time") + if (time) { + const [minTime, maxTime] = getTimeDomainFromQueryString(time) + if (minTime === maxTime) return GRAPHER_CHART_TYPES.DiscreteBar + } + + if ( + chartConfig.minTime !== undefined && + chartConfig.minTime === chartConfig.maxTime + ) + return GRAPHER_CHART_TYPES.DiscreteBar + + return chartType } diff --git a/packages/@ourworldindata/grapher/src/index.ts b/packages/@ourworldindata/grapher/src/index.ts index af37b72a2e..41ac14a60e 100644 --- a/packages/@ourworldindata/grapher/src/index.ts +++ b/packages/@ourworldindata/grapher/src/index.ts @@ -87,4 +87,7 @@ export { } from "./slideshowController/SlideShowController" export { defaultGrapherConfig } from "./schema/defaultGrapherConfig" export { migrateGrapherConfigToLatestVersion } from "./schema/migrations/migrate" -export { generateGrapherImageSrcSet } from "./chart/ChartUtils.js" +export { + generateGrapherImageSrcSet, + mapQueryParamToChartTypeName, +} from "./chart/ChartUtils.js" diff --git a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts index a87a4fc14d..6b60d70663 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts @@ -1,4 +1,8 @@ -import { GrapherTabOption, RelatedChart } from "../grapherTypes/GrapherTypes.js" +import { + GrapherChartOrMapType, + GrapherTabOption, + RelatedChart, +} from "../grapherTypes/GrapherTypes.js" import { BreadcrumbItem } from "../domainTypes/Site.js" import { TocHeadingWithTitleSupertitle } from "../domainTypes/Toc.js" import { ImageMetadata } from "./Image.js" @@ -171,7 +175,7 @@ export type OwidGdocDataInsightIndexItem = Pick< | "grapher-url" | "figma-url" | "approved-by" - > & { "explorer-url"?: string } + > & { "explorer-url"?: string; chartType?: GrapherChartOrMapType } export const DATA_INSIGHTS_INDEX_PAGE_SIZE = 20 From 0b37b0e75e46c31a27517609acddd63d945ca87a Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Fri, 24 Jan 2025 21:32:22 +0100 Subject: [PATCH 03/26] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20add=20image=20pr?= =?UTF-8?q?eview=20to=20data=20insight=20index=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 12 +++ adminSiteServer/apiRoutes/dataInsights.ts | 76 +++++++++++++------ .../types/src/gdocTypes/Gdoc.ts | 10 ++- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 7e2ece0942..5f61647543 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -13,6 +13,7 @@ import { import { GdocsStoreContext } from "./GdocsStoreContext.js" import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" import { dayjs, startCase } from "@ourworldindata/utils" +import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js" function createColumns(ctx: { highlightFn: ( @@ -20,6 +21,17 @@ function createColumns(ctx: { ) => React.ReactElement | string }): ColumnsType { return [ + { + title: "Preview", + key: "preview", + render: (_, dataInsight) => + dataInsight.image?.cloudflareId ? ( + + ) : undefined, + }, { title: "Title", dataIndex: "title", diff --git a/adminSiteServer/apiRoutes/dataInsights.ts b/adminSiteServer/apiRoutes/dataInsights.ts index 542fe90cd4..e765fbe400 100644 --- a/adminSiteServer/apiRoutes/dataInsights.ts +++ b/adminSiteServer/apiRoutes/dataInsights.ts @@ -19,6 +19,11 @@ import { getTagsGroupedByGdocId } from "../../db/model/Gdoc/GdocFactory.js" import { mapQueryParamToChartTypeName } from "@ourworldindata/grapher" import { getTimeDomainFromQueryString } from "@ourworldindata/utils" +type DataInsightRow = DbRawPostGdoc & + OwidGdocDataInsightIndexItem["image"] & { + chartConfig?: DbRawChartConfig["full"] + } + export async function getAllDataInsightIndexItems( req: Request, res: e.Response>, @@ -30,44 +35,68 @@ export async function getAllDataInsightIndexItems( async function getAllDataInsightIndexItemsOrderedByUpdatedAt( knex: db.KnexReadonlyTransaction ): Promise { - const dataInsights = await db.knexRaw< - DbRawPostGdoc & { chartConfig?: DbRawChartConfig["full"] } - >( + const dataInsights = await db.knexRaw( knex, `-- sql - SELECT - pg.*, - COALESCE(cc_narrativeView.full, cc_grapherUrl.full) AS chartConfig - FROM posts_gdocs pg - -- extract slugs from URLs of the format /grapher/slug - LEFT JOIN chart_configs cc_grapherUrl - ON cc_grapherUrl.slug = SUBSTRING_INDEX(SUBSTRING_INDEX(content ->> '$."grapher-url"', '/grapher/', -1), '\\?', 1) - LEFT JOIN chart_views cw - ON cw.name = content ->> '$."narrative-view"' - LEFT JOIN chart_configs cc_narrativeView - ON cc_narrativeView.id = cw.chartConfigId - WHERE type = 'data-insight' - ORDER BY pg.updatedAt DESC` + WITH latestImages AS ( + SELECT filename, cloudflareId, originalWidth, originalHeight + FROM images + WHERE replacedBy IS NULL + ) + SELECT + pg.*, + COALESCE(cc_narrativeView.full, cc_grapherUrl.full) AS chartConfig, + -- only works if the image block comes first, but that's usually the case for data insights + COALESCE(pg.content ->> '$.body[0].smallFilename', pg.content ->> '$.body[0].filename') AS filename, + i.cloudflareId, + i.originalWidth, + i.originalHeight + FROM posts_gdocs pg + -- extract slugs from URLs of the format /grapher/slug + LEFT JOIN chart_configs cc_grapherUrl + ON cc_grapherUrl.slug = SUBSTRING_INDEX(SUBSTRING_INDEX(content ->> '$."grapher-url"', '/grapher/', -1), '\\?', 1) + LEFT JOIN chart_views cw + ON cw.name = content ->> '$."narrative-view"' + LEFT JOIN chart_configs cc_narrativeView + ON cc_narrativeView.id = cw.chartConfigId + -- only works if the image block comes first, but that's usually the case for data insights + LEFT JOIN latestImages i + ON i.filename = COALESCE(pg.content ->> '$.body[0].smallFilename', pg.content ->> '$.body[0].filename') + WHERE pg.type = 'data-insight' + ORDER BY pg.updatedAt DESC;` ) const groupedTags = await getTagsGroupedByGdocId( knex, dataInsights.map((gdoc) => gdoc.id) ) return dataInsights.map((gdoc) => - extractDataInsightIndexItem( - { + extractDataInsightIndexItem({ + gdoc: { ...(parsePostsGdocsRow(gdoc) as OwidGdocDataInsightInterface), tags: groupedTags[gdoc.id] ? groupedTags[gdoc.id] : null, }, - gdoc.chartConfig ? parseChartConfig(gdoc.chartConfig) : undefined - ) + imageMetadata: { + cloudflareId: gdoc.cloudflareId, + originalWidth: gdoc.originalWidth, + originalHeight: gdoc.originalHeight, + filename: gdoc.filename, + }, + chartConfig: gdoc.chartConfig + ? parseChartConfig(gdoc.chartConfig) + : undefined, + }) ) } -function extractDataInsightIndexItem( - gdoc: OwidGdocDataInsightInterface, +function extractDataInsightIndexItem({ + gdoc, + imageMetadata, + chartConfig, +}: { + gdoc: OwidGdocDataInsightInterface + imageMetadata?: OwidGdocDataInsightIndexItem["image"] chartConfig?: GrapherInterface -): OwidGdocDataInsightIndexItem { +}): OwidGdocDataInsightIndexItem { const grapherUrl = gdoc.content["grapher-url"]?.trim() const isGrapherUrl = grapherUrl?.startsWith( "https://ourworldindata.org/grapher/" @@ -91,6 +120,7 @@ function extractDataInsightIndexItem( "explorer-url": isExplorerUrl ? grapherUrl : undefined, "figma-url": gdoc.content["figma-url"], chartType: detectChartType(gdoc, chartConfig), + image: imageMetadata, } } diff --git a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts index 6b60d70663..43b56858b7 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts @@ -18,6 +18,7 @@ import { import { MinimalTag } from "../dbTypes/Tags.js" import { DbEnrichedLatestWork } from "../domainTypes/Author.js" import { QueryParams } from "../domainTypes/Various.js" +import { DbRawImage } from "../dbTypes/Images.js" export enum OwidGdocPublicationContext { unlisted = "unlisted", @@ -175,7 +176,14 @@ export type OwidGdocDataInsightIndexItem = Pick< | "grapher-url" | "figma-url" | "approved-by" - > & { "explorer-url"?: string; chartType?: GrapherChartOrMapType } + > & { + "explorer-url"?: string + chartType?: GrapherChartOrMapType + image?: Pick< + DbRawImage, + "cloudflareId" | "filename" | "originalWidth" | "originalHeight" + > + } export const DATA_INSIGHTS_INDEX_PAGE_SIZE = 20 From cb33b72b8f2b27d2e6ace56106e37218972516cf Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Sat, 25 Jan 2025 20:49:06 +0100 Subject: [PATCH 04/26] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20filter=20by=20pu?= =?UTF-8?q?blication=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 64 ++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 5f61647543..15fc2a1ca9 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect, useMemo, useState } from "react" import * as React from "react" -import { Button, Flex, Input, Space, Table, Tag } from "antd" +import { Button, Flex, Input, Select, Space, Table, Tag } from "antd" import { AdminLayout } from "./AdminLayout.js" import { Timeago } from "./Forms.js" @@ -15,6 +15,10 @@ import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" import { dayjs, startCase } from "@ourworldindata/utils" import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js" +type PublicationFilter = "all" | "published" | "scheduled" | "draft" + +const DEFAULT_PUBLICATION_FILTER: PublicationFilter = "all" + function createColumns(ctx: { highlightFn: ( text: string | null | undefined @@ -170,6 +174,8 @@ export function DataInsightIndexPage() { OwidGdocDataInsightIndexItem[] >([]) const [searchValue, setSearchValue] = useState("") + const [publicationFilter, setPublicationFilter] = + useState(DEFAULT_PUBLICATION_FILTER) const searchWords = useMemo( () => buildSearchWordsFromSearchString(searchValue), @@ -177,7 +183,27 @@ export function DataInsightIndexPage() { ) const filteredDataInsights = useMemo(() => { - const filterFn = filterFunctionForSearchWords( + const publicationFilterFn = ( + dataInsight: OwidGdocDataInsightIndexItem + ) => { + switch (publicationFilter) { + case "draft": + return !dataInsight.published + case "scheduled": + return ( + dataInsight.published && + dayjs(dataInsight.publishedAt).isAfter(dayjs()) + ) + case "published": + return ( + dataInsight.published && + dayjs(dataInsight.publishedAt).isBefore(dayjs()) + ) + default: + return true + } + } + const searchFilterFn = filterFunctionForSearchWords( searchWords, (dataInsight: OwidGdocDataInsightIndexItem) => [ dataInsight.title, @@ -189,8 +215,8 @@ export function DataInsightIndexPage() { ] ) - return dataInsights.filter(filterFn) - }, [dataInsights, searchWords]) + return dataInsights.filter(publicationFilterFn).filter(searchFilterFn) + }, [dataInsights, publicationFilter, searchWords]) const highlightFn = useMemo( () => highlightFunctionForSearchWords(searchWords), @@ -213,7 +239,7 @@ export function DataInsightIndexPage() { return (
    - + +
    From b0c44a003f1076b8690a50ae2aae1ccf4c4df763 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 09:29:12 +0100 Subject: [PATCH 05/26] =?UTF-8?q?=F0=9F=94=A8=20rename=20narrative-view=20?= =?UTF-8?q?to=20narrative-chart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 4 ++-- adminSiteServer/apiRoutes/dataInsights.ts | 6 +++--- packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 15fc2a1ca9..2bc8a0e9c8 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -121,9 +121,9 @@ function createColumns(ctx: { > Preview - {dataInsight["narrative-view"] && ( + {dataInsight["narrative-chart"] && ( @@ -125,6 +133,7 @@ function createColumns(ctx: { @@ -133,6 +142,7 @@ function createColumns(ctx: { @@ -141,12 +151,17 @@ function createColumns(ctx: { )} {dataInsight["figma-url"] && ( - )} @@ -160,6 +175,7 @@ function createColumns(ctx: { From 8d7cbee65218b15bd7c189e967be0153f77c9494 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 09:40:00 +0100 Subject: [PATCH 07/26] =?UTF-8?q?=F0=9F=94=A8=20simplify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 68048ede6c..768bd00b73 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -268,16 +268,10 @@ export function DataInsightIndexPage() { setSearchValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") setSearchValue("") - }} - style={{ width: 500, marginBottom: 20 }} - /> - setSearchValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") setSearchValue("") + }} + style={{ width: 500, marginBottom: 20 }} + /> +
    + {layout === "list" && ( + + )} + {layout === "gallery" && ( + + )} ) } + +function DataInsightList({ + dataInsights, + searchWords, +}: { + dataInsights: OwidGdocDataInsightIndexItem[] + searchWords: SearchWord[] +}) { + const highlightFn = useMemo( + () => highlightFunctionForSearchWords(searchWords), + [searchWords] + ) + + const columns = useMemo(() => createColumns({ highlightFn }), [highlightFn]) + + return
    +} + +function DataInsightGallery({ + dataInsights, +}: { + dataInsights: OwidGdocDataInsightIndexItem[] +}) { + const dataInsightsWithPreview = dataInsights.filter( + (dataInsight) => dataInsight.image?.cloudflareId + ) + return ( + + {dataInsightsWithPreview.map((dataInsight, index) => ( + + ))} + + ) +} + +function DataInsightCard({ + dataInsight, +}: { + dataInsight: OwidGdocDataInsightIndexItem +}) { + const preview = ( + + ) + + return ( + + + Preview + + {" / "} + + GDoc + + {dataInsight["figma-url"] && ( + <> + {" / "} + + Figma + + + )} + + ) +} + +function makePreviewLink(dataInsight: OwidGdocDataInsightIndexItem) { + return `/admin/gdocs/${dataInsight.id}/preview` +} + +function makeGDocEditLink(dataInsight: OwidGdocDataInsightIndexItem) { + return `https://docs.google.com/document/d/${dataInsight.id}/edit` +} + +function makePreviewImageSrc(dataInsight: OwidGdocDataInsightIndexItem) { + const { cloudflareId, originalWidth } = dataInsight.image ?? {} + return `${CLOUDFLARE_IMAGES_URL}/${cloudflareId}/w=${originalWidth}` +} diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index e8dd41ae65..5907cf56a7 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -1291,3 +1291,13 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) { margin-top: -8px; margin-bottom: 0; } + +.DataInsightIndexPage { + .ant-card-body { + padding: 8px; + + a { + color: rgba(0, 0, 0, 0.88); + } + } +} From 1b3c7339d4e9007eaa211c41f4b805fa3c797015 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 10:30:50 +0100 Subject: [PATCH 11/26] =?UTF-8?q?=E2=9C=A8=20make=20preview=20image=20in?= =?UTF-8?q?=20gallery=20slightly=20smaller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 4afd5062ca..53d4f20068 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -364,8 +364,8 @@ function DataInsightCard({ From f4c6764142379daf745eb42709986ba2094077da Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 10:50:31 +0100 Subject: [PATCH 12/26] =?UTF-8?q?=F0=9F=90=9B=20fix=20narrative=20chart=20?= =?UTF-8?q?edit=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 40 +++++++++++-------- adminSiteServer/apiRoutes/dataInsights.ts | 6 +++ .../types/src/gdocTypes/Gdoc.ts | 5 ++- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 53d4f20068..307dfc8ed3 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -142,15 +142,6 @@ function createColumns(ctx: { > Preview - {dataInsight["narrative-chart"] && ( - - )} {dataInsight["grapher-url"] && ( + + + {dataInsight["narrative-chart"] && ( + + )} + ), }, ] @@ -412,6 +416,10 @@ function makeGDocEditLink(dataInsight: OwidGdocDataInsightIndexItem) { return `https://docs.google.com/document/d/${dataInsight.id}/edit` } +function makeNarrativeChartEditLink(dataInsight: OwidGdocDataInsightIndexItem) { + return `/admin/chartViews/${dataInsight.narrativeChartId}/edit` +} + function makePreviewImageSrc(dataInsight: OwidGdocDataInsightIndexItem) { const { cloudflareId, originalWidth } = dataInsight.image ?? {} return `${CLOUDFLARE_IMAGES_URL}/${cloudflareId}/w=${originalWidth}` diff --git a/adminSiteServer/apiRoutes/dataInsights.ts b/adminSiteServer/apiRoutes/dataInsights.ts index 732ce43264..fe63a00eb9 100644 --- a/adminSiteServer/apiRoutes/dataInsights.ts +++ b/adminSiteServer/apiRoutes/dataInsights.ts @@ -22,6 +22,7 @@ import { getTimeDomainFromQueryString } from "@ourworldindata/utils" type DataInsightRow = DbRawPostGdoc & OwidGdocDataInsightIndexItem["image"] & { chartConfig?: DbRawChartConfig["full"] + narrativeChartId?: number } export async function getAllDataInsightIndexItems( @@ -51,6 +52,7 @@ async function getAllDataInsightIndexItemsOrderedByUpdatedAt( ) SELECT pg.*, + cw.id AS narrativeChartId, -- prefer narrative charts over grapher URLs COALESCE(cc_narrativeView.chartConfig, cc_grapherUrl.chartConfig) AS chartConfig, -- only works for data insights where the image block comes first @@ -91,6 +93,7 @@ async function getAllDataInsightIndexItemsOrderedByUpdatedAt( chartConfig: gdoc.chartConfig ? parseChartConfig(gdoc.chartConfig) : undefined, + narrativeChartId: gdoc.narrativeChartId, }) ) } @@ -99,10 +102,12 @@ function extractDataInsightIndexItem({ gdoc, imageMetadata, chartConfig, + narrativeChartId, }: { gdoc: OwidGdocDataInsightInterface imageMetadata?: OwidGdocDataInsightIndexItem["image"] chartConfig?: GrapherInterface + narrativeChartId?: number }): OwidGdocDataInsightIndexItem { const grapherUrl = gdoc.content["grapher-url"]?.trim() const isGrapherUrl = grapherUrl?.startsWith( @@ -126,6 +131,7 @@ function extractDataInsightIndexItem({ "grapher-url": isGrapherUrl ? grapherUrl : undefined, "explorer-url": isExplorerUrl ? grapherUrl : undefined, "figma-url": gdoc.content["figma-url"], + narrativeChartId: narrativeChartId, chartType: detectChartType(gdoc, chartConfig), image: imageMetadata, } diff --git a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts index 9dadf49b8e..eeaab7bc3a 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts @@ -19,6 +19,7 @@ import { MinimalTag } from "../dbTypes/Tags.js" import { DbEnrichedLatestWork } from "../domainTypes/Author.js" import { QueryParams } from "../domainTypes/Various.js" import { DbRawImage } from "../dbTypes/Images.js" +import { DbPlainChartView } from "../dbTypes/ChartViews.js" export enum OwidGdocPublicationContext { unlisted = "unlisted", @@ -168,7 +169,8 @@ export interface OwidGdocDataInsightContent { export type OwidGdocDataInsightIndexItem = Pick< OwidGdocBaseInterface, "id" | "slug" | "tags" | "published" | "publishedAt" | "markdown" -> & { "explorer-url"?: string } & Pick< +> & + Pick< OwidGdocDataInsightContent, | "title" | "authors" @@ -179,6 +181,7 @@ export type OwidGdocDataInsightIndexItem = Pick< > & { "explorer-url"?: string chartType?: GrapherChartOrMapType + narrativeChartId?: number image?: Pick< DbRawImage, "cloudflareId" | "filename" | "originalWidth" | "originalHeight" From 034ff5b42293014d48f5102ff739ac9a6272e30d Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 13:46:08 +0100 Subject: [PATCH 13/26] =?UTF-8?q?=F0=9F=94=A8=20add=20react=20key=20for=20?= =?UTF-8?q?table=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 307dfc8ed3..34f81a5727 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -336,7 +336,13 @@ function DataInsightList({ const columns = useMemo(() => createColumns({ highlightFn }), [highlightFn]) - return
    + return ( +
    dataInsight.id} + /> + ) } function DataInsightGallery({ From f89295443f641f84392dd2dcb20740bbca519a48 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Mon, 27 Jan 2025 18:08:38 +0100 Subject: [PATCH 14/26] =?UTF-8?q?=F0=9F=92=84=20fix=20eslint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts index eeaab7bc3a..288b7bc050 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts @@ -19,7 +19,6 @@ import { MinimalTag } from "../dbTypes/Tags.js" import { DbEnrichedLatestWork } from "../domainTypes/Author.js" import { QueryParams } from "../domainTypes/Various.js" import { DbRawImage } from "../dbTypes/Images.js" -import { DbPlainChartView } from "../dbTypes/ChartViews.js" export enum OwidGdocPublicationContext { unlisted = "unlisted", From 11cb86403dcce26c0d8dd77a6323d5feb9cafa08 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Tue, 28 Jan 2025 10:28:19 +0100 Subject: [PATCH 15/26] =?UTF-8?q?=F0=9F=94=A8=20use=20admin=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 34f81a5727..79d3ac96aa 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -24,10 +24,10 @@ import { highlightFunctionForSearchWords, SearchWord, } from "../adminShared/search.js" -import { GdocsStoreContext } from "./GdocsStoreContext.js" import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" import { dayjs, startCase } from "@ourworldindata/utils" import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js" +import { AdminAppContext } from "./AdminAppContext.js" type PublicationFilter = "all" | "published" | "scheduled" | "draft" type Layout = "list" | "gallery" @@ -202,7 +202,8 @@ function createColumns(ctx: { } export function DataInsightIndexPage() { - const context = useContext(GdocsStoreContext) + const { admin } = useContext(AdminAppContext) + const [dataInsights, setDataInsights] = useState< OwidGdocDataInsightIndexItem[] >([]) @@ -254,14 +255,14 @@ export function DataInsightIndexPage() { useEffect(() => { const fetchAllDataInsights = async () => - (await context?.admin.getJSON( + (await admin.getJSON( "/api/dataInsights" )) as OwidGdocDataInsightIndexItem[] void fetchAllDataInsights().then((dataInsights) => setDataInsights(dataInsights) ) - }, [context?.admin]) + }, [admin]) return ( From eb2bf7f665375e503f881d36b0b41bc23180607b Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Wed, 29 Jan 2025 09:32:21 +0100 Subject: [PATCH 16/26] =?UTF-8?q?=F0=9F=94=A8=20refactor=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 58 +++-- adminSiteServer/apiRoutes/dataInsights.ts | 226 +++++++++++------- .../types/src/gdocTypes/Gdoc.ts | 20 +- packages/@ourworldindata/utils/src/Util.ts | 2 + packages/@ourworldindata/utils/src/index.ts | 1 + 5 files changed, 183 insertions(+), 124 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 79d3ac96aa..c5fddbf77f 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -25,7 +25,7 @@ import { SearchWord, } from "../adminShared/search.js" import { DbPlainTag, OwidGdocDataInsightIndexItem } from "@ourworldindata/types" -import { dayjs, startCase } from "@ourworldindata/utils" +import { dayjs, RequiredBy, startCase } from "@ourworldindata/utils" import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js" import { AdminAppContext } from "./AdminAppContext.js" @@ -49,7 +49,7 @@ function createColumns(ctx: { title: "Preview", key: "preview", render: (_, dataInsight) => - dataInsight.image?.cloudflareId ? ( + hasImage(dataInsight) ? ( ))} - {dataInsight["approved-by"] && - ` (approved by ${dataInsight["approved-by"]})`} + {dataInsight.approvedBy && + ` (approved by ${dataInsight.approvedBy})`} ), }, @@ -142,27 +142,27 @@ function createColumns(ctx: { > Preview - {dataInsight["grapher-url"] && ( + {dataInsight.grapherUrl && ( )} - {dataInsight["explorer-url"] && ( + {dataInsight.explorerUrl && ( )} - {dataInsight["figma-url"] && ( + {dataInsight.figmaUrl && ( - {dataInsight["narrative-chart"] && ( + {hasNarrativeChart(dataInsight) && ( + ) : undefined, }, { From 5fa8c20f034caa36d9009712018e3f9838fed665 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Wed, 29 Jan 2025 12:36:45 +0100 Subject: [PATCH 19/26] =?UTF-8?q?=E2=9C=A8=20add=20image=20border?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/DataInsightIndexPage.tsx | 22 +++++----------------- adminSiteClient/admin.scss | 4 ++++ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/adminSiteClient/DataInsightIndexPage.tsx b/adminSiteClient/DataInsightIndexPage.tsx index 3e5b845081..ba4b874e86 100644 --- a/adminSiteClient/DataInsightIndexPage.tsx +++ b/adminSiteClient/DataInsightIndexPage.tsx @@ -1,16 +1,6 @@ import { useContext, useEffect, useMemo, useState } from "react" import * as React from "react" -import { - Button, - Card, - Flex, - Input, - Radio, - Select, - Space, - Table, - Tag, -} from "antd" +import { Button, Card, Flex, Input, Radio, Select, Space, Table } from "antd" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faCopy, @@ -62,8 +52,9 @@ function createColumns(ctx: { hasImage(dataInsight) ? ( <>