[TikiWiki-commits] [Git][tikiwiki/tiki][master] [ENH]: Search usability: when searching for related items to associate to a...

Benoit Grégoire (@benoitg) via TikiWiki-cvs <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69ba900e2d1be_3bc3bedc948bf@gitlab-sidekiq-low-urgency-cpu-bound-v2-7845844f5c-9wdmd.mail>

Benoit Grégoire pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
bdc02b36 by Grace Nshokano at 2026-03-18T11:36:36+00:00
[ENH]: Search usability: when searching for related items to associate to a tiki tracker, search results should have a popover tooltip for details (such as extended description)
---
* [FIX] search: stop silently ignoring tracker popup errors

* fix(search): use Tiki popover-md and render tracker infobox as a bordered table

* refactor(search): replace hand-rolled popup HTML with Services_Object_Controller::getTrackerItemPopupContent()

* [FIX] related search popup: remove tracker 6 hardcode and use dynamic popup fields fallback

* [FIX] fix: adding the popover

See merge request tikiwiki/tiki!8851

- - - - -


7 changed files:

- lib/core/Services/Object/Controller.php
- lib/core/Services/Search/Controller.php
- lib/core/Tracker/Field/Relation.php
- lib/jquery_tiki/tiki-jquery.js
- lib/smarty_tiki/FunctionHandler/ObjectLink.php
- templates/object/infobox/trackeritem.tpl
- templates/trackeroutput/relation.tpl


Changes:

=====================================
lib/core/Services/Object/Controller.php
=====================================
@@ -181,6 +181,58 @@ class Services_Object_Controller
         return [];
     }
 
