From 9397b5f6aaa3e1676012ac8e99d1907e3259f0e3 Mon Sep 17 00:00:00 2001 From: aB0t Date: Thu, 9 Apr 2026 15:24:59 +0000 Subject: [PATCH] Release of v6.1.6-alpha1 Add Health Check buttons to the JCB Compiler and Component list view to verify system PHP configuration. Add the Toolbar Custom Buttons subform to the Global Subform settings for improved usability and presentation. Fix the Field edit view to ensure full compliance with Joomla 6 standards and updated UI/UX conventions. Begin migration from UIkit to Bootstrap 5 to align JCB with Joomla 6+ standards. Complete Bootstrap 5 refactor of all primary views and core UI. Continue phased removal of UIkit with full transition planned for upcoming releases. Fix deprecated JText usage in Admin Custom Tabs area for Joomla 5+ compatibility. #1302. --- CHANGELOG.md | 159 +- ComponentbuilderInstallerScript.php | 6 +- README.md | 6 +- admin/README.txt | 6 +- admin/access.xml | 2 + admin/assets/css/layout.css | 3 - admin/assets/js/initialization_selection.js | 921 ++++- admin/assets/js/pull_selection.js | 921 ++++- admin/config.xml | 12 +- admin/forms/admin_fields_conditions.xml | 2 +- admin/forms/admin_view.xml | 1 - admin/forms/component_updates.xml | 2 +- admin/forms/custom_admin_view.xml | 1 - admin/forms/fieldtype.xml | 2 +- admin/forms/library.xml | 2 +- admin/forms/site_view.xml | 1 - .../en-GB/en-GB.com_componentbuilder.ini | 48 +- .../en-GB/en-GB.com_componentbuilder.sys.ini | 4 + .../{filedisplay.php => filedisplayjsix.php} | 0 admin/layouts/repeatablejcbjfive.php | 82 - admin/layouts/repeatablejcbjsix.php | 124 + admin/layouts/reposelectioncardbody.php | 66 - admin/layouts/reposelectioncardbodyjsix.php | 92 + admin/layouts/sectionjcbjfive.php | 63 - admin/layouts/sectionjcbjsix.php | 116 + ...translationimportercolumnsdisplayjsix.php} | 2 +- ...=> translationimportereasymappingjsix.php} | 0 admin/sql/install.mysql.utf8.sql | 2 +- .../updates/mysql/{4_1_3.sql => 4_1_4.sql} | 0 .../updates/mysql/{5_1_4.sql => 5_1_5.sql} | 0 admin/sql/updates/mysql/6_1_5.sql | 1 + admin/src/Controller/Admin_viewController.php | 3 +- admin/src/Controller/CompilerController.php | 43 + .../Joomla_componentsController.php | 43 + admin/src/Helper/ComponentbuilderHelper.php | 1474 ++++--- admin/src/Model/Admin_viewModel.php | 11 +- admin/src/Model/Admin_viewsModel.php | 8 +- admin/src/Model/AjaxModel.php | 2388 +++++++---- .../src/Model/Component_admin_viewsModel.php | 102 + .../Component_custom_admin_viewsModel.php | 102 + admin/src/Model/Component_site_viewsModel.php | 102 + admin/src/Model/Component_updatesModel.php | 3 - admin/src/Model/Custom_admin_viewModel.php | 3 + admin/src/Model/FieldModel.php | 1 + admin/src/Model/FieldsModel.php | 16 +- admin/src/Model/Site_viewModel.php | 43 +- admin/src/Model/Site_viewsModel.php | 2 + admin/src/View/Admin_view/HtmlView.php | 21 +- admin/src/View/Class_extends/HtmlView.php | 21 +- admin/src/View/Class_method/HtmlView.php | 21 +- admin/src/View/Class_property/HtmlView.php | 21 +- admin/src/View/Compiler/HtmlView.php | 10 +- .../View/Component_admin_views/HtmlView.php | 15 +- .../Component_custom_admin_views/HtmlView.php | 15 +- .../View/Component_site_views/HtmlView.php | 15 +- admin/src/View/Custom_admin_view/HtmlView.php | 21 +- admin/src/View/Dynamic_get/HtmlView.php | 21 +- admin/src/View/Field/HtmlView.php | 11 +- admin/src/View/Fieldtype/HtmlView.php | 59 +- .../src/View/Import_translations/HtmlView.php | 9 +- .../Initialization_selection/HtmlView.php | 6 - admin/src/View/Joomla_component/HtmlView.php | 21 +- admin/src/View/Joomla_components/HtmlView.php | 5 + admin/src/View/Joomla_module/HtmlView.php | 56 +- admin/src/View/Joomla_plugin/HtmlView.php | 83 +- admin/src/View/Layout/HtmlView.php | 21 +- admin/src/View/Power/HtmlView.php | 59 +- admin/src/View/Pull_selection/HtmlView.php | 6 - admin/src/View/Site_view/HtmlView.php | 21 +- admin/src/View/Template/HtmlView.php | 21 +- admin/src/View/Validation_rule/HtmlView.php | 21 +- admin/tmpl/admin_view/default.php | 55 +- admin/tmpl/custom_code/default.php | 68 +- admin/tmpl/field/default.php | 1 + admin/tmpl/fields/default.php | 1 + admin/tmpl/fields/modal.php | 1 + admin/tmpl/import_translations/default.php | 2 +- ...> default_translationimporterareajsix.php} | 0 .../tmpl/initialization_selection/default.php | 2 +- ...area.php => default_selectionareajsix.php} | 74 +- admin/tmpl/placeholder/default.php | 16 +- admin/tmpl/pull_selection/default.php | 2 +- ....php => default_pullselectionareajsix.php} | 82 +- changelog.xml | 191 +- componentbuilder.xml | 9 +- componentbuilder_update_server.xml | 49 +- .../Compiler/Model/JoomlaFive/Customtabs.php | 6 +- .../Compiler/Model/JoomlaSix/Customtabs.php | 6 +- media/footable-v3/LICENSE-GPLv3 | 14 - media/footable-v3/css/footable.bootstrap.css | 266 -- .../css/footable.bootstrap.min.css | 1 - .../css/footable.core.bootstrap.css | 175 - .../css/footable.core.bootstrap.min.css | 1 - .../css/footable.core.standalone.css | 675 ---- .../css/footable.core.standalone.min.css | 1 - media/footable-v3/css/footable.editing.css | 50 - .../footable-v3/css/footable.editing.min.css | 1 - media/footable-v3/css/footable.filtering.css | 55 - .../css/footable.filtering.min.css | 1 - media/footable-v3/css/footable.paging.css | 31 - media/footable-v3/css/footable.paging.min.css | 1 - media/footable-v3/css/footable.sorting.css | 46 - .../footable-v3/css/footable.sorting.min.css | 1 - media/footable-v3/css/footable.standalone.css | 727 ---- .../css/footable.standalone.min.css | 1 - media/footable-v3/css/index.html | 1 - media/footable-v3/index.html | 1 - media/footable-v3/js/footable.core.js | 3507 ----------------- media/footable-v3/js/footable.core.min.js | 9 - media/footable-v3/js/footable.editing.js | 802 ---- media/footable-v3/js/footable.editing.min.js | 8 - media/footable-v3/js/footable.filtering.js | 1044 ----- .../footable-v3/js/footable.filtering.min.js | 8 - media/footable-v3/js/footable.min.js | 9 - media/footable-v3/js/footable.paging.js | 761 ---- media/footable-v3/js/footable.paging.min.js | 8 - media/footable-v3/js/footable.sorting.js | 490 --- media/footable-v3/js/footable.sorting.min.js | 8 - media/footable-v3/js/footable.state.js | 362 -- media/footable-v3/js/footable.state.min.js | 8 - media/footable-v3/js/index.html | 1 - media/js/admin_view.js | 292 +- media/js/custom_code.js | 117 +- media/js/placeholder.js | 79 +- media/js/validation_rule.js | 214 +- 125 files changed, 5752 insertions(+), 12160 deletions(-) rename admin/layouts/{filedisplay.php => filedisplayjsix.php} (100%) delete mode 100644 admin/layouts/repeatablejcbjfive.php create mode 100644 admin/layouts/repeatablejcbjsix.php delete mode 100644 admin/layouts/reposelectioncardbody.php create mode 100644 admin/layouts/reposelectioncardbodyjsix.php delete mode 100644 admin/layouts/sectionjcbjfive.php create mode 100644 admin/layouts/sectionjcbjsix.php rename admin/layouts/{translationimportercolumnsdisplay.php => translationimportercolumnsdisplayjsix.php} (96%) rename admin/layouts/{translationimportereasymapping.php => translationimportereasymappingjsix.php} (100%) rename admin/sql/updates/mysql/{4_1_3.sql => 4_1_4.sql} (100%) rename admin/sql/updates/mysql/{5_1_4.sql => 5_1_5.sql} (100%) create mode 100644 admin/sql/updates/mysql/6_1_5.sql rename admin/tmpl/import_translations/{default_translationimporterarea.php => default_translationimporterareajsix.php} (100%) rename admin/tmpl/initialization_selection/{default_selectionarea.php => default_selectionareajsix.php} (73%) rename admin/tmpl/pull_selection/{default_pullselectionarea.php => default_pullselectionareajsix.php} (60%) delete mode 100644 media/footable-v3/LICENSE-GPLv3 delete mode 100644 media/footable-v3/css/footable.bootstrap.css delete mode 100644 media/footable-v3/css/footable.bootstrap.min.css delete mode 100644 media/footable-v3/css/footable.core.bootstrap.css delete mode 100644 media/footable-v3/css/footable.core.bootstrap.min.css delete mode 100644 media/footable-v3/css/footable.core.standalone.css delete mode 100644 media/footable-v3/css/footable.core.standalone.min.css delete mode 100644 media/footable-v3/css/footable.editing.css delete mode 100644 media/footable-v3/css/footable.editing.min.css delete mode 100644 media/footable-v3/css/footable.filtering.css delete mode 100644 media/footable-v3/css/footable.filtering.min.css delete mode 100644 media/footable-v3/css/footable.paging.css delete mode 100644 media/footable-v3/css/footable.paging.min.css delete mode 100644 media/footable-v3/css/footable.sorting.css delete mode 100644 media/footable-v3/css/footable.sorting.min.css delete mode 100644 media/footable-v3/css/footable.standalone.css delete mode 100644 media/footable-v3/css/footable.standalone.min.css delete mode 100644 media/footable-v3/css/index.html delete mode 100644 media/footable-v3/index.html delete mode 100644 media/footable-v3/js/footable.core.js delete mode 100644 media/footable-v3/js/footable.core.min.js delete mode 100644 media/footable-v3/js/footable.editing.js delete mode 100644 media/footable-v3/js/footable.editing.min.js delete mode 100644 media/footable-v3/js/footable.filtering.js delete mode 100644 media/footable-v3/js/footable.filtering.min.js delete mode 100644 media/footable-v3/js/footable.min.js delete mode 100644 media/footable-v3/js/footable.paging.js delete mode 100644 media/footable-v3/js/footable.paging.min.js delete mode 100644 media/footable-v3/js/footable.sorting.js delete mode 100644 media/footable-v3/js/footable.sorting.min.js delete mode 100644 media/footable-v3/js/footable.state.js delete mode 100644 media/footable-v3/js/footable.state.min.js delete mode 100644 media/footable-v3/js/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index eba6b5c1e..8f05e0878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# v6.1.6-alpha1 + +- Add Health Check buttons to the JCB Compiler and Component list view to verify system PHP configuration. +- Add the Toolbar Custom Buttons subform to the Global Subform settings for improved usability and presentation. +- Fix the Field edit view to ensure full compliance with Joomla 6 standards and updated UI/UX conventions. +- Begin migration from UIkit to Bootstrap 5 to align JCB with Joomla 6+ standards. +- Complete Bootstrap 5 refactor of all primary views and core UI. +- Continue phased removal of UIkit with full transition planned for upcoming releases. +- Fix deprecated JText usage in Admin Custom Tabs area for Joomla 5+ compatibility. #1302 + # v6.1.5 - Fix variable name mismatch for organization in the Network Resolve class. #1291 @@ -125,138 +135,29 @@ - Update the SQL file dump generator to handle large SQL dumps in batches, ensuring safe and reliable database imports. - Update example hints in the field expert mode options. -# v5.1.4 +# v5.1.5 -- Fix the linked [new] button to use the correct init_default params. -- Fix the Components list view to better display long website urls. -- Fix language text area css. #1264 -- Refactor the JS selText method. -- Add fix for empty currentFullPath in the Structuresingle class. -- Fix the buildSelectPart method to not access an empty select value in the Queries class. -- Fix that ensure that we always load and int into loadUserById. -- Fix the subform permission behaviour to use removeField. -- Add version 6 to the version 5 update server, for easier upgrade path. -- Fixed linked admin views with New and New & Close buttons to correctly initialize fields using global unique IDs and default conventions. -- Resolved missing language strings when linked admin views are also configured as site edit views. -- Corrected misconfiguration of the isModal property for site-linked views to default to false since modals are not yet supported on the site area. -- Enhanced security in edit views by moving access validation into the model, ensuring protection even when Joomla bypasses the controller layer. -- Added user access verification logic in the model to prevent unauthorized direct item loading. -- Refactored the view class to introduce a class constructor that initializes key dependencies more reliably. -- Began aligning the view class structure with Joomla 6's updated MVC architecture for future compatibility. -- Improved initialization handling in the view to reduce dependency errors during rendering. -- Fixed a bug caused by missing option settings in the model configuration. -- Updated and refined the site view class for greater consistency with backend view logic. -- Continued progressive improvements across view classes to ensure compatibility with Joomla's evolving MVC patterns. -- Fix permission issue in the list view. #1266 -- Refactor the list view buttons. -- Refactor the getActions helper method. -- Rename Custom Buttons tab to Toolbar for clearer grouping of toolbar functions. #1268 -- Add options for overriding default toolbars in all Admin, Site, and Custom Admin Views. #1268 -- Refactored toolbar into version specific classes to resolve getToolbar errors. #1267 -- Added method to unify getDatabase handling across Joomla versions and prevent undefined method calls. #1270 -- Fix php8.3 deprecation warnings in compiler templates. #1265 -- Refactor and enhance the JCB Email Helper class for a more robust and maintainable design. -- Fix an issue where the Email Helper send() function was executed twice. -- Fix a issue where layouts added to the PHP Ajax Model were not loaded during compilation. -- Add new functionality to the toolbar override system, enabling the inclusion of custom buttons via placeholders. -- Fix the Compiler success message to display both build seconds and project weeks. -- Add Development Valuation Model to calculate JCB component pricing. -- Add functionality to push JCB packages to Self hosted Git system that uses a self signed SSL. -- Add functionality that will include Validation Rules linked to fields in JCB packages. #1273 -- Refactor and Improve Router Helper Class to new standard Site Router practice. -- Refactor and Improve JCB Search engine for better searching of JCB areas. -- Fix bug in the search editor in JCB Search Engine. -- Fix issue where Custom Gets linked to a Site view did not ship correctly with JCB packages. #1272 -- Fix issue where the Icon linked to a Custom Admin Menu did not ship with JCB packages. #1271 -- Fix and Improve Normalize Class to better handle different files Linked to JCB packages. #1274 -- Remove batch feature from JCB compiler Templates due to lack of use and outdated code. -- Refactor Utilities/Response classes for Joomla 6+ compatibility and improved version-agnostic behavior. -- Refactor the File Upload Manager to achieve improved maintainability, greater customization flexibility, and easier extensibility. -- Refactor the Item Importer to improve data management, structural clarity, and long-term maintainability of the import process. -- Add a Pull button to all main JCB entities, allowing a complete reset of a selected entity together with all entities linked to it from a selected remote repository. -- Add functionality to the compiler to automatically pull missing entities from a remote repository when they are referenced by a component but not present locally in JCB. -- Refactor the Packaging Engine into separate set and get classes to improve separation of concerns, readability, and overall maintainability. -- Refactor and restructure the demo console item import plugin to improve usability, maintainability, and reusability across other areas of JCB. -- Refactor the Compiler to remove deprecated code paths and legacy classes that are no longer maintained. -- Add new Compiler Classes to modernise ancient tech previously embedded in the compiler and replace it with a clean, modern architecture. -- Add a dedicated CLI plugin to the JCB core, enabling direct interaction with JCB through the Joomla CLI console. -- Add structured CLI commands and options to Init, Reset, Push, and Pull JCB entities via the Joomla CLI. -- Add full support for compiling Components directly via the CLI, enabling automated and headless build workflows. -- Add functionality to place Super Power classes into the component src folder for both Admin and Site areas, using the correct namespaced structure. -- Add functionality to the CLI compiler to directly install compiled components and apply compiler options used during compilation. -- Add functionality to the compiler to automatically pull a missing component from a remote repository when a compilation is triggered and the component is not locally available. -- Add functionality to add and replace classes in module and plugin src folders that are linked to a component, when using correct namespace resolution. -- Add a Documentation tab to the Joomla Component Builder dashboard. -- Fix an issue with the JCB version notice on Joomla 5. -- Fix an issue where components without a component image failed to compile successfully. #1282 -- Add functionality for importing translated language strings directly into the Language Translations area of JCB. -- Fix issue where the Language Translations export button was not displayed due to recent toolbar changes. -- Fix a bug that caused language filters to malfunction because of a broken field configuration. #1278 -- Fix issue where components without a layout added to the Component failed to compile correctly. #1132 +- Fix variable name mismatch for organization in the Network Resolve class. #1291 +- Fix issue where Admin Field relations using custom code did not automatically include multiple fields as expected. #1293 +- Fix issue where Admin Field relations did not correctly apply concatenation/glue options. #1294 +- Refactor Compiler functions to improve maintainability and align with modern PHP coding best practices. +- Refactor the JCB Dashboard, introducing a modernized layout, improved usability, and full responsiveness across desktop, tablet, and mobile devices. +- Fix deprecated warnings triggered in the JCB CLI. +- Fix resolve PHP syntax errors and PHP 8.4 deprecations across codebase. +- Fix four fatal parse/compile errors. +- Fix implicit nullable parameter deprecations. -# v4.1.3 +# v4.1.4 -- Fix the linked [new] button to use the correct init_default params. -- Fix the Components list view to better display long website urls. -- Fix language text area css. #1264 -- Refactor the JS selText method. -- Add fix for empty currentFullPath in the Structuresingle class. -- Fix the buildSelectPart method to not access an empty select value in the Queries class. -- Fix that ensure that we always load and int into loadUserById. -- Fix the subform permission behaviour to use removeField. -- Add version 5 to the version 4 update server, for easier upgrade path. -- Fixed linked admin views with New and New & Close buttons to correctly initialize fields using global unique IDs and default conventions. -- Resolved missing language strings when linked admin views are also configured as site edit views. -- Corrected misconfiguration of the isModal property for site-linked views to default to false since modals are not yet supported on the site area. -- Enhanced security in edit views by moving access validation into the model, ensuring protection even when Joomla bypasses the controller layer. -- Added user access verification logic in the model to prevent unauthorized direct item loading. -- Refactored the view class to introduce a class constructor that initializes key dependencies more reliably. -- Began aligning the view class structure with Joomla 6's updated MVC architecture for future compatibility. -- Improved initialization handling in the view to reduce dependency errors during rendering. -- Fixed a bug caused by missing option settings in the model configuration. -- Updated and refined the site view class for greater consistency with backend view logic. -- Continued progressive improvements across view classes to ensure compatibility with Joomla's evolving MVC patterns. -- Fix permission issue in the list view. #1266 -- Refactor the list view buttons -- Refactor the getActions helper method -- Refactor toolbar into version-specific classes to resolve getToolbar errors. #1267 -- Add method to unify getDatabase handling across Joomla versions and prevent undefined method calls. #1270 -- Refactor and enhance the JCB Email Helper class for a more robust and maintainable design. -- Fix an issue where the Email Helper send() function was executed twice. -- Fix a issue where layouts added to the PHP Ajax Model were not loaded during compilation. -- Fix the Compiler success message to display both build seconds and project weeks. -- Add Development Valuation Model to calculate JCB component pricing. -- Add functionality to push JCB packages to Self hosted Git system that uses a self signed SSL. -- Add functionality that will include Validation Rules linked to fields in JCB packages. #1273 -- Refactor and Improve Router Helper Class to new standard Site Router practice. -- Refactor and Improve JCB Search engine for better searching of JCB areas. -- Fix bug in the search editor in JCB Search Engine. -- Fix issue where Custom Gets linked to a Site view did not ship correctly with JCB packages. #1272 -- Fix issue where the Icon linked to a Custom Admin Menu did not ship with JCB packages. #1271 -- Fix and Improve Normalize Class to better handle different files Linked to JCB packages. #1274 -- Remove batch feature from JCB compiler Templates due to lack of use and outdated code. -- Refactor Utilities/Response classes for Joomla 6+ compatibility and improved version-agnostic behavior. -- Refactor the File Upload Manager to achieve improved maintainability, greater customization flexibility, and easier extensibility. -- Refactor the Item Importer to improve data management, structural clarity, and long-term maintainability of the import process. -- Add a Pull button to all main JCB entities, allowing a complete reset of a selected entity together with all entities linked to it. -- Add functionality to the Compiler to automatically pull missing entities from a remote repository when they are referenced by a component but not present locally in JCB. -- Refactor the Packaging Engine into separate set and get classes to improve separation of concerns, readability, and overall maintainability. -- Refactor and restructure the demo console item import plugin to improve usability, maintainability, and reusability across other areas of JCB. -- Refactor the Compiler to remove deprecated code paths and legacy classes that are no longer maintained. -- Add new Compiler Classes to modernise ancient tech previously embedded in the compiler and replace it with a clean, modern architecture. -- Add a dedicated CLI plugin to the JCB core, enabling direct interaction with JCB through the Joomla CLI console. -- Add structured CLI commands and options to Init, Reset, Push, and Pull JCB entities via the Joomla CLI. -- Add full support for compiling Components directly via the CLI, enabling automated and headless build workflows. -- Add functionality to place Super Power classes into the component src folder for both Admin and Site areas, using the correct namespaced structure. -- Add functionality to the CLI compiler to directly install compiled components and apply compiler options used during compilation. -- Add functionality to the compiler to automatically pull a missing component from a remote repository when a compilation is triggered and the component is not locally available. -- Add functionality to add and replace classes in module and plugin src folders that are linked to a component, when using correct namespace resolution. -- Add a Documentation tab to the Joomla Component Builder dashboard. -- Fix an issue with the JCB version notice on Joomla 5. -- Fix an issue where components without a component image failed to compile successfully. #1282 -- Fix issue where the Language Translations export button was not displayed due to recent toolbar changes. -- Fix a bug that caused language filters to malfunction because of a broken field configuration. #1278 -- Fix issue where components without a layout added to the Component failed to compile correctly. #1132 +- Fix variable name mismatch for organization in the Network Resolve class. #1291 +- Fix issue where Admin Field relations using custom code did not automatically include multiple fields as expected. #1293 +- Fix issue where Admin Field relations did not correctly apply concatenation/glue options. #1294 +- Refactor Compiler functions to improve maintainability and align with modern PHP coding best practices. +- Refactor the JCB Dashboard, introducing a modernized layout, improved usability, and full responsiveness across desktop, tablet, and mobile devices. +- Fix deprecated warnings triggered in the JCB CLI. +- Fix resolve PHP syntax errors and PHP 8.4 deprecations across codebase. +- Fix four fatal parse/compile errors. +- Fix implicit nullable parameter deprecations. # v3.2.5 diff --git a/ComponentbuilderInstallerScript.php b/ComponentbuilderInstallerScript.php index b64cf8c9b..bb6092333 100644 --- a/ComponentbuilderInstallerScript.php +++ b/ComponentbuilderInstallerScript.php @@ -1517,10 +1517,10 @@ class Com_ComponentbuilderInstallerScript implements InstallerScriptInterface // Fix the assets table rules column size. - $this->setDatabaseAssetsRulesFix(106880, "MEDIUMTEXT"); + $this->setDatabaseAssetsRulesFix(107200, "MEDIUMTEXT"); // Install the global extension params. $this->setExtensionsParams( - '{"autorName":"Llewellyn van der Merwe","autorEmail":"joomla@vdm.io","subform_layouts":{"subform_layouts0":{"view_field":"default","layout":"repeatablejcbjfive"}},"editor":"none","allow_selfsigned_certificates":"0","set_browser_storage":"1","storage_time_to_live":"global","builder_gif_size":"480-272","compiler_plugin":["componentbuilderactionlogcompiler","componentbuilderfieldorderingcompiler","componentbuilderheaderscompiler","componentbuilderpowersautoloadercompiler","componentbuilderprivacycompiler"],"add_menu_prefix":"1","menu_prefix":"»","namespace_prefix":"JCB","minify":"0","language":"en-GB","percentagelanguageadd":"30","assets_table_fix":"2","compiler_field_builder_type":"2","field_name_builder":"2","type_name_builder":"2","avg_seconds_per_folder":"3.0","avg_seconds_per_file":"5.0","avg_seconds_per_line":"9.5","avg_lines_per_page":"56","avg_hourly_rate_usd":"75.0","debugging_factor":"0.25","planning_factor":"0.14","mapping_factor":"0.10","office_factor":"0.16","license_seat_annual_cost":"3000.0","assumed_developer_count":"1","workdays_per_year":"240","assumed_subscription_per_month":"100","market_multiplier_low":"0.9","market_multiplier_medium":"1.2","market_multiplier_high":"2.0","market_multiplier_revolutionary":"4.0","profit_margin_factor":"0.6","blueprint_replication_potential":"50","blueprint_risk_factor":"0.2","export_company":"Vast Development Method","export_owner":"Llewellyn van der Merwe","export_email":"joomla@vdm.io","export_website":"https://dev.vdm.io/","export_license":"GNU/GPL Version 2 or later - http://www.gnu.org/licenses/gpl-2.0.html","export_copyright":"Copyright (C) 2015. All Rights Reserved","check_in":"-1 day","save_history":"1","history_limit":"10","add_jquery_framework":"1","uikit_load":"1","uikit_min":"","uikit_style":""}' + '{"autorName":"Llewellyn van der Merwe","autorEmail":"joomla@vdm.io","subform_layouts":{"subform_layouts0":{"view_field":"default","layout":"repeatablejcbjsix"}},"editor":"none","allow_selfsigned_certificates":"0","set_browser_storage":"1","storage_time_to_live":"global","builder_gif_size":"480-272","compiler_plugin":["componentbuilderactionlogcompiler","componentbuilderfieldorderingcompiler","componentbuilderheaderscompiler","componentbuilderpowersautoloadercompiler","componentbuilderprivacycompiler"],"add_menu_prefix":"1","menu_prefix":"»","namespace_prefix":"JCB","minify":"0","language":"en-GB","percentagelanguageadd":"30","assets_table_fix":"2","compiler_field_builder_type":"2","field_name_builder":"2","type_name_builder":"2","avg_seconds_per_folder":"3.0","avg_seconds_per_file":"5.0","avg_seconds_per_line":"9.5","avg_lines_per_page":"56","avg_hourly_rate_usd":"75.0","debugging_factor":"0.25","planning_factor":"0.14","mapping_factor":"0.10","office_factor":"0.16","license_seat_annual_cost":"3000.0","assumed_developer_count":"1","workdays_per_year":"240","assumed_subscription_per_month":"100","market_multiplier_low":"0.9","market_multiplier_medium":"1.2","market_multiplier_high":"2.0","market_multiplier_revolutionary":"4.0","profit_margin_factor":"0.6","blueprint_replication_potential":"50","blueprint_risk_factor":"0.2","export_company":"Vast Development Method","export_owner":"Llewellyn van der Merwe","export_email":"joomla@vdm.io","export_website":"https://dev.vdm.io/","export_license":"GNU/GPL Version 2 or later - http://www.gnu.org/licenses/gpl-2.0.html","export_copyright":"Copyright (C) 2015. All Rights Reserved","check_in":"-1 day","save_history":"1","history_limit":"10","add_jquery_framework":"1","uikit_load":"1","uikit_min":"","uikit_style":""}' ); @@ -3293,7 +3293,7 @@ class Com_ComponentbuilderInstallerScript implements InstallerScriptInterface echo '
-

