Selecting a group in "Additional groups" now auto-saves it to the
caller's favorites, and each favorite gets a × affordance for one-click
removal. Favorites still live where they always have —
config.webex.bot[app].authorized[personId].groups — so nothing changes
for existing installations.
Backend
- New POST /CollabCentral/:app/user/groups/add. Body { id }. Validates
the caller is authorized for :app, validates :id resolves to a real
Webex group by looking it up in the cached org-wide group list
(blocks arbitrary strings from being stuffed into config.json),
dedups against the existing favorites array, writes config.json via
saveConfig, and returns the updated array.
- New POST /CollabCentral/:app/user/groups/remove. Body { id }.
Filters that id out of the caller's favorites, writes only when
something actually changed (a remove of an unknown id no-ops instead
of rewriting config.json), and returns the updated array.
- Both endpoints are safe against concurrent writes: index.js is
single-process and node is single-threaded, so read/mutate/write
runs atomically per request.
- Both log a compact audit line (last-8 of personId + group name) so
operators can see who is curating what.
Frontend (sendMessage.html + .js + app.css)
- New "Manage favorites" chip strip renders directly under the
Favorite Groups picker. Each favorite becomes a pill (uses .alias
when set, otherwise .name; long labels truncate with ellipsis).
Clicking × on a pill removes that favorite server-side and updates
the strip locally.
- Additional Groups picker wires a 'change' listener that diffs the
current selection against the previous one and only fires
/user/groups/add for newly-picked ids (never re-fires on the
reselect side of a deselect+reselect, never spams the API with the
full selection on every keystroke).
- Client keeps favoriteGroups mirrored to every API response so the
"already a favorite?" dedup check is a pure in-memory lookup — no
wasted round trips when the user re-picks a group they already
favorited.
- Transient status message ("Added to favorites." / "Removed from
favorites." / error variants) fades under the field label; sticks
around ~4s.
- New shared styles: .fieldLabelRow (label + inline status),
.fieldStatus (with --error variant), .chipStrip, .chip,
.chip__label, .chip__close (with hover/focus states).
Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
4.8 KiB
HTML
105 lines
4.8 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>CollabCentral</title>
|
||
<link id="appFavicon" rel="icon" type="image/x-icon" href="favicon.ico">
|
||
<link rel="stylesheet" href="css/app.css">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||
<link rel="stylesheet" href="js/virtual-select.min.css">
|
||
<script src="js/virtual-select.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||
</head>
|
||
|
||
<body data-page="send" data-title-suffix="Send Message">
|
||
|
||
<header class="appHeader">
|
||
<div class="appHeader__inner">
|
||
<div class="appHeader__brand">
|
||
<div id="appAvatar" class="appHeader__avatar"></div>
|
||
<div id="appLabel" class="appHeader__label">Loading…</div>
|
||
</div>
|
||
<nav class="appHeader__nav">
|
||
<a href="./sendMessage.html" data-nav="send">Send Message</a>
|
||
<a href="./monitorJobs.html" data-nav="monitor">Monitor Jobs</a>
|
||
</nav>
|
||
<div class="appHeader__spacer"></div>
|
||
<div id="appUser" class="appHeader__user"></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="unauthorizedPanel" class="panel hidden">
|
||
<h2>You don't have access to this bot</h2>
|
||
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||
</div>
|
||
|
||
<div id="formPanel" class="container hidden">
|
||
<div class="card">
|
||
<h1 class="pageTitle">Compose a message</h1>
|
||
<p class="pageSubtitle">Pick your recipients, write the message, and send now or schedule it for later.</p>
|
||
|
||
<form id="sendMessage" enctype="multipart/form-data">
|
||
<div class="field">
|
||
<label for="my-text-area">Message</label>
|
||
<textarea id="my-text-area"></textarea>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="uploadImage">Attach image (optional)</label>
|
||
<input type="file" id="uploadImage" accept="image/*" name="uploadImage" onchange="showImagePreview(event);">
|
||
<img id="file-ip-1-preview" alt="">
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="fieldLabelRow">
|
||
<label for="favGroups">Favorite groups</label>
|
||
<span id="favGroupsStatus" class="fieldStatus" role="status" aria-live="polite"></span>
|
||
</div>
|
||
<select id="favGroups" multiple name="favGroups" placeholder="Favorite groups"
|
||
data-silent-initial-value-set="false"></select>
|
||
<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>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="newGroups">Additional groups</label>
|
||
<select id="newGroups" multiple name="newGroups" placeholder="Search all groups"
|
||
data-silent-initial-value-set="false"></select>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="uploadCSV">Upload CSV of recipients (optional)</label>
|
||
<input type="file" id="uploadCSV" name="uploadCSV" accept="text/csv">
|
||
<span class="hint">CSV of Webex person IDs or emails to add to the recipient list.</span>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="scheduledFor">Schedule for (leave blank to send immediately)</label>
|
||
<input type="datetime-local" id="scheduledFor" name="scheduledFor">
|
||
</div>
|
||
|
||
<div class="formActions">
|
||
<button id="submit" type="submit" class="btn btn-primary">Review & send</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="modal hidden" aria-modal="true" role="dialog">
|
||
<button class="modal__close" onclick="continueEditing()" aria-label="Close">×</button>
|
||
<h3>Ready to send?</h3>
|
||
<p id="modalMessage">This message will be sent.</p>
|
||
<div class="modal__actions">
|
||
<button class="btn btn-secondary" onclick="continueEditing()">Keep editing</button>
|
||
<button class="btn btn-primary" onclick="submit()">Send</button>
|
||
</div>
|
||
</section>
|
||
<div class="overlay hidden"></div>
|
||
|
||
<script src="js/app.js"></script>
|
||
<script src="sendMessage.js"></script>
|
||
</body>
|
||
|
||
</html>
|