feat(chats): align EditChat form + add per-channel Remove buttons (#1271)#1272
Conversation
Fix two rough edges in the unified EditChat form shipped in #1262: - EnableMessages/MessagesDelay inputs used col-4 while every sibling row used col-6, causing visual misalignment. Aligned to col-6. - Only the header "Remove chat" link existed (deletes the whole record). Added per-channel Remove buttons (Telegram/Slack/Mattermost) in the footer next to the existing Send-test buttons. Each clears only its channel; the Chat row, folder bindings, and other channels stay intact. Required backend additions: ChatUpdate gains ClearTelegramBinding / ClearSlackWebhook / ClearMattermostWebhook flags (ApplyUpdate's ?? coalescing prevents null submission from clearing a saved webhook today). Chat.TelegramType and Chat.AuthorizationTime move from init to internal set so ApplyUpdate can null them on clear; ToEntity() already gated the Telegram block on TelegramType.HasValue, so the cleared fields drop on round-trip. Three new NotificationsController endpoints (RemoveTelegramBinding / ClearSlackWebhook / ClearMattermostWebhook) route through ConcurrentStorage.TryUpdate so the Updated event fires and _telegramChatIds index is rekeyed. Closes #1271 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…w cycle 1) - ChatUpdate.cs comment referenced "private-set" but TelegramType / AuthorizationTime are internal-set after this PR — sync the explanation. - Remove anchors had data-chat-id="@Model.Id" that the JS removeChannel() helper never reads (URL is built inline via Razor @Model.Id). Drop the attribute so the markup doesn't promise a contract the code doesn't honor; sendTest* anchors keep data-chat-id because Telegram Send-test actually needs Model.TelegramChatId (different value). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
removeChannel previously had only a success handler. If ChatsManager.TryUpdate returned false (chat deleted between page render and click) the controller responds NotFound and the AJAX call failed silently — user saw a stale form with no toast. Add error: handler matching the success: option pattern already used by removeChat, mirroring sendTest*'s .fail() toast contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eview cycle 3)
The Remove-Slack flow ends with window.location.reload(). The previous
assertion block ran immediately after the OK click, relying solely on
Playwright's auto-waiting to settle the toHaveValue('') check. On a slow
local run or under CI load the assertion can race the still-rendering
previous DOM (which still has the populated webhook value), causing a flake.
Add waitForLoadState('domcontentloaded') between the click and the
assertions so the reload is guaranteed to have landed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…1271 alignment) Previous fix (col-4 → col-6) equalised the column width but left the inputs visually shifted ~12px left of Name/Description/SlackWebhookUrl/MattermostWebhookUrl: - EnableMessages: the col-6 div also carried `form-check form-switch`, and Bootstrap's .form-switch rule overrides the .col-6 padding-left to 2.5em. The .form-check-input margin-left: -2.5em cancels the padding shift, so the checkbox visually sits at col-6 outer-left instead of col-6 outer-left + 0.75rem (the default col padding every other input gets). - MessagesDelay: the col-6 div had `p-0`, removing the default 0.75rem col padding. The input-group hugged the left edge of the column. Fix: wrap both .form-check and .input-group in a plain inner <div>, so the col-6 keeps its 0.75rem padding (matching every sibling row) and the form-switch / input-group styles stay scoped to the inner element. Net visual result: EnableMessages switch and MessagesDelay input left edges line up with Name / Description / Slack / Mattermost inputs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s in EditChat Follow-up to the per-channel Remove buttons in PR #1272. The form was linear: 7 stacked rows (Name / Description / EnableMessages / MessagesDelay / Telegram status / Slack webhook / Mattermost webhook), with channel-specific Send-test + Remove buttons clustered in a footer group. As the three channels became first-class, the linear layout scattered each channel's input, status, and actions across different visual groups. Now three Bootstrap nav-tabs (Telegram / Slack / Mattermost) sit between MessagesDelay and Connected Folders. Each tab pane owns its channel's full UI — status badge or setup-help link + Send-test + Remove for Telegram, webhook input + Send-test + Remove for Slack, webhook input + Remove for Mattermost. Footer is reduced to Save only. Active-tab rule (in Razor at top of the view): - isTelegramBound → telegram - HasSlack || isNewChat → slack (new chats can't fill Telegram from the form) - HasMattermost → mattermost - fallback → telegram (shows the bot-invite setup help) This makes the Slack-create Playwright path work without a manual tab click on both AddChat GET (isNewChat → slack) and on EditChat reopen for a Slack-only chat (HasSlack → slack). Pattern follows Configuration/Index.cshtml:17-66 and Folders/EditFolder.cshtml (button + data-bs-toggle="tab" + data-bs-target, not anchor-based). JS handlers from cycle 2 of PR #1272 are unchanged — the sendTest* / remove* anchor IDs are preserved verbatim inside their respective tab panes. Backend untouched: NotificationsController.RemoveTelegramBinding / ClearSlackWebhook / ClearMattermostWebhook endpoints and ChatUpdate Clear* flags from PR #1272 are reused as-is. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cle 1) Slack/Mattermost tab icons used fa-brands while the sibling _NewChatHelpModal and the rest of the form use fab (the FontAwesome 6 short alias). Normalize to fab so all three brand icons in EditChat match the surrounding markup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
channelTabsContent was carried over from early planning but is never referenced from JS, CSS, or another view. Matches the canonical pattern in Configuration/Index.cshtml:40 which leaves tab-content without an id. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ew cycle 3) Previous fallback chain returned "telegram" when an existing chat had no configured channels, which is exactly the state right after ClearSlackWebhook or ClearTelegramBinding. The user just acted on Slack/Telegram and lands on the *other* channel's tab — confusing. Restructure the defaultTab ternary so the Telegram tab only opens for Telegram-bound chats, Mattermost only wins when it's the sole configured channel, and Slack is the fallback in all other cases (new chat, post-clear, or chat with neither channel set). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #1272 Review — Per-channel Remove buttons & tabbed EditChat editorReviewed Overall this is a clean, well-tested change. The core mechanism — negative-clear flags on Comments below, most important first. 1. Stored XSS via
|
Summary
Closes #1271. Two rough edges in the unified
EditChatform shipped in #1262:Field alignment —
EnableMessagesswitch andMessagesDelayinput usedcol-4while every sibling row usedcol-6, causing them to render shifted left. Both changed tocol-6inEditChat.cshtml:73,83.Per-channel Remove buttons — previously the only destructive action was the header "Remove chat" link, which deletes the whole
Chat. Now each configured channel (Telegram / Slack / Mattermost) has its own Remove button in the footer button group on the right, next to the existing Send-test buttons. Each button clears only its channel; theChatrow, folder bindings, and other channels stay intact.Backend
ChatUpdategainsClearTelegramBinding/ClearSlackWebhook/ClearMattermostWebhooknullable-bool flags. The existing??coalescing inChat.ApplyUpdateswallows nulls (a null update value means "don't change"), so submitting an empty webhook through EditChat cannot clear a saved value today — these explicit flags route around that.Chat.TelegramTypeandChat.AuthorizationTimemove frominittointernal setsoApplyUpdatecan null them on clear (existing object initializers inChatsManager.TryConnectandChatsManagerTestsstill compile viaInternalsVisibleTo).Chat.ToEntity()already gated the Telegram block onTelegramType.HasValue, so the cleared fields drop from the entity on round-trip.NotificationsControllerendpoints (RemoveTelegramBinding/ClearSlackWebhook/ClearMattermostWebhook), each[HttpPost]+[TelegramRoleFilterById(nameof(id), ProductRoleEnum.ProductManager)](same auth asRemoveChat). Routes throughChatsManager.TryUpdateso theUpdatedevent fires forNotificationsCenter/SlackNotificationChannelsubscribers and_telegramChatIdsindex is rekeyed by the existing override.Frontend (
EditChat.cshtml)col-4→col-6).Remove Telegram/Remove Slack/Remove Mattermostbuttons (channel-grouped with their Send-test sibling;btn-outline-dangerto distinguish frombtn-outline-secondary). Each is gated byisTelegramBound/Model.HasSlack/Model.HasMattermostso it only renders for configured channels.removeChannel(channelName, actionUrl, confirmText)JS helper mirrors the existingremoveChat()pattern:showConfirmationModal()→ AJAX POST →window.location.reload().HasMattermostalready existed onChatViewModel.cs:55— issue body was wrong about needing to add it.Test plan
dotnet build src/server/HSMServer/HSMServer.csproj— 0 errors (33 pre-existing warnings)dotnet test --filter ChatsManagerTests— 12/12 pass (8 existing + 4 new):TryUpdate_ClearSlackWebhook_FlagSetsFieldToNullTryUpdate_ClearMattermostWebhook_FlagSetsFieldToNullTryUpdate_ClearTelegramBinding_FlagClearsFieldsAndRekeysIndexChat_AfterClearTelegramBinding_ToEntityOmitsTelegramFieldsHSMServer.Core.Testssuite — 506/508 pass (the 2 failures areAgentInstallerBundleTests.StaticUninstallScript_MatchesGenerated, unrelated to this PR — they fail only when running from isolated-o /tmp/...output becauseRepoFile()can't walk up to the repo root; pass in normal CI)modify_folder_chat.spec.ts— 3 tests (existing 2 + newEditChat: per-channel Remove clears Slack webhook without deleting the chat). CI must confirm.EnableMessagesswitch andMessagesDelayinput align horizontally withName/Description/SlackWebhookUrl/MattermostWebhookUrlOut of scope (noted in issue)
ChannelAccumulatormay still hold buffered alerts.Chat.SlackAccumulator/TelegramAccumulatorreturnnullwhen the channel is cleared (Chat.cs:49-51), so dispatch already skips them; no incorrect delivery. Buffer is memory-only and bounded by the aggregation period. Defer to a follow-up.HasMattermostso it appears only if legacy/test data has the field set; stays hidden in normal use until the channel ships.🤖 Generated with Claude Code