From da5b7d6e1e2d546f40d10f5d345579711e701b51 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 16 Sep 2026 12:33:22 +0000 Subject: [PATCH 1/3] Move some logic from generate_jsonmap to utils so it can be used elsewhere --- src/techui_builder/generate_jsonmap.py | 63 ++------------------------ src/techui_builder/utils.py | 59 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/techui_builder/generate_jsonmap.py b/src/techui_builder/generate_jsonmap.py index 5075b3d0..ee907209 100644 --- a/src/techui_builder/generate_jsonmap.py +++ b/src/techui_builder/generate_jsonmap.py @@ -13,6 +13,7 @@ from techui_builder._logger import Logger from techui_builder.models import Component, TechUi +from techui_builder.utils import _get_action_group, _get_macros, _get_nav_tabs logger_ = logging.getLogger(__name__) @@ -214,12 +215,12 @@ def _child_file_crawl( # Use file, name, and macro elements file_elem = open_display.file name_elem = widget_elem.name.text - macro_dict = self._get_macros(open_display) + macro_dict = _get_macros(open_display) case "embedded": file_elem = widget_elem.file name_elem = widget_elem.name.text - macro_dict = self._get_macros(widget_elem) + macro_dict = _get_macros(widget_elem) case "navtabs": tabs = _get_nav_tabs(widget_elem) @@ -229,7 +230,7 @@ def _child_file_crawl( for tab in tabs: name_elem = tab.name.text file_elem = tab.file - macro_dict = self._get_macros(tab) + macro_dict = _get_macros(tab) # Extract file path from file_elem # Keep raw string to preserve urls @@ -346,17 +347,6 @@ def _get_component_label( display_name = child_labels[name_elem] return display_name - def _get_macros(self, element: ObjectifiedElement): - if hasattr(element, "macros"): - macros = element.macros.getchildren() - if macros is not None: - return { - str(macro.tag): macro.text - for macro in macros - if macro.text is not None - } - return {} - def _parse_display_name(self, name: str | None, file_path: Path) -> str | None: """Parse display name from tag or file_path""" @@ -459,51 +449,6 @@ def _check_default(key: str, value: Any): return d -# File and desc are under the "actions", -# so the corresponding tag needs to be found -def _get_action_group(element: ObjectifiedElement) -> ObjectifiedElement | None: - try: - actions = element.actions - assert actions is not None - for action in actions.iterchildren("action"): - if action.get("type", default=None) == "open_display": - return action - return None - except AttributeError: - # TODO: Find better way of handling there being no "actions" group - # TODO: Do widgets always have a name attr, or _can_ it be empty?? - name = element.name - - parent_name = p.name if (p := element.getparent()) is not None else None - - logger_.error( - f"Actions group not found in component [bold]{name}[/bold] on " - f"[bold]{parent_name}[/bold]" - ) - - -def _get_nav_tabs(element: ObjectifiedElement) -> list[ObjectifiedElement] | None: - try: - element_tabs = element.tabs - assert element_tabs is not None - - tabs = list(element_tabs.iterchildren("tab")) - - return tabs - - except AttributeError: - # TODO: Find better way of handling there being no "tabs" group - # TODO: Do widgets always have a name attr, or _can_ it be empty?? - name = element.name - - parent_name = p.name if (p := element.getparent()) is not None else None - - logger_.error( - f"Tabs group not found in component [bold]{name}[/bold] on " - f"[bold]{parent_name}[/bold]" - ) - - @app.callback(invoke_without_command=True) def generate_jsonmap( bob_path: Annotated[ diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index fa1c35b7..3b5523fd 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -1,6 +1,10 @@ +import logging + from lxml import objectify from lxml.objectify import ObjectifiedElement +logger_ = logging.getLogger(__name__) + def read_bob(path): # Read the bob file @@ -32,3 +36,58 @@ def get_widgets(root: ObjectifiedElement): groups_widgets = get_widgets(child) widgets.update(groups_widgets) return widgets + + +def _get_macros(element: ObjectifiedElement): + if hasattr(element, "macros"): + macros = element.macros.getchildren() + if macros is not None: + return { + str(macro.tag): macro.text for macro in macros if macro.text is not None + } + return {} + + +# File and desc are under the "actions", +# so the corresponding tag needs to be found +def _get_action_group(element: ObjectifiedElement) -> ObjectifiedElement | None: + try: + actions = element.actions + assert actions is not None + for action in actions.iterchildren("action"): + if action.get("type", default=None) == "open_display": + return action + return None + except AttributeError: + # TODO: Find better way of handling there being no "actions" group + # TODO: Do widgets always have a name attr, or _can_ it be empty?? + name = element.name + + parent_name = p.name if (p := element.getparent()) is not None else None + + logger_.error( + f"Actions group not found in component [bold]{name}[/bold] on " + f"[bold]{parent_name}[/bold]" + ) + + +def _get_nav_tabs(element: ObjectifiedElement) -> list[ObjectifiedElement] | None: + try: + element_tabs = element.tabs + assert element_tabs is not None + + tabs = list(element_tabs.iterchildren("tab")) + + return tabs + + except AttributeError: + # TODO: Find better way of handling there being no "tabs" group + # TODO: Do widgets always have a name attr, or _can_ it be empty?? + name = element.name + + parent_name = p.name if (p := element.getparent()) is not None else None + + logger_.error( + f"Tabs group not found in component [bold]{name}[/bold] on " + f"[bold]{parent_name}[/bold]" + ) From b59ac20fe2a0934b9a397c95acb1202f43f7709c Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 16 Sep 2026 12:35:35 +0000 Subject: [PATCH 2/3] Add navtabs case to _get_widgets in utils.py --- src/techui_builder/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index 3b5523fd..4fdfcd30 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from lxml import objectify from lxml.objectify import ObjectifiedElement @@ -35,6 +36,33 @@ def get_widgets(root: ObjectifiedElement): # Get all the widgets inside of the group objects groups_widgets = get_widgets(child) widgets.update(groups_widgets) + case "navtabs": + # There is a switch to toggle between screens + # e.g. for different hutches on the main index.bob + # so we need to extract those files and the widgets + # on them. + tabs = _get_nav_tabs(child) + + if tabs is None: + continue + + for tab in tabs: + # name_elem = tab.name.text + file_elem = tab.file + # macro_dict = _get_macros(tab) + + # Extract file path from file_elem + # Keep raw string to preserve urls + file_text = file_elem.text.strip() if file_elem.text else "" + file_path = Path(file_text) + + # If file is already a .bob file, skip it + if not file_path.suffix == ".bob": + continue + + _, sub_widgets = read_bob(file_path) + widgets.update(sub_widgets) + return widgets From cea6b8458678bc11a1ae90e791cee7e42d793503 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 16 Sep 2026 14:46:35 +0000 Subject: [PATCH 3/3] Fix finding of navtab screen --- src/techui_builder/utils.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index 4fdfcd30..a96261b4 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -60,7 +60,19 @@ def get_widgets(root: ObjectifiedElement): if not file_path.suffix == ".bob": continue - _, sub_widgets = read_bob(file_path) + assert root.base, ( + f"The file path for the screen is invalid: {root.base}" + ) + root_file_dir = Path(root.base).parent + + # try to find the navtab screen next to the parent screen + sub_screen_path = root_file_dir / file_path + assert sub_screen_path.exists(), ( + f"The navtab screen '{file_path}' does not exist next to" + f" name {Path(root.base).name}" + ) + + _, sub_widgets = read_bob(sub_screen_path) widgets.update(sub_widgets) return widgets