Announcements: target specific teams, rich body, and an in-app toast - #8239
Announcements: target specific teams, rich body, and an in-app toast#8239dimitrieh wants to merge 6 commits into
Conversation
|
Two pre-existing issues found while reviewing this branch. Both predate it and are left untouched here, noting them so they are not lost.
A Cypress selector will break if a fixture grows. |
90405fb to
e916913
Compare
|
@allthedoll can you take a look here? This is a request from sales to help with the migration of pro/starter on FlowFuse Cloud. It builds out the notification mechanism we already have to be able to target specific teams and accept markdown inputs/rich text formatting so we can put in a youtube embed, links to documentation, and/or a button to reach out to sales. Check screenshots in the body of this PR. Happy to discuss this proposal next week. Ideally, need a dev to review this. This can happen any time just before the migration. |
The Notifications Hub could only address whole team types with a plain text message whose only affordance was a card-wide link. Migrating a named set of teams onto a new product needs narrower targeting and a message that can explain itself. - audience: search teams by name and target them explicitly; the team type filter is ignored when specific teams are selected - body: optional markdown, a validated YouTube embed, and a button with its own label and url; the admin form previews exactly what is sent - video and button urls are normalised and validated server side, so the embed src is built from a known-good id rather than admin input - rich announcements render in a card that is not itself a link, because the body owns its links, video and button; plain announcements keep the existing card-wide behaviour - unread announcements also surface as a bottom-right toast, cleared when the notifications drawer is opened Also fixes the billing state audience filter, which only applied when a team type filter was set.
Building an audience of hundreds of teams out of thousands does not work through a search-and-add box: the admin needs to see the teams, filter them, and select in bulk. - the audience is now a filterable, paginated table of teams with row checkboxes, reusing the filters from the admin teams list - batch actions: select every team matching the current filter, select the rows currently loaded, and clear the selection - selection is owned outside the table so it survives searching, filtering and paging; unchecking the header box now clears only the rows on screen instead of the whole selection - new admin route returns the ids of every team matching a filter, so selecting all of them is one request rather than paging the list; it shares its where-clause with the teams list so the two cannot drift - the id list and the addressable audience share one cap, and a truncated select-all says so rather than silently selecting a prefix ff-data-table grows an optional `checked` prop. Left unset it owns its selection exactly as before.
- the toast stack sits to the left of an open right drawer instead of under it, measuring the drawer so it follows a drag-resize and the open and close animations rather than assuming a width - the stack scrolls itself when it is taller than the viewport; two announcements carrying video used to run off the top of the screen - the close button overlays the header instead of taking a grid column, which was reserving a dead strip down the full height of the body - team picker: fifty rows a page, breathing room around the load-more control, and the bulk action reads "Select visible" rather than naming a count that was easy to misread as the whole matching set
The audience was expressed twice on one screen: team type and billing
state as selectors, and the same two again as filters on the team table.
An explicit choice replaces the conflict, and only that mode's controls
render, so the greyed-out checkboxes and the "overridden by" note are
gone. Audience moves to a full-width band directly above the table,
because as a short third column it left a large gap between the choice
and the list it applies to.
Review findings addressed:
- link validation is now shared by the button and the whole-card link,
and rejects targets that only look like an in-app path ('//host' and
'/\host' both resolve off-platform despite the leading slash). The
card link previously reached an href unvalidated, so a plain
announcement could carry a javascript: url
- the controlled selection on ff-data-table compared object identity,
which can never match across a prop round-trip, so the two watchers
bounced updates off each other. It compares contents now
- an uncontrolled table clears its whole selection again when the header
box is unticked, so the existing consumer keeps the behaviour it had
- opening the notifications drawer dismisses every unread announcement,
not just the two on screen, which used to let the next two slide in
over the open drawer
- the toast stack sits below the dialog layer, so a modal and its
backdrop cover it instead of competing with it
- suspended teams are no longer selectable: they are filtered out when
the announcement is addressed, so offering them built a selection that
could never receive anything
- announcements render markdown through a plain renderer; the shared one
emits code-block and table furniture whose styling and copy handler
live only in the Expert chat
- an in-app button routes instead of reloading the whole app
- the video preview checks the host, as the server does, rather than
claiming parity it did not have
- the toast tracks the viewport width instead of reading a
non-reactive value inside a computed
Second review of the previous commit found the hardened link check was still bypassable and the toast layering traded one bug for another. - reject tab, newline and carriage return in a link. The URL parser strips them before parsing, so '/<tab>/host' reached the browser as an off-platform '//host' while passing the in-app path check - validate `to.url` as well. The front-end prefers `to` over `url` and opens it directly, so hardening only `url` left the same sink open under another name - the toast stack stays below the dialog layer, but a drawer wider than the space beside it would have buried it with no way to reach it. Rather than pick a layer to lose to, hold the announcements back while there is no room and show them once the drawer is out of the way - the announcement body checks the links it renders. The admin preview draws whatever has been typed so far, which the server has not seen - match the server's video check exactly, protocol and path included, rather than claiming a parity the preview did not have - enforce every field length the API enforces. An over-long button label used to fail with a bare "Bad Request" and no indication of the field - cap the card link length, as its siblings already were - drop a duplicated suspended clause when filtering active teams
The new admin route and the announcement body properties change the generated OpenAPI types, which CI checks for drift. Generated through the same path CI uses. Running `npm run generate:types` against a local flowforge.local.yml instead boots without the tables feature and drops its routes from the spec, which would have removed valid types rather than adding the new ones.
e916913 to
f5acc27
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8239 +/- ##
==========================================
+ Coverage 75.94% 76.04% +0.09%
==========================================
Files 445 447 +2
Lines 23990 24094 +104
Branches 6404 6443 +39
==========================================
+ Hits 18220 18323 +103
- Misses 5770 5771 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Description
The Notifications Hub could send a plain text announcement to whole team types, with the whole card as its only affordance. Moving a named set of teams onto a new product needs narrower targeting and a message that can explain itself.
Raised as a proposal, so opening as a draft.
Suspended teams are excluded from selection: they are filtered out when an announcement is addressed, so offering them built a selection that could never receive anything.
ff-data-tablegains an optionalcheckedprop. Left unset it owns its selection exactly as before.Context: #8230
Screenshots
Test plan
Verified against a local platform with 63 teams across three team types
Backend, unit tested:
to.urland button url reject anything that can execute or resolve off platform, including targets hiding a host behind a stripped tabBrowser:
Related Issue(s)
None. Raised from a question about informing teams in app when they are moved onto a new product.
Checklist
flowforge.yml?FlowFuse/helmto update ConfigMap TemplateFlowFuse/CloudProjectto update values for Staging/ProductionLabels
area:migrationlabel