From ffd2aa799b18c2ade2c767d0aa96e67d33e8d43e Mon Sep 17 00:00:00 2001 From: Corvo <60719165+brothercorvo@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:10:16 -0300 Subject: [PATCH] Refine EmergencyManagement web UI layout --- TASK.md | 1 + .../EmergencyManagement/webui/src/index.css | 43 +++++++++++++++++++ .../EmergencyActionMessagesPage.tsx | 43 +++++++++++++++---- .../EmergencyActionMessages/MessageForm.tsx | 5 ++- .../EmergencyActionMessages/MessagesTable.tsx | 7 +++ .../EmergencyActionMessagesPage.test.tsx | 2 + .../webui/src/pages/Events/EventForm.tsx | 5 ++- .../webui/src/pages/Events/EventsPage.tsx | 43 +++++++++++++++---- .../webui/src/pages/Events/EventsTable.tsx | 14 +++++- .../Events/__tests__/EventsPage.test.tsx | 2 + 10 files changed, 142 insertions(+), 23 deletions(-) diff --git a/TASK.md b/TASK.md index 4fc2380..939f297 100644 --- a/TASK.md +++ b/TASK.md @@ -80,4 +80,5 @@ - [x] Add HTTP integration tests for the EmergencyManagement web UI message and event flows. - [x] Surface active Reticulum interfaces in the EmergencyManagement gateway startup logs and dashboard. - [x] Upgrade esbuild dependency to version 0.25.0 or later to address the development server request vulnerability. +- [x] Simplify EmergencyManagement web UI tables with a drawer form triggered by a New button. (2025-09-25) diff --git a/examples/EmergencyManagement/webui/src/index.css b/examples/EmergencyManagement/webui/src/index.css index fec21c2..00498f6 100644 --- a/examples/EmergencyManagement/webui/src/index.css +++ b/examples/EmergencyManagement/webui/src/index.css @@ -461,6 +461,13 @@ a { overflow-x: auto; } +.table-card__footer { + display: flex; + justify-content: flex-end; + margin-top: auto; + padding-top: 0.5rem; +} + .table-card table { width: 100%; border-collapse: collapse; @@ -507,6 +514,42 @@ a { color: var(--text-muted); } +.form-drawer { + position: fixed; + inset: 0; + display: flex; + justify-content: flex-end; + align-items: stretch; + z-index: 30; +} + +.form-drawer__backdrop { + position: absolute; + inset: 0; + background: rgba(2, 6, 23, 0.6); + border: none; + padding: 0; + margin: 0; + cursor: pointer; +} + +.form-drawer__panel { + position: relative; + width: min(480px, 100%); + background: linear-gradient(165deg, rgba(12, 24, 45, 0.98) 0%, rgba(11, 22, 39, 0.92) 100%); + border-left: 1px solid rgba(148, 163, 184, 0.18); + box-shadow: -24px 0 60px rgba(8, 15, 35, 0.65); + padding: 2rem 1.75rem; + overflow-y: auto; + z-index: 1; +} + +.form-drawer__panel .form-card { + height: 100%; + max-height: 100%; + margin: 0; +} + .button { border: none; border-radius: 0.75rem; diff --git a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/EmergencyActionMessagesPage.tsx b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/EmergencyActionMessagesPage.tsx index ed7e6e0..1c9266d 100644 --- a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/EmergencyActionMessagesPage.tsx +++ b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/EmergencyActionMessagesPage.tsx @@ -25,6 +25,12 @@ export function EmergencyActionMessagesPage(): JSX.Element { const queryClient = useQueryClient(); const { pushToast } = useToast(); const [editingMessage, setEditingMessage] = useState(null); + const [isFormVisible, setFormVisible] = useState(false); + + const handleCloseForm = useCallback(() => { + setEditingMessage(null); + setFormVisible(false); + }, []); const messagesQuery = useQuery({ queryKey: QUERY_KEY, @@ -48,7 +54,7 @@ export function EmergencyActionMessagesPage(): JSX.Element { return sortMessages([...filtered, created]); }); pushToast({ type: 'success', message: `Created ${created.callsign}.` }); - setEditingMessage(null); + handleCloseForm(); }, onError: (error, variables) => { pushToast({ @@ -76,7 +82,7 @@ export function EmergencyActionMessagesPage(): JSX.Element { ); }); pushToast({ type: 'success', message: `Updated ${updated.callsign}.` }); - setEditingMessage(null); + handleCloseForm(); }, onError: (error, variables) => { pushToast({ @@ -94,7 +100,7 @@ export function EmergencyActionMessagesPage(): JSX.Element { ); pushToast({ type: 'success', message: `Deleted ${callsign}.` }); if (editingMessage?.callsign === callsign) { - setEditingMessage(null); + handleCloseForm(); } }, onError: (error, callsign) => { @@ -118,6 +124,7 @@ export function EmergencyActionMessagesPage(): JSX.Element { const handleEdit = useCallback((message: EmergencyActionMessage) => { setEditingMessage(message); + setFormVisible(true); }, []); const handleDelete = useCallback( @@ -132,6 +139,11 @@ export function EmergencyActionMessagesPage(): JSX.Element { [deleteMutation], ); + const handleCreateNew = useCallback(() => { + setEditingMessage(null); + setFormVisible(true); + }, []); + return (
@@ -145,14 +157,27 @@ export function EmergencyActionMessagesPage(): JSX.Element { isLoading={messagesQuery.isFetching && !messagesQuery.isFetched} onEdit={handleEdit} onDelete={handleDelete} - /> - setEditingMessage(null)} - isSubmitting={createMutation.isPending || updateMutation.isPending} + onCreateNew={handleCreateNew} /> + {isFormVisible && ( +
+
+ )}
); } diff --git a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessageForm.tsx b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessageForm.tsx index 8989f47..6b62a50 100644 --- a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessageForm.tsx +++ b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessageForm.tsx @@ -71,6 +71,7 @@ export function MessageForm({ }, [initialValue]); const isEditing = useMemo(() => Boolean(initialValue?.callsign), [initialValue]); + const dismissLabel = isEditing ? 'Cancel edit' : 'Close'; const statusFields: Array = useMemo( () => [ @@ -130,14 +131,14 @@ export function MessageForm({

{isEditing ? `Update ${state.values.callsign}` : 'Create new message'}

Capture an updated readiness snapshot for each callsign.

- {isEditing && ( + {onCancelEdit && ( )} diff --git a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessagesTable.tsx b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessagesTable.tsx index b2c5674..d26d5be 100644 --- a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessagesTable.tsx +++ b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/MessagesTable.tsx @@ -7,6 +7,7 @@ export interface MessagesTableProps { isLoading: boolean; onEdit: (message: EmergencyActionMessage) => void; onDelete: (message: EmergencyActionMessage) => void; + onCreateNew: () => void; } export function MessagesTable({ @@ -14,6 +15,7 @@ export function MessagesTable({ isLoading, onEdit, onDelete, + onCreateNew, }: MessagesTableProps): JSX.Element { return (
@@ -99,6 +101,11 @@ export function MessagesTable({ )}
+
+ +
); } diff --git a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/__tests__/EmergencyActionMessagesPage.test.tsx b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/__tests__/EmergencyActionMessagesPage.test.tsx index 7a4f45b..341d378 100644 --- a/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/__tests__/EmergencyActionMessagesPage.test.tsx +++ b/examples/EmergencyManagement/webui/src/pages/EmergencyActionMessages/__tests__/EmergencyActionMessagesPage.test.tsx @@ -89,6 +89,7 @@ describe('EmergencyActionMessagesPage', () => { }); const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: 'New' })); await user.type(screen.getByLabelText('Callsign'), 'Charlie-3'); await user.type(screen.getByLabelText('Group name'), 'Charlie'); @@ -124,6 +125,7 @@ describe('EmergencyActionMessagesPage', () => { renderPage(); const user = userEvent.setup(); + await user.click(await screen.findByRole('button', { name: 'New' })); await user.click(await screen.findByRole('button', { name: 'Create message' })); expect(await screen.findByText('Callsign is required.')).toBeInTheDocument(); diff --git a/examples/EmergencyManagement/webui/src/pages/Events/EventForm.tsx b/examples/EmergencyManagement/webui/src/pages/Events/EventForm.tsx index db2c6dd..7f2e592 100644 --- a/examples/EmergencyManagement/webui/src/pages/Events/EventForm.tsx +++ b/examples/EmergencyManagement/webui/src/pages/Events/EventForm.tsx @@ -166,6 +166,7 @@ export function EventForm({ }, [initialValue]); const isEditing = useMemo(() => Boolean(initialValue), [initialValue]); + const dismissLabel = isEditing ? 'Cancel edit' : 'Close'; const accessOptions = useMemo(() => { const unique = new Set(DEFAULT_ACCESS_OPTIONS); if (initialValue?.access) { @@ -283,14 +284,14 @@ export function EventForm({

{isEditing ? `Update event ${state.uid}` : 'Log new event'}

Track incident reports, dispatches, and situational notes.

- {isEditing && ( + {onCancelEdit && ( )} diff --git a/examples/EmergencyManagement/webui/src/pages/Events/EventsPage.tsx b/examples/EmergencyManagement/webui/src/pages/Events/EventsPage.tsx index 2980ea8..1030618 100644 --- a/examples/EmergencyManagement/webui/src/pages/Events/EventsPage.tsx +++ b/examples/EmergencyManagement/webui/src/pages/Events/EventsPage.tsx @@ -21,6 +21,12 @@ export function EventsPage(): JSX.Element { const queryClient = useQueryClient(); const { pushToast } = useToast(); const [editingEvent, setEditingEvent] = useState(null); + const [isFormVisible, setFormVisible] = useState(false); + + const handleCloseForm = useCallback(() => { + setEditingEvent(null); + setFormVisible(false); + }, []); const eventsQuery = useQuery({ queryKey: QUERY_KEY, @@ -46,7 +52,7 @@ export function EventsPage(): JSX.Element { return [...filtered, created].sort((a, b) => a.uid - b.uid); }); pushToast({ type: 'success', message: `Created event ${created.uid}.` }); - setEditingEvent(null); + handleCloseForm(); }, onError: (error, variables) => { pushToast({ @@ -74,7 +80,7 @@ export function EventsPage(): JSX.Element { .sort((a, b) => a.uid - b.uid); }); pushToast({ type: 'success', message: `Updated event ${updated.uid}.` }); - setEditingEvent(null); + handleCloseForm(); }, onError: (error, variables) => { pushToast({ @@ -92,7 +98,7 @@ export function EventsPage(): JSX.Element { ); pushToast({ type: 'success', message: `Deleted event ${uid}.` }); if (editingEvent?.uid === Number(uid)) { - setEditingEvent(null); + handleCloseForm(); } }, onError: (error, uid) => { @@ -116,6 +122,7 @@ export function EventsPage(): JSX.Element { const handleEdit = useCallback((event: EventRecord) => { setEditingEvent(event); + setFormVisible(true); }, []); const handleDelete = useCallback( @@ -128,6 +135,11 @@ export function EventsPage(): JSX.Element { [deleteMutation], ); + const handleCreateNew = useCallback(() => { + setEditingEvent(null); + setFormVisible(true); + }, []); + return (
@@ -141,14 +153,27 @@ export function EventsPage(): JSX.Element { isLoading={eventsQuery.isFetching && !eventsQuery.isFetched} onEdit={handleEdit} onDelete={handleDelete} - /> - setEditingEvent(null)} - isSubmitting={createMutation.isPending || updateMutation.isPending} + onCreateNew={handleCreateNew} /> + {isFormVisible && ( +
+
+ )}
); } diff --git a/examples/EmergencyManagement/webui/src/pages/Events/EventsTable.tsx b/examples/EmergencyManagement/webui/src/pages/Events/EventsTable.tsx index 275b65b..9268663 100644 --- a/examples/EmergencyManagement/webui/src/pages/Events/EventsTable.tsx +++ b/examples/EmergencyManagement/webui/src/pages/Events/EventsTable.tsx @@ -5,6 +5,7 @@ export interface EventsTableProps { isLoading: boolean; onEdit: (event: EventRecord) => void; onDelete: (event: EventRecord) => void; + onCreateNew: () => void; } interface StatusEntry { @@ -55,7 +56,13 @@ function renderDetail(detail: EventRecord['detail']): JSX.Element { ); } -export function EventsTable({ events, isLoading, onEdit, onDelete }: EventsTableProps): JSX.Element { +export function EventsTable({ + events, + isLoading, + onEdit, + onDelete, + onCreateNew, +}: EventsTableProps): JSX.Element { return (
@@ -112,6 +119,11 @@ export function EventsTable({ events, isLoading, onEdit, onDelete }: EventsTable )}
+ ); } diff --git a/examples/EmergencyManagement/webui/src/pages/Events/__tests__/EventsPage.test.tsx b/examples/EmergencyManagement/webui/src/pages/Events/__tests__/EventsPage.test.tsx index bf0afcd..97dc8fb 100644 --- a/examples/EmergencyManagement/webui/src/pages/Events/__tests__/EventsPage.test.tsx +++ b/examples/EmergencyManagement/webui/src/pages/Events/__tests__/EventsPage.test.tsx @@ -109,6 +109,7 @@ describe('EventsPage', () => { }); const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: 'New' })); await user.type(screen.getByLabelText('UID'), '2'); await user.type(screen.getByLabelText('Type'), 'New'); await user.type(screen.getByLabelText('Start'), '2025-09-19T10:30'); @@ -182,6 +183,7 @@ describe('EventsPage', () => { renderPage(); const user = userEvent.setup(); + await user.click(await screen.findByRole('button', { name: 'New' })); await user.click(await screen.findByRole('button', { name: 'Create event' })); expect(await screen.findByText('UID must be a valid number.')).toBeInTheDocument();