From 21df6c7da512e8a1b9f426788a046c08f433ab5a Mon Sep 17 00:00:00 2001 From: Nick Stokoe Date: Tue, 19 Mar 2024 21:35:39 +0000 Subject: [PATCH] app/{model/config-schema.ts,presenter/sidebar.ts} - make defaultPanel typesafe Use typescipt's typing to restrict the possible values of defaultPanel to one of the allowed ones. Ran into a problem triggering ERR_REQUIRE_ESM with the d3 library, just by changing the imports... turns out this is a thorny thing to solve, so avoided it by not importing the things. Instead I needed to list the allowed panel IDs in the documentation manually, rather than programatically. But ESM modules are "the future" so we probably need to convert Mykomap to support it (and support CommonJS for cases that need that, probably using Babel transpilation) --- src/map-app/app/model/config-schema.ts | 25 +++++--- src/map-app/app/presenter/sidebar.ts | 89 +++++++++++++++----------- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/map-app/app/model/config-schema.ts b/src/map-app/app/model/config-schema.ts index 353680f9..e168d15a 100644 --- a/src/map-app/app/model/config-schema.ts +++ b/src/map-app/app/model/config-schema.ts @@ -34,6 +34,7 @@ import type { import { Initiative, InitiativeObj } from './initiative'; import { isIso6391Code, Iso6391Code } from '../../localisations'; +import { SidebarId } from '../presenter/sidebar'; class TypeDef { constructor(params: { @@ -133,7 +134,7 @@ export interface ReadableConfig { getShowDatasetsPanel(): boolean; getShowDirectoryPanel(): boolean; getShowSearchPanel(): boolean; - getDefaultPanel(): string; + getDefaultPanel(): SidebarId; getSidebarButtonColour(): string; getSoftwareGitCommit(): string; getSoftwareTimestamp(): string; @@ -165,7 +166,7 @@ export interface WritableConfig { setShowDatasetsPanel(val: boolean): void; setShowDirectoryPanel(val: boolean): void; setShowSearchPanel(val: boolean): void; - setDefaultPanel(val: string): void; + setDefaultPanel(val: SidebarId): void; } export interface ConfigSchema { @@ -234,7 +235,7 @@ export class ConfigData { showDatasetsPanel: boolean = true; showDirectoryPanel: boolean = true; showSearchPanel: boolean = true; - defaultPanel: string = ''; + defaultPanel: SidebarId = 'directory'; sidebarButtonColour: string = '#39cccc'; tileUrl?: string; timestamp: string = '2000-01-01T00:00:00.000Z'; @@ -406,6 +407,14 @@ const types = { name: '{InitiativeRenderFunction}', descr: 'A function which accepts an Initiative instance and returns an HTML string', }), + sidebarId: new TypeDef({ + name: '{SidebarId}', + // Would be sensible to generate this from the keys of a `new SidebarPanels()` + // - except if we import that we hit ERR_REQUIRE_ESM because d3 is a pure ESM module. + // And this module is currently not pure ESM. Argh. And it's not trivial to switch! + // See, for example https://commerce.nearform.com/blog/2022/victory-esm/ + descr: 'One of these strings: "diretory", "initiatives", "about" or "datasets"' + }), vocabSources: new TypeDef({ name: '{AnyVocabSource[]}', descr: 'An array of vocab source definitions, defining a SPARQL endpoint URL, '+ @@ -745,11 +754,11 @@ export class Config implements ReadableConfig, WritableConfig { }, defaultPanel: { id: "defaultPanel", - descr: "The string `about`, `directory`, `datasets`, or `initiatives` (i.e. search)", - defaultDescr: "the leftmost panel, which is usually the `directory`", + descr: "Defines which panel opens by default.", + defaultDescr: "If unset, the default is 'directory'", getter: "getDefaultPanel", setter: "setDefaultPanel", - type: types.string, + type: types.sidebarId, }, sidebarButtonColour: { id: "sidebarButtonColour", @@ -1021,7 +1030,7 @@ ${def.descr} getShowSearchPanel(): boolean { return this.data.showSearchPanel; } - getDefaultPanel(): string { + getDefaultPanel(): SidebarId { return this.data.defaultPanel; } getSidebarButtonColour(): string { @@ -1109,7 +1118,7 @@ ${def.descr} setShowSearchPanel(val: boolean): void { this.data.showSearchPanel = val; } - setDefaultPanel(val: string): void { + setDefaultPanel(val: SidebarId): void { this.data.defaultPanel = val; } diff --git a/src/map-app/app/presenter/sidebar.ts b/src/map-app/app/presenter/sidebar.ts index 4c880a9b..49abde69 100644 --- a/src/map-app/app/presenter/sidebar.ts +++ b/src/map-app/app/presenter/sidebar.ts @@ -1,22 +1,35 @@ -import { Dictionary } from '../../common-types'; import { EventBus } from '../../eventbus'; import { MapUI } from '../map-ui'; import { SidebarView } from '../view/sidebar'; import { BasePresenter } from './base'; import { AboutSidebarPresenter } from './sidebar/about'; -import { BaseSidebarPresenter } from './sidebar/base'; import { DatasetsSidebarPresenter } from './sidebar/datasets'; import { DirectorySidebarPresenter } from './sidebar/directory'; import { InitiativesSidebarPresenter } from './sidebar/initiatives'; +/// A collection of sidebar panels, by name +export class SidebarPanels { + directory?: DirectorySidebarPresenter = undefined; + initiatives?: InitiativesSidebarPresenter = undefined; + about?: AboutSidebarPresenter = undefined; + datasets?: DatasetsSidebarPresenter = undefined; + + // A list of the IDs as a convenience + static readonly ids = Object.keys(new SidebarPanels()) as SidebarId[]; +} + +/// This type can contain only sidebar ID names +export type SidebarId = keyof SidebarPanels; + export class SidebarPresenter extends BasePresenter { readonly view: SidebarView; readonly showDirectoryPanel: boolean; readonly showSearchPanel: boolean; readonly showAboutPanel: boolean; readonly showDatasetsPanel: boolean; - private children: Dictionary = {}; - private sidebarName?: string; + private readonly children = new SidebarPanels(); + + private sidebarName?: SidebarId; constructor(readonly mapui: MapUI) { super(); @@ -24,20 +37,13 @@ export class SidebarPresenter extends BasePresenter { this.showSearchPanel = mapui.config.getShowSearchPanel(); this.showAboutPanel = mapui.config.getShowAboutPanel(); this.showDatasetsPanel = mapui.config.getShowDatasetsPanel(); - const defaultPanel = mapui.config.getDefaultPanel() || undefined; + const defaultPanel = mapui.config.getDefaultPanel(); this.view = new SidebarView( this, mapui.dataServices.getSidebarButtonColour() ); this._eventbusRegister(); - this.createSidebars(); - this.changeSidebar(defaultPanel); - } - - createSidebars() { - this.children = {}; - if(this.showingDirectory()) this.children.directory = new DirectorySidebarPresenter(this); @@ -49,39 +55,50 @@ export class SidebarPresenter extends BasePresenter { if(this.showingDatasets()) this.children.datasets = new DatasetsSidebarPresenter(this); + + this.changeSidebar(defaultPanel); } // Changes or refreshes the sidebar // - // @param name - the sidebar to change (needs to be one of the keys - // of this.sidebar) - changeSidebar(name?: string) { - if (name !== undefined) { - // Validate name - if (!(name in this.children)) { - console.warn(`ignoring request to switch to non-existant sidebar '${name}'`); - name = undefined; + // @param name - the sidebar to change + changeSidebar(name?: SidebarId): void { + if (!name) { + if (this.sidebarName) { + // Just refresh the currently showing sidebar. + this.children[this.sidebarName]?.refreshView(); + } + else { + // If nothing is showing, refresh the first in the list. Or nothing, if none. + let key: SidebarId; + for(key in this.children) { + const child = this.children[key]; + if (!child) + continue; + + this.sidebarName = key; + child.refreshView(); + break; + } } + return; } - if (name !== undefined) { - // If name is set, change the current sidebar and then refresh - this.sidebarName = name; - this.children[this.sidebarName]?.refreshView(); - } - else { - // Just refresh the currently showing sidebar. - // If nothing is showing, show the first. Or nothing, if none. - if (!this.sidebarName) { - const names = Object.keys(this.children); - if (names.length > 0) - this.sidebarName = names[0]; - else - return; // No sidebars? Can't do anything. + if (name in this.children) { + // A valid SidebarId. If it's present, change the current sidebar to that, and then refresh + const child = this.children[name]; + if (child !== undefined) { + this.sidebarName = name; + child.refreshView(); } - - this.children[this.sidebarName]?.refreshView(); + return; } + + // If we get here it's not a valid sidebar (possibly it wasn't configured) + console.warn( + "Attempting to call SidebarPresenter.changeSidebar() with a "+ + `non-existant sidebar '${name}' - ignoring.` + ); } showSidebar() {