// Page script for sendMessage.html. Bootstrapping (auth cookie check, /info // fetch, header rendering, active-nav highlight) all live in js/app.js; // this file just wires the compose form. var modal = document.querySelector('.modal'); var overlay = document.querySelector('.overlay'); var message = new EasyMDE({ element: document.getElementById('my-text-area'), toolbar: [ 'bold', 'italic', '|', 'heading-1', 'heading-2', 'heading-3', '|', 'code', 'unordered-list', 'ordered-list', 'link', 'quote', '|', 'preview' ], maxHeight: '260px', }); // In-memory mirror of the server-side favorites list for this user + bot. // Kept in sync with every /user/groups/add and /user/groups/remove response // so the chip strip and the "already a favorite?" check stay authoritative // without a round-trip. var favoriteGroups = []; CollabCentral.init(function (ctx) { // User is authenticated and authorized. Reveal the form + populate the // group selectors from the backend. document.getElementById('formPanel').classList.remove('hidden'); loadFavoriteGroups(ctx.appName); loadNewGroups(ctx.appName); }); document.getElementById('sendMessage').onsubmit = function (event) { event.preventDefault(); var appName = CollabCentral.appName; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function () { if (this.readyState === this.DONE && this.status === 200) { var result = JSON.parse(xhttp.response); var scheduled = document.getElementById('scheduledFor').value; var text = 'This message will be sent to ' + result.memberList.length + ' recipient(s).

