diff --git a/html/css/app.css b/html/css/app.css
index a72f7cb..f920023 100644
--- a/html/css/app.css
+++ b/html/css/app.css
@@ -334,6 +334,16 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
color: #b91c1c;
}
+/* Right-aligned row of buttons directly below a picker (e.g. the "Save
+ selection to favorites" affordance under the Additional groups
+ picker). Sits between the picker and its hint. */
+.fieldActions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5em;
+ margin-top: 0.4em;
+}
+
/* Chip strip: compact list of removable pills used by the "Manage
favorites" affordance below the favorite-groups picker. Chips are
click-target friendly on touch (min 32px tall) and truncate long
diff --git a/html/sendMessage.html b/html/sendMessage.html
index e087d66..d61162a 100644
--- a/html/sendMessage.html
+++ b/html/sendMessage.html
@@ -60,13 +60,17 @@
- Selecting a group under Additional groups saves it to your favorites. Click the × on a chip to remove that favorite.
+ Pick the favorites you want for this message. Click the × on a chip to remove a favorite you no longer use.
+
+
+
+ Pick any additional org groups for this message. They stay one-off unless you save them.
diff --git a/html/sendMessage.js b/html/sendMessage.js
index b798bd2..d318c94 100644
--- a/html/sendMessage.js
+++ b/html/sendMessage.js
@@ -159,45 +159,110 @@ function onRemoveFavorite(event) {
});
}
-// Fires when the "Additional groups" multi-select changes. For each newly
-// selected id that isn't already a favorite we POST /user/groups/add. The
-// picker itself keeps the selection (so this send still uses the group);
-// the group starts appearing in the Favorite Groups picker on the next
-// page load, and immediately in the chip strip below.
-function onNewGroupsChange(newlySelectedIds) {
- if (!newlySelectedIds || !newlySelectedIds.length) return;
+// 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 pending = 0;
- var successes = 0;
+ var favGroupsEl = document.getElementById('favGroups');
+ var btn = document.getElementById('saveFavoritesBtn');
+ if (!newGroupsEl) return;
- newlySelectedIds.forEach(function (id) {
- if (favoriteGroups.some(function (g) { return g.id === id; })) return;
- pending++;
+ 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: id })
+ body: JSON.stringify({ id: c.id })
})
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(function (updatedFavorites) {
- favoriteGroups = updatedFavorites || favoriteGroups;
- successes++;
- renderFavoritesManager();
+ if (Array.isArray(updatedFavorites)) latestFavorites = updatedFavorites;
+ successIds.push(c.id);
})
.catch(function (err) {
- console.error('Failed to add favorite ' + id + ':', 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 && successes > 0) {
- flashFavoritesStatus(successes === 1
- ? 'Added to favorites.'
- : 'Added ' + successes + ' to favorites.');
+ 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.');
});
});
}
@@ -226,20 +291,9 @@ function loadNewGroups(appName) {
el.add(option);
}
VirtualSelect.init({ ele: '#newGroups' });
-
- // VirtualSelect emits a standard 'change' event on the original
- //