The nav link race:
- Every page's header used `<a id="jobLink" href=''>` and the real
destination was only assigned later, after /info returned. An empty
href resolves to the current document URL, so clicking "Monitor Jobs"
on sendMessage before /info completed silently reloaded sendMessage.
- Fixed structurally: nav links now use static relative hrefs baked
into the HTML ("./sendMessage.html", "./monitorJobs.html"), so the
destination is correct the moment the DOM parses.
Shared UI:
- New html/css/app.css: design tokens (palette, radius, shadow, font),
sticky compact top header (bot avatar + label on the left, nav pills
in the middle, current user on the right, active-page highlight via
aria-current), card containers, form styling with focus rings,
DataTables theme overrides, status pills, modal, and responsive
breakpoints.
- New html/js/app.js: shared browser bootstrap. Parses appName from
the URL, redirects to OAuth if the id cookie is missing, fetches
/info once, populates the header, applies aria-current to the
active nav link, and invokes a per-page onReady callback with
{ info, appName }. Also exports getCookie, escapeHtml, and formatDate
helpers so each page stops shipping its own copy.
Per-page rewrites:
- sendMessage.html/.js: form now lives in a card, image preview only
shows when a file is attached, EasyMDE + VirtualSelect styled to
match the theme, submit is a primary button, confirmation modal
redesigned. All bootstrap code deleted (delegated to app.js).
- monitorJobs.html/.js: three cards (Running / Scheduled / Completed)
with themed DataTables. Completed table sorts by start time desc,
paginates, and searches; message column truncates HTML previews to
~120 chars. Empty-state text per table. `jobId` and running-row
"view" links go to jobDetail via safe relative URLs.
- jobDetail.html/.js: same shared header + card layout; summary grid,
message preview, and recipient table styled to match the new
palette.
Sanity checks:
- All 43 helper tests still pass.
- Server boots cleanly on port 3001.
- Curl of sendMessage/monitorJobs/jobDetail all return 200 with the
shared header markup.
- /CollabCentral/:app/css/app.css and /CollabCentral/:app/js/app.js
both serve 200 (shared static mount is per-bot as expected).
- No local href in any page is empty; every nav target resolves at
parse time.
- /info and requireBot 404 gate unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
// 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',
|
|
});
|
|
|
|
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 <strong>' + result.memberList.length + '</strong> recipient(s).<br><br>';
|
|
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';
|
|
}
|
|
|
|
function loadFavoriteGroups(appName) {
|
|
var el = document.getElementById('favGroups');
|
|
fetch('/CollabCentral/' + appName + '/user/groups/list')
|
|
.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].name;
|
|
option.value = groups[i].id;
|
|
el.add(option);
|
|
}
|
|
VirtualSelect.init({ ele: '#favGroups' });
|
|
});
|
|
}
|
|
|
|
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' });
|
|
});
|
|
}
|
|
|
|
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';
|
|
}
|