Skip to content

Commit

Permalink
feat(gdocs): narrative charts component (#4295)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelgerber authored Jan 14, 2025
1 parent 61b3820 commit 558d3ad
Show file tree
Hide file tree
Showing 41 changed files with 997 additions and 225 deletions.
13 changes: 5 additions & 8 deletions adminSiteClient/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
} from "@fortawesome/free-solid-svg-icons"

import { ETL_WIZARD_URL } from "../settings/clientSettings.js"
import { chartViewsFeatureEnabled } from "./ChartViewEditor.js"

export const AdminSidebar = (): React.ReactElement => (
<aside className="AdminSidebar">
Expand All @@ -34,13 +33,11 @@ export const AdminSidebar = (): React.ReactElement => (
<FontAwesomeIcon icon={faChartBar} /> Charts
</Link>
</li>
{chartViewsFeatureEnabled && (
<li>
<Link to="/chartViews">
<FontAwesomeIcon icon={faPanorama} /> Narrative views
</Link>
</li>
)}
<li>
<Link to="/chartViews">
<FontAwesomeIcon icon={faPanorama} /> Narrative charts
</Link>
</li>
<li>
<Link to="/posts">
<FontAwesomeIcon icon={faFile} /> Posts
Expand Down
26 changes: 9 additions & 17 deletions adminSiteClient/ChartEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
getParentVariableIdFromChartConfig,
mergeGrapherConfigs,
isEmpty,
slugify,
omit,
CHART_VIEW_PROPS_TO_OMIT,
} from "@ourworldindata/utils"
Expand Down Expand Up @@ -200,23 +199,13 @@ export class ChartEditor extends AbstractChartEditor<ChartEditorManager> {
)
}

