From 67f20829f7f4ba52fd79dbba668c90b3bccecff8 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 09:57:41 +0200 Subject: [PATCH] refactor(admin): share the dev notification tester between desktop and phone Both panels loaded the same trips and users, built the same twelve test payloads and sent them the same way. That now lives in useDevNotifications; each panel keeps its own markup and the phone keeps its own wording of the failure toast. --- .../Admin/DevNotificationsPanel.tsx | 154 ++----------- .../Admin/useDevNotifications.test.ts | 144 ++++++++++++ .../components/Admin/useDevNotifications.ts | 208 ++++++++++++++++++ .../admin/MAdminDevNotificationsPanel.tsx | 156 ++----------- 4 files changed, 396 insertions(+), 266 deletions(-) create mode 100644 client/src/components/Admin/useDevNotifications.test.ts create mode 100644 client/src/components/Admin/useDevNotifications.ts diff --git a/client/src/components/Admin/DevNotificationsPanel.tsx b/client/src/components/Admin/DevNotificationsPanel.tsx index 61d967651..7e44749bf 100644 --- a/client/src/components/Admin/DevNotificationsPanel.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.tsx @@ -1,27 +1,13 @@ -import React, { useState, useEffect, useId } from 'react' -import { adminApi, tripsApi } from '../../api/client' -import { getApiErrorMessage } from '../../utils/apiError' -import { useAuthStore } from '../../store/authStore' -import { useToast } from '../shared/Toast' +import React, { useId } from 'react' import { fs } from '../shared/DialogShell' import { EditorField, INPUT } from '../shared/dialogParts' import { SettingsCard, StatusPill } from '../Settings/settingsKit' +import { useDevNotifications } from './useDevNotifications' import { Bell, Zap, CheckCircle, Navigation, User, Calendar, Clock, Image, MessageSquare, Tag, UserPlus, Download, MapPin, ChevronDown, FlaskConical, Loader2, Map as MapIcon, ShieldCheck, } from 'lucide-react' -interface Trip { - id: number - title: string -} - -interface AppUser { - id: number - username: string - email: string -} - /** A native select in the editors' box look, with the chevron the CustomSelect draws. */ const SELECT = `${INPUT} appearance-none pe-9` @@ -40,44 +26,11 @@ function ButtonGrid({ children }: { children: React.ReactNode }): React.ReactEle } export default function DevNotificationsPanel(): React.ReactElement { - const toast = useToast() const ids = useId() - const user = useAuthStore(s => s.user) - const [sending, setSending] = useState(null) - const [trips, setTrips] = useState([]) - const [selectedTripId, setSelectedTripId] = useState(null) - const [users, setUsers] = useState([]) - const [selectedUserId, setSelectedUserId] = useState(null) - - useEffect(() => { - tripsApi.list().then(data => { - const list = (data.trips || data || []) as Trip[] - setTrips(list) - if (list.length > 0) setSelectedTripId(list[0].id) - }).catch(() => {}) - adminApi.users().then(data => { - const list = (data.users || data || []) as AppUser[] - setUsers(list) - if (list.length > 0) setSelectedUserId(list[0].id) - }).catch(() => {}) - }, []) - - const fire = async (label: string, payload: Record) => { - setSending(label) - try { - await adminApi.sendTestNotification(payload) - toast.success(`Sent: ${label}`) - } catch (err: unknown) { - toast.error(getApiErrorMessage(err, 'Failed')) - } finally { - setSending(null) - } - } - - const selectedTrip = trips.find(t => t.id === selectedTripId) - const selectedUser = users.find(u => u.id === selectedUserId) - const username = user?.username || 'Admin' - const tripTitle = selectedTrip?.title || 'Test Trip' + const { + sending, trips, selectedTripId, setSelectedTripId, users, selectedUserId, setSelectedUserId, + tripInviteId, vacayInviteId, send, + } = useDevNotifications() // ── Helpers ────────────────────────────────────────────────────────────── @@ -114,41 +67,16 @@ export default function DevNotificationsPanel(): React.ReactElement { > fire('simple-me', { - event: 'test_simple', - scope: 'user', - targetId: user?.id, - params: {}, - })} + onClick={send.simpleMe} /> fire('boolean-me', { - event: 'test_boolean', - scope: 'user', - targetId: user?.id, - params: {}, - inApp: { - type: 'boolean', - positiveCallback: { action: 'test_approve', payload: {} }, - negativeCallback: { action: 'test_deny', payload: {} }, - }, - })} + onClick={send.booleanMe} /> fire('navigate-me', { - event: 'test_navigate', - scope: 'user', - targetId: user?.id, - params: {}, - })} + onClick={send.navigateMe} /> fire('simple-admins', { - event: 'test_simple', - scope: 'admin', - targetId: 0, - params: {}, - })} + onClick={send.simpleAdmins} /> @@ -167,46 +95,19 @@ export default function DevNotificationsPanel(): React.ReactElement { selectedTripId && fire('booking_change', { - event: 'booking_change', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, booking: 'Test Hotel', type: 'hotel', tripId: String(selectedTripId) }, - })} + onClick={send.bookingChange} /> selectedTripId && fire('trip_reminder', { - event: 'trip_reminder', - scope: 'trip', - targetId: selectedTripId, - params: { trip: tripTitle, tripId: String(selectedTripId) }, - })} + onClick={send.tripReminder} /> selectedTripId && fire('photos_shared', { - event: 'photos_shared', - scope: 'trip', - targetId: selectedTripId, - // A string on purpose: notification params are Record end to end, the server - // sends count: String(added), and resolveTemplate reads a numeric string as the plural count. - params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) }, - })} + onClick={send.photosShared} /> selectedTripId && fire('collab_message', { - event: 'collab_message', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, preview: 'This is a test message preview.', tripId: String(selectedTripId) }, - })} + onClick={send.collabMessage} /> selectedTripId && fire('packing_tagged', { - event: 'packing_tagged', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, category: 'Clothing', tripId: String(selectedTripId) }, - })} + onClick={send.packingTagged} /> @@ -226,28 +127,18 @@ export default function DevNotificationsPanel(): React.ReactElement { selectedUserId && fire(`trip_invite-${selectedUserId}`, { - event: 'trip_invite', - scope: 'user', - targetId: selectedUserId, - params: { actor: username, trip: tripTitle, invitee: selectedUser?.email || '', tripId: String(selectedTripId ?? 0) }, - })} + onClick={send.tripInvite} /> selectedUserId && fire(`vacay_invite-${selectedUserId}`, { - event: 'vacay_invite', - scope: 'user', - targetId: selectedUserId, - params: { actor: username, planId: '1' }, - })} + onClick={send.vacayInvite} /> @@ -257,12 +148,7 @@ export default function DevNotificationsPanel(): React.ReactElement { fire('version_available', { - event: 'version_available', - scope: 'admin', - targetId: 0, - params: { version: '9.9.9-test' }, - })} + onClick={send.versionAvailable} /> diff --git a/client/src/components/Admin/useDevNotifications.test.ts b/client/src/components/Admin/useDevNotifications.test.ts new file mode 100644 index 000000000..1a2affe4c --- /dev/null +++ b/client/src/components/Admin/useDevNotifications.test.ts @@ -0,0 +1,144 @@ +// FE-COMP-DEVNOTIF-HOOK-001 to -008: the shared dev notification tester behind both admin shells. +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { buildUser } from '../../../tests/helpers/factories'; +import { resetAllStores, seedStore } from '../../../tests/helpers/store'; +import { adminApi, tripsApi } from '../../api/client'; +import { useAuthStore } from '../../store/authStore'; +import { useDevNotifications } from './useDevNotifications'; + +const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn() })); +vi.mock('../shared/Toast', () => ({ useToast: () => toast })); + +const ME = buildUser({ id: 7, username: 'tester' }); +const TRIPS = [ + { id: 11, title: 'Paris' }, + { id: 12, title: 'Tokyo' }, +]; +const USERS = [ + { id: 21, username: 'alice', email: 'alice@example.com' }, + { id: 22, username: 'bob', email: 'bob@example.com' }, +]; + +beforeEach(() => { + resetAllStores(); + seedStore(useAuthStore, { user: ME, isAuthenticated: true }); + vi.spyOn(tripsApi, 'list').mockResolvedValue({ trips: TRIPS }); + vi.spyOn(adminApi, 'users').mockResolvedValue({ users: USERS }); + vi.spyOn(adminApi, 'sendTestNotification').mockResolvedValue({}); + toast.success.mockReset(); + toast.error.mockReset(); +}); +afterEach(() => vi.restoreAllMocks()); + +async function loaded(options?: Parameters[0]) { + const hook = renderHook(() => useDevNotifications(options)); + await waitFor(() => expect(hook.result.current.selectedUserId).toBe(21)); + await waitFor(() => expect(hook.result.current.selectedTripId).toBe(11)); + return hook; +} + +describe('useDevNotifications', () => { + it('FE-COMP-DEVNOTIF-HOOK-001: loads trips and users and preselects the first of each', async () => { + const { result } = await loaded(); + expect(result.current.trips).toEqual(TRIPS); + expect(result.current.users).toEqual(USERS); + expect(result.current.tripInviteId).toBe('trip_invite-21'); + expect(result.current.vacayInviteId).toBe('vacay_invite-21'); + }); + + it('FE-COMP-DEVNOTIF-HOOK-002: a failed load leaves the lists empty and nothing selected', async () => { + vi.mocked(tripsApi.list).mockRejectedValue(new Error('down')); + vi.mocked(adminApi.users).mockRejectedValue(new Error('down')); + const { result } = renderHook(() => useDevNotifications()); + await waitFor(() => expect(adminApi.users).toHaveBeenCalled()); + expect(result.current.trips).toEqual([]); + expect(result.current.selectedTripId).toBeNull(); + expect(result.current.selectedUserId).toBeNull(); + }); + + it('FE-COMP-DEVNOTIF-HOOK-003: a self-addressed send targets the signed-in user and toasts the label', async () => { + const { result } = await loaded(); + await act(async () => { + await result.current.send.booleanMe(); + }); + expect(adminApi.sendTestNotification).toHaveBeenCalledWith({ + event: 'test_boolean', + scope: 'user', + targetId: 7, + params: {}, + inApp: { + type: 'boolean', + positiveCallback: { action: 'test_approve', payload: {} }, + negativeCallback: { action: 'test_deny', payload: {} }, + }, + }); + expect(toast.success).toHaveBeenCalledWith('Sent: boolean-me'); + expect(result.current.sending).toBeNull(); + }); + + it('FE-COMP-DEVNOTIF-HOOK-004: trip events carry the selected trip, its title and the actor', async () => { + const { result } = await loaded(); + act(() => result.current.setSelectedTripId(12)); + await act(async () => { + await result.current.send.photosShared(); + }); + expect(adminApi.sendTestNotification).toHaveBeenCalledWith({ + event: 'photos_shared', + scope: 'trip', + targetId: 12, + params: { actor: 'tester', trip: 'Tokyo', count: '5', tripId: '12' }, + }); + }); + + it('FE-COMP-DEVNOTIF-HOOK-005: user events go to the selected recipient under an id naming them', async () => { + const { result } = await loaded(); + act(() => result.current.setSelectedUserId(22)); + await act(async () => { + await result.current.send.tripInvite(); + }); + expect(adminApi.sendTestNotification).toHaveBeenCalledWith({ + event: 'trip_invite', + scope: 'user', + targetId: 22, + params: { actor: 'tester', trip: 'Paris', invitee: 'bob@example.com', tripId: '11' }, + }); + expect(toast.success).toHaveBeenCalledWith('Sent: trip_invite-22'); + }); + + it('FE-COMP-DEVNOTIF-HOOK-006: trip and user events do nothing while nothing is selected', async () => { + vi.mocked(tripsApi.list).mockResolvedValue({ trips: [] }); + vi.mocked(adminApi.users).mockResolvedValue({ users: [] }); + const { result } = renderHook(() => useDevNotifications()); + await waitFor(() => expect(adminApi.users).toHaveBeenCalled()); + await act(async () => { + await result.current.send.bookingChange(); + await result.current.send.vacayInvite(); + }); + expect(adminApi.sendTestNotification).not.toHaveBeenCalled(); + }); + + it('FE-COMP-DEVNOTIF-HOOK-007: by default a failure toasts the server error field, else Failed', async () => { + const { result } = await loaded(); + vi.mocked(adminApi.sendTestNotification).mockRejectedValueOnce({ response: { data: { error: 'No channel' } } }); + await act(async () => { + await result.current.send.versionAvailable(); + }); + expect(toast.error).toHaveBeenLastCalledWith('No channel'); + vi.mocked(adminApi.sendTestNotification).mockRejectedValueOnce(new Error('boom')); + await act(async () => { + await result.current.send.versionAvailable(); + }); + expect(toast.error).toHaveBeenLastCalledWith('Failed'); + expect(result.current.sending).toBeNull(); + }); + + it('FE-COMP-DEVNOTIF-HOOK-008: a shell can word the failure toast itself', async () => { + const { result } = await loaded({ errorMessage: (err) => (err instanceof Error ? err.message : 'Failed') }); + vi.mocked(adminApi.sendTestNotification).mockRejectedValueOnce(new Error('boom')); + await act(async () => { + await result.current.send.simpleAdmins(); + }); + expect(toast.error).toHaveBeenCalledWith('boom'); + }); +}); diff --git a/client/src/components/Admin/useDevNotifications.ts b/client/src/components/Admin/useDevNotifications.ts new file mode 100644 index 000000000..1230660c0 --- /dev/null +++ b/client/src/components/Admin/useDevNotifications.ts @@ -0,0 +1,208 @@ +import { useEffect, useState } from 'react'; + +import { adminApi, tripsApi } from '../../api/client'; +import { useAuthStore } from '../../store/authStore'; +import { getApiErrorMessage } from '../../utils/apiError'; +import { useToast } from '../shared/Toast'; + +interface DevTrip { + id: number; + title: string; +} + +interface DevAppUser { + id: number; + username: string; + email: string; +} + +interface UseDevNotificationsOptions { + /** The error toast text for a failed send. Defaults to the server's `error` field, else 'Failed'. */ + errorMessage?: (err: unknown) => string; +} + +const serverErrorMessage = (err: unknown) => getApiErrorMessage(err, 'Failed'); + +/** + * The dev-only notification tester behind both admin shells (the desktop panel and the + * phone panel render their own buttons over it). Loads the trips and users to pick a + * target from, and builds and sends each test event. Every handler is named after the + * button that calls it; `sending` holds the id of the send in flight. + */ +export function useDevNotifications({ errorMessage = serverErrorMessage }: UseDevNotificationsOptions = {}) { + const toast = useToast(); + const user = useAuthStore((s) => s.user); + const [sending, setSending] = useState(null); + const [trips, setTrips] = useState([]); + const [selectedTripId, setSelectedTripId] = useState(null); + const [users, setUsers] = useState([]); + const [selectedUserId, setSelectedUserId] = useState(null); + + useEffect(() => { + tripsApi + .list() + .then((data) => { + const list = (data.trips || data || []) as DevTrip[]; + setTrips(list); + if (list.length > 0) setSelectedTripId(list[0].id); + }) + .catch(() => {}); + adminApi + .users() + .then((data) => { + const list = (data.users || data || []) as DevAppUser[]; + setUsers(list); + if (list.length > 0) setSelectedUserId(list[0].id); + }) + .catch(() => {}); + }, []); + + const fire = async (label: string, payload: Record) => { + setSending(label); + try { + await adminApi.sendTestNotification(payload); + toast.success(`Sent: ${label}`); + } catch (err: unknown) { + toast.error(errorMessage(err)); + } finally { + setSending(null); + } + }; + + const selectedTrip = trips.find((t) => t.id === selectedTripId); + const selectedUser = users.find((u) => u.id === selectedUserId); + const username = user?.username || 'Admin'; + const tripTitle = selectedTrip?.title || 'Test Trip'; + const tripInviteId = `trip_invite-${selectedUserId}`; + const vacayInviteId = `vacay_invite-${selectedUserId}`; + + const send = { + simpleMe: () => + fire('simple-me', { + event: 'test_simple', + scope: 'user', + targetId: user?.id, + params: {}, + }), + booleanMe: () => + fire('boolean-me', { + event: 'test_boolean', + scope: 'user', + targetId: user?.id, + params: {}, + inApp: { + type: 'boolean', + positiveCallback: { action: 'test_approve', payload: {} }, + negativeCallback: { action: 'test_deny', payload: {} }, + }, + }), + navigateMe: () => + fire('navigate-me', { + event: 'test_navigate', + scope: 'user', + targetId: user?.id, + params: {}, + }), + simpleAdmins: () => + fire('simple-admins', { + event: 'test_simple', + scope: 'admin', + targetId: 0, + params: {}, + }), + bookingChange: () => + selectedTripId && + fire('booking_change', { + event: 'booking_change', + scope: 'trip', + targetId: selectedTripId, + params: { + actor: username, + trip: tripTitle, + booking: 'Test Hotel', + type: 'hotel', + tripId: String(selectedTripId), + }, + }), + tripReminder: () => + selectedTripId && + fire('trip_reminder', { + event: 'trip_reminder', + scope: 'trip', + targetId: selectedTripId, + params: { trip: tripTitle, tripId: String(selectedTripId) }, + }), + photosShared: () => + selectedTripId && + fire('photos_shared', { + event: 'photos_shared', + scope: 'trip', + targetId: selectedTripId, + // A string on purpose: notification params are Record end to end, the server + // sends count: String(added), and resolveTemplate reads a numeric string as the plural count. + params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) }, + }), + collabMessage: () => + selectedTripId && + fire('collab_message', { + event: 'collab_message', + scope: 'trip', + targetId: selectedTripId, + params: { + actor: username, + trip: tripTitle, + preview: 'This is a test message preview.', + tripId: String(selectedTripId), + }, + }), + packingTagged: () => + selectedTripId && + fire('packing_tagged', { + event: 'packing_tagged', + scope: 'trip', + targetId: selectedTripId, + params: { actor: username, trip: tripTitle, category: 'Clothing', tripId: String(selectedTripId) }, + }), + tripInvite: () => + selectedUserId && + fire(tripInviteId, { + event: 'trip_invite', + scope: 'user', + targetId: selectedUserId, + params: { + actor: username, + trip: tripTitle, + invitee: selectedUser?.email || '', + tripId: String(selectedTripId ?? 0), + }, + }), + vacayInvite: () => + selectedUserId && + fire(vacayInviteId, { + event: 'vacay_invite', + scope: 'user', + targetId: selectedUserId, + params: { actor: username, planId: '1' }, + }), + versionAvailable: () => + fire('version_available', { + event: 'version_available', + scope: 'admin', + targetId: 0, + params: { version: '9.9.9-test' }, + }), + }; + + return { + sending, + trips, + selectedTripId, + setSelectedTripId, + users, + selectedUserId, + setSelectedUserId, + tripInviteId, + vacayInviteId, + send, + }; +} diff --git a/client/src/mobile/screens/admin/MAdminDevNotificationsPanel.tsx b/client/src/mobile/screens/admin/MAdminDevNotificationsPanel.tsx index 8e897fabb..ae55f49c5 100644 --- a/client/src/mobile/screens/admin/MAdminDevNotificationsPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminDevNotificationsPanel.tsx @@ -1,24 +1,14 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { Bell, Zap, CheckCircle, Navigation, Calendar, Clock, Image, MessageSquare, Tag, UserPlus, Download, MapPin, Loader2, } from 'lucide-react' import type { LucideIcon } from 'lucide-react' -import { adminApi, tripsApi } from '../../../api/client' -import { useAuthStore } from '../../../store/authStore' -import { useToast } from '../../../components/shared/Toast' +import { useDevNotifications } from '../../../components/Admin/useDevNotifications' import { MAdminCard, MAdminCardHead } from './MAdminUi' -interface Trip { - id: number - title: string -} - -interface AppUser { - id: number - username: string - email: string -} +/** The phone panel toasts the thrown error's own message rather than the server's error field. */ +const errorMessage = (err: unknown) => (err instanceof Error ? err.message : 'Failed') const SELECT_CLASS = 'mb-2 h-[42px] w-full rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-3 text-[0.84375rem] text-m-ink outline-none focus:border-[color:var(--m-faint)]' @@ -57,46 +47,13 @@ function Btn({ } // Dev-only notification testing panel, re-skinned to the mobile admin system. -// All state, fetches and fire() payloads are ported verbatim from the desktop -// DevNotificationsPanel — only the presentation layer changes. +// All state, fetches and payloads come from useDevNotifications, shared +// with the desktop DevNotificationsPanel; only the presentation layer differs. export default function MAdminDevNotificationsPanel(): React.ReactElement { - const toast = useToast() - const user = useAuthStore(s => s.user) - const [sending, setSending] = useState(null) - const [trips, setTrips] = useState([]) - const [selectedTripId, setSelectedTripId] = useState(null) - const [users, setUsers] = useState([]) - const [selectedUserId, setSelectedUserId] = useState(null) - - useEffect(() => { - tripsApi.list().then(data => { - const list = (data.trips || data || []) as Trip[] - setTrips(list) - if (list.length > 0) setSelectedTripId(list[0].id) - }).catch(() => {}) - adminApi.users().then(data => { - const list = (data.users || data || []) as AppUser[] - setUsers(list) - if (list.length > 0) setSelectedUserId(list[0].id) - }).catch(() => {}) - }, []) - - const fire = async (label: string, payload: Record) => { - setSending(label) - try { - await adminApi.sendTestNotification(payload) - toast.success(`Sent: ${label}`) - } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : 'Failed') - } finally { - setSending(null) - } - } - - const selectedTrip = trips.find(t => t.id === selectedTripId) - const selectedUser = users.find(u => u.id === selectedUserId) - const username = user?.username || 'Admin' - const tripTitle = selectedTrip?.title || 'Test Trip' + const { + sending, trips, selectedTripId, setSelectedTripId, users, selectedUserId, setSelectedUserId, + tripInviteId, vacayInviteId, send, + } = useDevNotifications({ errorMessage }) return (
@@ -118,41 +75,16 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { />
fire('simple-me', { - event: 'test_simple', - scope: 'user', - targetId: user?.id, - params: {}, - })} + onClick={send.simpleMe} /> fire('boolean-me', { - event: 'test_boolean', - scope: 'user', - targetId: user?.id, - params: {}, - inApp: { - type: 'boolean', - positiveCallback: { action: 'test_approve', payload: {} }, - negativeCallback: { action: 'test_deny', payload: {} }, - }, - })} + onClick={send.booleanMe} /> fire('navigate-me', { - event: 'test_navigate', - scope: 'user', - targetId: user?.id, - params: {}, - })} + onClick={send.navigateMe} /> fire('simple-admins', { - event: 'test_simple', - scope: 'admin', - targetId: 0, - params: {}, - })} + onClick={send.simpleAdmins} />
@@ -174,44 +106,19 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
selectedTripId && fire('booking_change', { - event: 'booking_change', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, booking: 'Test Hotel', type: 'hotel', tripId: String(selectedTripId) }, - })} + onClick={send.bookingChange} /> selectedTripId && fire('trip_reminder', { - event: 'trip_reminder', - scope: 'trip', - targetId: selectedTripId, - params: { trip: tripTitle, tripId: String(selectedTripId) }, - })} + onClick={send.tripReminder} /> selectedTripId && fire('photos_shared', { - event: 'photos_shared', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) }, - })} + onClick={send.photosShared} /> selectedTripId && fire('collab_message', { - event: 'collab_message', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, preview: 'This is a test message preview.', tripId: String(selectedTripId) }, - })} + onClick={send.collabMessage} /> selectedTripId && fire('packing_tagged', { - event: 'packing_tagged', - scope: 'trip', - targetId: selectedTripId, - params: { actor: username, trip: tripTitle, category: 'Clothing', tripId: String(selectedTripId) }, - })} + onClick={send.packingTagged} />
@@ -236,31 +143,21 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
selectedUserId && fire(`trip_invite-${selectedUserId}`, { - event: 'trip_invite', - scope: 'user', - targetId: selectedUserId, - params: { actor: username, trip: tripTitle, invitee: selectedUser?.email || '', tripId: String(selectedTripId ?? 0) }, - })} + onClick={send.tripInvite} /> selectedUserId && fire(`vacay_invite-${selectedUserId}`, { - event: 'vacay_invite', - scope: 'user', - targetId: selectedUserId, - params: { actor: username, planId: '1' }, - })} + onClick={send.vacayInvite} />
@@ -275,12 +172,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { />
fire('version_available', { - event: 'version_available', - scope: 'admin', - targetId: 0, - params: { version: '9.9.9-test' }, - })} + onClick={send.versionAvailable} />