From 9a16e9c2c9845d73cd8c21054eee8f67d0c0e3bf Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 9 Jan 2025 20:35:47 +0100 Subject: [PATCH] feat: implement refs tab for narrative charts --- adminSiteClient/ChartViewEditor.ts | 6 +-- adminSiteClient/ChartViewEditorPage.tsx | 19 ++++++- adminSiteClient/EditorReferencesTab.tsx | 24 +++++++++ adminSiteClient/SaveButtons.tsx | 2 +- adminSiteServer/apiRouter.ts | 67 +++++++++++++++++++++---- db/model/Link.ts | 30 ++++++++--- 6 files changed, 127 insertions(+), 21 deletions(-) diff --git a/adminSiteClient/ChartViewEditor.ts b/adminSiteClient/ChartViewEditor.ts index 578ec7028c3..79a9775a633 100644 --- a/adminSiteClient/ChartViewEditor.ts +++ b/adminSiteClient/ChartViewEditor.ts @@ -29,6 +29,7 @@ export interface Chart { export interface ChartViewEditorManager extends AbstractChartEditorManager { chartViewId: number parentChartId: number + references: References | undefined } export class ChartViewEditor extends AbstractChartEditor { @@ -48,9 +49,8 @@ export class ChartViewEditor extends AbstractChartEditor return tabs } - @computed get references(): References | undefined { - // Not yet implemented for chart views - return undefined + @computed get references() { + return this.manager.references } @computed override get patchConfig(): GrapherInterface { diff --git a/adminSiteClient/ChartViewEditorPage.tsx b/adminSiteClient/ChartViewEditorPage.tsx index 46ed7ea330c..6e07f5a7bcb 100644 --- a/adminSiteClient/ChartViewEditorPage.tsx +++ b/adminSiteClient/ChartViewEditorPage.tsx @@ -1,11 +1,12 @@ import React from "react" import { observer } from "mobx-react" -import { computed, action } from "mobx" +import { computed, action, runInAction, observable } from "mobx" import { GrapherInterface } from "@ourworldindata/types" import { Admin } from "./Admin.js" import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" import { ChartEditorView, ChartEditorViewManager } from "./ChartEditorView.js" import { ChartViewEditor, ChartViewEditorManager } from "./ChartViewEditor.js" +import { References } from "./AbstractChartEditor.js" @observer export class ChartViewEditorPage @@ -26,9 +27,11 @@ export class ChartViewEditorPage isInheritanceEnabled: boolean | undefined = true + @observable references: References | undefined = undefined + async fetchChartViewData(): Promise { const data = await this.context.admin.getJSON( - `/api/chartViews/${this.chartViewId}` + `/api/chartViews/${this.chartViewId}.config.json` ) this.idAndName = { id: data.id, name: data.name } @@ -50,8 +53,20 @@ export class ChartViewEditorPage return new ChartViewEditor({ manager: this }) } + async fetchRefs(): Promise { + const { admin } = this.context + const json = + this.chartViewId === undefined + ? {} + : await admin.getJSON( + `/api/chartViews/${this.chartViewId}.references.json` + ) + runInAction(() => (this.references = json.references)) + } + @action.bound refresh(): void { void this.fetchChartViewData() + void this.fetchRefs() } componentDidMount(): void { diff --git a/adminSiteClient/EditorReferencesTab.tsx b/adminSiteClient/EditorReferencesTab.tsx index 02ee7aaa2cb..3d3d14a67cd 100644 --- a/adminSiteClient/EditorReferencesTab.tsx +++ b/adminSiteClient/EditorReferencesTab.tsx @@ -22,6 +22,10 @@ import { isIndicatorChartEditorInstance, } from "./IndicatorChartEditor.js" import { Section } from "./Forms.js" +import { + ChartViewEditor, + isChartViewEditorInstance, +} from "./ChartViewEditor.js" const BASE_URL = BAKED_GRAPHER_URL.replace(/^https?:\/\//, "") @@ -37,6 +41,8 @@ export class EditorReferencesTab< return else if (isIndicatorChartEditorInstance(editor)) return + else if (isChartViewEditorInstance(editor)) + return else return null } } @@ -268,6 +274,24 @@ export class EditorReferencesTabForChart extends Component<{ } } +export class EditorReferencesTabForChartView extends Component<{ + editor: ChartViewEditor +}> { + @computed get references() { + return this.props.editor.references + } + + render() { + return ( +
+
+ +
+
+ ) + } +} + @observer class AddRedirectForm extends Component<{ editor: Editor diff --git a/adminSiteClient/SaveButtons.tsx b/adminSiteClient/SaveButtons.tsx index 0bdbafca275..82e44184ff7 100644 --- a/adminSiteClient/SaveButtons.tsx +++ b/adminSiteClient/SaveButtons.tsx @@ -254,7 +254,7 @@ class SaveButtonsForChartView extends Component<{ onClick={this.onSaveChart} disabled={isSavingDisabled} > - Save chart view + Save narrative chart {" "} link.sourceSlug).join(", ") + const sources = links.map((link) => link.slug).join(", ") throw new Error( `Cannot delete chart in-use in the following published documents: ${sources}` ) @@ -3693,7 +3695,7 @@ getRouteWithROTransaction(apiRouter, "/chartViews", async (req, res, trx) => { getRouteWithROTransaction( apiRouter, - "/chartViews/:id", + "/chartViews/:id.config.json", async (req, res, trx) => { const id = expectInt(req.params.id) @@ -3858,16 +3860,35 @@ deleteRouteWithRWTransaction( async (req, res, trx) => { const id = expectInt(req.params.id) - const chartConfigId: string | undefined = await trx(ChartViewsTableName) - .select("chartConfigId") - .where({ id }) - .first() - .then((row) => row?.chartConfigId) - - if (!chartConfigId) { + const { + name, + chartConfigId, + }: { name: string | undefined; chartConfigId: string | undefined } = + await trx(ChartViewsTableName) + .select("name", "chartConfigId") + .where({ id }) + .first() + .then((row) => row ?? {}) + + if (!chartConfigId || !name) { throw new JsonError(`No chart view found for id ${id}`, 404) } + const references = await getPublishedLinksTo( + trx, + [name], + OwidGdocLinkType.ChartView + ) + + if (references.length) { + throw new JsonError( + `Cannot delete chart view "${name}" because it is referenced by the following posts: ${references + .map((r) => r.slug) + .join(", ")}`, + 400 + ) + } + await trx.table(ChartViewsTableName).where({ id }).delete() await deleteGrapherConfigFromR2ByUUID(chartConfigId) @@ -3881,4 +3902,32 @@ deleteRouteWithRWTransaction( } ) +getRouteWithROTransaction( + apiRouter, + "/chartViews/:id.references.json", + async (req, res, trx) => { + const id = expectInt(req.params.id) + const name: string | undefined = await trx(ChartViewsTableName) + .select("name") + .where({ id }) + .first() + .then((row) => row?.name) + + if (!name) { + throw new JsonError(`No chart view found for id ${id}`, 404) + } + + const references = { + references: { + postsGdocs: await getPublishedLinksTo( + trx, + [name], + OwidGdocLinkType.ChartView + ).then((refs) => uniqBy(refs, "slug")), + }, + } + return references + } +) + export { apiRouter } diff --git a/db/model/Link.ts b/db/model/Link.ts index bb0d2941cf4..31ab13fc1ec 100644 --- a/db/model/Link.ts +++ b/db/model/Link.ts @@ -8,23 +8,41 @@ import { OwidGdocLinkType, } from "@ourworldindata/types" import { KnexReadonlyTransaction, knexRaw } from "../db.js" +import { BAKED_BASE_URL } from "../../settings/clientSettings.js" export async function getPublishedLinksTo( knex: KnexReadonlyTransaction, ids: string[], linkType?: OwidGdocLinkType -): Promise<(DbPlainPostGdocLink & { sourceSlug: string })[]> { +): Promise< + (DbPlainPostGdocLink & { + title: string + slug: string + id: string + url: string + })[] +> { const linkTypeClause = linkType ? "AND linkType = ?" : "" const params = linkType ? [ids, linkType] : [ids] - const rows = await knexRaw( + const rows = await knexRaw< + DbPlainPostGdocLink & { + title: string + slug: string + id: string + url: string + } + >( knex, `-- sql SELECT - posts_gdocs_links.*, - posts_gdocs.slug AS sourceSlug + pg.content ->> '$.title' AS title, + pg.slug AS slug, + pg.id AS id, + CONCAT("${BAKED_BASE_URL}","/",pg.slug) as url, + pgl.* FROM - posts_gdocs_links - JOIN posts_gdocs ON posts_gdocs_links.sourceId = posts_gdocs.id + posts_gdocs_links pgl + JOIN posts_gdocs pg ON pgl.sourceId = pg.id WHERE target IN (?) ${linkTypeClause}