+    /**
+     * Build HTML popup content for a tracker item for use as `popup_content` in search results.
+     * Reuses the existing infobox template and trackeroutput field rendering (including REL fields).
+     * Returns null on any failure or when nothing useful can be shown.
+     */
+    public static function getTrackerItemPopupContent(int $itemId): ?string
+    {
+        $trklib = TikiLib::lib('trk');
+        $item = $trklib->get_tracker_item($itemId);
+        if (! $item) {
+            return null;
+        }
+
+        $definition = Tracker_Definition::get($item['trackerId']);
+        if (! $definition) {
+            return null;
+        }
+
+        $itemObject = Tracker_Item::fromInfo($item);
+        if (! $itemObject->canView()) {
+            return null;
+        }
+
+        $fields = [];
+        foreach ($definition->getPopupFields() as $fieldId) {
+            if ($itemObject->canViewField($fieldId) && $field = $definition->getField($fieldId)) {
+                $fields[] = $field;
+            }
+        }
+
+        if (empty($fields)) {
+            foreach ($definition->getFields() as $field) {
+                if (isset($field['fieldId']) && $itemObject->canViewField($field['fieldId'])) {
+                    $fields[] = $field;
+                }
+            }
+        }
+
+        if (empty($fields)) {
+            return null;
+        }
+
+        $smarty = TikiLib::lib('smarty');
+        $smarty->assign('fields', $fields);
+        $smarty->assign('item', $item);
+        $smarty->assign('can_modify', false);
+        $smarty->assign('can_remove', false);
+        $smarty->assign('mode', 'table');
+
+        return $smarty->fetch('object/infobox/trackeritem.tpl');
+    }
+
     /**
      * Generic function to allow consistently formatted errors from javascript using Feedback
      *


=====================================
lib/core/Services/Search/Controller.php
=====================================
@@ -253,6 +253,15 @@ class Services_Search_Controller
                 ];
                 if ($item['object_type'] == 'trackeritem') {
                     $transformed['status_icon'] = smarty_function_tracker_item_status_icon(['item' => $item['object_id']], $smarty->getEmptyInternalTemplate());
+
+                    try {
+                        $popupContent = Services_Object_Controller::getTrackerItemPopupContent((int) $item['object_id']);
+                        if ($popupContent !== null) {
+                            $transformed['popup_content'] = $popupContent;
+                        }
+                    } catch (Exception $e) {
+                        trigger_error($e->getMessage(), E_USER_WARNING);
+                    }
                 }
                 return $transformed;
             });


=====================================
lib/core/Tracker/Field/Relation.php
=====================================
@@ -129,6 +129,15 @@ class Tracker_Field_Relation extends \Tracker\Field\AbstractItemField implements
                         'description' => tr('Key to filter objects by using the value from the Extra Filter Field above.'),
                         'filter' => 'text',
                     ],
+                    'linkTarget' => [
+                        'name' => tr('Link Target'),
+                        'description' => tr('Control whether relation links open in the same window or a new tab.'),
+                        'filter' => 'word',
+                        'options' => [
+                            '' => tr('Same window'),
+                            '_blank' => tr('New tab'),
+                        ],
+                    ],
                 ],
             ],
         ];
@@ -348,7 +357,8 @@ class Tracker_Field_Relation extends \Tracker\Field\AbstractItemField implements
                 [
                     'display' => $display,
                     'relations' => $relations,
-                    'format' => $this->trackerField->getOption('format')
+                    'format' => $this->trackerField->getOption('format'),
+                    'linkTarget' => $this->trackerField->getOption('linkTarget')
                 ]
             );
         }


=====================================
lib/jquery_tiki/tiki-jquery.js
=====================================
@@ -1867,7 +1867,7 @@ $(document).tiki('copy')(
             });
         }
     }
-    $._object_selector_add_item = function (type, $select, $results, parent_title, item, title, status_icon, selected) {
+    $._object_selector_add_item = function (type, $select, $results, parent_title, item, title, status_icon, selected, popup_content) {
         var checkname = $select.closest('.object-selector, .object-selector-multi')
             .find('.primary').attr('id') + '_sel';
         var suffix = $results.find('.form-check').length || 0;
@@ -1879,8 +1879,43 @@ $(document).tiki('copy')(
             .prop('selected', selected)
             .appendTo($select);
 
+        // Build clickable link for the title if item identifier is available
+        var titleHtml = title;
+        if (item && typeof item === 'string' && item.indexOf(':') !== -1) {
+            var parts = item.split(':');
+            var objectType = parts[0];
+            var objectId = parts[1];
+            var url = '';
+
+            // Build URL based on object type
+            if (objectType === 'trackeritem') {
+                url = 'tiki-view_tracker_item.php?itemId=' + encodeURIComponent(objectId);
+            } else if (objectType === 'wiki page') {
+                url = 'tiki-index.php?page=' + encodeURIComponent(objectId);
+            } else if (objectType === 'article') {
+                url = 'tiki-read_article.php?articleId=' + encodeURIComponent(objectId);
+            } else if (objectType === 'blog post') {
+                url = 'tiki-view_blog_post.php?postId=' + encodeURIComponent(objectId);
+            } else if (objectType === 'file') {
+                url = 'tiki-download_file.php?fileId=' + encodeURIComponent(objectId);
+            }
+            // Add more object types as needed
+
+            if (url) {
+                var popoverAttrs = '';
+                if (popup_content) {
+                    var escapedContent = popup_content.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
+                    popoverAttrs = ' data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-html="true" data-bs-content="' + escapedContent + '" data-bs-delay=\'{"show":250,"hide":500}\'';
+                }
+                titleHtml = '<a href="' + url + '" target="_blank" class="object-link"' + popoverAttrs + '>' + title + '</a>';
+            }
+        }
+
+        // Build the label content: status_icon + titleHtml
+        var labelContent = status_icon ? status_icon + ' ' + titleHtml : titleHtml;
+
         $('<div class="form-check"><input type="' + type + '" class="form-check-input" ><label class="form-check-label"></label></div>')
-            .find('label').append(status_icon ? status_icon + ' ' + title : title).end()
+            .find('label').append(labelContent).end()
             .find(':radio, :checkbox')
                 .attr('name', checkname)
                 .prop('checked', selected)
@@ -1940,19 +1975,106 @@ $(document).tiki('copy')(
 
             if (selection.indexOf(current) === -1) {
                 if (initial) {
-                    $._object_selector_add_item(type, $select, $([]), value.parent_title, current, value.title, value.status_icon, selected);
+                    $._object_selector_add_item(type, $select, $([]), value.parent_title, current, value.title, value.status_icon, selected, value.popup_content);
                 } else {
-                    $._object_selector_add_item(type, $select, $results, value.parent_title, current, value.title, value.status_icon, selected);
+                    $._object_selector_add_item(type, $select, $results, value.parent_title, current, value.title, value.status_icon, selected, value.popup_content);
                 }
             } else {
                 const option = $("option[value='" + current + "']", $select);
                 if (!option.length) {
-                    $._object_selector_add_item(type, $select, $results, value.parent_title, current, value.title, value.status_icon, true);
+                    $._object_selector_add_item(type, $select, $results, value.parent_title, current, value.title, value.status_icon, true, value.popup_content);
                 } else {
                     option.text(value.title);
                 }
             }
         });
+
+        if ($results.length) {
+            var $popovers = $results.find('[data-bs-toggle="popover"]');
+            if ($popovers.length > 0) {
+                setTimeout(function() {
+                    $popovers.each(function() {
+                        if (typeof bootstrap !== 'undefined' && bootstrap.Popover) {
+                            var popoverInstance = new bootstrap.Popover(this, {
+                                container: 'body',
+                                html: true,
+                                trigger: 'manual',
+                                delay: {"show": 250, "hide": 500},
+                                sanitize: false,
+                                boundary: "window",
+                                placement: $.tikiPopoverWhereToPlace,
+                                customClass: 'popover-md'
+                            });
+
+                            var showTimer, hideTimer;
+                            var elem = this;
+
+                            var showPopover = function() {
+                                clearTimeout(hideTimer);
+                                showTimer = setTimeout(function() {
+                                    popoverInstance.show();
+                                }, 250);
+                            };
+
+                            var hidePopover = function() {
+                                clearTimeout(showTimer);
+                                hideTimer = setTimeout(function() {
+                                    popoverInstance.hide();
+                                }, 500);
+                            };
+
+                            elem.addEventListener('mouseenter', showPopover);
+                            elem.addEventListener('mouseleave', hidePopover);
+
+                            elem.addEventListener('shown.bs.popover', function() {
+                                var popoverId = this.getAttribute('aria-describedby');
+                                if (popoverId) {
+                                    var popoverEl = document.getElementById(popoverId);
+                                    if (popoverEl) {
+                                        popoverEl.addEventListener('mouseenter', function() {
+                                            clearTimeout(hideTimer);
+                                        });
+                                        popoverEl.addEventListener('mouseleave', hidePopover);
+                                    }
+                                }
+                            });
+
+                            this.addEventListener('shown.bs.popover', function() {
+                                var popoverId = this.getAttribute('aria-describedby');
+                                if (popoverId) {
+                                    var popoverEl = document.getElementById(popoverId);
+                                    if (popoverEl) {
+                                        popoverEl.style.zIndex = '9999';
+
+                                        popoverEl.addEventListener('click', function(e) {
+                                            e.stopPropagation();
+                                            e.stopImmediatePropagation();
+                                        }, true);
+
+                                        var links = popoverEl.querySelectorAll('a');
+                                        links.forEach(function(link) {
+                                            link.addEventListener('click', function(e) {
+                                                e.preventDefault();
+                                                e.stopPropagation();
+                                                e.stopImmediatePropagation();
+
+                                                var href = this.getAttribute('href');
+                                                if (href) {
+                                                    window.open(href, '_blank', 'noopener,noreferrer');
+                                                }
+
+                                                clearTimeout(hideTimer);
+                                                return false;
+                                            }, true);
+                                        });
+                                    }
+                                }
+                            });
+                        }
+                    });
+                }, 100);
+            }
+        }
     };
 
     $.fn.object_selector = function (action, value, title) {


=====================================
lib/smarty_tiki/FunctionHandler/ObjectLink.php
=====================================
@@ -159,6 +159,12 @@ class ObjectLink extends Base
 
         $class = ' class="' . implode(' ', $classList) . '"';
 
+        // Add target attribute if specified
+        $targetAttribute = '';
+        if (! empty($params['target'])) {
+            $targetAttribute = ' target="' . smarty_modifier_escape($params['target']) . '"';
+        }
+
         if (! str_contains($escapedHref, '://')) {
             //$html = '<a href="' . $base_url . $escapedHref . '"' . $class . $titleAttribute . $metadata . '>' . $escapedText . '</a>';
             // When the link is created for a tiki page, then we do NOT want the baseurl included,
@@ -166,9 +172,9 @@ class ObjectLink extends Base
             // configured for teh ip adress we run our webserver.
             // Eaxmple: Fqdn = tiki.mydomain.com -> port forwarding/nat to: 192.168.1.110.
             // In this case links should NOT be generated as absolut urls pointing to  192.168.1.110 which would be the part of the baseUrl.
-            $html = '<a href="' . $escapedHref . '"' . $class . $titleAttribute . $metadata . '>' . $escapedText . '</a>';
+            $html = '<a href="' . $escapedHref . '"' . $class . $titleAttribute . $targetAttribute . $metadata . '>' . $escapedText . '</a>';
         } else {
-            $html = '<a rel="external" href="' . $escapedHref . '"' . $class . $titleAttribute . $metadata . '>' . $escapedText . '</a>';
+            $html = '<a rel="external" href="' . $escapedHref . '"' . $class . $titleAttribute . $targetAttribute . $metadata . '>' . $escapedText . '</a>';
         }
 
         $attributelib = \TikiLib::lib('attribute');


=====================================
templates/object/infobox/trackeritem.tpl
=====================================
@@ -1,12 +1,14 @@
 {if $mode eq 'table'}
-    <table>
-        {foreach from=$fields item=field}
-            <tr class="field_{$field.fieldId}">
-                <th>{tr}{$field.name|escape}{/tr}</th>
-                <td>{trackeroutput field=$field item=$item process=y showlinks=n}</td>
-            </tr>
-        {/foreach}
-    </table>
+    <div class="table-responsive">
+        <table class="table table-sm table-bordered mb-0">
+            {foreach from=$fields item=field}
+                <tr class="field_{$field.fieldId}">
+                    <th class="text-nowrap" style="width: 1%;">{tr}{$field.name|escape}{/tr}</th>
+                    <td>{trackeroutput field=$field item=$item process=y showlinks=n}</td>
+                </tr>
+            {/foreach}
+        </table>
+    </div>
 {else}
     {foreach from=$fields item=field}
         <h6 class="field_{$field.fieldId}">{tr}{$field.name|escape}{/tr}</h6>


=====================================
templates/trackeroutput/relation.tpl
=====================================
@@ -8,7 +8,7 @@
             {/if}
             <ul class="relation-list">
                 {foreach from=$data.relations item=rel}
-                    <li>{object_link identifier=[$rel.target.type,$rel.target.itemId]|join:':' format=$data.format metaItemId=$rel->getMetadataItemId()}</li>
+                    <li>{object_link identifier=[$rel.target.type,$rel.target.itemId]|join:':' format=$data.format metaItemId=$rel->getMetadataItemId() target='_blank'}</li>
                 {/foreach}
             </ul>
         </div>



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/bdc02b36ea02e78c6d206b7df8d8fbc099d5e01b

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/bdc02b36ea02e78c6d206b7df8d8fbc099d5e01b
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.