diff --git a/client/src/helpers/site/window-panel-helper.js b/client/src/helpers/site/window-panel-helper.js
new file mode 100644
index 0000000000..818e24622f
--- /dev/null
+++ b/client/src/helpers/site/window-panel-helper.js
@@ -0,0 +1,131 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM – Open Source CRM application.
+ * Copyright (C) 2014-2026 EspoCRM, Inc.
+ * Website: https://www.espocrm.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+import {inject} from 'di';
+import ThemeManager from 'theme-manager';
+
+export default class WindowPanelHelper {
+
+ /**
+ * @type {import('view').default}
+ * @private
+ */
+ view
+
+ /**
+ * @private
+ * @type {boolean}
+ */
+ overflowWasHidden = false
+
+ /**
+ * @private
+ */
+ onResizeBind
+
+ /**
+ * @private
+ * @type {ThemeManager}
+ */
+ @inject(ThemeManager)
+ themeManager
+
+ /**
+ * @param {import('view').default} view
+ */
+ constructor(view) {
+ this.view = view;
+
+ view.listenToOnce(view, 'remove', () => {
+ window.removeEventListener('resize', this.onResizeBind)
+
+ if (this.overflowWasHidden) {
+ document.body.style.overflow = 'unset';
+
+ this.overflowWasHidden = false;
+ }
+ });
+
+ this.onResizeBind = this.onResize.bind(this);
+
+ window.addEventListener('resize', this.onResizeBind);
+
+ this.navbarPanelHeightSpace = this.themeManager.getParam('navbarPanelHeightSpace') ?? 100;
+ this.navbarPanelBodyMaxHeight = this.themeManager.getParam('navbarPanelBodyMaxHeight') ?? 600;
+ this.xsWidth = this.themeManager.getParam('screenWidthXs');
+
+ this.onResize();
+ }
+
+ /**
+ * @private
+ */
+ onResize() {
+ const windowHeight = window.innerHeight;
+ const windowWidth = window.innerWidth;
+
+ const panelBody = this.view.element?.querySelector('.panel-body');
+ const heading = this.view.element?.querySelector('.panel-heading');
+
+ if (!(panelBody instanceof HTMLElement)) {
+ return;
+ }
+
+ const diffHeight = heading?.outerHeight ?? 0;
+
+ const cssParams = {};
+
+ if (windowWidth <= this.xsWidth) {
+ cssParams.height = (windowHeight - diffHeight) + 'px';
+ cssParams.overflow = 'auto';
+
+ document.body.style.overflow = 'hidden';
+
+ this.overflowWasHidden = true;
+ } else {
+ cssParams.height = 'unset';
+ cssParams.overflow = 'none';
+
+ if (this.overflowWasHidden) {
+ panelBody.style.overflow = 'unset';
+
+ this.overflowWasHidden = false;
+ }
+
+ if (windowHeight - this.navbarPanelBodyMaxHeight < this.navbarPanelHeightSpace) {
+ const maxHeight = windowHeight - this.navbarPanelHeightSpace;
+
+ cssParams.maxHeight = maxHeight + 'px';
+ }
+ }
+
+ for (const [param, value] of Object.entries(cssParams)) {
+ panelBody.style[param] = value;
+ }
+ }
+}
diff --git a/client/src/views/global-search/global-search.js b/client/src/views/global-search/global-search.js
index d019bcaf0c..041fd3c8c5 100644
--- a/client/src/views/global-search/global-search.js
+++ b/client/src/views/global-search/global-search.js
@@ -29,6 +29,7 @@
import Autocomplete from 'ui/autocomplete';
import TabsHelper from 'helpers/site/tabs';
import SiteNavbarItemView from 'views/site/navbar/item';
+import WindowPanelHelper from 'helpers/site/window-panel-helper';
/** @module views/global-search/global-search */
@@ -314,8 +315,10 @@ class GlobalSearchView extends SiteNavbarItemView {
this.createView('panel', 'views/global-search/panel', {
fullSelector: '#global-search-panel',
collection: this.collection,
- }, view => {
- view.render();
+ }).then(async view => {
+ await view.render();
+
+ new WindowPanelHelper(view);
this.listenToOnce(view, 'close', this.closePanel);
});
diff --git a/client/src/views/global-search/panel.js b/client/src/views/global-search/panel.js
index 2b66f379b9..43e27bd36d 100644
--- a/client/src/views/global-search/panel.js
+++ b/client/src/views/global-search/panel.js
@@ -36,19 +36,6 @@ class GlobalSearchPanel extends View {
this.addHandler('click', '[data-action="closePanel"]', () => this.close());
this.maxSize = this.getConfig().get('globalSearchMaxSize') || 10;
-
- this.navbarPanelHeightSpace = this.getThemeManager().getParam('navbarPanelHeightSpace') || 100;
- this.navbarPanelBodyMaxHeight = this.getThemeManager().getParam('navbarPanelBodyMaxHeight') || 600;
- }
-
- onRemove() {
- $(window).off('resize.global-search-height');
-
- if (this.overflowWasHidden) {
- $('body').css('overflow', 'unset');
-
- this.overflowWasHidden = false;
- }
}
afterRender() {
@@ -58,13 +45,6 @@ class GlobalSearchPanel extends View {
this.collection.fetch()
.then(() => this.createRecordView())
.then(view => view.render());
-
- const $window = $(window);
-
- $window.off('resize.global-search-height');
- $window.on('resize.global-search-height', this.processSizing.bind(this));
-
- this.processSizing();
}
/**
@@ -99,44 +79,6 @@ class GlobalSearchPanel extends View {
});
}
- processSizing() {
- const $window = $(window);
-
- const windowHeight = $window.height();
- const windowWidth = $window.width();
-
- const diffHeight = this.$el.find('.panel-heading').outerHeight();
-
- const cssParams = {};
-
- if (windowWidth <= this.getThemeManager().getParam('screenWidthXs')) {
- cssParams.height = (windowHeight - diffHeight) + 'px';
- cssParams.overflow = 'auto';
-
- $('body').css('overflow', 'hidden');
-
- this.overflowWasHidden = true;
- }
- else {
- cssParams.height = 'unset';
- cssParams.overflow = 'none';
-
- if (this.overflowWasHidden) {
- $('body').css('overflow', 'unset');
-
- this.overflowWasHidden = false;
- }
-
- if (windowHeight - this.navbarPanelBodyMaxHeight < this.navbarPanelHeightSpace) {
- const maxHeight = windowHeight - this.navbarPanelHeightSpace;
-
- cssParams.maxHeight = maxHeight + 'px';
- }
- }
-
- this.$el.find('.panel-body').css(cssParams);
- }
-
close() {
this.trigger('close');
}
diff --git a/client/src/views/modals/last-viewed.js b/client/src/views/modals/last-viewed.js
index a0c2dd1414..b93eb667b5 100644
--- a/client/src/views/modals/last-viewed.js
+++ b/client/src/views/modals/last-viewed.js
@@ -68,8 +68,7 @@ class LastViewedModalView extends ModalView {
}
loadList() {
- const viewName = this.getMetadata().get('clientDefs.' + this.scope + '.recordViews.listLastViewed') ||
- 'views/record/list';
+ const viewName = 'views/record/list';
this.listenToOnce(this.collection, 'sync', () => {
this.createView('list', viewName, {
@@ -79,7 +78,6 @@ class LastViewedModalView extends ModalView {
checkboxes: false,
massActionsDisabled: true,
rowActionsView: false,
- searchManager: this.searchManager,
checkAllResultDisabled: true,
buttonsDisabled: true,
headerDisabled: true,
diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js
index d0fdf930ef..79b77d91b1 100644
--- a/client/src/views/notification/badge.js
+++ b/client/src/views/notification/badge.js
@@ -29,6 +29,7 @@
import View from 'view';
import {inject} from 'di';
import WebSocketManager from 'web-socket-manager';
+import WindowPanelHelper from 'helpers/site/window-panel-helper';
class NotificationBadgeView extends View {
@@ -556,8 +557,10 @@ class NotificationBadgeView extends View {
this.createView('panel', 'views/notification/panel', {
fullSelector: '#notifications-panel',
- }, view => {
- view.render();
+ }).then(async view => {
+ await view.render();
+
+ new WindowPanelHelper(view);
this.$el.closest('.navbar-body').removeClass('in');
diff --git a/client/src/views/notification/panel.js b/client/src/views/notification/panel.js
index c8f374ad66..e2f94d80f7 100644
--- a/client/src/views/notification/panel.js
+++ b/client/src/views/notification/panel.js
@@ -55,18 +55,7 @@ class NotificationPanelView extends View {
this.wait(promise);
- this.navbarPanelHeightSpace = this.getThemeManager().getParam('navbarPanelHeightSpace') || 100;
- this.navbarPanelBodyMaxHeight = this.getThemeManager().getParam('navbarPanelBodyMaxHeight') || 600;
-
this.once('remove', () => {
- $(window).off('resize.notifications-height');
-
- if (this.overflowWasHidden) {
- $('body').css('overflow', 'unset');
-
- this.overflowWasHidden = false;
- }
-
if (this.collection) {
this.collection.abortLastFetch();
}
@@ -78,13 +67,6 @@ class NotificationPanelView extends View {
.then(() => this.createRecordView())
.then(view => view.render());
- const $window = $(window);
-
- $window.off('resize.notifications-height');
- $window.on('resize.notifications-height', this.processSizing.bind(this));
-
- this.processSizing();
-
$('#navbar li.notifications-badge-container').addClass('open');
this.$el.find('> .panel').focus();
@@ -131,45 +113,6 @@ class NotificationPanelView extends View {
.then(() => this.trigger('all-read'));
}
- processSizing() {
- const $window = $(window);
- const windowHeight = $window.height();
- const windowWidth = $window.width();
-
- const diffHeight = this.$el.find('.panel-heading').outerHeight();
-
- const cssParams = {};
-
- if (windowWidth <= this.getThemeManager().getParam('screenWidthXs')) {
- cssParams.height = (windowHeight - diffHeight) + 'px';
- cssParams.overflow = 'auto';
-
- $('body').css('overflow', 'hidden');
- this.overflowWasHidden = true;
-
- this.$el.find('.panel-body').css(cssParams);
-
- return;
- }
-
- cssParams.height = 'unset';
- cssParams.overflow = 'none';
-
- if (this.overflowWasHidden) {
- $('body').css('overflow', 'unset');
-
- this.overflowWasHidden = false;
- }
-
- if (windowHeight - this.navbarPanelBodyMaxHeight < this.navbarPanelHeightSpace) {
- const maxHeight = windowHeight - this.navbarPanelHeightSpace;
-
- cssParams.maxHeight = maxHeight + 'px';
- }
-
- this.$el.find('.panel-body').css(cssParams);
- }
-
close() {
this.trigger('close');
}
diff --git a/client/src/views/site/navbar/last-viewed.js b/client/src/views/site/navbar/last-viewed.js
new file mode 100644
index 0000000000..85f87a1138
--- /dev/null
+++ b/client/src/views/site/navbar/last-viewed.js
@@ -0,0 +1,239 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM – Open Source CRM application.
+ * Copyright (C) 2014-2026 EspoCRM, Inc.
+ * Website: https://www.espocrm.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+import SiteNavbarItemView from 'views/site/navbar/item';
+import View from 'view';
+import ListRecordView from 'views/record/list';
+import WindowPanelHelper from 'helpers/site/window-panel-helper';
+
+// noinspection JSUnusedGlobalSymbols
+export default class LastViewedSiteNavbarItemView extends SiteNavbarItemView {
+
+ // language=Handlebars
+ templateContent = `
+
+
+ `
+
+ /**
+ * @private
+ * @type {HTMLDivElement|null}
+ */
+ panelElement = null
+
+ isAvailable() {
+ return !this.getConfig().get('actionHistoryDisabled');
+ }
+
+ setup() {
+ this.addActionHandler('showLastViewed', () => this.showPanel());
+
+ this.onMouseUpBind = this.onMouseUp.bind(this);
+ this.onClickBind = this.onClick.bind(this);
+ }
+
+ /**
+ * @private
+ */
+ async showPanel() {
+ const container = this.element.querySelector('.last-viewed-panel-container');
+
+ if (!(container instanceof HTMLDivElement)) {
+ this.panelElement = null;
+
+ throw Error("No last-viewed-panel-container container.");
+ }
+
+ const panel = document.createElement('div');
+ panel.id = 'last-viewed-panel';
+
+ this.panelElement = panel;
+
+ container.append(panel);
+
+ const view = new PanelView({
+ onClose: () => this.closePanel(),
+ });
+
+ await this.assignView('panel', view, '#last-viewed-panel');
+ await view.render();
+
+ new WindowPanelHelper(view);
+
+ document.addEventListener('mouseup', this.onMouseUpBind);
+ document.addEventListener('click', this.onClickBind);
+
+ await view.processFetch();
+ }
+
+ /**
+ * @private
+ */
+ closePanel() {
+ if (this.hasView('panel')) {
+ this.getView('panel').remove();
+ }
+
+ this.panelElement?.parentElement?.removeChild(this.panelElement);
+
+ document.removeEventListener('mouseup', this.onMouseUpBind);
+ document.removeEventListener('click', this.onClickBind);
+ }
+
+ /**
+ * @param {MouseEvent} e
+ * @private
+ */
+ onMouseUp(e) {
+ if (e.button !== 0) {
+ return;
+ }
+
+ const target = e.target;
+
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ if (
+ this.panelElement === target ||
+ this.panelElement.contains(target) ||
+ target.classList.contains('modal') ||
+ target.closest('.dialog.modal')
+ ) {
+ return;
+ }
+
+ return this.closePanel();
+ }
+
+ /**
+ * @param {MouseEvent} e
+ * @private
+ */
+ onClick(e) {
+ const target = e.target;
+
+ if (!(target instanceof HTMLAnchorElement)) {
+ return;
+ }
+
+ if (
+ target.dataset.action === 'showMore' ||
+ target.dataset.action === 'showLastViewed'
+ ) {
+ return;
+ }
+
+ setTimeout(() => this.closePanel(), 100);
+ }
+}
+
+class PanelView extends View {
+
+ // language=Handlebars
+ templateContent = `
+
+ `
+
+ /**
+ *
+ * @param {{
+ * onClose: function(),
+ * }} options
+ */
+ constructor(options) {
+ super(options);
+
+ this.options = options;
+ }
+
+ setup() {
+ this.addActionHandler('closePanel', () => this.options.onClose());
+
+ this.once('remove', () => {
+
+ });
+ }
+
+ onRemove() {
+ this.collection?.abortLastFetch();
+ }
+
+ async processFetch() {
+ const collection = await this.getCollectionFactory().create('ActionHistoryRecord');
+ collection.maxSize = this.getConfig().get('globalSearchMaxSize') ?? 10;
+ collection.url = 'LastViewed';
+
+ this.collection = collection;
+
+ await collection.fetch();
+
+ const view = new ListRecordView({
+ collection: collection,
+ fullSelector: this.containerSelector + ' .list-container',
+ selectable: false,
+ checkboxes: false,
+ massActionsDisabled: true,
+ rowActionsView: false,
+ checkAllResultDisabled: true,
+ buttonsDisabled: true,
+ headerDisabled: true,
+ layoutName: 'listForLastViewed',
+ layoutAclDisabled: true,
+ });
+
+ await this.assignView('list', view, '.list-container');
+ await view.render();
+ }
+}
diff --git a/frontend/less/espo-rtl/custom.less b/frontend/less/espo-rtl/custom.less
index 24bdc6b311..3b92a3a169 100644
--- a/frontend/less/espo-rtl/custom.less
+++ b/frontend/less/espo-rtl/custom.less
@@ -194,7 +194,8 @@ td.cell[data-name="buttons"] > .btn-group {
margin-right: 14px;
}
-#global-search-panel {
+#global-search-panel,
+#last-viewed-panel {
left: 35px;
right: auto;
}
diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less
index d65b5827c9..2f2d0f09a2 100644
--- a/frontend/less/espo/custom.less
+++ b/frontend/less/espo/custom.less
@@ -1961,18 +1961,21 @@ textarea.auto-height {
overflow-x: hidden;
}
-#global-search-panel {
+#global-search-panel,
+#last-viewed-panel {
> .panel {
.panel-heading {
.close-link {
display: none;
}
}
+
> .panel-body {
max-height: @navbar-panel-body-max-height;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: var(--panel-padding);
+ scrollbar-gutter: stable;
}
}
}
@@ -3114,7 +3117,8 @@ table.table td.cell .html-container {
left: auto;
}
-#global-search-panel {
+#global-search-panel,
+#last-viewed-panel {
position: absolute;
width: @global-search-panel-width;
z-index: 1001;
@@ -3625,7 +3629,8 @@ table.table-admin-panel {
height: 100%;
}
- #global-search-panel {
+ #global-search-panel,
+ #last-viewed-panel {
position: fixed;
width: 100%;
top: 0;
@@ -3635,7 +3640,8 @@ table.table-admin-panel {
height: 100%;
}
- #global-search-panel {
+ #global-search-panel,
+ #last-viewed-panel {
> .panel {
.panel-heading {
.close-link {
diff --git a/frontend/less/espo/layout-side.less b/frontend/less/espo/layout-side.less
index e0f970ae3b..aeb7cede74 100644
--- a/frontend/less/espo/layout-side.less
+++ b/frontend/less/espo/layout-side.less
@@ -940,10 +940,8 @@ body[data-navbar="side"] {
border-radius: 0 0 var(--border-radius) var(--border-radius) !important;
}
- #global-search-panel {
- margin-top: var(--top-bar-height);
- }
-
+ #global-search-panel,
+ #last-viewed-panel,
#notifications-panel {
margin-top: var(--top-bar-height);
}
diff --git a/frontend/less/espo/layout-top.less b/frontend/less/espo/layout-top.less
index 0633ff8f13..7cedc0e9e2 100644
--- a/frontend/less/espo/layout-top.less
+++ b/frontend/less/espo/layout-top.less
@@ -191,7 +191,8 @@ body:not([data-navbar="side"]) {
padding-top: var(--4px);
}
- #global-search-panel {
+ #global-search-panel,
+ #last-viewed-panel {
margin-top: var(--1px);
}
diff --git a/frontend/less/glass/custom.less b/frontend/less/glass/custom.less
index 5abe442e85..018489bf97 100644
--- a/frontend/less/glass/custom.less
+++ b/frontend/less/glass/custom.less
@@ -130,7 +130,8 @@ body {
}
#notifications-panel,
- #global-search-panel {
+ #global-search-panel,
+ #last-viewed-panel {
> .panel {
backdrop-filter: @backdrop-filter;
> .panel-heading {