'; if (scheduled) { text += 'Scheduled for ' + CollabCentral.escapeHtml(scheduled) + '.'; } else { text += 'It will be sent immediately once you confirm.'; } document.getElementById('modalMessage').innerHTML = text; openModal(); } }; var formData = new FormData(); formData.append('uploadImage', document.getElementById('uploadImage').files[0]); formData.append('message', message.value()); formData.append('uploadCSV', document.getElementById('uploadCSV').files[0]); formData.append('appName', appName); var selectedGroups = []; for (var group of document.querySelector('#favGroups').getSelectedOptions()) { selectedGroups.push(group.value); } for (var group of document.querySelector('#newGroups').getSelectedOptions()) { selectedGroups.push(group.value); } formData.append('groups', selectedGroups); formData.append('scheduledFor', document.getElementById('scheduledFor').value); xhttp.open('POST', '/CollabCentral/' + appName + '/jobs/edit'); xhttp.send(formData); }; function showImagePreview(event) { if (!event.target.files || !event.target.files.length) return; var src = URL.createObjectURL(event.target.files[0]); var preview = document.getElementById('file-ip-1-preview'); preview.src = src; preview.style.display = 'block'; } // ---- Favorites ----------------------------------------------------------- function loadFavoriteGroups(appName) { var el = document.getElementById('favGroups'); fetch('/CollabCentral/' + appName + '/user/groups/list') .then(function (res) { return res.json(); }) .then(function (groups) { favoriteGroups = Array.isArray(groups) ? groups.slice() : []; for (var i = 0; i < favoriteGroups.length; i++) { var option = document.createElement('option'); option.text = favoriteGroups[i].name; option.value = favoriteGroups[i].id; el.add(option); } VirtualSelect.init({ ele: '#favGroups' }); renderFavoritesManager(); }); } // Renders the chip strip below the favorites picker. Each chip has an // × button that removes the group from favorites. The chip strip is // intentionally separate from the VirtualSelect widget: the picker answers // "which favorites am I sending to right now?", the chip strip answers // "which groups do I want to remember for next time?". function renderFavoritesManager() { var el = document.getElementById('favGroupsManager'); if (!el) return; el.innerHTML = ''; if (!favoriteGroups.length) { el.classList.add('hidden'); return; } el.classList.remove('hidden'); for (var i = 0; i < favoriteGroups.length; i++) { var g = favoriteGroups[i]; var chip = document.createElement('span'); chip.className = 'chip'; var label = document.createElement('span'); label.className = 'chip__label'; label.textContent = g.alias ? g.alias : g.name; if (g.alias && g.name && g.alias !== g.name) { chip.title = g.name; } var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'chip__close'; btn.setAttribute('aria-label', 'Remove ' + (g.name || 'favorite')); btn.textContent = '×'; btn.dataset.groupId = g.id; btn.addEventListener('click', onRemoveFavorite); chip.appendChild(label); chip.appendChild(btn); el.appendChild(chip); } } function onRemoveFavorite(event) { var groupId = event.currentTarget.dataset.groupId; if (!groupId) return; event.currentTarget.disabled = true; fetch('/CollabCentral/' + CollabCentral.appName + '/user/groups/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: groupId }) }) .then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(function (updatedFavorites) { favoriteGroups = updatedFavorites || []; renderFavoritesManager(); flashFavoritesStatus('Removed from favorites.'); }) .catch(function (err) { console.error('Failed to remove favorite:', err); flashFavoritesStatus('Could not remove favorite.', true); event.currentTarget.disabled = false; }); } // Explicit "Save selection to favorites" action. Reads whatever is // currently selected in the "Additional groups" picker, POSTs // /user/groups/add for each id that isn't already a favorite, and on // success rewires the widget state so the newly-saved groups move from // the Additional picker into the Favorite picker without losing this // message's recipient list. // // Kept explicit (button-driven) rather than firing on every change so a // one-off pick — the common case, since every message tends to target a // different set of groups — doesn't pollute favorites forever. function saveSelectionToFavorites() { var newGroupsEl = document.getElementById('newGroups'); var favGroupsEl = document.getElementById('favGroups'); var btn = document.getElementById('saveFavoritesBtn'); if (!newGroupsEl) return; var selectedOptions = newGroupsEl.getSelectedOptions() || []; var candidates = []; for (var i = 0; i < selectedOptions.length; i++) { var id = selectedOptions[i].value; if (favoriteGroups.some(function (g) { return g.id === id; })) continue; candidates.push({ id: id, name: selectedOptions[i].label || selectedOptions[i].text }); } if (!candidates.length) { flashFavoritesStatus('Nothing new to save.'); return; } if (btn) btn.disabled = true; var latestFavorites = favoriteGroups; var successIds = []; var pending = candidates.length; candidates.forEach(function (c) { fetch('/CollabCentral/' + CollabCentral.appName + '/user/groups/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: c.id }) }) .then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(function (updatedFavorites) { if (Array.isArray(updatedFavorites)) latestFavorites = updatedFavorites; successIds.push(c.id); }) .catch(function (err) { console.error('Failed to add favorite ' + c.id + ':', err); flashFavoritesStatus('Could not save one or more favorites.', true); }) .finally(function () { pending--; if (pending > 0) return; if (btn) btn.disabled = false; if (!successIds.length) return; // Capture the current favorite selection BEFORE // rebuilding the options list — setOptions can clear // the selection depending on the VirtualSelect // internals, and we want this message's existing // favorite picks to survive. var currentFavIds = []; if (favGroupsEl && typeof favGroupsEl.getSelectedOptions === 'function') { var favSel = favGroupsEl.getSelectedOptions() || []; for (var j = 0; j < favSel.length; j++) currentFavIds.push(favSel[j].value); } // Adopt the freshest server view of favorites, then // rebuild the favorites VirtualSelect options list so // the just-saved groups become pickable there. favoriteGroups = latestFavorites; renderFavoritesManager(); if (favGroupsEl && typeof favGroupsEl.setOptions === 'function') { favGroupsEl.setOptions(favoriteGroups.map(function (g) { return { label: g.name, value: g.id }; })); } // Preserve this message's recipient set: move the // saved ids from the Additional picker into the // Favorite picker. Users can still deselect either // way manually, but the default is "you saved these, // they're still going to those groups". if (favGroupsEl && typeof favGroupsEl.setValue === 'function') { for (var k = 0; k < successIds.length; k++) { if (currentFavIds.indexOf(successIds[k]) === -1) currentFavIds.push(successIds[k]); } favGroupsEl.setValue(currentFavIds); } if (typeof newGroupsEl.setValue === 'function') { var remaining = []; var addSel = newGroupsEl.getSelectedOptions() || []; for (var m = 0; m < addSel.length; m++) { var mid = addSel[m].value; if (successIds.indexOf(mid) === -1) remaining.push(mid); } newGroupsEl.setValue(remaining); } flashFavoritesStatus(successIds.length === 1 ? 'Added to favorites.' : 'Added ' + successIds.length + ' to favorites.'); }); }); } function flashFavoritesStatus(text, isError) { var el = document.getElementById('favGroupsStatus'); if (!el) return; el.textContent = text; el.classList.toggle('fieldStatus--error', !!isError); clearTimeout(flashFavoritesStatus._t); flashFavoritesStatus._t = setTimeout(function () { el.textContent = ''; el.classList.remove('fieldStatus--error'); }, 4000); } function loadNewGroups(appName) { var el = document.getElementById('newGroups'); fetch('/CollabCentral/' + appName + '/user/groups/find') .then(function (res) { return res.json(); }) .then(function (groups) { for (var i = 0; i < groups.length; i++) { var option = document.createElement('option'); option.text = groups[i].displayName; option.value = groups[i].id; el.add(option); } VirtualSelect.init({ ele: '#newGroups' }); // Additional groups are per-message only — no auto-save on // change. Users promote picks to favorites via the explicit // "Save selection to favorites" button (saveSelectionToFavorites). }); } // ---- cancel / discard ----------------------------------------------------- // POSTs /jobs/cancel to drop the caller's in-progress "building" job for // this bot. Idempotent server-side, so it's safe to call whether or not // the user has already hit "Review & send" (which is what creates the // building entry). Fires an unawaited request — clearing the local form // doesn't depend on the response. function requestServerCancel() { fetch('/CollabCentral/' + CollabCentral.appName + '/jobs/cancel', { method: 'POST' }) .catch(function (err) { console.error('Cancel request failed:', err); }); } // Cancel button on the compose form. Confirms with the user (a stray // click shouldn't nuke a half-written draft), tells the server to drop // any in-progress building entry, then clears the local form. function cancelMessage() { var hasDraft = !!message.value() || document.getElementById('uploadImage').files.length || document.getElementById('uploadCSV').files.length || document.querySelector('#favGroups').getSelectedOptions().length || document.querySelector('#newGroups').getSelectedOptions().length || document.getElementById('scheduledFor').value; if (hasDraft && !window.confirm('Discard this message?')) return; requestServerCancel(); clearForm(); } // "Discard" action from inside the review modal — same as Cancel but // also closes the modal (which is open at this point). function discardMessage() { requestServerCancel(); clearForm(); closeModal(); } // ---- send-flow modal ------------------------------------------------------ function submit() { var appName = CollabCentral.appName; var scheduled = document.getElementById('scheduledFor').value; var url = scheduled ? '/CollabCentral/' + appName + '/jobs/schedule' : '/CollabCentral/' + appName + '/jobs/runNow'; fetch(url, { method: 'POST', redirect: 'follow' }) .then(function (response) { clearForm(); if (response.redirected) window.location.href = response.url; }) .catch(function (err) { console.error('Submit failed:', err); }); closeModal(); } function continueEditing() { closeModal(); } function openModal() { modal.classList.remove('hidden'); overlay.classList.remove('hidden'); } function closeModal() { modal.classList.add('hidden'); overlay.classList.add('hidden'); } overlay.addEventListener('click', closeModal); function clearForm() { document.getElementById('uploadImage').value = ''; message.value(''); document.getElementById('uploadCSV').value = ''; document.getElementById('scheduledFor').value = ''; var preview = document.getElementById('file-ip-1-preview'); preview.removeAttribute('src'); preview.style.display = 'none'; // VirtualSelect exposes setValue([]) to clear a multi-select without // reinitializing the widget. Guard for the case where init hasn't run // yet (e.g. clearForm() called before the group lists finished loading). var fav = document.querySelector('#favGroups'); if (fav && typeof fav.setValue === 'function') fav.setValue([]); var newG = document.querySelector('#newGroups'); if (newG && typeof newG.setValue === 'function') newG.setValue([]); }