Skip to content

Commit

Permalink
feat: use nice modal for entering narrative chart name, gracefully ha…
Browse files Browse the repository at this point in the history
…ndle duplicate names
  • Loading branch information
marcelgerber committed Jan 9, 2025
1 parent 4c6ed34 commit a7d896e
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 32 deletions.
32 changes: 16 additions & 16 deletions adminSiteClient/ChartEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
References,
} from "./AbstractChartEditor.js"
import { Admin } from "./Admin.js"
import { Form, Input, Modal } from "antd"

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Form' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Input' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Modal' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Form' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Input' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 29 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'Modal' is defined but never used. Allowed unused vars must match /^_/u
import React, { useState } from "react"

Check warning on line 30 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'React' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 30 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'useState' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 30 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'React' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 30 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'useState' is defined but never used. Allowed unused vars must match /^_/u

export interface Log {
userId: number
Expand Down Expand Up @@ -200,22 +202,17 @@ export class ChartEditor extends AbstractChartEditor<ChartEditorManager> {
)
}

async saveAsChartView(): Promise<void> {
const { patchConfig, grapher } = this

const chartJson = omit(patchConfig, CHART_VIEW_PROPS_TO_OMIT)

openNarrativeChartNameModal(): void {
const { grapher } = this
const suggestedName = grapher.title ? slugify(grapher.title) : undefined

Check warning on line 207 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'suggestedName' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 207 in adminSiteClient/ChartEditor.ts

View workflow job for this annotation

GitHub Actions / eslint

'suggestedName' is assigned a value but never used. Allowed unused vars must match /^_/u
}

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

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

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

const body = {
name,
Expand All @@ -228,11 +225,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
129 changes: 113 additions & 16 deletions adminSiteClient/SaveButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component } from "react"
import { Component, useEffect, useMemo, useRef, useState } from "react"
import { ChartEditor, isChartEditorInstance } from "./ChartEditor.js"
import { action, computed } from "mobx"
import { action, computed, observable } from "mobx"
import { observer } from "mobx-react"
import { excludeUndefined, omit } from "@ourworldindata/utils"
import { excludeUndefined, omit, slugify } from "@ourworldindata/utils"
import {
IndicatorChartEditor,
isIndicatorChartEditorInstance,
Expand All @@ -17,6 +17,7 @@ import {
chartViewsFeatureEnabled,
isChartViewEditorInstance,
} from "./ChartViewEditor.js"
import { Form, Input, InputRef, Modal, Spin } from "antd"

@observer
export class SaveButtons<Editor extends AbstractChartEditor> extends Component<{
Expand Down Expand Up @@ -61,10 +62,6 @@ class SaveButtonsForChart extends Component<{
void this.props.editor.saveAsNewGrapher()
}

@action.bound onSaveAsChartView() {
void this.props.editor.saveAsChartView()
}

@action.bound onPublishToggle() {
if (this.props.editor.grapher.isPublished)
this.props.editor.unpublishGrapher()
Expand All @@ -79,6 +76,29 @@ class SaveButtonsForChart extends Component<{
])
}

@computed get initialNarrativeChartName(): string {
return slugify(this.props.editor.grapher.title ?? "")
}

@observable narrativeChartNameModalOpen:
| "open"
| "open-loading"
| "closed" = "closed"
@observable narrativeChartNameModalError: string | undefined = undefined

@action.bound async onSubmitNarrativeChartButton(name: string) {
const { editor } = this.props

this.narrativeChartNameModalOpen = "open-loading"
const res = await editor.saveAsChartView(name)
if (res.success) {
this.narrativeChartNameModalOpen = "closed"
} else {
this.narrativeChartNameModalOpen = "open"
this.narrativeChartNameModalError = res.errorMsg
}
}

render() {
const { editingErrors } = this
const { editor } = this.props
Expand Down Expand Up @@ -117,15 +137,30 @@ class SaveButtonsForChart extends Component<{
</button>
</div>
{chartViewsFeatureEnabled && (
<div className="mt-2">
<button
className="btn btn-primary"
onClick={this.onSaveAsChartView}
disabled={isSavingDisabled}
>
Save as narrative chart
</button>
</div>
<>
<div className="mt-2">
<button
className="btn btn-primary"
onClick={() => {
this.narrativeChartNameModalOpen = "open"
this.narrativeChartNameModalError =
undefined
}}
disabled={isSavingDisabled}
>
Save as narrative chart
</button>
</div>
<NarrativeChartNameModal
open={this.narrativeChartNameModalOpen}
initialName={this.initialNarrativeChartName}
errorMsg={this.narrativeChartNameModalError}
onSubmit={this.onSubmitNarrativeChartButton}
onCancel={() =>
(this.narrativeChartNameModalOpen = "closed")
}
/>
</>
)}
{editingErrors.map((error, i) => (
<div key={i} className="alert alert-danger mt-2">
Expand Down Expand Up @@ -238,3 +273,65 @@ class SaveButtonsForChartView extends Component<{
)
}
}

const NarrativeChartNameModal = (props: {

Check failure on line 277 in adminSiteClient/SaveButtons.tsx

View workflow job for this annotation

GitHub Actions / eslint

Fast refresh only works when a file only exports components. Move your component(s) to a separate file

Check failure on line 277 in adminSiteClient/SaveButtons.tsx

View workflow job for this annotation

GitHub Actions / eslint

Fast refresh only works when a file only exports components. Move your component(s) to a separate file
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>
)
}
12 changes: 12 additions & 0 deletions adminSiteServer/apiRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3762,6 +3762,18 @@ postRouteWithRWTransaction(apiRouter, "/chartViews", async (req, res, trx) => {
throw new JsonError("Invalid request", 400)
}

const chartViewWithName = await trx
.table(ChartViewsTableName)
.where({ name })
.first()

if (chartViewWithName) {
return {
success: false,
errorMsg: `Narrative chart with name "${name}" already exists`,
}
}

const { patchConfig, fullConfig, queryParams } =
await createPatchConfigAndQueryParamsForChartView(
trx,
Expand Down

0 comments on commit a7d896e

Please sign in to comment.