fix: aggiunta nota ticket unito. feat: possiblità di visualizzare ticket in schede. feat: possibilità di creare un nuovo gruppo dall'interfaccia del ticket.

This commit is contained in:
2026-07-12 19:17:07 +02:00
parent e764c37d46
commit 748f777a31
10 changed files with 585 additions and 82 deletions
+131 -7
View File
@@ -13,6 +13,108 @@ const App = {
lookupsLoaded: false,
demotivationalPhrases: [],
motivationalPhrases: [],
drafts: {},
tabs: [],
loadTabs() {
try {
const saved = localStorage.getItem('otrs_turbo_tabs');
if (saved) this.tabs = JSON.parse(saved);
const savedDrafts = localStorage.getItem('otrs_turbo_drafts');
if (savedDrafts) this.drafts = JSON.parse(savedDrafts);
} catch (e) {}
this.renderTabs();
},
saveTabs() {
localStorage.setItem('otrs_turbo_tabs', JSON.stringify(this.tabs));
this.renderTabs();
},
saveDraft(ticketId, draft) {
if (!this.drafts[ticketId]) this.drafts[ticketId] = {};
this.drafts[ticketId][draft.type] = draft;
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
},
getDraft(ticketId, type) {
return this.drafts[ticketId] ? this.drafts[ticketId][type] : null;
},
clearDraft(ticketId, type) {
if (this.drafts[ticketId] && this.drafts[ticketId][type]) {
delete this.drafts[ticketId][type];
if (Object.keys(this.drafts[ticketId]).length === 0) {
delete this.drafts[ticketId];
}
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
}
},
openTab(id, tn, title) {
const exists = this.tabs.find(t => String(t.id) === String(id));
if (!exists) {
this.tabs.push({ id, tn, title });
this.saveTabs();
}
},
addTabWithoutRedirect(id, tn, title) {
const exists = this.tabs.find(t => String(t.id) === String(id));
if (!exists) {
this.tabs.push({ id, tn, title });
this.saveTabs();
} else {
if (title && exists.title !== title) {
exists.title = title;
this.saveTabs();
} else {
this.renderTabs();
}
}
},
closeTab(id, e) {
if (e) e.stopPropagation();
this.tabs = this.tabs.filter(t => String(t.id) !== String(id));
this.saveTabs();
// Clear drafts for closed tab
delete this.drafts[id];
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
const hash = window.location.hash;
if (hash === `#/tickets/${id}`) {
if (this.tabs.length > 0) {
window.location.hash = `#/tickets/${this.tabs[this.tabs.length - 1].id}`;
} else {
window.location.hash = '#/tickets';
}
}
},
renderTabs() {
const bar = document.getElementById('tabs-bar');
if (!bar) return;
if (this.tabs.length === 0) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
const currentHash = window.location.hash;
bar.innerHTML = this.tabs.map(t => {
const isActive = currentHash === `#/tickets/${t.id}`;
const displayTitle = t.title ? (t.title.length > 25 ? t.title.substring(0, 22) + '...' : t.title) : `#${t.tn}`;
return `
<div class="tab-item ${isActive ? 'active' : ''}" onclick="window.location.hash = '#/tickets/${t.id}'" title="${App.escapeHtml(t.title || '')}">
<span>${App.escapeHtml(displayTitle)}</span>
<button class="tab-close" onclick="App.closeTab(${t.id}, event)">✕</button>
</div>
`;
}).join('');
},
get currentAgentId() {
return parseInt(localStorage.getItem('activeAgentId') || '1', 10);
@@ -23,6 +125,7 @@ const App = {
this.initTheme();
this.loadDemotivationalPhrases();
this.loadMotivationalPhrases();
this.loadTabs();
Toast.init();
// Hash-based SPA router
@@ -112,6 +215,27 @@ const App = {
const hash = fullHash.split('?')[0];
const titleEl = document.getElementById('page-title');
// Save draft for previous ticket before routing
if (typeof TicketDetailView !== 'undefined' && TicketDetailView.ticketId) {
TicketDetailView.saveDraft();
if (typeof EmailCompose !== 'undefined') {
EmailCompose.saveDraft();
const overlay = document.getElementById('email-compose-overlay');
if (overlay) overlay.remove();
}
}
this.renderTabs();
const container = document.getElementById('view-container');
if (container) {
if (hash.match(/^#\/tickets\/(\d+)$/)) {
container.classList.add('ticket-view-active');
} else {
container.classList.remove('ticket-view-active');
}
}
// Update active nav link
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
@@ -243,11 +367,11 @@ const App = {
try {
// Sync LDAP customer users to local DB cache
await this.api('/api/customer-users/sync', { method: 'POST' });
await this.ensureLookups(true);
await this.initAgentSelector();
Toast.success('Dati locali (code, utenti, ecc.) aggiornati con successo!');
// If we are on a view that needs lookups, we can re-render it
const hash = window.location.hash;
if (hash === '#/tickets/bulk') {
@@ -325,7 +449,7 @@ const App = {
await this.ensureLookups();
// Populate select dropdown
select.innerHTML = (this.lookups.users || []).map(u =>
select.innerHTML = (this.lookups.users || []).map(u =>
`<option value="${u.id}">${u.first_name} ${u.last_name} (${u.login})</option>`
).join('');
@@ -505,7 +629,7 @@ const App = {
if (timerEl) {
timerEl.classList.add('clickable-auto-time');
timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`);
//timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`);
timerEl.onclick = triggerAction;
}
} else {
@@ -580,7 +704,7 @@ const App = {
async updateSidebarBadges() {
try {
const stats = await this.api('/api/dashboard/stats');
const badge = document.getElementById('open-ticket-count');
if (badge) {
badge.textContent = stats.total_open > 0 ? stats.total_open : '';
@@ -657,7 +781,7 @@ const App = {
btnCancel.addEventListener('click', () => cleanUp(false));
btnOk.addEventListener('click', () => cleanUp(true));
// Close on backdrop click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanUp(false);
@@ -807,7 +931,7 @@ const App = {
}
});
}
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanUp(null);
});
+7 -1
View File
@@ -681,7 +681,13 @@ const Filters = {
const presetId = e.target.value;
if (!presetId) {
this.selectedPresetId = null;
if (onFilterChange) onFilterChange();
const resetBtn = document.getElementById('filter-reset');
if (resetBtn) {
resetBtn.click();
} else {
this.reset();
if (onFilterChange) onFilterChange();
}
return;
}
+136 -19
View File
@@ -8,6 +8,9 @@ const EmailCompose = (() => {
let quillEditor = null;
let attachmentsList = [];
let currentOptions = {};
let toTagsCtrl = null;
let ccTagsCtrl = null;
let bccTagsCtrl = null;
// ── CSS ──────────────────────────────────────────────────────────────────────
function injectStyles() {
@@ -70,6 +73,10 @@ const EmailCompose = (() => {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb,99,102,241),0.12);
}
#email-compose-modal .ec-tags-input.drag-over {
border-color: var(--accent-primary);
background: rgba(99,102,241,0.06);
}
#email-compose-modal .ec-tag {
display: inline-flex; align-items: center; gap: 4px;
background: var(--accent-primary); color: #fff;
@@ -145,7 +152,7 @@ const EmailCompose = (() => {
}
// ── Tag Input Helper ──────────────────────────────────────────────────────────
function makeTagInput(containerId, initialEmails = [], onFocus = null) {
function makeTagInput(containerId, initialEmails = [], onFocus = null, controllers = null) {
const container = document.getElementById(containerId);
const tags = [...initialEmails];
@@ -156,8 +163,16 @@ const EmailCompose = (() => {
tags.forEach((email, idx) => {
const tagEl = document.createElement('span');
tagEl.className = 'ec-tag';
tagEl.draggable = true;
tagEl.innerHTML = `${App.escapeHtml(email)}<button type="button" data-idx="${idx}">✕</button>`;
tagEl.querySelector('button').addEventListener('click', () => {
tagEl.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', email);
e.dataTransfer.setData('source-container-id', containerId);
});
tagEl.querySelector('button').addEventListener('click', (e) => {
e.stopPropagation();
tags.splice(idx, 1);
render();
});
@@ -197,8 +212,48 @@ const EmailCompose = (() => {
container.addEventListener('click', () => input.focus());
}
container.addEventListener('dragover', (e) => {
e.preventDefault();
container.classList.add('drag-over');
});
container.addEventListener('dragleave', () => {
container.classList.remove('drag-over');
});
container.addEventListener('drop', (e) => {
e.preventDefault();
container.classList.remove('drag-over');
const email = e.dataTransfer.getData('text/plain');
const sourceContainerId = e.dataTransfer.getData('source-container-id');
if (email && sourceContainerId && sourceContainerId !== containerId && controllers) {
const sourceCtrl = controllers[sourceContainerId];
if (sourceCtrl) {
sourceCtrl.removeTag(email);
ctrl.addTag(email);
}
}
});
render();
return { getTags: () => [...tags], addTag: (email) => { if (!tags.includes(email)) { tags.push(email); render(); } } };
const ctrl = {
getTags: () => [...tags],
addTag: (email) => {
if (!tags.includes(email)) {
tags.push(email);
render();
}
},
removeTag: (email) => {
const idx = tags.indexOf(email);
if (idx > -1) {
tags.splice(idx, 1);
render();
}
}
};
return ctrl;
}
// ── Build Modal HTML ──────────────────────────────────────────────────────────
@@ -356,19 +411,30 @@ const EmailCompose = (() => {
document.body.appendChild(overlay);
// Init tag inputs with focus tracking
const initialTo = options.initialTo || (options.customerEmail ? [options.customerEmail] : []);
const initialCc = options.initialCc || [];
const draft = options.draft;
const initialTo = draft ? draft.to : (options.initialTo || (options.customerEmail ? [options.customerEmail] : []));
const initialCc = draft ? draft.cc : (options.initialCc || []);
const initialBcc = draft ? draft.bcc : [];
let lastFocusedCtrl = null;
const toTagsCtrl = makeTagInput('ec-to-container', initialTo, () => { lastFocusedCtrl = toTagsCtrl; });
const ccTagsCtrl = makeTagInput('ec-cc-container', initialCc, () => { lastFocusedCtrl = ccTagsCtrl; });
const bccTagsCtrl = makeTagInput('ec-bcc-container', [], () => { lastFocusedCtrl = bccTagsCtrl; });
const controllers = {};
toTagsCtrl = makeTagInput('ec-to-container', initialTo, () => { lastFocusedCtrl = toTagsCtrl; }, controllers);
ccTagsCtrl = makeTagInput('ec-cc-container', initialCc, () => { lastFocusedCtrl = ccTagsCtrl; }, controllers);
bccTagsCtrl = makeTagInput('ec-bcc-container', initialBcc, () => { lastFocusedCtrl = bccTagsCtrl; }, controllers);
controllers['ec-to-container'] = toTagsCtrl;
controllers['ec-cc-container'] = ccTagsCtrl;
controllers['ec-bcc-container'] = bccTagsCtrl;
lastFocusedCtrl = toTagsCtrl;
// Subject
const subjectEl = document.getElementById('ec-subject');
const tn = options.ticketTn || '';
const title = options.ticketTitle || '';
subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title;
if (draft) {
subjectEl.value = draft.subject || '';
} else {
const tn = options.ticketTn || '';
const title = options.ticketTitle || '';
subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title;
}
// Signature and groups select
const sigSelect = document.getElementById('ec-signature-select');
@@ -377,6 +443,11 @@ const EmailCompose = (() => {
const defaultSigHtml = await loadSignatures(agentId, sigSelect);
await loadAddressGroups(agentId, groupsSelect);
if (draft) {
sigSelect.value = draft.signature || '';
document.getElementById('ec-helpdesk-cc-select').value = draft.helpdeskCc || '0';
}
groupsSelect.addEventListener('change', () => {
const selectedOpt = groupsSelect.options[groupsSelect.selectedIndex];
if (!selectedOpt || !selectedOpt.value) return;
@@ -410,13 +481,21 @@ const EmailCompose = (() => {
// Insert initial body and signature
let initialHtml = '';
if (options.initialBodyHtml) {
initialHtml += options.initialBodyHtml;
if (draft) {
if (draft.body) {
initialHtml = draft.body;
}
attachmentsList = draft.attachments || [];
renderFileList();
} else {
initialHtml += '<p><br></p>';
}
if (defaultSigHtml) {
initialHtml += '<!-- sig -->' + defaultSigHtml;
if (options.initialBodyHtml) {
initialHtml += options.initialBodyHtml;
} else {
initialHtml += '<p><br></p>';
}
if (defaultSigHtml) {
initialHtml += '<!-- sig -->' + defaultSigHtml;
}
}
quillEditor.clipboard.dangerouslyPasteHTML(initialHtml);
quillEditor.setSelection(0, 0);
@@ -454,7 +533,15 @@ const EmailCompose = (() => {
// Close handlers
document.getElementById('ec-close').addEventListener('click', close);
document.getElementById('ec-cancel').addEventListener('click', close);
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
saveDraft();
const ov = document.getElementById('email-compose-overlay');
if (ov) ov.remove();
if (quillEditor) { quillEditor = null; }
attachmentsList = [];
}
});
// Send
document.getElementById('ec-send').addEventListener('click', () => sendEmail(toTagsCtrl, ccTagsCtrl, bccTagsCtrl));
@@ -505,6 +592,7 @@ const EmailCompose = (() => {
});
Toast.success(`Email inviata a ${to.join(', ')}`);
App.clearDraft(currentOptions.ticketId, 'email');
close();
} catch (err) {
Toast.error('Errore invio email: ' + err.message);
@@ -515,12 +603,41 @@ const EmailCompose = (() => {
// ── Close ─────────────────────────────────────────────────────────────────────
function close() {
if (currentOptions.ticketId) {
App.clearDraft(currentOptions.ticketId, 'email');
}
const overlay = document.getElementById('email-compose-overlay');
if (overlay) overlay.remove();
if (quillEditor) { quillEditor = null; }
attachmentsList = [];
}
return { open, close };
function saveDraft() {
const overlay = document.getElementById('email-compose-overlay');
if (!overlay || !currentOptions.ticketId) return;
const to = toTagsCtrl ? toTagsCtrl.getTags() : [];
const cc = ccTagsCtrl ? ccTagsCtrl.getTags() : [];
const bcc = bccTagsCtrl ? bccTagsCtrl.getTags() : [];
const subject = document.getElementById('ec-subject') ? document.getElementById('ec-subject').value.trim() : '';
const body = quillEditor ? quillEditor.root.innerHTML.trim() : '';
const signature = document.getElementById('ec-signature-select') ? document.getElementById('ec-signature-select').value : '';
const helpdeskCc = document.getElementById('ec-helpdesk-cc-select') ? document.getElementById('ec-helpdesk-cc-select').value : '0';
App.saveDraft(currentOptions.ticketId, {
type: 'email',
to,
cc,
bcc,
subject,
body,
signature,
helpdeskCc,
attachments: [...attachmentsList],
options: currentOptions
});
}
return { open, close, saveDraft };
})();
window.EmailCompose = EmailCompose;
+74 -7
View File
@@ -8,6 +8,25 @@ const TicketDetailView = {
originalValues: {},
noteAttachments: [],
saveDraft() {
if (!this.ticketId) return;
const body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : '';
const subject = document.getElementById('note-subject') ? document.getElementById('note-subject').value.trim() : '';
const time_unit = document.getElementById('note-time-units') ? document.getElementById('note-time-units').value.trim() : '';
if ((body !== '<p><br></p>' && body !== '') || subject !== '' || time_unit !== '' || this.noteAttachments.length > 0) {
App.saveDraft(this.ticketId, {
type: 'note',
body,
subject,
time_unit,
attachments: [...this.noteAttachments]
});
} else {
App.clearDraft(this.ticketId, 'note');
}
},
updateNoteAttachmentList() {
const listEl = document.getElementById('note-file-list');
if (!listEl) return;
@@ -41,6 +60,7 @@ const TicketDetailView = {
await App.ensureLookups();
const data = await App.api(`/api/tickets/${id}`);
const { ticket, articles, attachments } = data;
App.addTabWithoutRedirect(ticket.id, ticket.tn, ticket.title);
let groupsData = { asMaster: [], asMember: [] };
try {
@@ -75,6 +95,7 @@ const TicketDetailView = {
queue_id: ticket.queue_id,
queue_name: ticket.queue_name,
user_id: ticket.user_id,
responsible_user_id: ticket.responsible_user_id,
type_id: ticket.type_id,
customer_id: ticket.customer_id,
customer_user_id: ticket.customer_user_id,
@@ -143,6 +164,15 @@ const TicketDetailView = {
<select class="quick-edit-select" id="qe-owner" data-field="user_id">
${(App.lookups.users || []).map(u =>
`<option value="${u.id}" ${u.id === ticket.user_id ? 'selected' : ''}>${u.first_name} ${u.last_name}</option>`
).join('')}
</select>
</div>
<div class="quick-edit-field">
<label class="quick-edit-label">Responsabile</label>
<select class="quick-edit-select" id="qe-responsible" data-field="responsible_user_id">
<option value="">—</option>
${(App.lookups.users || []).map(u =>
`<option value="${u.id}" ${u.id === ticket.responsible_user_id ? 'selected' : ''}>${u.first_name} ${u.last_name}</option>`
).join('')}
</select>
</div>
@@ -333,12 +363,14 @@ const TicketDetailView = {
<span class="meta-value">${ticket.type_name}</span>
</div>
` : ''}
${ticket.responsible_first ? `
<div class="meta-row">
<span class="meta-label">Responsabile</span>
<span class="meta-value">${ticket.responsible_first} ${ticket.responsible_last}</span>
</div>
` : ''}
<div class="meta-row">
<span class="meta-label">Owner</span>
<span class="meta-value">${ticket.owner_first ? `${ticket.owner_first} ${ticket.owner_last}` : (ticket.owner_login || '—')}</span>
</div>
<div class="meta-row">
<span class="meta-label">Responsabile</span>
<span class="meta-value">${ticket.responsible_first ? `${ticket.responsible_first} ${ticket.responsible_last}` : '—'}</span>
</div>
${totalTime > 0 ? `
<div class="meta-row">
<span class="meta-label">Tempo Totale</span>
@@ -457,6 +489,36 @@ const TicketDetailView = {
this.noteQuill = null;
}
// Restore Note Draft
const noteDraft = App.getDraft(id, 'note');
if (noteDraft) {
if (this.noteQuill && noteDraft.body) {
this.noteQuill.root.innerHTML = noteDraft.body;
}
if (document.getElementById('note-subject')) {
document.getElementById('note-subject').value = noteDraft.subject || '';
}
if (document.getElementById('note-time-units')) {
document.getElementById('note-time-units').value = noteDraft.time_unit || '';
}
this.noteAttachments = noteDraft.attachments || [];
this.updateNoteAttachmentList();
}
// Restore Email Compose Draft
const emailDraft = App.getDraft(id, 'email');
if (emailDraft) {
// Re-open Email Compose with draft options
setTimeout(() => {
if (typeof EmailCompose !== 'undefined') {
EmailCompose.open({
...emailDraft.options,
draft: emailDraft
});
}
}, 100);
}
this.bindEvents(ticket, articles, container, groupsData);
} catch (err) {
@@ -760,6 +822,7 @@ const TicketDetailView = {
this.noteAttachments = []; // Clear attachments
Toast.success(res.message || 'Nota aggiunta!');
App.clearDraft(this.ticketId, 'note');
App.updateDailyTimer();
this.render(this.ticketId);
} catch (err) {
@@ -1107,7 +1170,11 @@ const TicketDetailView = {
btnAssociate.addEventListener('click', async () => {
const select = document.getElementById('group-association-select');
const groupId = select.value;
if (!groupId) return;
if (!groupId) {
sessionStorage.setItem('otrs_create_group_with_master_tn', ticket.tn);
window.location.hash = '#/tickets/groups';
return;
}
try {
btnAssociate.disabled = true;
+9 -1
View File
@@ -80,7 +80,15 @@ const TicketGroupsView = {
await this.loadGroups();
if (this.selectedGroupId) {
const prefillMasterTn = sessionStorage.getItem('otrs_create_group_with_master_tn');
if (prefillMasterTn) {
sessionStorage.removeItem('otrs_create_group_with_master_tn');
this.openGroupModal();
const masterInput = document.getElementById('group-master-input');
if (masterInput) {
masterInput.value = prefillMasterTn;
}
} else if (this.selectedGroupId) {
this.selectGroup(this.selectedGroupId);
}
},
+45
View File
@@ -109,6 +109,7 @@ const TicketListView = {
</div>
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
<button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: var(--accent-secondary); border-color: var(--accent-secondary); margin-left: 8px;">Unisci Selezionati</button>
<button class="btn btn-primary btn-sm" id="batch-open-tabs" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Apri ticket in schede</button>
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
</div>
@@ -147,6 +148,7 @@ const TicketListView = {
<span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; vertical-align:middle; line-height:16px;">O</span>
</a>
` : ''}
<button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
</span>
</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDateTime(t.create_time)}</td>
@@ -304,6 +306,39 @@ const TicketListView = {
batchMerge.addEventListener('click', () => this.mergeBatch());
}
// Batch open tabs
const batchOpenTabsBtn = document.getElementById('batch-open-tabs');
if (batchOpenTabsBtn) {
batchOpenTabsBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (this.selectedIds.size === 0) return;
let count = 0;
this.selectedIds.forEach(id => {
const row = document.querySelector(`.ticket-table tbody tr[data-ticket-id="${id}"]`);
if (row) {
const tnLink = row.querySelector('.ticket-tn-link');
const titleLink = row.querySelector('.ticket-title-link');
const tn = tnLink ? tnLink.textContent.trim() : '';
const title = titleLink ? titleLink.textContent.trim() : '';
App.openTab(parseInt(id, 10), tn, title);
count++;
}
});
if (count > 0) {
Toast.success(`${count} ticket aperti in nuove schede!`);
this.selectedIds.clear();
this.selectedOrder = [];
document.querySelectorAll('.ticket-table tbody tr').forEach(tr => {
tr.classList.remove('selected');
tr.classList.remove('first-selected');
});
this.updateBatchBar();
}
});
}
// Batch Copy Ticket Numbers
const batchCopyTns = document.getElementById('batch-copy-tns');
if (batchCopyTns) {
@@ -505,6 +540,16 @@ const TicketListView = {
mergeBtn.disabled = true;
}
}
// Enable/disable open tabs button
const openTabsBtn = document.getElementById('batch-open-tabs');
if (openTabsBtn) {
if (this.selectedIds.size > 0) {
openTabsBtn.disabled = false;
} else {
openTabsBtn.disabled = true;
}
}
},
async applyBatch() {