Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gdocs): narrative charts component #4295

Merged
merged 27 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
94de57f
feat: pre-fetch chart views metadata in gdocs
marcelgerber Dec 11, 2024
3423d49
feat: NarrativeChart component
marcelgerber Dec 11, 2024
aef775b
refactor: properly attach gdocs attachments
marcelgerber Dec 12, 2024
baa90cb
enhance: ability to filter `chartViewMetadata`
marcelgerber Dec 12, 2024
9d122b4
refactor: add narrative-chart to `extractGdocComponentInfo`
marcelgerber Dec 16, 2024
6421de0
refactor: chartViewMetadata -> narrativeViewInfo
marcelgerber Dec 17, 2024
a017bfe
enhance: narrative views are reflected as links in gdocs
marcelgerber Dec 17, 2024
d6f787f
refactor: filter down `narrativeViewsInfo`
marcelgerber Dec 17, 2024
b1f0f14
refactor: change `linkType` enum
marcelgerber Dec 17, 2024
71a9ec8
fix: fix error when publishing NarrativeChart with error
marcelgerber Dec 18, 2024
a88527e
enhance: add narrative chart support to MultiEmbedder
marcelgerber Dec 18, 2024
e49bf1e
enhance: basic config to hide some grapher elements
marcelgerber Dec 18, 2024
6a67822
fix: correctly generate narrative view query params
marcelgerber Dec 18, 2024
ca49e91
refactor: use consistent names -- chart views & narrative charts
marcelgerber Dec 19, 2024
703ad18
feat: correctly generate query params for chart view
marcelgerber Dec 19, 2024
2ad9c6f
style: remove unused import
marcelgerber Dec 19, 2024
9250fc5
enhance: enable narrative charts on staging server
marcelgerber Dec 19, 2024
cb987c1
enhance: show static preview of chart view
marcelgerber Dec 19, 2024
af4bc66
enhance: make "explore the data" button blue
marcelgerber Dec 19, 2024
933e5e1
feat: use nice modal for entering narrative chart name, gracefully ha…
marcelgerber Jan 9, 2025
69fcf04
style: fix eslint warnings
marcelgerber Jan 9, 2025
abd1fda
enhance(admin): don't show slug for narrative charts
marcelgerber Jan 9, 2025
cb48517
feat: implement refs tab for narrative charts
marcelgerber Jan 9, 2025
bc813fc
fix(admin): show working gdocs link for refs
marcelgerber Jan 10, 2025
6287939
enhance: explore the data button styles
marcelgerber Jan 10, 2025
775d64b
enhance(analytics): analytics tracking for narrative charts
marcelgerber Jan 13, 2025
70798fd
enhance(admin): remove narrative charts feature flag
marcelgerber Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading