From 7437d8dafad66a1ed44b4697caccb7fc888f13cd Mon Sep 17 00:00:00 2001 From: Pierre Bazoge Date: Thu, 12 Nov 2026 17:11:17 +0000 Subject: [PATCH 1/6] feat(console): save bar, full drawer and feed test options for the broadcasts screens SaveBar takes a save label, a disabled state with its reason, and can leave navigation unblocked. FullDrawer takes a title in its bar. The feed Test buttons explain why they are disabled before the broadcast is saved. A table row can show the hover background while its detail is open. --- .../broadcasts/FeedSettings.test.tsx | 35 ++++++++++++++++ .../broadcasts/GlobalFeedSettings.tsx | 23 ++++++---- .../broadcasts/RecipientFeedSettings.tsx | 21 ++++++---- .../src/components/layout/FullDrawer.test.tsx | 6 +++ console/src/components/layout/FullDrawer.tsx | 10 ++++- .../src/components/layout/SaveBar.test.tsx | 20 +++++++++ console/src/components/layout/SaveBar.tsx | 42 +++++++++++++++---- console/src/theme/antd-overrides.css | 8 ++++ 8 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 console/src/components/broadcasts/FeedSettings.test.tsx diff --git a/console/src/components/broadcasts/FeedSettings.test.tsx b/console/src/components/broadcasts/FeedSettings.test.tsx new file mode 100644 index 000000000..ca7c883fb --- /dev/null +++ b/console/src/components/broadcasts/FeedSettings.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { App as AntApp } from 'antd' +import type { ReactNode } from 'react' +import { GlobalFeedSettings } from './GlobalFeedSettings' +import { RecipientFeedSettings } from './RecipientFeedSettings' + +const wrap = (node: ReactNode) => + render( + + {node} + + ) + +const feed = { enabled: true, url: 'https://api.example.com/feed', headers: [] } + +describe.each([ + ['GlobalFeedSettings', (id?: string) => ], + ['RecipientFeedSettings', (id?: string) => ] +])('%s test button', (_name, element) => { + it('is disabled until the broadcast is saved, and says so', async () => { + wrap(element()) + const button = screen.getByRole('button', { name: 'Test' }) + expect(button).toBeDisabled() + fireEvent.mouseEnter(button.parentElement as HTMLElement) + fireEvent.mouseOver(button) + expect(await screen.findByText('Save the broadcast first')).toBeInTheDocument() + }) + + it('is enabled once the broadcast exists', () => { + wrap(element('bc1')) + expect(screen.getByRole('button', { name: 'Test' })).toBeEnabled() + }) +}) diff --git a/console/src/components/broadcasts/GlobalFeedSettings.tsx b/console/src/components/broadcasts/GlobalFeedSettings.tsx index dad0fa716..8559309b5 100644 --- a/console/src/components/broadcasts/GlobalFeedSettings.tsx +++ b/console/src/components/broadcasts/GlobalFeedSettings.tsx @@ -112,15 +112,20 @@ export function GlobalFeedSettings({ onChange={(e) => handleChange('url', e.target.value)} disabled={disabled} /> - + {/* The test calls the feed for a stored broadcast; a disabled button fires no mouse event, hence the span. */} + + + + + diff --git a/console/src/components/broadcasts/RecipientFeedSettings.tsx b/console/src/components/broadcasts/RecipientFeedSettings.tsx index c50d2513d..d440c8ec7 100644 --- a/console/src/components/broadcasts/RecipientFeedSettings.tsx +++ b/console/src/components/broadcasts/RecipientFeedSettings.tsx @@ -121,14 +121,19 @@ export function RecipientFeedSettings({ onChange={(e) => handleChange('url', e.target.value)} disabled={disabled} /> - + {/* The test calls the feed for a stored broadcast; a disabled button fires no mouse event, hence the span. */} + + + + + diff --git a/console/src/components/layout/FullDrawer.test.tsx b/console/src/components/layout/FullDrawer.test.tsx index c27a577ca..1ca7d515f 100644 --- a/console/src/components/layout/FullDrawer.test.tsx +++ b/console/src/components/layout/FullDrawer.test.tsx @@ -67,6 +67,12 @@ describe('FullDrawer', () => { expect(screen.getByText('Pro plan')).toBeInTheDocument() }) + it('puts a bar title in the bar, after the back button', () => { + renderDrawer({ barTitle: November news }) + const bar = document.querySelector('[data-drawer-bar]')! + expect(bar).toContainElement(screen.getByText('November news')) + }) + it('draws no previous or next without nav', () => { renderDrawer() expect(screen.queryByRole('button', { name: 'Previous' })).toBeNull() diff --git a/console/src/components/layout/FullDrawer.tsx b/console/src/components/layout/FullDrawer.tsx index 6670c0fe0..faa013cea 100644 --- a/console/src/components/layout/FullDrawer.tsx +++ b/console/src/components/layout/FullDrawer.tsx @@ -13,6 +13,8 @@ export interface FullDrawerProps { nav?: DrawerNav /** A copy-link button (default). */ copyLink?: boolean + /** Left side of the bar, after the back button: an editable name, a status. */ + barTitle?: ReactNode /** Right side of the bar, before copy and close. */ topActions?: ReactNode children?: ReactNode @@ -22,7 +24,7 @@ export interface FullDrawerProps { * A full-width drawer over the whole console, sidebar included, with a 40px navigation bar. * Escape closes it (antd). Its content sits on the canvas (antd-overrides.css, `.app-full-drawer`). */ -export function FullDrawer({ open, onClose, backLabel, nav, copyLink = true, topActions, children }: FullDrawerProps) { +export function FullDrawer({ open, onClose, backLabel, nav, copyLink = true, barTitle, topActions, children }: FullDrawerProps) { const { t } = useLingui() const barRef = useRef(null) useDrawerNavKeys({ enabled: open, onPrev: nav?.onPrev, onNext: nav?.onNext, anchorRef: barRef }) @@ -53,6 +55,12 @@ export function FullDrawer({ open, onClose, backLabel, nav, copyLink = true, top ) : null} + {barTitle ? ( + <> +