Upgrade to Version 6.1.5 Was Successful! Let us know if anything is not working as expected.

'; +

Upgrade to Version 6.1.6-alpha1 Was Successful! Let us know if anything is not working as expected.

'; // Add/Update component in the action logs extensions table. $this->setActionLogsExtensions(); diff --git a/README.md b/README.md index b44842487..4fa94ef47 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is a professional-grade [Joomla 6.x](https://extensions.joomla.org/extensio JCB generates native Joomla components, plugins, and modules for Joomla 3.x, 4.x, and 5.x - and is already prepared for Joomla 6. Every compiled project is tailored for the specific version without needing backward compatibility plugins. With integrated version-aware compiling, smart boilerplating, and Git-powered project syncing, JCB is much more than a code generator-it's a **full-stack development pipeline for Joomla extensions**. -You can install this component easily. The latest release (**6.1.5**) is available on [Releases](https://git.vdm.dev/joomla/pkg-component-builder/releases) and updated frequently with full source access. +You can install this component easily. The latest release (**6.1.6-alpha1**) is available on [Releases](https://git.vdm.dev/joomla/pkg-component-builder/releases) and updated frequently with full source access. Upgrades are seamless through Joomla's built-in extension update mechanism. @@ -196,9 +196,9 @@ JCB is developed by developers for developers. Its purpose is to democratize hig * **Company:** [Vast Development Method](https://dev.vdm.io) * **Author:** [Llewellyn van der Merwe](mailto:joomla@vdm.io) * **Component:** [Component Builder](https://git.vdm.dev/joomla/Component-Builder) -* **Created:** 30th April, 2015 · **Last Build:** 24th March, 2026 · **Version:** 6.1.5 +* **Created:** 30th April, 2015 · **Last Build:** 9th April, 2026 · **Version:** 6.1.6-alpha1 * **License:** GNU General Public License version 2 or later; see LICENSE.txt · **Copyright:** Copyright (C) 2015 Vast Development Method. All rights reserved. -* **Lines:** 1347496 · **Fields:** 2129 · **Files:** 8831 · **Folders:** 810 +* **Lines:** 1350169 · **Fields:** 2129 · **Files:** 8832 · **Folders:** 809 > Generated with [JCB](https://www.joomlacomponentbuilder.com) — The Smartest Way to Build Joomla Extensions. diff --git a/admin/README.txt b/admin/README.txt index b44842487..4fa94ef47 100644 --- a/admin/README.txt +++ b/admin/README.txt @@ -9,7 +9,7 @@ This is a professional-grade [Joomla 6.x](https://extensions.joomla.org/extensio JCB generates native Joomla components, plugins, and modules for Joomla 3.x, 4.x, and 5.x - and is already prepared for Joomla 6. Every compiled project is tailored for the specific version without needing backward compatibility plugins. With integrated version-aware compiling, smart boilerplating, and Git-powered project syncing, JCB is much more than a code generator-it's a **full-stack development pipeline for Joomla extensions**. -You can install this component easily. The latest release (**6.1.5**) is available on [Releases](https://git.vdm.dev/joomla/pkg-component-builder/releases) and updated frequently with full source access. +You can install this component easily. The latest release (**6.1.6-alpha1**) is available on [Releases](https://git.vdm.dev/joomla/pkg-component-builder/releases) and updated frequently with full source access. Upgrades are seamless through Joomla's built-in extension update mechanism. @@ -196,9 +196,9 @@ JCB is developed by developers for developers. Its purpose is to democratize hig * **Company:** [Vast Development Method](https://dev.vdm.io) * **Author:** [Llewellyn van der Merwe](mailto:joomla@vdm.io) * **Component:** [Component Builder](https://git.vdm.dev/joomla/Component-Builder) -* **Created:** 30th April, 2015 · **Last Build:** 24th March, 2026 · **Version:** 6.1.5 +* **Created:** 30th April, 2015 · **Last Build:** 9th April, 2026 · **Version:** 6.1.6-alpha1 * **License:** GNU General Public License version 2 or later; see LICENSE.txt · **Copyright:** Copyright (C) 2015 Vast Development Method. All rights reserved. -* **Lines:** 1347496 · **Fields:** 2129 · **Files:** 8831 · **Folders:** 810 +* **Lines:** 1350169 · **Fields:** 2129 · **Files:** 8832 · **Folders:** 809 > Generated with [JCB](https://www.joomlacomponentbuilder.com) — The Smartest Way to Build Joomla Extensions. diff --git a/admin/access.xml b/admin/access.xml index dd8eff7fc..facb37ba4 100644 --- a/admin/access.xml +++ b/admin/access.xml @@ -115,6 +115,7 @@ + @@ -330,6 +331,7 @@ + diff --git a/admin/assets/css/layout.css b/admin/assets/css/layout.css index c9fa7864a..c1cf583c2 100644 --- a/admin/assets/css/layout.css +++ b/admin/assets/css/layout.css @@ -23,6 +23,3 @@ width: 100% !important; .jform_dynamic_get_input_width { width: 90% !important; } -.uk-scrollable-box { -height: 380px !important; -} diff --git a/admin/assets/js/initialization_selection.js b/admin/assets/js/initialization_selection.js index 68cea2685..726a63ce7 100644 --- a/admin/assets/js/initialization_selection.js +++ b/admin/assets/js/initialization_selection.js @@ -10,77 +10,282 @@ /* JS Document */ -const memoryinitialization = []; +const memoryinitialization = {}; -function setSessionMemory(key, values, merge = true) { - if (merge) { - values = mergeSessionMemory(key, values); - } else { - values = JSON.stringify(values); +/** + * Check whether a value looks like a plain object. + * + * @param {*} value The value to inspect. + * + * @returns {boolean} True if the value is a plain object. + */ +function isPlainObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +/** + * Check whether a value is valid JSON text. + * + * @param {*} value The value to test. + * + * @returns {boolean} True if the value can be JSON parsed. + */ +function isJsonString(value) { + if (typeof value !== 'string' || value === '') { + return false; } - if (typeof Storage !== "undefined") { - sessionStorage.setItem(key, values); - } else { - memoryinitialization[key] = values; + try { + JSON.parse(value); + return true; + } catch (error) { + return false; } } -function mergeSessionMemory(key, values) { - const oldValues = getSessionMemory(key); - - if (oldValues) { - values = { ...oldValues, ...values }; +/** + * Convert a value to JSON text. + * + * @param {*} value The value to encode. + * + * @returns {string|null} The JSON text, or null on failure. + */ +function encodeSessionValue(value) { + try { + return JSON.stringify(value); + } catch (error) { + console.error('Failed to encode session value:', error); + return null; } - - return JSON.stringify(values); } +/** + * Decode JSON text from session memory. + * + * @param {*} value The stored JSON text. + * @param {*} defaultValue The fallback value. + * + * @returns {*} The decoded value or default. + */ +function decodeSessionValue(value, defaultValue = null) { + if (!isJsonString(value)) { + return defaultValue; + } + + try { + return JSON.parse(value); + } catch (error) { + console.error('Failed to decode session value:', error); + return defaultValue; + } +} + +/** + * Cached session storage availability flag. + * + * Evaluated once at load time to avoid repeated write-tests + * on every get/set call. + * + * @type {boolean} + */ +const _sessionStorageAvailable = (() => { + try { + if (typeof window === 'undefined' || !window.sessionStorage) { + return false; + } + + const testKey = '__jcb_session_test__'; + window.sessionStorage.setItem(testKey, '1'); + window.sessionStorage.removeItem(testKey); + + return true; + } catch (error) { + return false; + } +})(); + +/** + * Determine whether session storage is usable. + * + * @returns {boolean} True when sessionStorage is available. + */ +function hasSessionStorage() { + return _sessionStorageAvailable; +} + +/** + * Retrieve a parsed value from session storage. + * + * @param {string} key The storage key. + * @param {*} defaultValue The fallback value. + * + * @returns {*} The stored value or default. + */ function getSessionMemory(key, defaultValue = null) { - if (typeof Storage !== "undefined") { - const localValue = sessionStorage.getItem(key); + if (typeof key !== 'string' || key === '') { + return defaultValue; + } - if (isJsonString(localValue)) { - defaultValue = JSON.parse(localValue); + if (hasSessionStorage()) { + try { + return decodeSessionValue(window.sessionStorage.getItem(key), defaultValue); + } catch (error) { + console.error('Failed to read session storage:', error); + return defaultValue; } - } else if (typeof memoryinitialization[key] !== "undefined") { - const localValue = memoryinitialization[key]; + } - if (isJsonString(localValue)) { - defaultValue = JSON.parse(localValue); - } + if (typeof memoryinitialization[key] !== 'undefined') { + return decodeSessionValue(memoryinitialization[key], defaultValue); } return defaultValue; } -function isJsonString(str) { - try { - JSON.parse(str); - } catch (e) { +/** + * Merge new values into an existing session storage entry. + * + * @param {string} key The storage key. + * @param {*} values The values to merge. + * + * @returns {string|null} Encoded merged value. + */ +function mergeSessionMemory(key, values) { + const oldValues = getSessionMemory(key, null); + + if (isPlainObject(oldValues) && isPlainObject(values)) { + return encodeSessionValue({ ...oldValues, ...values }); + } + + return encodeSessionValue(values); +} + +/** + * Store a value in session storage. + * + * @param {string} key The storage key. + * @param {*} values The values to store. + * @param {boolean} merge Whether to merge plain objects with existing data. + * + * @returns {boolean} True on success. + */ +function setSessionMemory(key, values, merge = true) { + if (typeof key !== 'string' || key === '') { return false; } + const payload = merge ? mergeSessionMemory(key, values) : encodeSessionValue(values); + + if (payload === null) { + return false; + } + + if (hasSessionStorage()) { + try { + window.sessionStorage.setItem(key, payload); + return true; + } catch (error) { + console.error('Failed to write session storage:', error); + } + } + + memoryinitialization[key] = payload; + return true; } +/** + * Ensure a value is returned as an array. + * + * @param {*} items The value to normalize. + * + * @returns {Array} The normalized array. + */ function getArrayFormat(items) { - // Check if items is an object and not an array - if (typeof items === 'object' && !Array.isArray(items)) { + if (Array.isArray(items)) { + return items; + } + + if (isPlainObject(items)) { return Object.values(items); } - return items; + + return []; +} + +/** + * Get a translated string safely. + * + * @param {string} key The translation key. + * @param {string} fallbackText The fallback text. + * + * @returns {string} The translated or fallback text. + */ +function translate(key, fallbackText = '') { + if ( + typeof Joomla !== 'undefined' + && Joomla.Text + && typeof Joomla.Text._ === 'function' + ) { + const translated = Joomla.Text._(key); + + if (typeof translated === 'string' && translated !== '') { + return translated; + } + } + + return fallbackText || key; } class InitializationManager { + /** @type {HTMLElement|null} */ #repoArea = document.getElementById('select-repo-area'); - #powersArea = document.getElementById('select-powers-area'); - #initButton = document.getElementById('init-selected-powers'); - #backButton = document.getElementById('back-to-select-repo'); - #loadingDiv = window.loadingDiv || null; - #buildTable = typeof buildPowerSelectionTable === 'function' ? buildPowerSelectionTable : null; - #drawTable = typeof drawPowerSelectionTable === 'function' ? drawPowerSelectionTable : null; + /** @type {HTMLElement|null} */ + #powersArea = document.getElementById('select-powers-area'); + + /** @type {HTMLButtonElement|null} */ + #initButton = document.getElementById('init-selected-powers'); + + /** @type {HTMLButtonElement|null} */ + #backButton = document.getElementById('back-to-select-repo'); + + /** @type {HTMLElement|null} */ + #loadingDiv = window.loadingDiv || null; + + /** @type {Function|null} */ + #buildTable = typeof window.buildPowerSelectionTable === 'function' + ? window.buildPowerSelectionTable + : null; + + /** @type {Function|null} */ + #drawTable = typeof window.drawPowerSelectionTable === 'function' + ? window.drawPowerSelectionTable + : null; + + /** @type {AbortController|null} */ + #repoRequestController = null; + + /** @type {AbortController|null} */ + #initRequestController = null; + + /** @type {boolean} */ + #isLoadingRepo = false; + + /** @type {boolean} */ + #isInitializing = false; + + /** + * The current repository GUID. + * + * @type {string|null} + */ currentRepo = null; + + /** + * The current area key. + * + * @type {string|null} + */ currentArea = null; constructor() { @@ -89,29 +294,49 @@ class InitializationManager { this._updateInitButtonState(); } - /** Getter for selected items using global window reference. */ + /** + * Get the shared selected items collection. + * + * @returns {Array} The selected items. + */ get selectedItems() { if (!Array.isArray(window.selectedPowerItems)) { window.selectedPowerItems = []; } + return window.selectedPowerItems; } - /** Setter for selected items with sync to window and button state update. */ + /** + * Set the shared selected items collection. + * + * @param {Array} items The selected items. + * + * @returns {void} + */ set selectedItems(items) { - window.selectedPowerItems = items; + window.selectedPowerItems = Array.isArray(items) ? items : []; this._updateInitButtonState(); } - /** Add items to selection if not already selected. */ + /** + * Add items not already selected. + * + * @param {Array|Object} data The items to add. + * + * @returns {void} + */ addSelectedItems(data) { - if (!data || typeof data.length !== 'number') return; + const items = getArrayFormat(data); + + if (items.length === 0) { + return; + } const updated = [...this.selectedItems]; - for (let i = 0; i < data.length; i++) { - const item = data[i]; - if (!updated.some(existing => this.#isSameItem(existing, item))) { + for (const item of items) { + if (!updated.some((existing) => this.#isSameItem(existing, item))) { updated.push(item); } } @@ -119,284 +344,624 @@ class InitializationManager { this.selectedItems = updated; } - /** Remove items from selection based on identity comparison. */ + /** + * Remove items from the selection. + * + * @param {Array|Object} data The items to remove. + * + * @returns {void} + */ removeSelectedItems(data) { - if (!data || typeof data.length !== 'number') return; + const items = getArrayFormat(data); - const updated = this.selectedItems.filter(existing => { - for (let i = 0; i < data.length; i++) { - if (this.#isSameItem(existing, data[i])) { - return false; - } - } - return true; + if (items.length === 0) { + return; + } + + this.selectedItems = this.selectedItems.filter((existing) => { + return !items.some((item) => this.#isSameItem(existing, item)); }); - - this.selectedItems = updated; } - /** Check if two items are the same using GUID. */ + /** + * Compare two items by GUID. + * + * @param {Object} a First item. + * @param {Object} b Second item. + * + * @returns {boolean} True if both items match. + */ #isSameItem(a, b) { - return a?.guid && b?.guid && a.guid === b.guid; + return Boolean(a?.guid && b?.guid && a.guid === b.guid); } - /** Enable/disable the init button based on selection state. */ + /** + * Update action button state. + * + * @returns {void} + */ _updateInitButtonState() { if (this.#initButton) { - this.#initButton.disabled = this.selectedItems.length === 0; + this.#initButton.disabled = ( + this.selectedItems.length === 0 + || this.#isInitializing + || this.#isLoadingRepo + ); + } + + if (this.#backButton) { + this.#backButton.disabled = this.#isInitializing || this.#isLoadingRepo; } } - _bindRepoButtons() { - document.querySelectorAll('.select-repo-to-load').forEach(button => { - button.addEventListener('click', (e) => this._handleRepoClick(e)); + /** + * Toggle repository buttons disabled state. + * + * @param {boolean} disabled Whether buttons should be disabled. + * + * @returns {void} + */ + _setRepoButtonsDisabled(disabled) { + document.querySelectorAll('.select-repo-to-load').forEach((button) => { + button.disabled = disabled; }); } + /** + * Bind repository buttons. + * + * @returns {void} + */ + _bindRepoButtons() { + document.querySelectorAll('.select-repo-to-load').forEach((button) => { + button.addEventListener('click', (event) => this._handleRepoClick(event)); + }); + } + + /** + * Bind init and back buttons. + * + * @returns {void} + */ _bindInitSelectedPowers() { if (this.#initButton) { this.#initButton.addEventListener('click', () => this._handleInitSelectedPowers()); } + if (this.#backButton) { this.#backButton.addEventListener('click', () => this._handleBackToRepos()); } } + /** + * Handle repository click. + * + * @param {Event} event The click event. + * + * @returns {Promise} + */ async _handleRepoClick(event) { + if (this.#isLoadingRepo || this.#isInitializing) { + return; + } + const button = event.currentTarget; const repo = button?.dataset?.repo; const area = button?.dataset?.area; if (!repo || !area) { - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA"), "danger"); + this._notify( + translate('COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA'), + 'danger' + ); return; } + this.#isLoadingRepo = true; + this._updateInitButtonState(); + this._setRepoButtonsDisabled(true); this._showLoading(); - clearPowerSelectionTable(); + this._abortRepoRequest(); + + // Reset previous selection when switching repositories. + this.selectedItems = []; + + if (typeof window.clearPowerSelectionTable === 'function') { + window.clearPowerSelectionTable(); + } + + this.#repoRequestController = new AbortController(); const url = `${UrlAjax}getRepoIndex&repo=${encodeURIComponent(repo)}&area=${encodeURIComponent(area)}`; try { - const response = await fetch(url); + const response = await fetch(url, { + method: 'GET', + signal: this.#repoRequestController.signal, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } const data = await response.json(); - if (data.success && data.index && this.#buildTable) { - const repoData = data.index[0]; - const { - path = 'joomla/super-powers', - read_branch = 'master', - target, - base = 'https://git.vdm.dev' - } = repoData; + if (!data?.success || !data?.index || !this.#buildTable) { + this._notify( + data?.message || translate('COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX'), + 'danger' + ); + return; + } - const repo_base = (target === 'github') ? 'https://github.com' : base; - const repo_path = (target === 'github') ? 'tree' : 'src/branch'; + const repoData = Array.isArray(data.index) ? data.index[0] : data.index; - window.targetPowerRepoUrl = `${repo_base}/${path}/${repo_path}/${read_branch}/`; + if (!repoData || typeof repoData !== 'object') { + this._notify( + translate('COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX'), + 'danger' + ); + return; + } - this.#buildTable(repoData.index); + const { + path = 'joomla/super-powers', + read_branch = 'master', + target = '', + base = 'https://git.vdm.dev', + guid = null, + index = [] + } = repoData; - setTimeout(() => { - this._transitionTo(this.#repoArea, this.#powersArea); - this._hideLoading(); - }, 500); + const repoBase = target === 'github' ? 'https://github.com' : base; + const repoPath = target === 'github' ? 'tree' : 'src/branch'; - this.currentRepo = repoData.guid; - this.currentArea = area; - } else { - this._notify(data.message || Joomla.Text._("COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX"), "danger"); - this._hideLoading(); + window.targetPowerRepoUrl = `${repoBase}/${path}/${repoPath}/${read_branch}/`; + + // The index may be an array or an object keyed by GUID. + this.#buildTable(index); + + this.currentRepo = guid; + this.currentArea = area; + + await this._transitionTo(this.#repoArea, this.#powersArea); + + if (this.#drawTable) { + this.#drawTable(); } } catch (error) { - console.error("Fetch error:", error); + if (error.name !== 'AbortError') { + console.error('Fetch error:', error); + this._notify( + translate('COM_COMPONENTBUILDER_NETWORK_OR_SERVER_ERROR_OCCURRED_WHILE_FETCHING_INDEX'), + 'danger' + ); + } + } finally { + this.#isLoadingRepo = false; this._hideLoading(); - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_NETWORK_OR_SERVER_ERROR_OCCURRED_WHILE_FETCHING_INDEX"), "danger"); + this._setRepoButtonsDisabled(false); + this._updateInitButtonState(); } } + /** + * Go back to repository selection. + * + * @returns {void} + */ _handleBackToRepos() { - this._transitionTo(this.#powersArea, this.#repoArea); - } - - async _handleInitSelectedPowers() { - if (!Array.isArray(this.selectedItems) || this.selectedItems.length === 0) { - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_NO_ITEMS_SELECTED"), "warning"); + if (this.#isLoadingRepo || this.#isInitializing) { return; } + this._transitionTo(this.#powersArea, this.#repoArea) + .then(() => { + if (this.#drawTable) { + this.#drawTable(); + } + }) + .catch((error) => { + console.error('Transition error:', error); + }); + } + + /** + * Submit selected powers. + * + * @returns {Promise} + */ + async _handleInitSelectedPowers() { + if (this.#isInitializing || this.#isLoadingRepo) { + return; + } + + if (!Array.isArray(this.selectedItems) || this.selectedItems.length === 0) { + this._notify( + translate('COM_COMPONENTBUILDER_NO_ITEMS_SELECTED'), + 'warning' + ); + return; + } + + const area = this.currentArea; + const repo = this.currentRepo; + + if (!area || !repo) { + this._notify( + translate('COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA'), + 'danger' + ); + return; + } + + this.#isInitializing = true; + this._updateInitButtonState(); + this._setRepoButtonsDisabled(true); this._showLoading(); + this._abortInitRequest(); - const area = this.currentArea || 'error'; - const repo = this.currentRepo || 'error'; - - const func = 'initSelectedPackages'; + this.#initRequestController = new AbortController(); try { - // Convert selected items to form data const formData = new FormData(); - // Assuming Joomla expects `selected[]` for multiple values for (const item of this.selectedItems) { - formData.append('selected[]', item.guid); // Only sending GUIDs + if (item?.guid) { + formData.append('selected[]', item.guid); + } } + formData.append('area', area); formData.append('repo', repo); - const response = await fetch(`${UrlAjax}${func}`, { + const response = await fetch(`${UrlAjax}initSelectedPackages`, { method: 'POST', - body: formData + body: formData, + signal: this.#initRequestController.signal, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } const data = await response.json(); - if (!data.success) { - this._notify(data.message || Joomla.Text._("COM_COMPONENTBUILDER_FAILED_TO_INITIALIZE_SELECTED_POWERS"), "danger"); - } else { - this._handleResultLog(data.result_log || {}); + if (!data?.success) { + this._notify( + data?.message || translate('COM_COMPONENTBUILDER_FAILED_TO_INITIALIZE_SELECTED_POWERS'), + 'danger' + ); + return; } - this._hideLoading(); - this._transitionTo(this.#powersArea, this.#repoArea); - } catch (error) { - console.error("Submission error:", error); - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_ERROR_OCCURRED_WHILE_INITIALIZING_POWERS"), "danger"); + // Process result log before clearing selection, + // since _getNamesFromGuids reads from selectedItems. + this._handleResultLog(data.result_log || {}); + this.selectedItems = []; + await this._transitionTo(this.#powersArea, this.#repoArea); + } catch (error) { + if (error.name !== 'AbortError') { + console.error('Submission error:', error); + this._notify( + translate('COM_COMPONENTBUILDER_ERROR_OCCURRED_WHILE_INITIALIZING_POWERS'), + 'danger' + ); + } + } finally { + this.#isInitializing = false; this._hideLoading(); - this._transitionTo(this.#powersArea, this.#repoArea); + this._setRepoButtonsDisabled(false); + this._updateInitButtonState(); } } + /** + * Process init result log. + * + * @param {Object} resultLog The result log object. + * + * @returns {void} + */ _handleResultLog(resultLog) { const localGuids = this._normalizeGuids(resultLog.local); const notFoundGuids = this._normalizeGuids(resultLog.not_found); const addedGuids = this._normalizeGuids(resultLog.added); if (localGuids.length > 0) { - this._notify( - this._generateResultMessage(localGuids, 'local'), + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_ALREADY_PRESENT_LOCALLY_AND_WERE_NOT_INITIALIZED'), + this._getNamesFromGuids(localGuids), 'info' ); } if (notFoundGuids.length > 0) { - this._notify( - this._generateResultMessage(notFoundGuids, 'not_found'), - 'info' + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_COULD_NOT_BE_FOUND_IN_THE_REMOTE_REPOSITORY_AND_WERE_NOT_INITIALIZED'), + this._getNamesFromGuids(notFoundGuids), + 'warning' ); } if (addedGuids.length > 0) { - this._notify( - this._generateResultMessage(addedGuids, 'added'), + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_SUCCESSFULLY_INITIALIZED'), + this._getNamesFromGuids(addedGuids), 'success' ); } } + /** + * Normalize log values to GUID arrays. + * + * @param {*} value The raw value. + * + * @returns {Array} The GUID array. + */ _normalizeGuids(value) { - if (!value) return []; - - if (Array.isArray(value)) { - return value; + if (!value) { + return []; } - if (typeof value === 'object') { + if (Array.isArray(value)) { + return value.filter((item) => typeof item === 'string' && item !== ''); + } + + if (isPlainObject(value)) { return Object.keys(value); } return []; } - _generateResultMessage(guids, type) { - const messages = { - local: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_ALREADY_PRESENT_LOCALLY_AND_WERE_NOT_INITIALIZED'), - not_found: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_COULD_NOT_BE_FOUND_IN_THE_REMOTE_REPOSITORY_AND_WERE_NOT_INITIALIZED'), - added: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_SUCCESSFULLY_INITIALIZED') - }; - + /** + * Resolve item names from GUIDs. + * + * @param {Array} guids The GUIDs. + * + * @returns {Array} The item names. + */ + _getNamesFromGuids(guids) { const names = []; for (const guid of guids) { - const item = this.selectedItems.find(i => i.guid === guid); - if (item?.name) { + const item = this.selectedItems.find((entry) => entry?.guid === guid); + + if (typeof item?.name === 'string' && item.name !== '') { names.push(item.name); } } - if (names.length === 0) { - return null; - } - - return `${messages[type]}\n
- ${names.join('\n
- ')}`; + return names; } - _transitionTo(hideEl, showEl) { - if (hideEl && showEl) { - UIkit.util.ready(() => { - UIkit.util.removeClass(hideEl, 'uk-animation-slide-top-small'); - UIkit.util.removeClass(showEl, 'uk-animation-slide-bottom-small'); - UIkit.util.addClass(hideEl, 'uk-animation-slide-top-small'); + /** + * Abort any running repository request. + * + * @returns {void} + */ + _abortRepoRequest() { + if (this.#repoRequestController) { + this.#repoRequestController.abort(); + this.#repoRequestController = null; + } + } - setTimeout(() => { + /** + * Abort any running init request. + * + * @returns {void} + */ + _abortInitRequest() { + if (this.#initRequestController) { + this.#initRequestController.abort(); + this.#initRequestController = null; + } + } + + /** + * Transition between two elements. + * + * @param {HTMLElement|null} hideEl The element to hide. + * @param {HTMLElement|null} showEl The element to show. + * + * @returns {Promise} Resolves after transition completes. + */ + _transitionTo(hideEl, showEl) { + if (!hideEl || !showEl || hideEl === showEl) { + return Promise.resolve(); + } + + const duration = 250; + + return new Promise((resolve) => { + hideEl.style.transition = `opacity ${duration}ms ease`; + showEl.style.transition = `opacity ${duration}ms ease`; + + hideEl.style.opacity = '1'; + showEl.style.display = 'none'; + showEl.style.opacity = '0'; + + requestAnimationFrame(() => { + hideEl.style.opacity = '0'; + + window.setTimeout(() => { hideEl.style.display = 'none'; showEl.style.display = ''; - UIkit.util.addClass(showEl, 'uk-animation-slide-bottom-small'); - if (this.#drawTable) this.#drawTable(); - }, 300); + showEl.style.opacity = '0'; + + requestAnimationFrame(() => { + showEl.style.opacity = '1'; + + window.setTimeout(() => { + hideEl.style.transition = ''; + hideEl.style.opacity = ''; + showEl.style.transition = ''; + showEl.style.opacity = ''; + resolve(); + }, duration); + }); + }, duration); }); + }); + } + + /** + * Show the loading overlay. + * + * @returns {void} + */ + _showLoading() { + if (this.#loadingDiv) { + this.#loadingDiv.style.display = 'block'; } } - _showLoading() { - if (this.#loadingDiv) this.#loadingDiv.style.display = 'block'; - } - + /** + * Hide the loading overlay. + * + * @returns {void} + */ _hideLoading() { - if (this.#loadingDiv) this.#loadingDiv.style.display = 'none'; + if (this.#loadingDiv) { + this.#loadingDiv.style.display = 'none'; + } } + /** + * Notify with a simple message. + * + * @param {string} message The message. + * @param {string} type The alert type. + * + * @returns {void} + */ _notify(message, type = 'info') { - const alertTypes = { - primary: 'alert-primary', - info: 'alert-info', - success: 'alert-success', - warning: 'alert-warning', - danger: 'alert-danger', - }; + if (typeof message !== 'string' || message.trim() === '') { + return; + } - const alertClass = alertTypes[type] || alertTypes.primary; + const body = document.createElement('div'); + body.textContent = message; + + this._renderAlert(body, type); + } + + /** + * Notify with a heading and a list. + * + * @param {string} heading The heading text. + * @param {Array} items The item names. + * @param {string} type The alert type. + * + * @returns {void} + */ + _notifyList(heading, items, type = 'info') { + if (!heading && (!Array.isArray(items) || items.length === 0)) { + return; + } + + const fragment = document.createDocumentFragment(); + + if (heading) { + const headingNode = document.createElement('div'); + headingNode.textContent = heading; + fragment.appendChild(headingNode); + } + + if (Array.isArray(items) && items.length > 0) { + const list = document.createElement('ul'); + list.className = 'mb-0 mt-2'; + + for (const item of items) { + const li = document.createElement('li'); + li.textContent = item; + list.appendChild(li); + } + + fragment.appendChild(list); + } + + const wrapper = document.createElement('div'); + wrapper.appendChild(fragment); + + this._renderAlert(wrapper, type); + } + + /** + * Render a Bootstrap alert. + * + * @param {Node} contentNode The content node. + * @param {string} type The alert type. + * + * @returns {void} + */ + _renderAlert(contentNode, type = 'info') { + const allowedTypes = ['primary', 'info', 'success', 'warning', 'danger']; + const alertType = allowedTypes.includes(type) ? type : 'primary'; let container = document.getElementById('alert-container'); + if (!container) { container = document.createElement('div'); container.id = 'alert-container'; container.className = 'position-fixed top-0 start-50 translate-middle-x p-3'; container.style.zIndex = '1060'; + container.style.maxWidth = 'min(92vw, 720px)'; + container.style.width = '100%'; document.body.appendChild(container); } const alert = document.createElement('div'); - alert.className = `alert ${alertClass} alert-dismissible fade show`; + alert.className = `alert alert-${alertType} alert-dismissible fade show shadow`; alert.setAttribute('role', 'alert'); - alert.innerHTML = ` - ${message} - - `; + const body = document.createElement('div'); + body.appendChild(contentNode); + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.className = 'btn-close'; + closeButton.setAttribute('data-bs-dismiss', 'alert'); + closeButton.setAttribute('aria-label', 'Close'); + + alert.appendChild(body); + alert.appendChild(closeButton); container.appendChild(alert); - setTimeout(() => { - alert.classList.remove('show'); - alert.classList.add('hide'); - alert.addEventListener('transitionend', () => { - alert.remove(); - }); + if ( + typeof bootstrap !== 'undefined' + && bootstrap.Alert + && typeof bootstrap.Alert.getOrCreateInstance === 'function' + ) { + const instance = bootstrap.Alert.getOrCreateInstance(alert); + + window.setTimeout(() => { + try { + instance.close(); + } catch (error) { + if (alert.parentNode) { + alert.remove(); + } + } + }, 5000); + + return; + } + + window.setTimeout(() => { + alert.remove(); }, 5000); } } \ No newline at end of file diff --git a/admin/assets/js/pull_selection.js b/admin/assets/js/pull_selection.js index 86dc8ab84..bd96fd143 100644 --- a/admin/assets/js/pull_selection.js +++ b/admin/assets/js/pull_selection.js @@ -10,77 +10,282 @@ /* JS Document */ -const memorypull = []; +const memorypull = {}; -function setSessionMemory(key, values, merge = true) { - if (merge) { - values = mergeSessionMemory(key, values); - } else { - values = JSON.stringify(values); +/** + * Check whether a value looks like a plain object. + * + * @param {*} value The value to inspect. + * + * @returns {boolean} True if the value is a plain object. + */ +function isPlainObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +/** + * Check whether a value is valid JSON text. + * + * @param {*} value The value to test. + * + * @returns {boolean} True if the value can be JSON parsed. + */ +function isJsonString(value) { + if (typeof value !== 'string' || value === '') { + return false; } - if (typeof Storage !== "undefined") { - sessionStorage.setItem(key, values); - } else { - memorypull[key] = values; + try { + JSON.parse(value); + return true; + } catch (error) { + return false; } } -function mergeSessionMemory(key, values) { - const oldValues = getSessionMemory(key); - - if (oldValues) { - values = { ...oldValues, ...values }; +/** + * Convert a value to JSON text. + * + * @param {*} value The value to encode. + * + * @returns {string|null} The JSON text, or null on failure. + */ +function encodeSessionValue(value) { + try { + return JSON.stringify(value); + } catch (error) { + console.error('Failed to encode session value:', error); + return null; } - - return JSON.stringify(values); } +/** + * Decode JSON text from session memory. + * + * @param {*} value The stored JSON text. + * @param {*} defaultValue The fallback value. + * + * @returns {*} The decoded value or default. + */ +function decodeSessionValue(value, defaultValue = null) { + if (!isJsonString(value)) { + return defaultValue; + } + + try { + return JSON.parse(value); + } catch (error) { + console.error('Failed to decode session value:', error); + return defaultValue; + } +} + +/** + * Cached session storage availability flag. + * + * Evaluated once at load time to avoid repeated write-tests + * on every get/set call. + * + * @type {boolean} + */ +const _sessionStorageAvailable = (() => { + try { + if (typeof window === 'undefined' || !window.sessionStorage) { + return false; + } + + const testKey = '__jcb_session_test__'; + window.sessionStorage.setItem(testKey, '1'); + window.sessionStorage.removeItem(testKey); + + return true; + } catch (error) { + return false; + } +})(); + +/** + * Determine whether session storage is usable. + * + * @returns {boolean} True when sessionStorage is available. + */ +function hasSessionStorage() { + return _sessionStorageAvailable; +} + +/** + * Retrieve a parsed value from session storage. + * + * @param {string} key The storage key. + * @param {*} defaultValue The fallback value. + * + * @returns {*} The stored value or default. + */ function getSessionMemory(key, defaultValue = null) { - if (typeof Storage !== "undefined") { - const localValue = sessionStorage.getItem(key); + if (typeof key !== 'string' || key === '') { + return defaultValue; + } - if (isJsonString(localValue)) { - defaultValue = JSON.parse(localValue); + if (hasSessionStorage()) { + try { + return decodeSessionValue(window.sessionStorage.getItem(key), defaultValue); + } catch (error) { + console.error('Failed to read session storage:', error); + return defaultValue; } - } else if (typeof memorypull[key] !== "undefined") { - const localValue = memorypull[key]; + } - if (isJsonString(localValue)) { - defaultValue = JSON.parse(localValue); - } + if (typeof memorypull[key] !== 'undefined') { + return decodeSessionValue(memorypull[key], defaultValue); } return defaultValue; } -function isJsonString(str) { - try { - JSON.parse(str); - } catch (e) { +/** + * Merge new values into an existing session storage entry. + * + * @param {string} key The storage key. + * @param {*} values The values to merge. + * + * @returns {string|null} Encoded merged value. + */ +function mergeSessionMemory(key, values) { + const oldValues = getSessionMemory(key, null); + + if (isPlainObject(oldValues) && isPlainObject(values)) { + return encodeSessionValue({ ...oldValues, ...values }); + } + + return encodeSessionValue(values); +} + +/** + * Store a value in session storage. + * + * @param {string} key The storage key. + * @param {*} values The values to store. + * @param {boolean} merge Whether to merge plain objects with existing data. + * + * @returns {boolean} True on success. + */ +function setSessionMemory(key, values, merge = true) { + if (typeof key !== 'string' || key === '') { return false; } + const payload = merge ? mergeSessionMemory(key, values) : encodeSessionValue(values); + + if (payload === null) { + return false; + } + + if (hasSessionStorage()) { + try { + window.sessionStorage.setItem(key, payload); + return true; + } catch (error) { + console.error('Failed to write session storage:', error); + } + } + + memorypull[key] = payload; + return true; } +/** + * Ensure a value is returned as an array. + * + * @param {*} items The value to normalize. + * + * @returns {Array} The normalized array. + */ function getArrayFormat(items) { - // Check if items is an object and not an array - if (typeof items === 'object' && !Array.isArray(items)) { + if (Array.isArray(items)) { + return items; + } + + if (isPlainObject(items)) { return Object.values(items); } - return items; + + return []; +} + +/** + * Get a translated string safely. + * + * @param {string} key The translation key. + * @param {string} fallbackText The fallback text. + * + * @returns {string} The translated or fallback text. + */ +function translate(key, fallbackText = '') { + if ( + typeof Joomla !== 'undefined' + && Joomla.Text + && typeof Joomla.Text._ === 'function' + ) { + const translated = Joomla.Text._(key); + + if (typeof translated === 'string' && translated !== '') { + return translated; + } + } + + return fallbackText || key; } class PullManager { + /** @type {HTMLElement|null} */ #repoArea = document.getElementById('select-repo-area'); - #powersArea = document.getElementById('select-powers-area'); - #pullButton = document.getElementById('pull-selected-powers'); - #backButton = document.getElementById('back-to-select-repo'); - #loadingDiv = window.loadingDiv || null; - #buildTable = typeof buildPowerSelectionTable === 'function' ? buildPowerSelectionTable : null; - #drawTable = typeof drawPowerSelectionTable === 'function' ? drawPowerSelectionTable : null; + /** @type {HTMLElement|null} */ + #powersArea = document.getElementById('select-powers-area'); + + /** @type {HTMLButtonElement|null} */ + #pullButton = document.getElementById('pull-selected-powers'); + + /** @type {HTMLButtonElement|null} */ + #backButton = document.getElementById('back-to-select-repo'); + + /** @type {HTMLElement|null} */ + #loadingDiv = window.loadingDiv || null; + + /** @type {Function|null} */ + #buildTable = typeof window.buildPowerSelectionTable === 'function' + ? window.buildPowerSelectionTable + : null; + + /** @type {Function|null} */ + #drawTable = typeof window.drawPowerSelectionTable === 'function' + ? window.drawPowerSelectionTable + : null; + + /** @type {AbortController|null} */ + #repoRequestController = null; + + /** @type {AbortController|null} */ + #pullRequestController = null; + + /** @type {boolean} */ + #isLoadingRepo = false; + + /** @type {boolean} */ + #isPulling = false; + + /** + * The current repository GUID. + * + * @type {string|null} + */ currentRepo = null; + + /** + * The current area key. + * + * @type {string|null} + */ currentArea = null; constructor() { @@ -89,29 +294,49 @@ class PullManager { this._updatePullButtonState(); } - /** Getter for selected items using global window reference. */ + /** + * Get the shared selected items collection. + * + * @returns {Array} The selected items. + */ get selectedItems() { if (!Array.isArray(window.selectedPowerItems)) { window.selectedPowerItems = []; } + return window.selectedPowerItems; } - /** Setter for selected items with sync to window and button state update. */ + /** + * Set the shared selected items collection. + * + * @param {Array} items The selected items. + * + * @returns {void} + */ set selectedItems(items) { - window.selectedPowerItems = items; + window.selectedPowerItems = Array.isArray(items) ? items : []; this._updatePullButtonState(); } - /** Add items to selection if not already selected. */ + /** + * Add items not already selected. + * + * @param {Array|Object} data The items to add. + * + * @returns {void} + */ addSelectedItems(data) { - if (!data || typeof data.length !== 'number') return; + const items = getArrayFormat(data); + + if (items.length === 0) { + return; + } const updated = [...this.selectedItems]; - for (let i = 0; i < data.length; i++) { - const item = data[i]; - if (!updated.some(existing => this.#isSameItem(existing, item))) { + for (const item of items) { + if (!updated.some((existing) => this.#isSameItem(existing, item))) { updated.push(item); } } @@ -119,284 +344,624 @@ class PullManager { this.selectedItems = updated; } - /** Remove items from selection based on identity comparison. */ + /** + * Remove items from the selection. + * + * @param {Array|Object} data The items to remove. + * + * @returns {void} + */ removeSelectedItems(data) { - if (!data || typeof data.length !== 'number') return; + const items = getArrayFormat(data); - const updated = this.selectedItems.filter(existing => { - for (let i = 0; i < data.length; i++) { - if (this.#isSameItem(existing, data[i])) { - return false; - } - } - return true; + if (items.length === 0) { + return; + } + + this.selectedItems = this.selectedItems.filter((existing) => { + return !items.some((item) => this.#isSameItem(existing, item)); }); - - this.selectedItems = updated; } - /** Check if two items are the same using GUID. */ + /** + * Compare two items by GUID. + * + * @param {Object} a First item. + * @param {Object} b Second item. + * + * @returns {boolean} True if both items match. + */ #isSameItem(a, b) { - return a?.guid && b?.guid && a.guid === b.guid; + return Boolean(a?.guid && b?.guid && a.guid === b.guid); } - /** Enable/disable the pull button based on selection state. */ + /** + * Update action button state. + * + * @returns {void} + */ _updatePullButtonState() { if (this.#pullButton) { - this.#pullButton.disabled = this.selectedItems.length === 0; + this.#pullButton.disabled = ( + this.selectedItems.length === 0 + || this.#isPulling + || this.#isLoadingRepo + ); + } + + if (this.#backButton) { + this.#backButton.disabled = this.#isPulling || this.#isLoadingRepo; } } - _bindRepoButtons() { - document.querySelectorAll('.select-repo-to-load').forEach(button => { - button.addEventListener('click', (e) => this._handleRepoClick(e)); + /** + * Toggle repository buttons disabled state. + * + * @param {boolean} disabled Whether buttons should be disabled. + * + * @returns {void} + */ + _setRepoButtonsDisabled(disabled) { + document.querySelectorAll('.select-repo-to-load').forEach((button) => { + button.disabled = disabled; }); } + /** + * Bind repository buttons. + * + * @returns {void} + */ + _bindRepoButtons() { + document.querySelectorAll('.select-repo-to-load').forEach((button) => { + button.addEventListener('click', (event) => this._handleRepoClick(event)); + }); + } + + /** + * Bind pull and back buttons. + * + * @returns {void} + */ _bindPullSelectedPowers() { if (this.#pullButton) { this.#pullButton.addEventListener('click', () => this._handlePullSelectedPowers()); } + if (this.#backButton) { this.#backButton.addEventListener('click', () => this._handleBackToRepos()); } } + /** + * Handle repository click. + * + * @param {Event} event The click event. + * + * @returns {Promise} + */ async _handleRepoClick(event) { + if (this.#isLoadingRepo || this.#isPulling) { + return; + } + const button = event.currentTarget; const repo = button?.dataset?.repo; const area = button?.dataset?.area; if (!repo || !area) { - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA"), "danger"); + this._notify( + translate('COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA'), + 'danger' + ); return; } + this.#isLoadingRepo = true; + this._updatePullButtonState(); + this._setRepoButtonsDisabled(true); this._showLoading(); - clearPowerSelectionTable(); + this._abortRepoRequest(); + + // Reset previous selection when switching repositories. + this.selectedItems = []; + + if (typeof window.clearPowerSelectionTable === 'function') { + window.clearPowerSelectionTable(); + } + + this.#repoRequestController = new AbortController(); const url = `${UrlAjax}getRepoIndex&repo=${encodeURIComponent(repo)}&area=${encodeURIComponent(area)}`; try { - const response = await fetch(url); + const response = await fetch(url, { + method: 'GET', + signal: this.#repoRequestController.signal, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } const data = await response.json(); - if (data.success && data.index && this.#buildTable) { - const repoData = data.index[0]; - const { - path = 'joomla/super-powers', - read_branch = 'master', - target, - base = 'https://git.vdm.dev' - } = repoData; + if (!data?.success || !data?.index || !this.#buildTable) { + this._notify( + data?.message || translate('COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX'), + 'danger' + ); + return; + } - const repo_base = (target === 'github') ? 'https://github.com' : base; - const repo_path = (target === 'github') ? 'tree' : 'src/branch'; + const repoData = Array.isArray(data.index) ? data.index[0] : data.index; - window.targetPowerRepoUrl = `${repo_base}/${path}/${repo_path}/${read_branch}/`; + if (!repoData || typeof repoData !== 'object') { + this._notify( + translate('COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX'), + 'danger' + ); + return; + } - this.#buildTable(repoData.index); + const { + path = 'joomla/super-powers', + read_branch = 'master', + target = '', + base = 'https://git.vdm.dev', + guid = null, + index = [] + } = repoData; - setTimeout(() => { - this._transitionTo(this.#repoArea, this.#powersArea); - this._hideLoading(); - }, 500); + const repoBase = target === 'github' ? 'https://github.com' : base; + const repoPath = target === 'github' ? 'tree' : 'src/branch'; - this.currentRepo = repoData.guid; - this.currentArea = area; - } else { - this._notify(data.message || Joomla.Text._("COM_COMPONENTBUILDER_FAILED_TO_RETRIEVE_REPOSITORY_INDEX"), "danger"); - this._hideLoading(); + window.targetPowerRepoUrl = `${repoBase}/${path}/${repoPath}/${read_branch}/`; + + // The index may be an array or an object keyed by GUID. + this.#buildTable(index); + + this.currentRepo = guid; + this.currentArea = area; + + await this._transitionTo(this.#repoArea, this.#powersArea); + + if (this.#drawTable) { + this.#drawTable(); } } catch (error) { - console.error("Fetch error:", error); + if (error.name !== 'AbortError') { + console.error('Fetch error:', error); + this._notify( + translate('COM_COMPONENTBUILDER_NETWORK_OR_SERVER_ERROR_OCCURRED_WHILE_FETCHING_INDEX'), + 'danger' + ); + } + } finally { + this.#isLoadingRepo = false; this._hideLoading(); - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_NETWORK_OR_SERVER_ERROR_OCCURRED_WHILE_FETCHING_INDEX"), "danger"); + this._setRepoButtonsDisabled(false); + this._updatePullButtonState(); } } + /** + * Go back to repository selection. + * + * @returns {void} + */ _handleBackToRepos() { - this._transitionTo(this.#powersArea, this.#repoArea); - } - - async _handlePullSelectedPowers() { - if (!Array.isArray(this.selectedItems) || this.selectedItems.length === 0) { - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_NO_ITEMS_SELECTED"), "warning"); + if (this.#isLoadingRepo || this.#isPulling) { return; } + this._transitionTo(this.#powersArea, this.#repoArea) + .then(() => { + if (this.#drawTable) { + this.#drawTable(); + } + }) + .catch((error) => { + console.error('Transition error:', error); + }); + } + + /** + * Submit selected powers. + * + * @returns {Promise} + */ + async _handlePullSelectedPowers() { + if (this.#isPulling || this.#isLoadingRepo) { + return; + } + + if (!Array.isArray(this.selectedItems) || this.selectedItems.length === 0) { + this._notify( + translate('COM_COMPONENTBUILDER_NO_ITEMS_SELECTED'), + 'warning' + ); + return; + } + + const area = this.currentArea; + const repo = this.currentRepo; + + if (!area || !repo) { + this._notify( + translate('COM_COMPONENTBUILDER_MISSING_REPOSITORY_OR_AREA_DATA'), + 'danger' + ); + return; + } + + this.#isPulling = true; + this._updatePullButtonState(); + this._setRepoButtonsDisabled(true); this._showLoading(); + this._abortPullRequest(); - const area = this.currentArea || 'error'; - const repo = this.currentRepo || 'error'; - - const func = 'pullSelectedPackages'; + this.#pullRequestController = new AbortController(); try { - // Convert selected items to form data const formData = new FormData(); - // Assuming Joomla expects `selected[]` for multiple values for (const item of this.selectedItems) { - formData.append('selected[]', item.guid); // Only sending GUIDs + if (item?.guid) { + formData.append('selected[]', item.guid); + } } + formData.append('area', area); formData.append('repo', repo); - const response = await fetch(`${UrlAjax}${func}`, { + const response = await fetch(`${UrlAjax}pullSelectedPackages`, { method: 'POST', - body: formData + body: formData, + signal: this.#pullRequestController.signal, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } const data = await response.json(); - if (!data.success) { - this._notify(data.message || Joomla.Text._("COM_COMPONENTBUILDER_FAILED_TO_PULL_SELECTED_POWERS"), "danger"); - } else { - this._handleResultLog(data.result_log || {}); + if (!data?.success) { + this._notify( + data?.message || translate('COM_COMPONENTBUILDER_FAILED_TO_PULL_SELECTED_POWERS'), + 'danger' + ); + return; } - this._hideLoading(); - this._transitionTo(this.#powersArea, this.#repoArea); - } catch (error) { - console.error("Submission error:", error); - this._notify(Joomla.Text._("COM_COMPONENTBUILDER_ERROR_OCCURRED_WHILE_PULLING_POWERS"), "danger"); + // Process result log before clearing selection, + // since _getNamesFromGuids reads from selectedItems. + this._handleResultLog(data.result_log || {}); + this.selectedItems = []; + await this._transitionTo(this.#powersArea, this.#repoArea); + } catch (error) { + if (error.name !== 'AbortError') { + console.error('Submission error:', error); + this._notify( + translate('COM_COMPONENTBUILDER_ERROR_OCCURRED_WHILE_PULLING_POWERS'), + 'danger' + ); + } + } finally { + this.#isPulling = false; this._hideLoading(); - this._transitionTo(this.#powersArea, this.#repoArea); + this._setRepoButtonsDisabled(false); + this._updatePullButtonState(); } } + /** + * Process pull result log. + * + * @param {Object} resultLog The result log object. + * + * @returns {void} + */ _handleResultLog(resultLog) { const localGuids = this._normalizeGuids(resultLog.local); const notFoundGuids = this._normalizeGuids(resultLog.not_found); const addedGuids = this._normalizeGuids(resultLog.added); if (localGuids.length > 0) { - this._notify( - this._generateResultMessage(localGuids, 'local'), + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_ALREADY_PRESENT_LOCALLY_AND_WERE_NOT_PULLED'), + this._getNamesFromGuids(localGuids), 'info' ); } if (notFoundGuids.length > 0) { - this._notify( - this._generateResultMessage(notFoundGuids, 'not_found'), - 'info' + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_COULD_NOT_BE_FOUND_IN_THE_REMOTE_REPOSITORY_AND_WERE_NOT_PULLED'), + this._getNamesFromGuids(notFoundGuids), + 'warning' ); } if (addedGuids.length > 0) { - this._notify( - this._generateResultMessage(addedGuids, 'added'), + this._notifyList( + translate('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_SUCCESSFULLY_PULLED'), + this._getNamesFromGuids(addedGuids), 'success' ); } } + /** + * Normalize log values to GUID arrays. + * + * @param {*} value The raw value. + * + * @returns {Array} The GUID array. + */ _normalizeGuids(value) { - if (!value) return []; - - if (Array.isArray(value)) { - return value; + if (!value) { + return []; } - if (typeof value === 'object') { + if (Array.isArray(value)) { + return value.filter((item) => typeof item === 'string' && item !== ''); + } + + if (isPlainObject(value)) { return Object.keys(value); } return []; } - _generateResultMessage(guids, type) { - const messages = { - local: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_ALREADY_PRESENT_LOCALLY_AND_WERE_NOT_PULLED'), - not_found: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_COULD_NOT_BE_FOUND_IN_THE_REMOTE_REPOSITORY_AND_WERE_NOT_PULLED'), - added: Joomla.Text._('COM_COMPONENTBUILDER_THESE_ITEMS_WERE_SUCCESSFULLY_PULLED') - }; - + /** + * Resolve item names from GUIDs. + * + * @param {Array} guids The GUIDs. + * + * @returns {Array} The item names. + */ + _getNamesFromGuids(guids) { const names = []; for (const guid of guids) { - const item = this.selectedItems.find(i => i.guid === guid); - if (item?.name) { + const item = this.selectedItems.find((entry) => entry?.guid === guid); + + if (typeof item?.name === 'string' && item.name !== '') { names.push(item.name); } } - if (names.length === 0) { - return null; - } - - return `${messages[type]}\n
- ${names.join('\n
- ')}`; + return names; } - _transitionTo(hideEl, showEl) { - if (hideEl && showEl) { - UIkit.util.ready(() => { - UIkit.util.removeClass(hideEl, 'uk-animation-slide-top-small'); - UIkit.util.removeClass(showEl, 'uk-animation-slide-bottom-small'); - UIkit.util.addClass(hideEl, 'uk-animation-slide-top-small'); + /** + * Abort any running repository request. + * + * @returns {void} + */ + _abortRepoRequest() { + if (this.#repoRequestController) { + this.#repoRequestController.abort(); + this.#repoRequestController = null; + } + } - setTimeout(() => { + /** + * Abort any running pull request. + * + * @returns {void} + */ + _abortPullRequest() { + if (this.#pullRequestController) { + this.#pullRequestController.abort(); + this.#pullRequestController = null; + } + } + + /** + * Transition between two elements. + * + * @param {HTMLElement|null} hideEl The element to hide. + * @param {HTMLElement|null} showEl The element to show. + * + * @returns {Promise} Resolves after transition completes. + */ + _transitionTo(hideEl, showEl) { + if (!hideEl || !showEl || hideEl === showEl) { + return Promise.resolve(); + } + + const duration = 250; + + return new Promise((resolve) => { + hideEl.style.transition = `opacity ${duration}ms ease`; + showEl.style.transition = `opacity ${duration}ms ease`; + + hideEl.style.opacity = '1'; + showEl.style.display = 'none'; + showEl.style.opacity = '0'; + + requestAnimationFrame(() => { + hideEl.style.opacity = '0'; + + window.setTimeout(() => { hideEl.style.display = 'none'; showEl.style.display = ''; - UIkit.util.addClass(showEl, 'uk-animation-slide-bottom-small'); - if (this.#drawTable) this.#drawTable(); - }, 300); + showEl.style.opacity = '0'; + + requestAnimationFrame(() => { + showEl.style.opacity = '1'; + + window.setTimeout(() => { + hideEl.style.transition = ''; + hideEl.style.opacity = ''; + showEl.style.transition = ''; + showEl.style.opacity = ''; + resolve(); + }, duration); + }); + }, duration); }); + }); + } + + /** + * Show the loading overlay. + * + * @returns {void} + */ + _showLoading() { + if (this.#loadingDiv) { + this.#loadingDiv.style.display = 'block'; } } - _showLoading() { - if (this.#loadingDiv) this.#loadingDiv.style.display = 'block'; - } - + /** + * Hide the loading overlay. + * + * @returns {void} + */ _hideLoading() { - if (this.#loadingDiv) this.#loadingDiv.style.display = 'none'; + if (this.#loadingDiv) { + this.#loadingDiv.style.display = 'none'; + } } + /** + * Notify with a simple message. + * + * @param {string} message The message. + * @param {string} type The alert type. + * + * @returns {void} + */ _notify(message, type = 'info') { - const alertTypes = { - primary: 'alert-primary', - info: 'alert-info', - success: 'alert-success', - warning: 'alert-warning', - danger: 'alert-danger', - }; + if (typeof message !== 'string' || message.trim() === '') { + return; + } - const alertClass = alertTypes[type] || alertTypes.primary; + const body = document.createElement('div'); + body.textContent = message; + + this._renderAlert(body, type); + } + + /** + * Notify with a heading and a list. + * + * @param {string} heading The heading text. + * @param {Array} items The item names. + * @param {string} type The alert type. + * + * @returns {void} + */ + _notifyList(heading, items, type = 'info') { + if (!heading && (!Array.isArray(items) || items.length === 0)) { + return; + } + + const fragment = document.createDocumentFragment(); + + if (heading) { + const headingNode = document.createElement('div'); + headingNode.textContent = heading; + fragment.appendChild(headingNode); + } + + if (Array.isArray(items) && items.length > 0) { + const list = document.createElement('ul'); + list.className = 'mb-0 mt-2'; + + for (const item of items) { + const li = document.createElement('li'); + li.textContent = item; + list.appendChild(li); + } + + fragment.appendChild(list); + } + + const wrapper = document.createElement('div'); + wrapper.appendChild(fragment); + + this._renderAlert(wrapper, type); + } + + /** + * Render a Bootstrap alert. + * + * @param {Node} contentNode The content node. + * @param {string} type The alert type. + * + * @returns {void} + */ + _renderAlert(contentNode, type = 'info') { + const allowedTypes = ['primary', 'info', 'success', 'warning', 'danger']; + const alertType = allowedTypes.includes(type) ? type : 'primary'; let container = document.getElementById('alert-container'); + if (!container) { container = document.createElement('div'); container.id = 'alert-container'; container.className = 'position-fixed top-0 start-50 translate-middle-x p-3'; container.style.zIndex = '1060'; + container.style.maxWidth = 'min(92vw, 720px)'; + container.style.width = '100%'; document.body.appendChild(container); } const alert = document.createElement('div'); - alert.className = `alert ${alertClass} alert-dismissible fade show`; + alert.className = `alert alert-${alertType} alert-dismissible fade show shadow`; alert.setAttribute('role', 'alert'); - alert.innerHTML = ` - ${message} - - `; + const body = document.createElement('div'); + body.appendChild(contentNode); + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.className = 'btn-close'; + closeButton.setAttribute('data-bs-dismiss', 'alert'); + closeButton.setAttribute('aria-label', 'Close'); + + alert.appendChild(body); + alert.appendChild(closeButton); container.appendChild(alert); - setTimeout(() => { - alert.classList.remove('show'); - alert.classList.add('hide'); - alert.addEventListener('transitionend', () => { - alert.remove(); - }); + if ( + typeof bootstrap !== 'undefined' + && bootstrap.Alert + && typeof bootstrap.Alert.getOrCreateInstance === 'function' + ) { + const instance = bootstrap.Alert.getOrCreateInstance(alert); + + window.setTimeout(() => { + try { + instance.close(); + } catch (error) { + if (alert.parentNode) { + alert.remove(); + } + } + }, 5000); + + return; + } + + window.setTimeout(() => { + alert.remove(); }, 5000); } } \ No newline at end of file diff --git a/admin/config.xml b/admin/config.xml index fbc8aec25..57123fdd1 100644 --- a/admin/config.xml +++ b/admin/config.xml @@ -73,6 +73,8 @@ + + + @@ -119,14 +125,14 @@ multiple="false" filter="CMD" required="true" - default="repeatablejcbjfive"> + default="repeatablejcbjsix"> - + diff --git a/admin/forms/admin_fields_conditions.xml b/admin/forms/admin_fields_conditions.xml index 1bffeb5b7..321f38e7d 100644 --- a/admin/forms/admin_fields_conditions.xml +++ b/admin/forms/admin_fields_conditions.xml @@ -115,7 +115,7 @@ type="subform" name="addconditions" label="COM_COMPONENTBUILDER_ADMIN_FIELDS_CONDITIONS_ADDCONDITIONS_LABEL" - layout="repeatablejcb" + layout="joomla.form.field.subform.repeatable-table" multiple="true" description="COM_COMPONENTBUILDER_ADMIN_FIELDS_CONDITIONS_ADDCONDITIONS_DESCRIPTION" default="" diff --git a/admin/forms/admin_view.xml b/admin/forms/admin_view.xml index b61acf5a8..847ae79dc 100644 --- a/admin/forms/admin_view.xml +++ b/admin/forms/admin_view.xml @@ -212,7 +212,6 @@ label="COM_COMPONENTBUILDER_ADMIN_VIEW_AJAX_INPUT_LABEL" layout="joomla.form.field.subform.repeatable-table" multiple="true" - default="" icon="list">