From aff09a731f31a49f834f7c67627df643713b9d6f Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Thu, 20 Aug 2026 15:05:00 +0200 Subject: [PATCH] =?UTF-8?q?=20N=C2=B09152=20-=20Add=20first=20bit=20of=20U?= =?UTF-8?q?I=20extensibility.=20Needs=20a=20lot=20of=20polishing.=20API=20?= =?UTF-8?q?endpoints=20are=20working=20and=20displaying=20in=20the=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backoffice/ApplicationPopupMenuItem.php | 11 ++ .../backoffice/iPopupMenuExtension.php | 16 +- application/cmdbabstract.class.inc.php | 8 + css/backoffice/components/_field.scss | 9 +- css/backoffice/layout/_all.scss | 4 +- .../activity-panel/_activity-action.scss | 62 ++++++++ .../layout/activity-panel/_all.scss | 1 + css/backoffice/layout/top-bar/_all.scss | 2 + .../layout/top-bar/_top-bar-action.scss | 30 ++++ .../layout/{ => top-bar}/_top-bar.scss | 0 js/layouts/activity-panel/activity-panel.js | 28 ++++ lib/composer/autoload_classmap.php | 11 ++ lib/composer/autoload_static.php | 11 ++ .../UI/Base/Common/Action/tActionCommon.php | 100 +++++++++++++ .../UI/Base/Common/Action/tActionJs.php | 70 +++++++++ .../UI/Base/Common/Action/tActionURL.php | 58 ++++++++ .../Component/Button/ButtonUIBlockFactory.php | 75 ++++++++++ .../UI/Base/Component/Field/Field.php | 44 +++++- .../Component/Field/FieldUIBlockFactory.php | 26 ++++ .../Layout/ActivityPanel/ActivityPanel.php | 28 +++- .../ActivityPanelAction.php | 104 +++++++++++++ .../ActivityPanelActionFactory.php | 138 ++++++++++++++++++ .../ActivityPanelActionJS.php | 43 ++++++ .../ActivityPanelActionURL.php | 43 ++++++ .../ActivityPanel/ActivityPanelFactory.php | 47 ++++++ .../CaseLogEntryFormFactory.php | 26 ++++ .../UI/Base/Layout/TopBar/TopBar.php | 41 +++++- .../UI/Base/Layout/TopBar/TopBarFactory.php | 10 ++ .../TopBarQuickAction/TopBarQuickAction.php | 99 +++++++++++++ .../TopBarQuickActionFactory.php | 59 ++++++++ .../TopBarQuickAction/TopBarQuickActionJS.php | 38 +++++ .../TopBarQuickActionURL.php | 37 +++++ .../base/components/field/layout.html.twig | 13 +- .../activity-action-js.html.twig | 17 +++ .../activity-action-js.js.twig | 1 + .../activity-action-url.html.twig | 17 +++ .../activity-action-url.js.twig | 1 + .../layouts/activity-panel/layout.html.twig | 7 + .../base/layouts/top-bar/layout.html.twig | 3 + .../top-bar-quick-action/layout.html.twig | 1 + .../top-bar-quick-action-js.html.twig | 13 ++ .../top-bar-quick-action-js.js.twig | 4 + .../top-bar-quick-action-url.html.twig | 14 ++ .../top-bar-quick-action-url.js.twig | 1 + 44 files changed, 1359 insertions(+), 12 deletions(-) create mode 100644 css/backoffice/layout/activity-panel/_activity-action.scss create mode 100644 css/backoffice/layout/top-bar/_all.scss create mode 100644 css/backoffice/layout/top-bar/_top-bar-action.scss rename css/backoffice/layout/{ => top-bar}/_top-bar.scss (100%) create mode 100644 sources/Application/UI/Base/Common/Action/tActionCommon.php create mode 100644 sources/Application/UI/Base/Common/Action/tActionJs.php create mode 100644 sources/Application/UI/Base/Common/Action/tActionURL.php create mode 100644 sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php create mode 100644 sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php create mode 100644 sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php create mode 100644 sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionURL.php create mode 100644 sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php create mode 100644 sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php create mode 100644 sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php create mode 100644 sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionURL.php create mode 100644 templates/base/layouts/activity-panel/activity-action/activity-action-js.html.twig create mode 100644 templates/base/layouts/activity-panel/activity-action/activity-action-js.js.twig create mode 100644 templates/base/layouts/activity-panel/activity-action/activity-action-url.html.twig create mode 100644 templates/base/layouts/activity-panel/activity-action/activity-action-url.js.twig create mode 100644 templates/base/layouts/top-bar/top-bar-quick-action/layout.html.twig create mode 100644 templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.html.twig create mode 100644 templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.js.twig create mode 100644 templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.html.twig create mode 100644 templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.js.twig diff --git a/application/applicationextension/backoffice/ApplicationPopupMenuItem.php b/application/applicationextension/backoffice/ApplicationPopupMenuItem.php index 0a1baaf6ba..94aa473b9b 100644 --- a/application/applicationextension/backoffice/ApplicationPopupMenuItem.php +++ b/application/applicationextension/backoffice/ApplicationPopupMenuItem.php @@ -58,6 +58,17 @@ abstract class ApplicationPopupMenuItem return $this->sLabel; } + /** + * @param string $sLabel + * + * @api + * @since 3.3.0 + */ + public function SetLabel($sLabel) + { + $this->sLabel = $sLabel; + } + /** * Get the CSS classes * diff --git a/application/applicationextension/backoffice/iPopupMenuExtension.php b/application/applicationextension/backoffice/iPopupMenuExtension.php index 0f9f507ff6..970cd4bf3d 100644 --- a/application/applicationextension/backoffice/iPopupMenuExtension.php +++ b/application/applicationextension/backoffice/iPopupMenuExtension.php @@ -95,7 +95,21 @@ interface iPopupMenuExtension * @todo */ public const PORTAL_MENU_ACTIONS = 10; - + /** + * Get the list of items to be added to the backoffice top-bar items + * $param is null for now, in a distant future we want to give a complete context + */ + public const MENU_TOPBAR_ACTIONS = 11; + /** + * Get the list of items to be added to the actions on a given form field (i.e. the attribute of an object) + * $param is an array: ['object' => DBObject, att_code, mode=edit/read] + */ + public const MENU_OBJDETAILS_FIELD_ACTIONS = 12; + /** + * Get the list of items to be added to the actions in the activity panel (read-only mode) + * $param = ['object' => DBObject, caselog_attcode => caselog attcode or 'activity'] + */ + public const MENU_OBJDETAILS_ACTIVITY_PANEL_ACTIONS = 13; /** * Get the list of items to be added to a menu. * diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index b3679a5b31..a2eaacaa42 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -1065,6 +1065,14 @@ HTML } } $val['value_raw'] = ($bExcludeRawValue === false) ? $this->Get($sAttCode) : ''; + $val['actions'] = []; + + /** @var \iPopupMenuExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) { + foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_OBJDETAILS_FIELD_ACTIONS, ['object' => $this, 'att_code' => $sAttCode, 'mode' => $bEditMode ? 'edit' : 'read']) as $oMenuItem) { + $val['actions'][] = $oMenuItem; + } + } // The field is visible, add it to the current column $oField = FieldUIBlockFactory::MakeFromParams($val); diff --git a/css/backoffice/components/_field.scss b/css/backoffice/components/_field.scss index 6bc1d458ff..268ad82f0a 100644 --- a/css/backoffice/components/_field.scss +++ b/css/backoffice/components/_field.scss @@ -264,4 +264,11 @@ $ibo-field--enable-bulk--checkbox--margin-left: $ibo-spacing-300 !default; .ibo-input-select--action-buttons a { @extend %ibo-hyperlink-inherited-colors; -} \ No newline at end of file +} + +.ibo-field--action { + padding: 3px 6px; + ~ .ibo-field--action { + margin-left: 2px; + } +} diff --git a/css/backoffice/layout/_all.scss b/css/backoffice/layout/_all.scss index f1dabffb0c..e3b32c3688 100644 --- a/css/backoffice/layout/_all.scss +++ b/css/backoffice/layout/_all.scss @@ -4,7 +4,6 @@ */ @import "navigation-menu"; -@import "top-bar"; @import "content"; @import "details"; @import "tab-container/tab-container"; @@ -15,4 +14,5 @@ @import "wizard-container/wizard-container"; @import "object/all"; @import "activity-panel/all"; -@import "extension/all"; \ No newline at end of file +@import "extension/all"; +@import "top-bar/all"; \ No newline at end of file diff --git a/css/backoffice/layout/activity-panel/_activity-action.scss b/css/backoffice/layout/activity-panel/_activity-action.scss new file mode 100644 index 0000000000..ebecc776b6 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_activity-action.scss @@ -0,0 +1,62 @@ +$ibo-activity-panel--activity-actions--right: 12px !default; +$ibo-activity-panel--activity-actions--top: 76px + $ibo-activity-panel--activity-actions--right +36px + 8px !default; +$ibo-activity-panel--activity-action--diameter: 36px !default; +$ibo-activity-panel--activity-action--background-color: $ibo-color-primary-600 !default; +$ibo-activity-panel--activity-action--background-color--on-hover: $ibo-color-primary-500 !default; +$ibo-activity-panel--activity-action--background-color--is-active: $ibo-color-primary-700 !default; +$ibo-activity-panel--activity-action--color: $ibo-color-white-100 !default; +$ibo-activity-panel--activity-action--border-radius: $ibo-border-radius-full !default; +$ibo-activity-panel--activity-action--box-shadow: $ibo-elevation-100 !default; +$ibo-activity-panel--activity-action--hover--box-shadow: $ibo-elevation-200 !default; + +$ibo-activity-panel--activity-action--icon--height: 100% !default; +$ibo-activity-panel--activity-action--icon--width: $ibo-activity-panel--activity-action--icon--height !default; +$ibo-activity-panel--activity-action--icon--font-size: $ibo-font-size-200 !default; +$ibo-activity-panel--activity-action--icon--line-height: 33px !default; + + +.ibo-activity-panel--activity-actions { + display: flex; + flex-direction: column; + gap: 8px; + position: absolute; + z-index: 1; + right: $ibo-activity-panel--activity-actions--right; + top: $ibo-activity-panel--activity-actions--top; + &.ibo-is-hidden{ + display: none; + } +} + +.ibo-activity-panel--activity-action { + @extend %ibo-baseline-centered-content; + + width: $ibo-activity-panel--activity-action--diameter; + height: $ibo-activity-panel--activity-action--diameter; + + background-color: $ibo-activity-panel--activity-action--background-color; + color: $ibo-activity-panel--activity-action--color; + + border-radius: $ibo-activity-panel--activity-action--border-radius; + box-shadow: $ibo-activity-panel--activity-action--box-shadow; + + > i{ + text-align: center; + height: $ibo-activity-panel--activity-action--icon--height; + width: $ibo-activity-panel--activity-action--icon--width; + font-size: $ibo-activity-panel--activity-action--icon--font-size; + line-height: $ibo-activity-panel--activity-action--icon--line-height; + } + &:hover { + color: $ibo-activity-panel--activity-action--color; + background-color: $ibo-activity-panel--activity-action--background-color--on-hover; + box-shadow: $ibo-activity-panel--activity-action--hover--box-shadow; + } + &:active { + color: $ibo-activity-panel--activity-action--color; + background-color: $ibo-activity-panel--activity-action--background-color--is-active; + } + &.ibo-is-hidden{ + display: none; + } +} diff --git a/css/backoffice/layout/activity-panel/_all.scss b/css/backoffice/layout/activity-panel/_all.scss index 57a8cdd54e..3c9cac0abe 100644 --- a/css/backoffice/layout/activity-panel/_all.scss +++ b/css/backoffice/layout/activity-panel/_all.scss @@ -10,3 +10,4 @@ @import "transition-entry"; @import "edits-entry"; @import "notification-entry"; +@import "activity-action"; diff --git a/css/backoffice/layout/top-bar/_all.scss b/css/backoffice/layout/top-bar/_all.scss new file mode 100644 index 0000000000..143c50d73a --- /dev/null +++ b/css/backoffice/layout/top-bar/_all.scss @@ -0,0 +1,2 @@ +@import "top-bar"; +@import "top-bar-action"; \ No newline at end of file diff --git a/css/backoffice/layout/top-bar/_top-bar-action.scss b/css/backoffice/layout/top-bar/_top-bar-action.scss new file mode 100644 index 0000000000..d98e464aed --- /dev/null +++ b/css/backoffice/layout/top-bar/_top-bar-action.scss @@ -0,0 +1,30 @@ +$ibo-top-bar-quick-action--head--background-color: $ibo-color-white-100 !default; + +$ibo-top-bar-quick-action--icon-padding-x: $ibo-spacing-500 !default; +$ibo-top-bar-quick-action--icon-padding-y: $ibo-spacing-0 !default; +$ibo-top-bar-quick-action--icon--color: $ibo-color-primary-600 !default; +$ibo-top-bar-quick-action--icon--color--on-hover: $ibo-color-primary-700 !default; +$ibo-top-bar-quick-action--icon--color--on-active: $ibo-color-primary-800 !default; + +.ibo-top-bar-quick-action{ + position: relative; + @extend %ibo-full-height-content; +} +.ibo-top-bar-quick-action--head{ + @extend %ibo-full-height-content; + height: 100%; + background-color: $ibo-top-bar-quick-action--head--background-color; +} +.ibo-top-bar-quick-action--icon{ + color: $ibo-top-bar-quick-action--icon--color; + align-self: center; + padding: $ibo-top-bar-quick-action--icon-padding-y $ibo-top-bar-quick-action--icon-padding-x; + @extend %ibo-font-ral-nor-400; + + &:hover{ + color: $ibo-top-bar-quick-action--icon--color--on-hover; + } + &:active{ + color: $ibo-top-bar-quick-action--icon--color--on-active; + } +} \ No newline at end of file diff --git a/css/backoffice/layout/_top-bar.scss b/css/backoffice/layout/top-bar/_top-bar.scss similarity index 100% rename from css/backoffice/layout/_top-bar.scss rename to css/backoffice/layout/top-bar/_top-bar.scss diff --git a/js/layouts/activity-panel/activity-panel.js b/js/layouts/activity-panel/activity-panel.js index 9172a337b4..70f6add62c 100644 --- a/js/layouts/activity-panel/activity-panel.js +++ b/js/layouts/activity-panel/activity-panel.js @@ -71,6 +71,8 @@ $(function() authors_count: '[data-role="ibo-activity-panel--tab-toolbar-info-authors-count"]', messages_count: '[data-role="ibo-activity-panel--tab-toolbar-info-messages-count"]', compose_button: '[data-role="ibo-activity-panel--add-caselog-entry-button"]', + activity_actions: '[data-role="ibo-activity-panel--activity-actions"]', + activity_action: '[data-role="ibo-activity-panel--activity-action"]', compose_menu: '#ibo-activity-panel--compose-menu', compose_menu_item: '#ibo-activity-panel--compose-menu [data-role="ibo-popover-menu--item"]', caselog_entry_form: '[data-role="ibo-caselog-entry-form"]', @@ -194,6 +196,11 @@ $(function() this.element.find(this.js_selectors.compose_button).on('click', function (oEvent) { me._onComposeButtonClick(oEvent); }); + + this.element.find('[data-role="ibo-activity-panel--activity-action"]').on('click', function (oEvent) { + me._onActivityActionClick(oEvent, $(this)); + }); + // - Click on the compose menu items this.element.find(this.js_selectors.compose_menu_item).on('click', function (oEvent) { me._onComposeMenuItemClick(oEvent, $(this)); @@ -375,6 +382,25 @@ $(function() // Else, the compose menu will open automatically }, + + _onActivityActionClick: function (oEvent, oActionElem) { + oEvent.preventDefault(); + const oActiveTabData = this._GetActiveTabData(); + const oActionElement = $(oEvent.target).closest(this.js_selectors.activity_action); + const oActionElementMenu = $('#'+oActionElement.attr('id')+'-menu'); + const oTabCode = oActiveTabData.type === 'caselog' ? oActiveTabData.att_code : oActiveTabData.type; + const oActionElementMenuItem = oActionElementMenu.find('[data-caselog-attribute-code="'+oTabCode+'"]'); + + // If the current tab has a corresponding menu item, click on it to trigger it directly + if (oActionElementMenuItem.length > 0) { + // Note: Stop propagation to avoid the menu to be opened automatically by the popover handler + oEvent.stopImmediatePropagation(); + + oActionElementMenuItem.click(); + } + // Else let the popover menu open automatically, the user will have to choose a case log + }, + /** * @param oEvent {Object} * @param oItemElem {Object} jQuery object representing the clicked item @@ -784,6 +810,7 @@ $(function() _ShowCaseLogsEntryForms: function () { this.element.find(this.js_selectors.caselog_entry_form).trigger('show_form.caselog_entry_form.itop'); this.element.find(this.js_selectors.compose_button).addClass(this.css_classes.is_hidden); + this.element.find(this.js_selectors.activity_actions).addClass(this.css_classes.is_hidden); }, /** * Hide all case logs entry forms. @@ -795,6 +822,7 @@ $(function() _HideCaseLogsEntryForms: function () { this.element.find(this.js_selectors.caselog_entry_form).trigger('hide_form.caselog_entry_form.itop'); this.element.find(this.js_selectors.compose_button).removeClass(this.css_classes.is_hidden); + this.element.find(this.js_selectors.activity_actions).removeClass(this.css_classes.is_hidden); }, /** * Empty all case logs entry forms diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 7b0a6ad759..eb352d2ca7 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -157,6 +157,9 @@ return array( 'Combodo\\iTop\\Application\\TwigBase\\UI\\UIBlockNode' => $baseDir . '/sources/Application/TwigBase/UI/UIBlockNode.php', 'Combodo\\iTop\\Application\\TwigBase\\UI\\UIBlockParser' => $baseDir . '/sources/Application/TwigBase/UI/UIBlockParser.php', 'Combodo\\iTop\\Application\\UI\\Base\\AbstractUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/AbstractUIBlockFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionCommon' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionCommon.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionJs' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionJs.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionURL' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => $baseDir . '/sources/Application/UI/Base/Component/Alert/Alert.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => $baseDir . '/sources/Application/UI/Base/Component/Badge/Badge.php', @@ -269,6 +272,10 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\NotificationEntry' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanel' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelAction' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionFactory' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionJS' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionURL' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelFactory' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelHelper' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelHelper.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelPrint' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelPrint.php', @@ -295,6 +302,10 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TabContainer\\Tab\\AjaxTab' => $baseDir . '/sources/Application/UI/Base/Layout/TabContainer/Tab/AjaxTab.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TabContainer\\Tab\\Tab' => $baseDir . '/sources/Application/UI/Base/Layout/TabContainer/Tab/Tab.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBar' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBar.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickAction' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionFactory' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionJS' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionURL' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarFactory' => $baseDir . '/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\UIContentBlock' => $baseDir . '/sources/Application/UI/Base/Layout/UIContentBlock.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\UIContentBlockUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Layout/UIContentBlockUIBlockFactory.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 292ff47043..10f275b36e 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -558,6 +558,9 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\TwigBase\\UI\\UIBlockNode' => __DIR__ . '/../..' . '/sources/Application/TwigBase/UI/UIBlockNode.php', 'Combodo\\iTop\\Application\\TwigBase\\UI\\UIBlockParser' => __DIR__ . '/../..' . '/sources/Application/TwigBase/UI/UIBlockParser.php', 'Combodo\\iTop\\Application\\UI\\Base\\AbstractUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/AbstractUIBlockFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionCommon' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionCommon.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionJs' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionJs.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionURL' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/Alert.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Badge/Badge.php', @@ -670,6 +673,10 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\NotificationEntry' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelAction' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionJS' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelAction\\ActivityPanelActionURL' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelHelper' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelHelper.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelPrint' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelPrint.php', @@ -696,6 +703,10 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TabContainer\\Tab\\AjaxTab' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TabContainer/Tab/AjaxTab.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TabContainer\\Tab\\Tab' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TabContainer/Tab/Tab.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBar' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBar.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickAction' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionJS' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarAction\\TopBarQuickActionURL' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionURL.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\TopBar\\TopBarFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\UIContentBlock' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/UIContentBlock.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\UIContentBlockUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/UIContentBlockUIBlockFactory.php', diff --git a/sources/Application/UI/Base/Common/Action/tActionCommon.php b/sources/Application/UI/Base/Common/Action/tActionCommon.php new file mode 100644 index 0000000000..6858b0cff7 --- /dev/null +++ b/sources/Application/UI/Base/Common/Action/tActionCommon.php @@ -0,0 +1,100 @@ +sUid; + } + + public function SetUID(string $sUid) + { + $this->sUid = $sUid; + return $this; + } + + public function GetLabel(): string + { + return $this->sLabel; + } + + public function SetLabel(string $sLabel) + { + $this->sLabel = $sLabel; + return $this; + } + + public function GetIconClass(): string + { + return $this->sIconClass; + } + + public function SetIconClass(string $sIconClass) + { + $this->sIconClass = $sIconClass; + return $this; + } + + public function GetTooltip(): ?string + { + return $this->sTooltip; + } + + public function SetTooltip(?string $sTooltip) + { + $this->sTooltip = $sTooltip; + return $this; + } + + public function GetCssClasses(): array + { + return $this->aCssClasses; + } + + public function SetCssClasses(array $aCssClasses) + { + $this->aCssClasses = $aCssClasses; + return $this; + } + + public function AddCssClass(string $sCssClass) + { + $this->aCssClasses[] = $sCssClass; + return $this; + } +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Common/Action/tActionJs.php b/sources/Application/UI/Base/Common/Action/tActionJs.php new file mode 100644 index 0000000000..4450aa3002 --- /dev/null +++ b/sources/Application/UI/Base/Common/Action/tActionJs.php @@ -0,0 +1,70 @@ +sJsCode; + } + + public function SetJsCode(string $sJsCode) + { + $this->sJsCode = $sJsCode; + return $this; + } + + public function GetUrl(): string + { + return $this->sUrl; + } + + public function SetUrl(string $sUrl) + { + $this->sUrl = $sUrl; + return $this; + } + + public function GetIncludeJSFiles(): array + { + return $this->aIncludeJSFiles; + } + + public function SetIncludeJSFiles(array $aIncludeJSFiles) + { + $this->aIncludeJSFiles = $aIncludeJSFiles; + return $this; + } +} diff --git a/sources/Application/UI/Base/Common/Action/tActionURL.php b/sources/Application/UI/Base/Common/Action/tActionURL.php new file mode 100644 index 0000000000..bdbfc3b194 --- /dev/null +++ b/sources/Application/UI/Base/Common/Action/tActionURL.php @@ -0,0 +1,58 @@ +sUrl; + } + + public function SetUrl(string $sUrl) + { + $this->sUrl = $sUrl; + return $this; + } + + public function GetTarget(): ?string + { + return $this->sTarget; + } + + public function SetTarget(?string $sTarget) + { + $this->sTarget = $sTarget; + return $this; + } +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php b/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php index 35dcc0067f..40686d5fde 100644 --- a/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php +++ b/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php @@ -22,6 +22,8 @@ namespace Combodo\iTop\Application\UI\Base\Component\Button; use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory; use Dict; +use JSPopupMenuItem; +use URLPopupMenuItem; use utils; /** @@ -376,6 +378,79 @@ class ButtonUIBlockFactory extends AbstractUIBlockFactory return $oButton; } + public static function MakeIconButtonFromApplicationPopupMenuItem($oPopupItem, $sColor = Button::ENUM_COLOR_SCHEME_NEUTRAL, $sActionType = Button::ENUM_ACTION_TYPE_ALTERNATIVE) + { + if ($oPopupItem instanceof JSPopupMenuItem) { + $oButton = self::MakeIconAction( + $oPopupItem->GetIconClass(), + $oPopupItem->GetTooltip() + ); + + $oButton->SetOnClickJsCode($oPopupItem->GetJsCode()); + $oButton->SetActionType($sActionType); + $oButton->SetColor($sColor); + + return $oButton; + } elseif ($oPopupItem instanceof URLPopupMenuItem) { + $oButton = self::MakeIconLink( + $oPopupItem->GetIconClass(), + $oPopupItem->GetTooltip(), + $oPopupItem->GetURL(), + $oPopupItem->GetTarget(), + ); + + $oButton->SetActionType($sActionType); + $oButton->SetColor($sColor); + + return $oButton; + } + + return null; + } + + public static function MakeButtonFromApplicationPopupMenuItem($oPopupItem, $sColor = Button::ENUM_COLOR_SCHEME_NEUTRAL, $sActionType = Button::ENUM_ACTION_TYPE_ALTERNATIVE) + { + if ($oPopupItem instanceof JSPopupMenuItem) { + $oButton = self::MakeForAction( + $oPopupItem->GetLabel(), + $sColor, + $sActionType, + ); + + if (utils::IsNotNullOrEmptyString($oPopupItem->GetIconClass())) { + $oButton->SetIconClass($oPopupItem->GetIconClass()); + } + if (utils::IsNotNullOrEmptyString($oPopupItem->GetTooltip())) { + $oButton->SetTooltip($oPopupItem->GetTooltip()); + } + + $oButton->SetOnClickJsCode($oPopupItem->GetJsCode()); + + return $oButton; + } elseif ($oPopupItem instanceof URLPopupMenuItem) { + $oButton = self::MakeForLink( + $oPopupItem->GetLabel(), + $oPopupItem->GetURL(), + $sColor, + $sActionType, + $oPopupItem->GetTarget() + ); + + if (utils::IsNotNullOrEmptyString($oPopupItem->GetIconClass())) { + $oButton->SetIconClass($oPopupItem->GetIconClass()); + } + if (utils::IsNotNullOrEmptyString($oPopupItem->GetTooltip())) { + $oButton->SetTooltip($oPopupItem->GetTooltip()); + } + if (utils::IsNotNullOrEmptyString($oPopupItem->GetTarget())) { + $oButton->SetTarget($oPopupItem->GetTarget()); + } + + return $oButton; + } + + return null; + } //---------------------------------------------------------------------------------------------- // Link buttons, mostly used outside forms, to redirect somewhere whilst keeping a button aspect //---------------------------------------------------------------------------------------------- diff --git a/sources/Application/UI/Base/Component/Field/Field.php b/sources/Application/UI/Base/Component/Field/Field.php index ce21f5e610..0f62cea361 100644 --- a/sources/Application/UI/Base/Component/Field/Field.php +++ b/sources/Application/UI/Base/Component/Field/Field.php @@ -57,14 +57,16 @@ class Field extends UIContentBlock /** @var string */ protected $sComments; + protected $oValue; + + protected $aActions = []; + public function __construct(string $sLabel, ?UIBlock $oValue = null, ?string $sId = null) { parent::__construct($sId); $this->sLabel = $sLabel; $this->sValueId = null; - if (!is_null($oValue)) { - $this->AddSubBlock($oValue); - } + $this->oValue = $oValue; } /** @@ -287,6 +289,10 @@ class Field extends UIContentBlock return $this; } + public function HasValue(): bool + { + return !is_null($this->oValue); + } /** * @return \Combodo\iTop\Application\UI\Base\UIBlock */ @@ -390,4 +396,36 @@ class Field extends UIContentBlock { return utils::IsNotNullOrEmptyString($this->GetDescription()); } + + public function GetActions(): array + { + return $this->aActions; + } + public function AddAction(UIBlock $oAction) + { + $this->aActions[] = $oAction; + return $this; + } + public function HasActions(): bool + { + return !empty($this->aActions); + } + + public function SetActions(array $aActions) + { + $this->aActions = $aActions; + return $this; + } + + public function GetSubBlocks(): array + { + $aSubBlocks = []; + if (!is_null($this->oValue)) { + $aSubBlocks[$this->oValue->GetId()] = $this->oValue; + } + foreach ($this->aActions as $oAction) { + $aSubBlocks[$oAction->GetId()] = $oAction; + } + return $aSubBlocks; + } } diff --git a/sources/Application/UI/Base/Component/Field/FieldUIBlockFactory.php b/sources/Application/UI/Base/Component/Field/FieldUIBlockFactory.php index 2eea31f441..1f62aa511d 100644 --- a/sources/Application/UI/Base/Component/Field/FieldUIBlockFactory.php +++ b/sources/Application/UI/Base/Component/Field/FieldUIBlockFactory.php @@ -8,7 +8,9 @@ namespace Combodo\iTop\Application\UI\Base\Component\Field; use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory; +use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Html\Html; +use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuFactory; use Combodo\iTop\Application\UI\Base\UIBlock; /** @@ -67,6 +69,30 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory } } + if (isset($aParams['actions'])) { + //TODO make this a configuration parameter + $iMaxActions = 3; + $aActions = []; + // Create a button for a few action or a kebab button for multiple actions that a more than what's configured + if (count($aParams['actions']) > $iMaxActions) { + $oKebabButton = ButtonUIBlockFactory::MakeIconAction('fa-ellipsis-v', 'More actions', 'kebab'); + $aKebabActions = []; + foreach ($aParams['actions'] as $oAction) { + $aKebabActions[] = $oAction->GetMenuItem(); + } + $oKebabPopoverMenu = PopoverMenuFactory::MakeMenuForActions($oField->GetId().'--kebab-menu', $aKebabActions); + + $aActions[] = $oKebabButton; + } else { + foreach ($aParams['actions'] as $oAction) { + $oActionButton = ButtonUIBlockFactory::MakeIconButtonFromApplicationPopupMenuItem($oAction); + $oActionButton->AddCSSClass('ibo-field--action'); + $aActions[] = $oActionButton; + } + } + $oField->SetActions($aActions); + } + return $oField; } diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php index c711eead4e..5e49eddba6 100644 --- a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanel.php @@ -101,7 +101,7 @@ class ActivityPanel extends UIBlock protected $bPrefilterStateChangesOnLogs; /** @var bool */ protected $bPrefilterEditsOnLogs; - + protected array $aActivityActions = []; /** * ActivityPanel constructor. * @@ -939,6 +939,10 @@ class ActivityPanel extends UIBlock $aSubBlocks[$this->GetComposeMenu()->GetId()] = $this->GetComposeMenu(); + foreach ($this->GetActivityActions() as $oActivityAction) { + $aSubBlocks[$oActivityAction->GetId()] = $oActivityAction; + } + return $aSubBlocks; } @@ -950,4 +954,26 @@ class ActivityPanel extends UIBlock { return \UserRights::IsActionAllowed($this->GetObjectClass(), UR_ACTION_MODIFY); } + + public function HasActivityActions(): bool + { + return count($this->aActivityActions) > 0; + } + + public function GetActivityActions(): array + { + return $this->aActivityActions; + } + + public function SetActivityActions(array $aActivityActions) + { + $this->aActivityActions = $aActivityActions; + return $this; + } + + public function AddActivityAction($oActivityAction) + { + $this->aActivityActions[] = $oActivityAction; + return $this; + } } diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php new file mode 100644 index 0000000000..f8eab0356d --- /dev/null +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php @@ -0,0 +1,104 @@ +> */ + protected array $aPopupItems = []; + protected ?PopoverMenu $oPopoverMenu = null; + + public function __construct(string $sLabel, string $sIconClass, ?string $sTooltip = null, ?string $sId = null) + { + parent::__construct($sId); + $this->sUid = $sId ?? $this->GetId(); + $this->sLabel = $sLabel; + $this->sIconClass = $sIconClass; + $this->sTooltip = $sTooltip; + } + + public function SetPopupItems(array $aPopupItems) + { + $this->aPopupItems = $aPopupItems; + return $this; + } + + public function AddPopupItem(string $sSectionId, \ApplicationPopupMenuItem $oPopupItem) + { + if (!array_key_exists($sSectionId, $this->aPopupItems)) { + $this->aPopupItems[$sSectionId] = []; + } + + $this->aPopupItems[$sSectionId][] = $oPopupItem; + return $this; + } + + public function GetPopupItems(): array + { + return $this->aPopupItems; + } + + public function SetPopoverMenu(PopoverMenu $oPopoverMenu) + { + $this->oPopoverMenu = $oPopoverMenu; + return $this; + } + + public function GetPopoverMenu(): ?PopoverMenu + { + return $this->oPopoverMenu; + } + + public function HasPopoverMenu(): bool + { + return $this->oPopoverMenu !== null; + } + + public function GetSubBlocks(): array + { + if (false === $this->HasPopoverMenu()) { + return []; + } + + return [$this->oPopoverMenu->GetId() => $this->oPopoverMenu]; + } +} diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php new file mode 100644 index 0000000000..3d1d707b7a --- /dev/null +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php @@ -0,0 +1,138 @@ + [ ApplicationPopupMenuItem1, ... ], ... ] + * or + * [ 'case-log-attribute-code' => ['label' => 'Displayed label', 'items' => [ ApplicationPopupMenuItem1, ... ]], ... ] + * @return ActivityPanelAction[] + * @throws \Exception + */ + public static function MakeFromApplicationPopupItems(array $aPopupItems) + { + $aActionsByUid = []; + + foreach ($aPopupItems as $sSectionId => $aSectionData) { + $sSectionLabel = (string) $sSectionId; + $aSectionPopupItems = $aSectionData; + if (is_array($aSectionData) && array_key_exists('items', $aSectionData)) { + $aSectionPopupItems = $aSectionData['items']; + if (array_key_exists('label', $aSectionData)) { + $sSectionLabel = (string) $aSectionData['label']; + } + } + + if (!is_array($aSectionPopupItems)) { + $aSectionPopupItems = [$aSectionPopupItems]; + } + + foreach ($aSectionPopupItems as $oPopupItem) { + if (!$oPopupItem instanceof ApplicationPopupMenuItem) { + continue; + } + + $sUid = $oPopupItem->GetUID(); + if (!isset($aActionsByUid[$sUid])) { + $oActivityAction = static::MakeFromApplicationPopupItem($oPopupItem); + if ($oActivityAction === null) { + continue; + } + + $oPopoverMenu = new PopoverMenu($oActivityAction->GetId().'-menu'); + $oPopoverMenu->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY); + $oPopoverMenu->SetTogglerFromBlock($oActivityAction); + $oActivityAction->SetPopoverMenu($oPopoverMenu); + + $aActionsByUid[$sUid] = $oActivityAction; + } + + $oPopupItemForPopoverMenuItem = clone $oPopupItem; + $oPopupItemForPopoverMenuItem->SetLabel($sSectionLabel); + $oPopupItemForPopoverMenuItem->SetIconClass(''); + + $oPopoverMenuItem = PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oPopupItemForPopoverMenuItem); + $oPopoverMenuItem->AddDataAttribute('caselog-attribute-code', (string) $sSectionId); + $aActionsByUid[$sUid]->GetPopoverMenu()->AddItem((string) $sSectionId, $oPopoverMenuItem); + $aActionsByUid[$sUid]->AddPopupItem((string) $sSectionId, $oPopupItem); + } + } + + return array_values($aActionsByUid); + } + + public static function MakeFromApplicationPopupItem($oPopupItem): ?ActivityPanelAction + { + if ($oPopupItem instanceof JSPopupMenuItem) { + $oAction = new ActivityPanelActionJS( + $oPopupItem->GetLabel(), + $oPopupItem->GetIconClass(), + $oPopupItem->GetTooltip(), + $oPopupItem->GetUID() + ); + $oAction->SetJsCode($oPopupItem->GetJsCode()); + $oAction->SetUrl($oPopupItem->GetUrl()); + $oAction->SetIncludeJSFiles($oPopupItem->GetLinkedScripts()); + + return $oAction; + } + + if ($oPopupItem instanceof URLPopupMenuItem) { + $oAction = new ActivityPanelActionURL( + $oPopupItem->GetLabel(), + $oPopupItem->GetIconClass(), + $oPopupItem->GetTooltip(), + $oPopupItem->GetUID() + ); + $oAction->SetUrl($oPopupItem->GetUrl()); + $oAction->SetTarget($oPopupItem->GetTarget()); + + return $oAction; + } + + return null; + } + + protected static function ClonePopupItemWithLabel(ApplicationPopupMenuItem $oPopupItem, string $sLabel): ApplicationPopupMenuItem + { + $oClonedPopupItem = clone $oPopupItem; + $oClonedPopupItem->SetLabel($sLabel); + + return $oClonedPopupItem; + } + +} diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php new file mode 100644 index 0000000000..be20b63b15 --- /dev/null +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionJS.php @@ -0,0 +1,43 @@ + $aCaseLogData) { + $aCaselogActions = []; + + // Let's iterate over the menu items for this case log + foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_OBJDETAILS_ACTIVITY_PANEL_ACTIONS, ['object' => $oObject, 'caselog_att_code' => $sCaseLogAttCode]) as $oMenuItem) { + $aCaselogActions[] = $oMenuItem; + } + + if (empty($aCaselogActions)) { + continue; + } + + // Create a new section for the case log if it doesn't exist yet + if (!array_key_exists($sCaseLogAttCode, $aActionItems)) { + $aActionItems[$sCaseLogAttCode] = [ + 'label' => MetaModel::GetLabel($sObjClass, $sCaseLogAttCode), + 'items' => [], + ]; + } + + $aActionItems[$sCaseLogAttCode]['items'] = array_merge($aActionItems[$sCaseLogAttCode]['items'], $aCaselogActions); + } + + $aActivityItems = []; + // Don't forget 1 last iteration for the activity tab + foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_OBJDETAILS_ACTIVITY_PANEL_ACTIONS, ['object' => $oObject, 'caselog_att_code' => 'activity']) as $oMenuItem) { + $aActivityItems[] = $oMenuItem; + } + if (!empty($aActivityItems)) { + $aActionItems['activity'] = [ + 'label' => Dict::S('UI:Layout:ActivityPanel:Tab:Activity:Title'), + 'items' => $aActivityItems, + ]; + } + } + + $oActivityPanel->SetActivityActions(ActivityPanelActionFactory::MakeFromApplicationPopupItems($aActionItems)); + } + $oKPI->ComputeStatsForExtension(new ActivityPanelFactory(), 'MakeForObjectDetails'); return $oActivityPanel; diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php b/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php index 02ef6d2491..bb618213da 100644 --- a/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php +++ b/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php @@ -12,10 +12,12 @@ use Combodo\iTop\Application\UI\Base\Component\Button\Button; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroupUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu; +use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuFactory; use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory; use DBObject; use DBObjectSet; use Dict; +use iPopupMenuExtension; use JSPopupMenuItem; use MetaModel; use UserRights; @@ -48,6 +50,30 @@ class CaseLogEntryFormFactory $oCaseLogEntryForm->AddMainActionButtons($oSaveButton); } + // Build extra actions + $aPopupItems = []; + /** @var \iPopupMenuExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) { + foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_OBJDETAILS_FIELD_ACTIONS, ['object' => $oObject, 'att_code' => $sCaseLogAttCode, 'mode' => $sObjectMode === cmdbAbstractObject::ENUM_DISPLAY_MODE_EDIT ? 'edit' : 'read']) as $oMenuItem) { + $aPopupItems[] = $oMenuItem; + } + } + + if (!empty($aPopupItems)) { + $aActions = []; + // Create a button for each extra action, the UI will be responsible for displaying them in a dropdown if there are too many + foreach ($aPopupItems as $oAction) { + $oActionButton = ButtonUIBlockFactory::MakeButtonFromApplicationPopupMenuItem($oAction); + $oActionButton->AddCSSClass('ibo-field--action'); + $aActions[] = $oActionButton; + } + + foreach ($aActions as $oAction) { + $oAction->AddCSSClass('ibo-caselog-entry-form--extra-action'); + $oCaseLogEntryForm->AddExtraActionButtons($oAction); + } + } + return $oCaseLogEntryForm; } diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBar.php b/sources/Application/UI/Base/Layout/TopBar/TopBar.php index 4d13b716e9..9a02df0cb8 100644 --- a/sources/Application/UI/Base/Layout/TopBar/TopBar.php +++ b/sources/Application/UI/Base/Layout/TopBar/TopBar.php @@ -24,6 +24,8 @@ use Combodo\iTop\Application\UI\Base\Component\Breadcrumbs\Breadcrumbs; use Combodo\iTop\Application\UI\Base\Component\GlobalSearch\GlobalSearch; use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreate; use Combodo\iTop\Application\UI\Base\Component\Toolbar\Toolbar; +use Combodo\iTop\Application\UI\Base\Layout\TopBar\TopBarAction\TopBarQuickAction; +use Combodo\iTop\Application\UI\Base\Layout\TopBar\TopBarAction\TopBarQuickActionJS; use Combodo\iTop\Application\UI\Base\UIBlock; /** @@ -48,6 +50,8 @@ class TopBar extends UIBlock protected $oBreadcrumbs; /** @var Toolbar|null */ protected $oToolbar; + /** @var array $aActions */ + protected array $aActions = []; /** * TopBar constructor. @@ -205,7 +209,8 @@ class TopBar extends UIBlock { $aSubBlocks = []; - $aSubBlocksNames = ['QuickCreate', 'GlobalSearch', 'Breadcrumbs', 'Toolbar']; + // Add single objects + $aSubBlocksNames = ['QuickCreate', 'GlobalSearch', 'Breadcrumbs', 'Toolbar', ]; foreach ($aSubBlocksNames as $sSubBlockName) { $sHasMethodName = 'Has'.$sSubBlockName; if (true === call_user_func_array([$this, $sHasMethodName], [])) { @@ -214,6 +219,40 @@ class TopBar extends UIBlock } } + // Add object arrays + $aSubBlocksNames = ['Actions']; + foreach ($aSubBlocksNames as $sSubBlockName) { + $sHasMethodName = 'Has'.$sSubBlockName; + if (true === call_user_func_array([$this, $sHasMethodName], [])) { + $sPropertyName = 'a'.$sSubBlockName; + foreach ($this->$sPropertyName as $oSubBlock) { + $aSubBlocks[$oSubBlock->GetId()] = $oSubBlock; + } + } + } + return $aSubBlocks; } + + public function HasActions(): bool + { + return count($this->GetActions()) > 0; + } + + public function GetActions(): array + { + return $this->aActions; + } + + public function SetActions(array $aActions) + { + $this->aActions = $aActions; + return $this; + } + + public function AddAction(TopBarQuickAction $oAction) + { + $this->aActions[] = $oAction; + return $this; + } } diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php b/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php index 5bf8bad66b..6b239cca06 100644 --- a/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarFactory.php @@ -23,6 +23,9 @@ namespace Combodo\iTop\Application\UI\Base\Layout\TopBar; use Combodo\iTop\Application\UI\Base\Component\Breadcrumbs\Breadcrumbs; use Combodo\iTop\Application\UI\Base\Component\GlobalSearch\GlobalSearchFactory; use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateFactory; +use Combodo\iTop\Application\UI\Base\Layout\TopBar\TopBarAction\TopBarQuickActionFactory; +use iPopupMenuExtension; +use MetaModel; use utils; /** @@ -58,6 +61,13 @@ class TopBarFactory $oTopBar->SetGlobalSearch(GlobalSearchFactory::MakeFromUserHistory()); } + /** @var \iPopupMenuExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) { + foreach ($oExtensionInstance::EnumItems(iPopupMenuExtension::MENU_TOPBAR_ACTIONS, []) as $oMenuItem) { + $oTopBar->AddAction(TopBarQuickActionFactory::MakeFromApplicationPopupItem($oMenuItem)); + } + } + if (utils::GetConfig()->Get('breadcrumb.enabled') === true) { $oBreadcrumbs = new Breadcrumbs($aBreadcrumbsEntry, Breadcrumbs::BLOCK_CODE); diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php new file mode 100644 index 0000000000..adba8ba97a --- /dev/null +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php @@ -0,0 +1,99 @@ +sLabel = $sLabel; + $this->sIconClass = $sIconClass; + $this->sTooltip = $sTooltip; + } + + public function GetLabel(): string + { + return $this->sLabel; + } + + public function SetLabel(string $sLabel): TopBarQuickAction + { + $this->sLabel = $sLabel; + return $this; + } + + public function GetIconClass(): string + { + return $this->sIconClass; + } + + public function SetIconClass(string $sIconClass): TopBarQuickAction + { + $this->sIconClass = $sIconClass; + return $this; + } + + public function GetTooltip(): ?string + { + return $this->sTooltip; + } + + public function SetTooltip(?string $sTooltip): TopBarQuickAction + { + $this->sTooltip = $sTooltip; + return $this; + } + +} diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php new file mode 100644 index 0000000000..59f913c165 --- /dev/null +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php @@ -0,0 +1,59 @@ +GetLabel(), + $oApplicationPopupItem->GetIconClass(), + $oApplicationPopupItem->GetTooltip(), + $oApplicationPopupItem->GetUID() + ); + $oTopBarAction->SetJsCode($oApplicationPopupItem->GetJsCode()); + $oTopBarAction->SetUrl($oApplicationPopupItem->GetUrl()); + return $oTopBarAction; + } elseif ($oApplicationPopupItem instanceof URLPopupMenuItem) { + $oTopBarAction = new TopBarQuickActionURL( + $oApplicationPopupItem->GetLabel(), + $oApplicationPopupItem->GetIconClass(), + $oApplicationPopupItem->GetTooltip(), + $oApplicationPopupItem->GetUID() + ); + $oTopBarAction->SetUrl($oApplicationPopupItem->GetUrl()); + $oTopBarAction->SetTarget($oApplicationPopupItem->GetTarget()); + return $oTopBarAction; + } + } +} diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php new file mode 100644 index 0000000000..c53785569a --- /dev/null +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionJS.php @@ -0,0 +1,38 @@ + {% endif %} + {% if oUIBlock.HasActions() %} + + {% for oAction in oUIBlock.GetActions() %} + {{ render_block(oAction, {aPage: aPage}) }} + {% endfor %} + + {% endif %} {% if oUIBlock.GetLayout() == constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %} {% if oUIBlock.GetComments() %}
{{ oUIBlock.GetComments()|raw }}
@@ -33,9 +40,9 @@ {% endif %}
- {% for oSubBlock in oUIBlock.GetSubBlocks() %} - {{ render_block(oSubBlock, {aPage: aPage}) }} - {% endfor %} + {% if oUIBlock.HasValue() %} + {{ render_block(oUIBlock.GetValue(), {aPage: aPage}) }} + {% endif %}
{% if oUIBlock.GetLayout() != constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %} {% if oUIBlock.GetComments() %} diff --git a/templates/base/layouts/activity-panel/activity-action/activity-action-js.html.twig b/templates/base/layouts/activity-panel/activity-action/activity-action-js.html.twig new file mode 100644 index 0000000000..970f2f7cae --- /dev/null +++ b/templates/base/layouts/activity-panel/activity-action/activity-action-js.html.twig @@ -0,0 +1,17 @@ + + {% if oUIBlock.GetIconClass() is not empty %} + + {% endif %} +{# {{ oUIBlock.GetLabel() }}#} + +{% if oUIBlock.HasPopoverMenu() %} + {{ render_block(oUIBlock.GetPopoverMenu()) }} +{% endif %} diff --git a/templates/base/layouts/activity-panel/activity-action/activity-action-js.js.twig b/templates/base/layouts/activity-panel/activity-action/activity-action-js.js.twig new file mode 100644 index 0000000000..9c08c40872 --- /dev/null +++ b/templates/base/layouts/activity-panel/activity-action/activity-action-js.js.twig @@ -0,0 +1 @@ +{# Probably not needed, activity panel will handle interactions with popovermenu #} \ No newline at end of file diff --git a/templates/base/layouts/activity-panel/activity-action/activity-action-url.html.twig b/templates/base/layouts/activity-panel/activity-action/activity-action-url.html.twig new file mode 100644 index 0000000000..29f1a0b3b9 --- /dev/null +++ b/templates/base/layouts/activity-panel/activity-action/activity-action-url.html.twig @@ -0,0 +1,17 @@ + + {% if oUIBlock.GetIconClass() is not empty %} + + {% endif %} +{# {{ oUIBlock.GetLabel() }}#} + +{% if oUIBlock.HasPopoverMenu() %} + {{ render_block(oUIBlock.GetPopoverMenu()) }} +{% endif %} \ No newline at end of file diff --git a/templates/base/layouts/activity-panel/activity-action/activity-action-url.js.twig b/templates/base/layouts/activity-panel/activity-action/activity-action-url.js.twig new file mode 100644 index 0000000000..9c08c40872 --- /dev/null +++ b/templates/base/layouts/activity-panel/activity-action/activity-action-url.js.twig @@ -0,0 +1 @@ +{# Probably not needed, activity panel will handle interactions with popovermenu #} \ No newline at end of file diff --git a/templates/base/layouts/activity-panel/layout.html.twig b/templates/base/layouts/activity-panel/layout.html.twig index e8f12b14a4..8c92b8dd90 100644 --- a/templates/base/layouts/activity-panel/layout.html.twig +++ b/templates/base/layouts/activity-panel/layout.html.twig @@ -84,6 +84,13 @@ {{ render_block(oUIBlock.GetComposeMenu()) }} {% endif %} {% endif %} + {% if oUIBlock.HasActivityActions() %} +
+ {% for oActivityAction in oUIBlock.GetActivityActions() %} + {{ render_block(oActivityAction) }} + {% endfor %} +
+ {% endif %}
{% if oUIBlock.GetGroupedEntries()|length > 0 %} {% for aEntryGroup in oUIBlock.GetGroupedEntries() %} diff --git a/templates/base/layouts/top-bar/layout.html.twig b/templates/base/layouts/top-bar/layout.html.twig index 161ebff348..99c8c1495f 100644 --- a/templates/base/layouts/top-bar/layout.html.twig +++ b/templates/base/layouts/top-bar/layout.html.twig @@ -6,6 +6,9 @@ {% if oUIBlock.HasGlobalSearch() %} {{ render_block(oUIBlock.GetGlobalSearch(), {aPage: aPage}) }} {% endif %} + {% for oAction in oUIBlock.GetActions() %} + {{ render_block(oAction, {aPage: aPage}) }} + {% endfor %}
{% if oUIBlock.HasBreadcrumbs() %} {{ render_block(oUIBlock.GetBreadcrumbs(), {aPage: aPage}) }} diff --git a/templates/base/layouts/top-bar/top-bar-quick-action/layout.html.twig b/templates/base/layouts/top-bar/top-bar-quick-action/layout.html.twig new file mode 100644 index 0000000000..3aa50aeaf0 --- /dev/null +++ b/templates/base/layouts/top-bar/top-bar-quick-action/layout.html.twig @@ -0,0 +1 @@ +{# Probably not needed, abstract #} \ No newline at end of file diff --git a/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.html.twig b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.html.twig new file mode 100644 index 0000000000..946bc005f2 --- /dev/null +++ b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.html.twig @@ -0,0 +1,13 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.js.twig b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.js.twig new file mode 100644 index 0000000000..298b0f234e --- /dev/null +++ b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-js.js.twig @@ -0,0 +1,4 @@ +$('#{{ oUIBlock.GetId() }} [data-role="ibo-top-bar-quick-action--icon"]').on('click', function (event) { + event.preventDefault(); + {{ oUIBlock.GetJsCode()|raw }} +}); \ No newline at end of file diff --git a/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.html.twig b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.html.twig new file mode 100644 index 0000000000..430c7c2006 --- /dev/null +++ b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.html.twig @@ -0,0 +1,14 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.js.twig b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.js.twig new file mode 100644 index 0000000000..0ba1e990aa --- /dev/null +++ b/templates/base/layouts/top-bar/top-bar-quick-action/top-bar-quick-action-url.js.twig @@ -0,0 +1 @@ +{# Probably not needed #} \ No newline at end of file