From ee214b84a829403612f34d5f0fd3bdd364abd800 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 12:29:33 +0100 Subject: [PATCH 1/6] feat(overrides): implement centralised overrides catalog, overrides template loader --- oarepo_ui/ext.py | 41 +++++++++++++++ oarepo_ui/loader.py | 22 ++++++++ oarepo_ui/resources/templating/catalog.py | 28 ++++------ oarepo_ui/templates/oarepo_ui/base_page.html | 1 + .../js/oarepo_ui/overridable-registry.js | 51 ++++++++++--------- oarepo_ui/utils.py | 13 +++++ oarepo_ui/views.py | 5 ++ 7 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 oarepo_ui/loader.py diff --git a/oarepo_ui/ext.py b/oarepo_ui/ext.py index af9cfa82..0734f318 100644 --- a/oarepo_ui/ext.py +++ b/oarepo_ui/ext.py @@ -2,12 +2,15 @@ import json from pathlib import Path +import deepmerge +import yaml from flask import Response, current_app from importlib_metadata import entry_points from invenio_base.utils import obj_or_import_string import oarepo_ui.cli # noqa from oarepo_ui.resources.templating.catalog import OarepoCatalog as Catalog +from oarepo_ui.utils import extract_priority class OARepoUIState: @@ -16,6 +19,7 @@ def __init__(self, app): self._resources = [] self.init_builder_plugin() self._catalog = None + self._ui_overrides = None def reinitialize_catalog(self): self._catalog = None @@ -31,6 +35,40 @@ def catalog(self): self._catalog = Catalog() return self._catalog_config(self._catalog, self.app.jinja_env) + @functools.cached_property + def ui_overrides(self) -> dict: + overrides = [] + eps = entry_points(group="oarepo.ui_overrides") + for ep in eps: + path = Path(obj_or_import_string(ep.module).__file__).parent / ep.attr + with path.open() as f: + overrides.append((ep.name, yaml.safe_load(f))) + + merger = deepmerge.Merger( + [(list, ["append_unique"]), (dict, ["merge"]), (set, ["union"])], + # next, choose the fallback strategies, + # applied to all other types: + ["override"], + # finally, choose the strategies in + # the case where the types conflict: + ["override"], + ) + prioritized_overrides = sorted(overrides, key=lambda name: extract_priority(name[0])[1]) + + ret = {} + for po in prioritized_overrides: + ret = merger.merge(ret, po[1]) + + return ret + + @property + def jinja_overrides(self) -> dict: + return self.ui_overrides.get('jinja', {}) + + @property + def react_overrides(self) -> dict: + return self.ui_overrides.get('react', {}) + def _catalog_config(self, catalog, env): context = {} env.policies.setdefault("json.dumps_kwargs", {}).setdefault("default", str) @@ -51,6 +89,9 @@ def _catalog_config(self, catalog, env): return catalog + def lookup_jinja_component(self, component_name: str) -> str: + return self.jinja_overrides.get(component_name, component_name) + def register_resource(self, ui_resource): self._resources.append(ui_resource) diff --git a/oarepo_ui/loader.py b/oarepo_ui/loader.py new file mode 100644 index 00000000..af1b9908 --- /dev/null +++ b/oarepo_ui/loader.py @@ -0,0 +1,22 @@ +from invenio_app.helpers import ThemeJinjaLoader + +from oarepo_ui.proxies import current_oarepo_ui + + +class OverridableThemeJinjaLoader(ThemeJinjaLoader): + """Overridable theme template loader. + + This loader acts as a wrapper for any type of Jinja loader. Before doing a + template lookup, the loader consults the ui_overrides configuration to determine + which template should be used. + """ + + def __init__(self, app, loader): + """Initialize loader. + """ + super().__init__(app, loader) + + def load(self, environment, name, globals=None): + name = current_oarepo_ui.lookup_jinja_component(name) + + return super().load(environment, name, globals) diff --git a/oarepo_ui/resources/templating/catalog.py b/oarepo_ui/resources/templating/catalog.py index e86e8c3c..70d220d8 100644 --- a/oarepo_ui/resources/templating/catalog.py +++ b/oarepo_ui/resources/templating/catalog.py @@ -14,6 +14,9 @@ from jinjax.exceptions import ComponentNotFound from jinjax.jinjax import JinjaX +from oarepo_ui.proxies import current_oarepo_ui +from oarepo_ui.utils import extract_priority + DEFAULT_URL_ROOT = "/static/components/" ALLOWED_EXTENSIONS = (".css", ".js") DEFAULT_PREFIX = "" @@ -158,7 +161,7 @@ def component_paths(self) -> Dict[str, Tuple[Path, Path]]: if hasattr(self, "_component_paths"): return self._component_paths - paths: Dict[str, Tuple[Path, Path, int]] = {} + paths: Dict[str, Tuple[Path, Path]] = {} for ( template_name, @@ -166,21 +169,18 @@ def component_paths(self) -> Dict[str, Tuple[Path, Path]]: relative_template_path, priority, ) in self.list_templates(): + # TODO: this is incorrect, doesn't work against e.g. components.EditForm split_template_name = template_name.split(DELIMITER) for idx in range(0, len(split_template_name)): partial_template_name = DELIMITER.join(split_template_name[idx:]) - partial_priority = priority - idx * 10 - - # if the priority is greater, replace the path + print(template_name, partial_template_name, idx, flush=True) if ( partial_template_name not in paths - or partial_priority > paths[partial_template_name][2] ): paths[partial_template_name] = ( absolute_template_path, relative_template_path, - partial_priority, ) self._component_paths = {k: (v[0], v[1]) for k, v in paths.items()} @@ -190,21 +190,10 @@ def component_paths(self) -> Dict[str, Tuple[Path, Path]]: def component_paths(self): self._component_paths = {} - def _extract_priority(self, filename): - # check if there is a priority on the file, if not, take default 0 - prefix_pattern = re.compile(r"^\d{3}-") - priority = 0 - if prefix_pattern.match(filename): - # Remove the priority from the filename - priority = int(filename[:3]) - filename = filename[4:] - return filename, priority - def _get_component_path( self, prefix: str, name: str, file_ext: "TFileExt" = "" ) -> "tuple[Path, Path]": name = name.replace(SLASH, DELIMITER) - paths = self.component_paths if name in paths: return paths[name] @@ -236,7 +225,7 @@ def list_templates(self): # extract priority split_name = list(template_name.rsplit(DELIMITER, 1)) - split_name[-1], priority = self._extract_priority(split_name[-1]) + split_name[-1], priority = extract_priority(split_name[-1]) template_name = DELIMITER.join(split_name) if stripped: @@ -254,6 +243,7 @@ def list_templates(self): def _get_from_source( self, *, name: str, url_prefix: str, source: str ) -> "Component": + name = current_oarepo_ui.lookup_jinja_component(name) return KeepGlobalContextComponent( self, super()._get_from_source(name=name, url_prefix=url_prefix, source=source), @@ -262,6 +252,7 @@ def _get_from_source( def _get_from_cache( self, *, prefix: str, name: str, url_prefix: str, file_ext: str ) -> "Component": + name = current_oarepo_ui.lookup_jinja_component(name) return KeepGlobalContextComponent( self, super()._get_from_cache( @@ -272,6 +263,7 @@ def _get_from_cache( def _get_from_file( self, *, prefix: str, name: str, url_prefix: str, file_ext: str ) -> "Component": + name = current_oarepo_ui.lookup_jinja_component(name) return KeepGlobalContextComponent( self, super()._get_from_file( diff --git a/oarepo_ui/templates/oarepo_ui/base_page.html b/oarepo_ui/templates/oarepo_ui/base_page.html index dd49cb80..040607e3 100644 --- a/oarepo_ui/templates/oarepo_ui/base_page.html +++ b/oarepo_ui/templates/oarepo_ui/base_page.html @@ -15,6 +15,7 @@ {%- block page_footer %} {% if not embedded %}{{ super() }}{% endif %} + {% endblock page_footer %} {%- block css%} diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index 73406867..2a433693 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -7,27 +7,32 @@ import { overrideStore } from "react-overridable"; // will be prioritized by leading prefix (e.g. 10-mapping.js will be processed // before 20-mapping.js). mapping.js without prefix will have lowest priority. -const requireMappingFiles = require.context( - "/templates/overridableRegistry/", - true, - /mapping.js$/ -); -requireMappingFiles - .keys() - .map((fileName) => { - const match = fileName.match(/\/(\d+)-mapping.js$/); - const priority = match ? parseInt(match[1], 10) : 0; - return { fileName, priority }; - }) - .sort((a, b) => a.priority - b.priority) - .forEach(({ fileName }) => { - const module = requireMappingFiles(fileName); - if (!module.default) { - console.error(`Mapping file ${fileName} does not have a default export.`); - } else { - for (const [key, value] of Object.entries(module.default)) { - overrideStore.add(key, value); - } - } - }); +// TODO: fetch react-overrides from base_page hidden input, register to overrideStore, +// import target component modules as lazily as possible + +// +// const requireMappingFiles = require.context( +// "/templates/overridableRegistry/", +// true, +// /mapping.js$/ +// ); +// +// requireMappingFiles +// .keys() +// .map((fileName) => { +// const match = fileName.match(/\/(\d+)-mapping.js$/); +// const priority = match ? parseInt(match[1], 10) : 0; +// return { fileName, priority }; +// }) +// .sort((a, b) => a.priority - b.priority) +// .forEach(({ fileName }) => { +// const module = requireMappingFiles(fileName); +// if (!module.default) { +// console.error(`Mapping file ${fileName} does not have a default export.`); +// } else { +// for (const [key, value] of Object.entries(module.default)) { +// overrideStore.add(key, value); +// } +// } +// }); diff --git a/oarepo_ui/utils.py b/oarepo_ui/utils.py index 3bbff477..0f2e3a7d 100644 --- a/oarepo_ui/utils.py +++ b/oarepo_ui/utils.py @@ -1,3 +1,5 @@ +import re + from marshmallow import Schema, fields from marshmallow.schema import SchemaMeta from marshmallow_utils.fields import NestedAttribute @@ -28,3 +30,14 @@ def dump_empty(schema_or_field): if isinstance(schema_or_field, fields.Dict): return {} return None + + +def extract_priority(filename): + # check if there is a priority on the file, if not, take default 0 + prefix_pattern = re.compile(r"^\d{3}-") + priority = 0 + if prefix_pattern.match(filename): + # Remove the priority from the filename + priority = int(filename[:3]) + filename = filename[4:] + return filename, priority diff --git a/oarepo_ui/views.py b/oarepo_ui/views.py index ddf43f85..ec5440dd 100644 --- a/oarepo_ui/views.py +++ b/oarepo_ui/views.py @@ -1,6 +1,8 @@ from flask import Blueprint from invenio_base.utils import obj_or_import_string +from .loader import OverridableThemeJinjaLoader + def create_blueprint(app): blueprint = Blueprint("oarepo_ui", __name__, template_folder="templates") @@ -24,7 +26,10 @@ def add_jinja_filters(state): for k, v in app.config["OAREPO_UI_JINJAX_GLOBALS"].items() } ) + env.globals.update({'react_overrides': ext.react_overrides}) env.policies.setdefault("json.dumps_kwargs", {}).setdefault("default", str) + app.jinja_env = env.overlay(loader=OverridableThemeJinjaLoader(app, env.loader)) + # the catalogue should not have been used at this point but if it was, we need to reinitialize it ext.reinitialize_catalog() From 42ad12cce40fcab2ff57b82f8db8ec7380debb32 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 13:01:48 +0100 Subject: [PATCH 2/6] feat(overrides): wire-up react-overrides to js code --- oarepo_ui/templates/oarepo_ui/base_page.html | 2 +- oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/hooks.js | 1 + .../assets/semantic-ui/js/oarepo_ui/overridable-registry.js | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/oarepo_ui/templates/oarepo_ui/base_page.html b/oarepo_ui/templates/oarepo_ui/base_page.html index 040607e3..4bafe035 100644 --- a/oarepo_ui/templates/oarepo_ui/base_page.html +++ b/oarepo_ui/templates/oarepo_ui/base_page.html @@ -15,7 +15,7 @@ {%- block page_footer %} {% if not embedded %}{{ super() }}{% endif %} - + {% endblock page_footer %} {%- block css%} diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/hooks.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/hooks.js index ac133ad5..a14c73c8 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/hooks.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/hooks.js @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { registerLocale } from "react-datepicker"; +import { getInputFromDOM } from "./util"; export const useLoadLocaleObjects = (localesArray = ["cs", "en-US"]) => { const [componentRendered, setComponentRendered] = useState(false); diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index 2a433693..9a7efe46 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -1,4 +1,5 @@ import { overrideStore } from "react-overridable"; +import { getInputFromDOM } from "./util"; // get all files below /templates/overridableRegistry that end with mapping.js. // The files shall be in a subfolder, in order to prevent clashing between mapping.js @@ -11,6 +12,11 @@ import { overrideStore } from "react-overridable"; // TODO: fetch react-overrides from base_page hidden input, register to overrideStore, // import target component modules as lazily as possible +const reactOverrides = getInputFromDOM("react-overrides"); +Object.entries(reactOverrides).forEach(([overridableId, importString]) => overrideStore.add(overridableId, importString)); + + +console.log("Loaded React component overrides:", overrideStore.getAll()) // // const requireMappingFiles = require.context( // "/templates/overridableRegistry/", From c858e85f9168a1e5e09b732b84e05418d4a86834 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 13:50:49 +0100 Subject: [PATCH 3/6] feat(overrides): wip lazy overridden comp import --- .../js/oarepo_ui/overridable-registry.js | 51 +++++-------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index 9a7efe46..64e6f391 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -1,44 +1,17 @@ +import * as React from 'react'; import { overrideStore } from "react-overridable"; import { getInputFromDOM } from "./util"; +import { importTemplate } from "@js/invenio_theme/templates"; -// get all files below /templates/overridableRegistry that end with mapping.js. -// The files shall be in a subfolder, in order to prevent clashing between mapping.js -// from different libraries. each mapping.js file shall have a default export -// that is an object with signature {"component-id": Component} the files -// will be prioritized by leading prefix (e.g. 10-mapping.js will be processed -// before 20-mapping.js). mapping.js without prefix will have lowest priority. - - -// TODO: fetch react-overrides from base_page hidden input, register to overrideStore, -// import target component modules as lazily as possible +const LazyOverriddenComponent = ({ importString }) => { + // TODO: try to import and render component module once rendered + const Component = importTemplate(importString); + console.log({Component}) +} const reactOverrides = getInputFromDOM("react-overrides"); -Object.entries(reactOverrides).forEach(([overridableId, importString]) => overrideStore.add(overridableId, importString)); - - -console.log("Loaded React component overrides:", overrideStore.getAll()) -// -// const requireMappingFiles = require.context( -// "/templates/overridableRegistry/", -// true, -// /mapping.js$/ -// ); -// -// requireMappingFiles -// .keys() -// .map((fileName) => { -// const match = fileName.match(/\/(\d+)-mapping.js$/); -// const priority = match ? parseInt(match[1], 10) : 0; -// return { fileName, priority }; -// }) -// .sort((a, b) => a.priority - b.priority) -// .forEach(({ fileName }) => { -// const module = requireMappingFiles(fileName); -// if (!module.default) { -// console.error(`Mapping file ${fileName} does not have a default export.`); -// } else { -// for (const [key, value] of Object.entries(module.default)) { -// overrideStore.add(key, value); -// } -// } -// }); +Object.entries(reactOverrides).forEach( + ([overridableId, importString]) => overrideStore.add( + overridableId, + )); +console.debug("Global React component overrides:", overrideStore.getAll()) From 2b8a91d61d06582f45b1df5c87eacb67717eb4b4 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 14:19:01 +0100 Subject: [PATCH 4/6] feat(overrides): implement lazy overridable closure to load component when needed --- .../js/oarepo_ui/overridable-registry.js | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index 64e6f391..8341f9b5 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -2,16 +2,32 @@ import * as React from 'react'; import { overrideStore } from "react-overridable"; import { getInputFromDOM } from "./util"; import { importTemplate } from "@js/invenio_theme/templates"; +import PropTypes from 'prop-types' -const LazyOverriddenComponent = ({ importString }) => { - // TODO: try to import and render component module once rendered - const Component = importTemplate(importString); - console.log({Component}) +function lazyOverridable (importString) { + function LazyOverride ({children, ...props}) { + const Override = importTemplate(importString); + + const name = Override.displayName || Override.name; + Override.displayName = `LazyOverride(${name})`; + + Override.propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + }; + Override.defaultProps = { + children: null, + }; + + return {children} + } + + return LazyOverride } + const reactOverrides = getInputFromDOM("react-overrides"); Object.entries(reactOverrides).forEach( ([overridableId, importString]) => overrideStore.add( - overridableId, + overridableId, lazyOverridable(importString) )); console.debug("Global React component overrides:", overrideStore.getAll()) From ef260a3eed0ba8940b99b9326df640929c664927 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 18:36:08 +0100 Subject: [PATCH 5/6] feat(overrides): implement lazy loading of referenced comp --- .../js/oarepo_ui/overridable-registry.js | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index 8341f9b5..cd255625 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -1,26 +1,66 @@ -import * as React from 'react'; +import React, { lazy } from 'react'; import { overrideStore } from "react-overridable"; import { getInputFromDOM } from "./util"; import { importTemplate } from "@js/invenio_theme/templates"; import PropTypes from 'prop-types' +function parseImportString (importString) { + if (importString.includes(':')) { + const [moduleName, exportName] = importString.split(':') + return { importType: 'module', moduleName, exportName } + } else { + const path = !(importString.endsWith('jsx') || importString.endsWith('js')) + ? `${importString}.jsx` + : importString + return { importType: 'template', path: importString } + } +} + function lazyOverridable (importString) { - function LazyOverride ({children, ...props}) { - const Override = importTemplate(importString); + function LazyOverride ({ children, ...props }) { + const { importType, ...importSpec } = parseImportString(importString) + const [OverrideComponent, setOverrideComponent] = React.useState(null); + + // Lazily load Component on mount + React.useEffect(() => { + console.log('Run effect') + async function loadOverrideComponent () { + let Component; - const name = Override.displayName || Override.name; - Override.displayName = `LazyOverride(${name})`; + if (importType === 'module') { + const { moduleName, exportName } = importSpec + // TODO: call dynamic webpack import here + } else if (importType === 'template') { + const { path } = importSpec + Component = await importTemplate(path); + console.log('Imported ', {Component}) + } else { + throw new Error(`Import type not supported for ${importString}`) + } - Override.propTypes = { - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - }; - Override.defaultProps = { - children: null, - }; + if (Component) { + const name = Component.displayName || Component.name; + Component.displayName = `LazyOverride(${name})`; + Component.propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + }; + Component.defaultProps = { + children: null, + }; - return {children} + setOverrideComponent(Component) + return Component + } + } + + loadOverrideComponent().catch(err => console.error(err)) + }, [importType, importSpec]) + + return OverrideComponent && {children} || Loading... } + LazyOverride.displayName = `LazyOverridable(${importString})`; + return LazyOverride } From bfd7ae2ae797c429ab9ffcf194ec38e485e21209 Mon Sep 17 00:00:00 2001 From: Miroslav Bauer Date: Fri, 8 Nov 2024 18:41:51 +0100 Subject: [PATCH 6/6] feat(overrides): fixup lazy loading of referenced comp --- .../semantic-ui/js/oarepo_ui/overridable-registry.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js index cd255625..79234a5b 100644 --- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js +++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/overridable-registry.js @@ -19,11 +19,12 @@ function parseImportString (importString) { function lazyOverridable (importString) { function LazyOverride ({ children, ...props }) { const { importType, ...importSpec } = parseImportString(importString) - const [OverrideComponent, setOverrideComponent] = React.useState(null); + const [OverrideComponent, setOverrideComponent] = React.useState(() => () => null); - // Lazily load Component on mount + // Lazily load a Component on mount, thanks to: https://stackoverflow.com/a/77028157 + // TODO: try if React.lazy could be somehow used in this scenario where we load either template or + // dynamic import (which React.lazy is supposed to only support) React.useEffect(() => { - console.log('Run effect') async function loadOverrideComponent () { let Component; @@ -48,13 +49,13 @@ function lazyOverridable (importString) { children: null, }; - setOverrideComponent(Component) + setOverrideComponent(() => Component) return Component } } loadOverrideComponent().catch(err => console.error(err)) - }, [importType, importSpec]) + }, []) return OverrideComponent && {children} || Loading... } @@ -70,4 +71,5 @@ Object.entries(reactOverrides).forEach( ([overridableId, importString]) => overrideStore.add( overridableId, lazyOverridable(importString) )); + console.debug("Global React component overrides:", overrideStore.getAll())