// 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([]);
}