async saveAsNarrativeView(): Promise<void> {
async saveAsChartView(
name: string
): Promise<{ success: boolean; errorMsg?: string }> {
const { patchConfig, grapher } = this

const chartJson = omit(patchConfig, CHART_VIEW_PROPS_TO_OMIT)

const suggestedName = grapher.title ? slugify(grapher.title) : undefined

const name = prompt(
"Please enter a programmatic name for the narrative view. Note that this name cannot be changed later.",
suggestedName
)

if (name === null) return

// Need to open intermediary tab before AJAX to avoid popup blockers
const w = window.open("/", "_blank") as Window

const body = {
name,
parentChartId: grapher.id,
Expand All @@ -228,11 +217,14 @@ export class ChartEditor extends AbstractChartEditor<ChartEditorManager> {
body,
"POST"
)

if (json.success)
w.location.assign(
if (json.success) {
window.open(
this.manager.admin.url(`chartViews/${json.chartViewId}/edit`)
)
return { success: true }
} else {
return { success: false, errorMsg: json.errorMsg }
}
}

publishGrapher(): void {
Expand Down
12 changes: 3 additions & 9 deletions adminSiteClient/ChartViewEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,13 @@ import {
References,
type EditorTab,
} from "./AbstractChartEditor.js"
import { ENV } from "../settings/clientSettings.js"
import {
CHART_VIEW_PROPS_TO_OMIT,
CHART_VIEW_PROPS_TO_PERSIST,
GrapherInterface,
} from "@ourworldindata/types"
import { diffGrapherConfigs, omit, pick } from "@ourworldindata/utils"

// Don't yet show chart views in the admin interface
// This is low-stakes - if it shows up anyhow (e.g. on staging servers), it's not a big deal.
// TODO: Remove this flag once we're launching this feature
export const chartViewsFeatureEnabled = ENV === "development"

export interface Chart {
id: number
title?: string
Expand All @@ -28,6 +22,7 @@ export interface Chart {
export interface ChartViewEditorManager extends AbstractChartEditorManager {
chartViewId: number
parentChartId: number
references: References | undefined
}

export class ChartViewEditor extends AbstractChartEditor<ChartViewEditorManager> {
Expand All @@ -47,9 +42,8 @@ export class ChartViewEditor extends AbstractChartEditor<ChartViewEditorManager>
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 {
Expand Down
19 changes: 17 additions & 2 deletions adminSiteClient/ChartViewEditorPage.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,9 +27,11 @@ export class ChartViewEditorPage

isInheritanceEnabled: boolean | undefined = true

@observable references: References | undefined = undefined

async fetchChartViewData(): Promise<void> {
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 }
Expand All @@ -50,8 +53,20 @@ export class ChartViewEditorPage
return new ChartViewEditor({ manager: this })
}

async fetchRefs(): Promise<void> {
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 {
Expand Down
6 changes: 3 additions & 3 deletions adminSiteClient/ChartViewIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AdminAppContext } from "./AdminAppContext.js"
import { Timeago } from "./Forms.js"
import { ColumnsType } from "antd/es/table/InternalTable.js"
import { ApiChartViewOverview } from "../adminShared/AdminTypes.js"
import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js"
import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings.js"
import { Link } from "./Link.js"
import {
buildSearchWordsFromSearchString,
Expand All @@ -28,7 +28,7 @@ function createColumns(
width: 200,
render: (chartConfigId) => (
<img
src={`${BAKED_GRAPHER_URL}/by-uuid/${chartConfigId}.svg`}
src={`${GRAPHER_DYNAMIC_THUMBNAIL_URL}/by-uuid/${chartConfigId}.svg`}
style={{ maxWidth: 200, maxHeight: 200 }}
/>
),
Expand Down Expand Up @@ -135,7 +135,7 @@ export function ChartViewIndexPage() {
}, [admin])

return (
<AdminLayout title="Narrative views">
<AdminLayout title="Narrative charts">
<main className="ChartViewIndexPage">
<Flex justify="space-between">
<Input
Expand Down
28 changes: 26 additions & 2 deletions adminSiteClient/EditorReferencesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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?:\/\//, "")

Expand All @@ -37,6 +41,8 @@ export class EditorReferencesTab<
return <EditorReferencesTabForChart editor={editor} />
else if (isIndicatorChartEditorInstance(editor))
return <EditorReferencesTabForIndicator editor={editor} />
else if (isChartViewEditorInstance(editor))
return <EditorReferencesTabForChartView editor={editor} />
else return null
}
}
Expand Down Expand Up @@ -81,7 +87,7 @@ export const ReferencesSection = (props: {
</a>{" "}
(
<a
href={`/admin/gdocs/${post.id}`}
href={`/admin/gdocs/${post.id}/preview`}
target="_blank"
rel="noopener"
>
Expand Down Expand Up @@ -127,7 +133,7 @@ export const ReferencesSection = (props: {

const chartViews = !!props.references?.chartViews?.length && (
<>
<p>Narrative views based on this chart</p>
<p>Narrative charts based on this chart</p>
<ul className="list-group">
{props.references.chartViews.map((chartView) => (
<li key={chartView.id} className="list-group-item">
Expand Down Expand Up @@ -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 (
<div>
<section>
<ReferencesSection references={this.references} />
</section>
</div>
)
}
}

@observer
class AddRedirectForm<Editor extends AbstractChartEditor> extends Component<{
editor: Editor
Expand Down
33 changes: 20 additions & 13 deletions adminSiteClient/EditorTextTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./Forms.js"
import { AbstractChartEditor } from "./AbstractChartEditor.js"
import { ErrorMessages } from "./ChartEditorTypes.js"
import { isChartViewEditorInstance } from "./ChartViewEditor.js"

@observer
export class EditorTextTab<
Expand Down Expand Up @@ -74,6 +75,10 @@ export class EditorTextTab<
return this.props.errorMessages
}

@computed get showChartSlug() {
return !isChartViewEditorInstance(this.props.editor)
}

@computed get showAnyAnnotationFieldInTitleToggle() {
const { features } = this.props.editor
return (
Expand Down Expand Up @@ -139,19 +144,21 @@ export class EditorTextTab<
/>
)}
{this.showAnyAnnotationFieldInTitleToggle && <hr />}
<AutoTextField
label="/grapher"
value={grapher.displaySlug}
onValue={this.onSlug}
isAuto={grapher.slug === undefined}
onToggleAuto={() =>
(grapher.slug =
grapher.slug === undefined
? grapher.displaySlug
: undefined)
}
helpText="Human-friendly URL for this chart"
/>
{this.showChartSlug && (
<AutoTextField
label="/grapher"
value={grapher.displaySlug}
onValue={this.onSlug}
isAuto={grapher.slug === undefined}
onToggleAuto={() =>
(grapher.slug =
grapher.slug === undefined
? grapher.displaySlug
: undefined)
}
helpText="Human-friendly URL for this chart"
/>
)}
<BindAutoStringExt
label="Subtitle"
readFn={(grapher) => grapher.currentSubtitle}
Expand Down
64 changes: 64 additions & 0 deletions adminSiteClient/NarrativeChartNameModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useMemo, useRef, useState } from "react"
import { Form, Input, InputRef, Modal, Spin } from "antd"

export const NarrativeChartNameModal = (props: {
initialName: string
open: "open" | "open-loading" | "closed"
errorMsg?: string
onSubmit: (name: string) => void
onCancel?: () => void
}) => {
const [name, setName] = useState<string>(props.initialName)
const inputField = useRef<InputRef>(null)
const isLoading = useMemo(() => props.open === "open-loading", [props.open])
const isOpen = useMemo(() => props.open !== "closed", [props.open])

useEffect(() => setName(props.initialName), [props.initialName])

useEffect(() => {
if (isOpen) {
inputField.current?.focus({ cursor: "all" })
}
}, [isOpen])

return (
<Modal
title="Save as narrative chart"
open={isOpen}
onOk={() => props.onSubmit(name)}
onCancel={props.onCancel}
onClose={props.onCancel}
okButtonProps={{ disabled: !name || isLoading }}
cancelButtonProps={{ disabled: isLoading }}
>
<div>
<p>
This will create a new narrative chart that is linked to
this chart. Any currently pending changes will be applied to
the narrative chart.
</p>
<p>
Please enter a programmatic name for the narrative chart.{" "}
<i>Note that this name cannot be changed later.</i>
</p>
<Form.Item label="Name">
<Input
ref={inputField}
onChange={(e) => setName(e.target.value)}
value={name}
disabled={isLoading}
/>
</Form.Item>
{isLoading && <Spin />}
{props.errorMsg && (
<div
className="alert alert-danger"
style={{ whiteSpace: "pre-wrap" }}
>
{props.errorMsg}
</div>
)}
</div>
</Modal>
)
}
Loading

0 comments on commit 558d3ad

Please sign in to comment.