-
-
Notifications
You must be signed in to change notification settings - Fork 229
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
/* ChartEditor.ts | ||
* ================ | ||
* | ||
* Mobx store that represents the current editor state and governs non-UI-related operations. | ||
* | ||
*/ | ||
|
||
import { Grapher } from "@ourworldindata/grapher" | ||
import { | ||
type DetailDictionary, | ||
Check warning on line 10 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
type RawPageview, | ||
Check warning on line 11 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
Topic, | ||
Check warning on line 12 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
PostReference, | ||
ChartRedirect, | ||
Check warning on line 14 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
DimensionProperty, | ||
Check warning on line 15 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
Json, | ||
GrapherInterface, | ||
diffGrapherConfigs, | ||
isEqual, | ||
omit, | ||
} from "@ourworldindata/utils" | ||
import { computed, observable, runInAction, when } from "mobx" | ||
Check warning on line 22 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js" | ||
Check warning on line 23 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
import { Admin } from "./Admin.js" | ||
import { EditorFeatures } from "./EditorFeatures.js" | ||
|
||
type EditorTab = string | ||
|
||
interface Variable { | ||
id: number | ||
name: string | ||
} | ||
|
||
export interface Dataset { | ||
id: number | ||
name: string | ||
namespace: string | ||
version: string | undefined | ||
variables: Variable[] | ||
isPrivate: boolean | ||
nonRedistributable: boolean | ||
} | ||
|
||
export interface Log { | ||
userId: number | ||
userName: string | ||
config: Json | ||
createdAt: string | ||
} | ||
|
||
export interface References { | ||
postsWordpress: PostReference[] | ||
postsGdocs: PostReference[] | ||
explorers: string[] | ||
} | ||
|
||
export const getFullReferencesCount = (references: References): number => { | ||
return ( | ||
references.postsWordpress.length + | ||
references.postsGdocs.length + | ||
references.explorers.length | ||
) | ||
} | ||
|
||
export interface Namespace { | ||
name: string | ||
description?: string | ||
isArchived: boolean | ||
} | ||
|
||
// This contains the dataset/variable metadata for the entire database | ||
// Used for variable selector interface | ||
|
||
export interface NamespaceData { | ||
datasets: Dataset[] | ||
} | ||
|
||
export class EditorDatabase { | ||
@observable.ref namespaces: Namespace[] | ||
@observable.ref variableUsageCounts: Map<number, number> = new Map() | ||
@observable dataByNamespace: Map<string, NamespaceData> = new Map() | ||
|
||
constructor(json: any) { | ||
this.namespaces = json.namespaces | ||
} | ||
} | ||
|
||
export type FieldWithDetailReferences = | ||
| "subtitle" | ||
| "note" | ||
| "axisLabelX" | ||
| "axisLabelY" | ||
|
||
export type DetailReferences = Record<FieldWithDetailReferences, string[]> | ||
|
||
export interface DimensionErrorMessage { | ||
displayName?: string | ||
} | ||
|
||
export interface AbstractChartEditorManager { | ||
admin: Admin | ||
grapher: Grapher | ||
baseGrapherConfig: GrapherInterface | ||
|
||
// logs: Log[] | ||
// references: References | undefined | ||
// redirects: ChartRedirect[] | ||
// pageviews?: RawPageview | ||
|
||
// database: EditorDatabase | ||
// allTopics: Topic[] | ||
// details: DetailDictionary | ||
// TODO | ||
// invalidDetailReferences: DetailReferences | ||
// errorMessages: Partial<Record<FieldWithDetailReferences, string>> | ||
// errorMessagesForDimensions: Record< | ||
// DimensionProperty, | ||
// DimensionErrorMessage[] | ||
// > | ||
} | ||
|
||
interface VariableIdUsageRecord { | ||
Check warning on line 122 in adminSiteClient/AbstractChartEditor.ts GitHub Actions / eslint
|
||
variableId: number | ||
usageCount: number | ||
} | ||
|
||
export abstract class AbstractChartEditor< | ||
Manager extends AbstractChartEditorManager = AbstractChartEditorManager, | ||
> { | ||
manager: Manager | ||
// Whether the current chart state is saved or not | ||
@observable.ref currentRequest: Promise<any> | undefined | ||
@observable.ref tab: EditorTab = "basic" | ||
@observable.ref errorMessage?: { title: string; content: string } // TODO: used for what? | ||
@observable.ref previewMode: "mobile" | "desktop" | ||
@observable.ref showStaticPreview = false | ||
@observable.ref savedPatchConfig: GrapherInterface = {} | ||
|
||
// This gets set when we save a new chart for the first time | ||
// so the page knows to update the url | ||
// @observable.ref newChartId?: number | ||
|
||
constructor(props: { manager: Manager }) { | ||
this.manager = props.manager | ||
this.previewMode = | ||
localStorage.getItem("editorPreviewMode") === "mobile" | ||
? "mobile" | ||
: "desktop" | ||
when( | ||
() => this.grapher.isReady, | ||
() => (this.savedPatchConfig = this.patchConfig) | ||
) | ||
} | ||
|
||
@computed get fullConfig(): GrapherInterface { | ||
return this.grapher.object | ||
} | ||
|
||
@computed get patchConfig(): GrapherInterface { | ||
const { baseGrapherConfig } = this.manager | ||
if (!baseGrapherConfig) return this.fullConfig | ||
return diffGrapherConfigs(this.fullConfig, baseGrapherConfig) | ||
} | ||
|
||
@computed get isModified(): boolean { | ||
return !isEqual( | ||
omit(this.patchConfig, "version"), | ||
omit(this.savedPatchConfig, "version") | ||
) | ||
} | ||
|
||
@computed get grapher() { | ||
return this.manager.grapher | ||
} | ||
|
||
// @computed get logs() { | ||
// return this.manager.logs | ||
// } | ||
|
||
// @computed get references() { | ||
// return this.manager.references | ||
// } | ||
|
||
// @computed get redirects() { | ||
// return this.manager.redirects | ||
// } | ||
|
||
// @computed get pageviews() { | ||
// return this.manager.pageviews | ||
// } | ||
|
||
// @computed get availableTabs(): EditorTab[] { | ||
// const tabs: EditorTab[] = ["basic", "data", "text", "customize"] | ||
// if (this.grapher.hasMapTab) tabs.push("map") | ||
// if (this.grapher.isScatter) tabs.push("scatter") | ||
// if (this.grapher.isMarimekko) tabs.push("marimekko") | ||
// tabs.push("revisions") | ||
// tabs.push("refs") | ||
// tabs.push("export") | ||
// return tabs | ||
// } | ||
|
||
// @computed get isNewGrapher() { | ||
// return this.grapher.id === undefined | ||
// } | ||
|
||
@computed get features() { | ||
return new EditorFeatures(this) | ||
} | ||
|
||
abstract get availableTabs(): EditorTab[] | ||
abstract saveGrapher(props?: { onError?: () => void }): Promise<void> | ||
|
||
// async saveGrapher({ | ||
// onError, | ||
// }: { onError?: () => void } = {}): Promise<void> { | ||
// const { grapher, isNewGrapher } = this | ||
// const currentGrapherObject = this.grapher.object | ||
|
||
// // Chart title and slug may be autocalculated from data, in which case they won't be in props | ||
// // But the server will need to know what we calculated in order to do its job | ||
// if (!currentGrapherObject.title) | ||
// currentGrapherObject.title = grapher.displayTitle | ||
|
||
// if (!currentGrapherObject.slug) | ||
// currentGrapherObject.slug = grapher.displaySlug | ||
|
||
// const targetUrl = isNewGrapher | ||
// ? "/api/charts" | ||
// : `/api/charts/${grapher.id}` | ||
|
||
// const json = await this.manager.admin.requestJSON( | ||
// targetUrl, | ||
// currentGrapherObject, | ||
// isNewGrapher ? "POST" : "PUT" | ||
// ) | ||
|
||
// if (json.success) { | ||
// if (isNewGrapher) { | ||
// this.newChartId = json.chartId | ||
// this.grapher.id = json.chartId | ||
// this.savedPatchConfig = json.savedPatch | ||
// } else { | ||
// runInAction(() => { | ||
// grapher.version += 1 | ||
// this.logs.unshift(json.newLog) | ||
// this.savedPatchConfig = json.savedPatch | ||
// }) | ||
// } | ||
// } else onError?.() | ||
// } | ||
|
||
// async saveAsNewGrapher(): Promise<void> { | ||
// const currentGrapherObject = this.grapher.object | ||
|
||
// const chartJson = { ...currentGrapherObject } | ||
// delete chartJson.id | ||
// delete chartJson.isPublished | ||
|
||
// // Need to open intermediary tab before AJAX to avoid popup blockers | ||
// const w = window.open("/", "_blank") as Window | ||
|
||
// const json = await this.manager.admin.requestJSON( | ||
// "/api/charts", | ||
// chartJson, | ||
// "POST" | ||
// ) | ||
// if (json.success) | ||
// w.location.assign( | ||
// this.manager.admin.url(`charts/${json.chartId}/edit`) | ||
// ) | ||
// } | ||
|
||
// publishGrapher(): void { | ||
// const url = `${BAKED_GRAPHER_URL}/${this.grapher.displaySlug}` | ||
|
||
// if (window.confirm(`Publish chart at ${url}?`)) { | ||
// this.grapher.isPublished = true | ||
// void this.saveGrapher({ | ||
// onError: () => (this.grapher.isPublished = undefined), | ||
// }) | ||
// } | ||
// } | ||
|
||
// unpublishGrapher(): void { | ||
// const message = | ||
// this.references && getFullReferencesCount(this.references) > 0 | ||
// ? "WARNING: This chart might be referenced from public posts, please double check before unpublishing. Try to remove the chart anyway?" | ||
// : "Are you sure you want to unpublish this chart?" | ||
// if (window.confirm(message)) { | ||
// this.grapher.isPublished = undefined | ||
// void this.saveGrapher({ | ||
// onError: () => (this.grapher.isPublished = true), | ||
// }) | ||
// } | ||
// } | ||
} |