diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..fa29cdff --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +** \ No newline at end of file diff --git a/CONFIG.md b/CONFIG.md index 1627fbc2..a3e26ee4 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -192,6 +192,18 @@ Set whether the sidebar is by default open on starting the app. +### `defaultPanel` + +- *type:* `{SidebarId}` One of these strings: "diretory", "initiatives", "about" or "datasets" +- *in string context:* parsed as-is +- *default:* If unset, the default is 'directory' +- *settable?:* yes + +Defines which panel opens by default. + + + + ### `dialogueSize` - *type:* `{DialogueSize}` An object containing only string values. @@ -479,7 +491,7 @@ Preset location of the data source script(s). - *default:* `true` - *settable?:* yes -If true this will load the datasets panel +If true this will load the about panel @@ -503,7 +515,7 @@ If true this will load the datasets panel - *default:* `true` - *settable?:* yes -If true this will load the datasets panel +If true this will load the directory panel @@ -515,7 +527,7 @@ If true this will load the datasets panel - *default:* `true` - *settable?:* yes -If true this will load the datasets panel +If true this will load the initiatives (i.e. search) panel diff --git a/src/map-app/app/model/config-schema.ts b/src/map-app/app/model/config-schema.ts index e2d7bf02..bc7a1de6 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: { @@ -54,7 +55,7 @@ class TypeDef { stringDescr?: string; // A string-parsing function parseString?: (val: string) => T; -}; +} export interface VocabSource { @@ -84,7 +85,7 @@ export interface DataSource { id: string; type: string; label: string; -}; +} export interface HostSparqlDataSource extends DataSource { type: 'hostSparql'; @@ -135,6 +136,7 @@ export interface ReadableConfig { getShowDatasetsPanel(): boolean; getShowDirectoryPanel(): boolean; getShowSearchPanel(): boolean; + getDefaultPanel(): SidebarId; getSidebarButtonColour(): string; getSoftwareGitCommit(): string; getSoftwareTimestamp(): string; @@ -144,7 +146,7 @@ export interface ReadableConfig { htmlTitle(): string; logo(): string | undefined; vocabularies(): AnyVocabSource[]; -}; +} export interface WritableConfig { setDefaultLatLng(val: Point2d): void; @@ -166,7 +168,8 @@ export interface WritableConfig { setShowDatasetsPanel(val: boolean): void; setShowDirectoryPanel(val: boolean): void; setShowSearchPanel(val: boolean): void; -}; + setDefaultPanel(val: SidebarId): void; +} export interface ConfigSchema { // An identifier string @@ -187,7 +190,7 @@ export interface DialogueSize { width?: string; height?: string; descriptionRatio?: number; -}; +} export type InitiativeRenderFunction = (initiative: Initiative, model: DataServices) => string; @@ -235,6 +238,7 @@ export class ConfigData { showDatasetsPanel: boolean = true; showDirectoryPanel: boolean = true; showSearchPanel: boolean = true; + defaultPanel: SidebarId = 'directory'; sidebarButtonColour: string = '#39cccc'; tileUrl?: string; timestamp: string = '2000-01-01T00:00:00.000Z'; @@ -247,12 +251,12 @@ export class ConfigData { constructor(params: Partial = {}) { Object.assign(this, params); } -}; +} // This type is constrained to have the same keys as ConfigData, and // values which are ConfigSchema of the appropriate type for the // ConfigData property in question. -export type ConfigSchemas = { [K in keyof ConfigData]: ConfigSchema }; +export type ConfigSchemas = { [K in keyof ConfigData]: ConfigSchema } // Validates/normalises a language code. @@ -406,6 +410,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, '+ @@ -417,7 +429,7 @@ const types = { descr: 'An array of data source definitions, defining the type, ID, and in certain cases '+ 'other source-secific parameters needed for the source type', }), -}; +} @@ -724,7 +736,7 @@ export class Config implements ReadableConfig, WritableConfig { }, showAboutPanel: { id: 'showAboutPanel', - descr: `If true this will load the datasets panel`, + descr: `If true this will load the about panel`, getter: 'getShowAboutPanel', setter: 'setShowAboutPanel', type: types.boolean, @@ -738,18 +750,26 @@ export class Config implements ReadableConfig, WritableConfig { }, showDirectoryPanel: { id: 'showDirectoryPanel', - descr: `If true this will load the datasets panel`, + descr: `If true this will load the directory panel`, getter: 'getShowDirectoryPanel', setter: 'setShowDirectoryPanel', type: types.boolean, }, showSearchPanel: { id: 'showSearchPanel', - descr: `If true this will load the datasets panel`, + descr: `If true this will load the initiatives (i.e. search) panel`, getter: 'getShowSearchPanel', setter: 'setShowSearchPanel', type: types.boolean, }, + defaultPanel: { + id: "defaultPanel", + descr: "Defines which panel opens by default.", + defaultDescr: "If unset, the default is 'directory'", + getter: "getDefaultPanel", + setter: "setDefaultPanel", + type: types.sidebarId, + }, sidebarButtonColour: { id: "sidebarButtonColour", descr: 'Set the css background-colour attribute for the open sidebar button. Defaults to teal', @@ -1150,6 +1170,9 @@ ${def.descr} getShowSearchPanel(): boolean { return this.data.showSearchPanel; } + getDefaultPanel(): SidebarId { + return this.data.defaultPanel; + } getSidebarButtonColour(): string { return this.data.sidebarButtonColour; } @@ -1251,8 +1274,9 @@ ${def.descr} setShowSearchPanel(val: boolean): void { this.data.showSearchPanel = val; } + setDefaultPanel(val: SidebarId): void { + this.data.defaultPanel = val; + } -// [id: string]: Getter | Setter; -}; - - + // [id: string]: Getter | Setter; +} diff --git a/src/map-app/app/presenter/sidebar.ts b/src/map-app/app/presenter/sidebar.ts index 3bdbc768..feede78e 100644 --- a/src/map-app/app/presenter/sidebar.ts +++ b/src/map-app/app/presenter/sidebar.ts @@ -1,23 +1,36 @@ -import { Dictionary } from '../../common-types'; import { EventBus } from '../../eventbus'; import { MapUI } from '../map-ui'; import { Initiative } from '../model/initiative'; 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(); @@ -25,16 +38,13 @@ export class SidebarPresenter extends BasePresenter { this.showSearchPanel = mapui.config.getShowSearchPanel(); this.showAboutPanel = mapui.config.getShowAboutPanel(); this.showDatasetsPanel = mapui.config.getShowDatasetsPanel(); - this.view = new SidebarView(this, mapui.dataServices.getSidebarButtonColour()); + const defaultPanel = mapui.config.getDefaultPanel(); + this.view = new SidebarView( + this, + mapui.dataServices.getSidebarButtonColour() + ); this._eventbusRegister(); - this.createSidebars(); - this.changeSidebar(); - } - - createSidebars() { - this.children = {}; - if(this.showingDirectory()) this.children.directory = new DirectorySidebarPresenter(this); @@ -46,61 +56,80 @@ export class SidebarPresenter extends BasePresenter { if(this.showingDatasets()) this.children.datasets = new DatasetsSidebarPresenter(this); + + this.changeSidebar(defaultPanel); } - + /** * Changes 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; + changeSidebar(name?: SidebarId): void { + if (!name) { + if (this.sidebarName) { + // Just refresh the currently showing sidebar. + this.children[this.sidebarName]?.refreshView(false); } + else { + // If no sidebar is set, pick the first one + let key: SidebarId; + for(key in this.children) { + const child = this.children[key]; + if (!child) + continue; + + this.sidebarName = key; + child.refreshView(true); + break; + } + console.warn('No sidebars to show'); + } + return; } - if (name !== undefined) { - // If name is set, change the current sidebar and then refresh - this.sidebarName = name; - this.children[this.sidebarName]?.refreshView(true); - } - 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]; - this.children[this.sidebarName]?.refreshView(true); - } else { - console.warn('No sidebars to show'); - return; // No sidebars? Can't do anything. - } - } else { - this.children[this.sidebarName]?.refreshView(false); + 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(true); } + 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.` + ); } /** * Fully refresh the sidebar */ refreshSidebar() { - if (!this.sidebarName) { + if (this.sidebarName) { + this.children[this.sidebarName]?.refreshView(true); + } else { // If no sidebar is set, pick the first one - const names = Object.keys(this.children); - if (names.length > 0) { - this.sidebarName = names[0]; - this.children[this.sidebarName]?.refreshView(true); - } else { - console.warn('No sidebars to show'); - return; // No sidebars? Can't do anything. + let key: SidebarId; + for(key in this.children) { + const child = this.children[key]; + if (!child) + continue; + + this.sidebarName = key; + child.refreshView(true); + break; } - } else { - this.children[this.sidebarName]?.refreshView(true); + console.warn('No sidebars to show'); } + + // 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() { diff --git a/test/configs/typical/config.json b/test/configs/typical/config.json index 5113089c..99ad1678 100644 --- a/test/configs/typical/config.json +++ b/test/configs/typical/config.json @@ -19,5 +19,7 @@ "disableClusteringAtZoom": 10, "maxZoomOnGroup":12, "maxZoomOnOne": 14, - "maxZoomOnSearch": 12 + "maxZoomOnSearch": 12, + "defaultPanel": "about", + "defaultOpenSidebar": true } diff --git a/test/test-model-config.js b/test/test-model-config.js index 6f5f5bed..b8dbfe80 100644 --- a/test/test-model-config.js +++ b/test/test-model-config.js @@ -1,5 +1,4 @@ 'use strict'; -//const fs = require('fs'); import { assert } from 'chai'; import { init as configBuilder } from '../src/map-app/app/model/config'; @@ -8,7 +7,6 @@ import rawConfig from './configs/typical/config.json'; import version from './configs/typical/version.json'; const about = `This is a dummy about.html! `; -//fs.readFileSync('test/configs/typical/about.html'); const combinedConfig = { ...rawConfig, ...version, aboutHtml: about }; describe('The config.js module', function () { @@ -39,6 +37,8 @@ describe('The config.js module', function () { assert.equal(config.getMaxZoomOnOne(), 14); assert.equal(config.getMaxZoomOnSearch(), 12); assert.equal(config.logo(), undefined); + assert.equal(config.getDefaultPanel(), "about"); + assert.equal(config.getDefaultOpenSidebar(), true); }); }); });