Make favorites explicit: kill auto-save on Additional Groups picks
Auto-saving every Additional-Groups pick into favorites made the
list a running audit log of "every group I've ever touched" instead
of a curated set of go-to groups. Since most messages target a
different set of groups (one office one week, three campuses the
next), the auto-save polluted favorites for every user immediately.
Now:
- The Additional Groups picker is per-message only. Changing it no
longer fires /user/groups/add.
- A new "Save selection to favorites" button under the Additional
picker deliberately promotes whatever's currently selected there
into the user's favorites. Explicit action, no surprises.
- On save, the just-promoted groups are moved from the Additional
picker into the Favorites picker so this message's recipient set
survives untouched, but the UI is visually consolidated to one
place (avoids the "same group ticked in two pickers" confusion).
- The favorites VirtualSelect gets its options list rebuilt via
setOptions() so the just-added groups are immediately pickable
without a page reload; the existing selection is captured first
because setOptions can reset it.
- Hint text updated to describe the new intent on both pickers.
CSS: added .fieldActions for a right-aligned button row under a
picker (sits between the picker and its hint).
Backend untouched — /user/groups/{add,remove,list} were already
per-id and the semantics still fit.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
90eb889523
commit
3816880801
3 changed files with 104 additions and 36 deletions
|
|
@ -334,6 +334,16 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
|
||||||
color: #b91c1c;
|
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
|
/* Chip strip: compact list of removable pills used by the "Manage
|
||||||
favorites" affordance below the favorite-groups picker. Chips are
|
favorites" affordance below the favorite-groups picker. Chips are
|
||||||
click-target friendly on touch (min 32px tall) and truncate long
|
click-target friendly on touch (min 32px tall) and truncate long
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,17 @@
|
||||||
<select id="favGroups" multiple name="favGroups" placeholder="Favorite groups"
|
<select id="favGroups" multiple name="favGroups" placeholder="Favorite groups"
|
||||||
data-silent-initial-value-set="false"></select>
|
data-silent-initial-value-set="false"></select>
|
||||||
<div id="favGroupsManager" class="chipStrip hidden" aria-label="Manage favorite groups"></div>
|
<div id="favGroupsManager" class="chipStrip hidden" aria-label="Manage favorite groups"></div>
|
||||||
<span class="hint">Selecting a group under <em>Additional groups</em> saves it to your favorites. Click the × on a chip to remove that favorite.</span>
|
<span class="hint">Pick the favorites you want for this message. Click the × on a chip to remove a favorite you no longer use.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="newGroups">Additional groups</label>
|
<label for="newGroups">Additional groups</label>
|
||||||
<select id="newGroups" multiple name="newGroups" placeholder="Search all groups"
|
<select id="newGroups" multiple name="newGroups" placeholder="Search all groups"
|
||||||
data-silent-initial-value-set="false"></select>
|
data-silent-initial-value-set="false"></select>
|
||||||
|
<div class="fieldActions">
|
||||||
|
<button type="button" id="saveFavoritesBtn" class="btn btn-secondary" onclick="saveSelectionToFavorites()">Save selection to favorites</button>
|
||||||
|
</div>
|
||||||
|
<span class="hint">Pick any additional org groups for this message. They stay one-off unless you save them.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
||||||
|
|
@ -159,45 +159,110 @@ function onRemoveFavorite(event) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fires when the "Additional groups" multi-select changes. For each newly
|
// Explicit "Save selection to favorites" action. Reads whatever is
|
||||||
// selected id that isn't already a favorite we POST /user/groups/add. The
|
// currently selected in the "Additional groups" picker, POSTs
|
||||||
// picker itself keeps the selection (so this send still uses the group);
|
// /user/groups/add for each id that isn't already a favorite, and on
|
||||||
// the group starts appearing in the Favorite Groups picker on the next
|
// success rewires the widget state so the newly-saved groups move from
|
||||||
// page load, and immediately in the chip strip below.
|
// the Additional picker into the Favorite picker without losing this
|
||||||
function onNewGroupsChange(newlySelectedIds) {
|
// message's recipient list.
|
||||||
if (!newlySelectedIds || !newlySelectedIds.length) return;
|
//
|
||||||
|
// 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 newGroupsEl = document.getElementById('newGroups');
|
||||||
var pending = 0;
|
var favGroupsEl = document.getElementById('favGroups');
|
||||||
var successes = 0;
|
var btn = document.getElementById('saveFavoritesBtn');
|
||||||
|
if (!newGroupsEl) return;
|
||||||
|
|
||||||
newlySelectedIds.forEach(function (id) {
|
var selectedOptions = newGroupsEl.getSelectedOptions() || [];
|
||||||
if (favoriteGroups.some(function (g) { return g.id === id; })) return;
|
var candidates = [];
|
||||||
pending++;
|
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', {
|
fetch('/CollabCentral/' + CollabCentral.appName + '/user/groups/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: id })
|
body: JSON.stringify({ id: c.id })
|
||||||
})
|
})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (updatedFavorites) {
|
.then(function (updatedFavorites) {
|
||||||
favoriteGroups = updatedFavorites || favoriteGroups;
|
if (Array.isArray(updatedFavorites)) latestFavorites = updatedFavorites;
|
||||||
successes++;
|
successIds.push(c.id);
|
||||||
renderFavoritesManager();
|
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.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);
|
flashFavoritesStatus('Could not save one or more favorites.', true);
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
pending--;
|
pending--;
|
||||||
if (pending === 0 && successes > 0) {
|
if (pending > 0) return;
|
||||||
flashFavoritesStatus(successes === 1
|
|
||||||
? 'Added to favorites.'
|
if (btn) btn.disabled = false;
|
||||||
: 'Added ' + successes + ' to favorites.');
|
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);
|
el.add(option);
|
||||||
}
|
}
|
||||||
VirtualSelect.init({ ele: '#newGroups' });
|
VirtualSelect.init({ ele: '#newGroups' });
|
||||||
|
// Additional groups are per-message only — no auto-save on
|
||||||
// VirtualSelect emits a standard 'change' event on the original
|
// change. Users promote picks to favorites via the explicit
|
||||||
// <select>. We diff against the previous selection so we only
|
// "Save selection to favorites" button (saveSelectionToFavorites).
|
||||||
// fire /user/groups/add for the newly-picked ids, not the whole
|
|
||||||
// set on every change.
|
|
||||||
var previous = [];
|
|
||||||
el.addEventListener('change', function () {
|
|
||||||
var current = [];
|
|
||||||
var selected = el.getSelectedOptions();
|
|
||||||
for (var j = 0; j < selected.length; j++) current.push(selected[j].value);
|
|
||||||
var added = current.filter(function (id) { return previous.indexOf(id) === -1; });
|
|
||||||
previous = current;
|
|
||||||
if (added.length) onNewGroupsChange(added);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue