@burdenoff/microfe-channel 2026.731.1 → 2026.801.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/channel/ChannelRoutes.js +1 -1
  2. package/dist/channel/ChannelRoutes.js.map +1 -1
  3. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +1 -1
  4. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
  5. package/dist/channel/modules/channels/pages/ChannelDetailsPage.js +1 -1
  6. package/dist/channel/modules/channels/pages/ChannelDetailsPage.js.map +1 -1
  7. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +1 -1
  8. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
  9. package/dist/channel/modules/channels/pages/ChannelUsersPage.js +1 -1
  10. package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
  11. package/dist/channel/modules/channels/pages/ChannelsListPage.js +1 -1
  12. package/dist/channel/modules/channels/pages/ChannelsListPage.js.map +1 -1
  13. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +1 -1
  14. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
  15. package/dist/channel/modules/channels/pages/ConnectionsListPage.js +1 -1
  16. package/dist/channel/modules/channels/pages/ConnectionsListPage.js.map +1 -1
  17. package/dist/channel/modules/channels/pages/CreateChannelPage.js +1 -1
  18. package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
  19. package/dist/channel/modules/channels/pages/CreateConnectionPage.js +1 -1
  20. package/dist/channel/modules/channels/pages/CreateConnectionPage.js.map +1 -1
  21. package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js +1 -1
  22. package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js.map +1 -1
  23. package/dist/channel/modules/channels/pages/EditChannelPage.js +1 -1
  24. package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
  25. package/dist/channel/modules/channels/pages/EditConnectionPage.js +1 -1
  26. package/dist/channel/modules/channels/pages/EditConnectionPage.js.map +1 -1
  27. package/dist/channel/modules/channels/pages/SendMessagePage.js +1 -1
  28. package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
  29. package/dist/channel/modules/channels/pages/WebhookEventsPage.js +1 -1
  30. package/dist/channel/modules/channels/pages/WebhookEventsPage.js.map +1 -1
  31. package/dist/channel/pages/ChannelLandingPage.js +1 -1
  32. package/dist/channel/pages/ChannelLandingPage.js.map +1 -1
  33. package/dist/channel/providers/ChannelProvider.js +1 -1
  34. package/dist/channel/providers/ChannelProvider.js.map +1 -1
  35. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n ResponsiveTable,\n IllustratedEmptyState,\n PagePurpose,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nconst PRESENCE_CONFIG: Record<UserPresenceStatus, { label: string; color: string }> = {\n ONLINE: { label: 'Online', color: 'text-status-success-text' },\n OFFLINE: { label: 'Offline', color: 'text-text-secondary' },\n AWAY: { label: 'Away', color: 'text-status-warning-text' },\n DND: { label: 'Do Not Disturb', color: 'text-status-error-text' },\n};\n\nconst PAIRING_CONFIG: Record<\n PairingStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: {\n label: 'Pending',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n APPROVED: {\n label: 'Approved',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n EXPIRED: { label: 'Expired', color: 'text-text-secondary bg-bg-sunken', icon: XCircle },\n};\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_USER_EXPORT);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n const [currentPage, setCurrentPage] = useState(0);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(\n async (page: number = 0) => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const result = await fetchUserPlatformConnections(limit, page * limit);\n setUsers(result.items);\n setTotal(result.total);\n } finally {\n setIsLoadingUsers(false);\n }\n },\n [channelId, fetchUserPlatformConnections, limit]\n );\n\n const loadUsersRef = useRef(loadUsers);\n useEffect(() => {\n loadUsersRef.current = loadUsers;\n });\n const isFirstPageRender = useRef(true);\n // Prevents the [currentPage] effect from double-loading when limit changes\n // reset the page back to 0 (the [limit] effect already handles that load).\n const limitChangingRef = useRef(false);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n // Set the guard before resetting page so the [currentPage] effect skips\n // the redundant load that would otherwise fire when page resets to 0.\n limitChangingRef.current = true;\n setCurrentPage(0);\n void loadUsersRef.current(0).finally(() => {\n limitChangingRef.current = false;\n });\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n if (limitChangingRef.current) return;\n void Promise.resolve().then(() => loadUsersRef.current(currentPage));\n }, [currentPage]);\n\n const totalPages = Math.max(1, Math.ceil(total / limit));\n const hasPrevPage = currentPage > 0;\n const hasNextPage = currentPage < totalPages - 1;\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n const userColumns: ResponsiveTableColumn<UserPlatformConnection>[] = [\n {\n key: 'user',\n header: tr('channel.user', 'User'),\n priority: 'primary',\n cell: (user) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {user.externalDisplayName || 'Unknown'}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-text-secondary\">{user.externalUsername}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'platformId',\n header: tr('channel.platformId', 'Platform ID'),\n priority: 'secondary',\n cellClassName: 'text-sm font-mono text-text-secondary',\n cell: (user) => user.externalUserId,\n },\n {\n key: 'source',\n header: tr('channel.source', 'Source'),\n priority: 'secondary',\n cell: (user) => (\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-2 py-1 rounded\">\n {user.source}\n </span>\n ),\n },\n {\n key: 'status',\n header: tr('channel.status', 'Status'),\n priority: 'primary',\n cell: (user) => {\n const presenceConfig = getPresenceConfig(user.presenceStatus as UserPresenceStatus);\n return (\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>{presenceConfig.label}</span>\n </div>\n );\n },\n },\n {\n key: 'pairing',\n header: tr('channel.pairing', 'Pairing'),\n priority: 'secondary',\n cell: (user) => {\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {pairingConfig.label}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">-</span>\n );\n },\n },\n {\n key: 'notifications',\n header: tr('channel.notifications', 'Notifications'),\n priority: 'secondary',\n cell: (user) =>\n user.notificationsEnabled ? (\n <span className=\"inline-flex items-center gap-1\">\n <Bell size={16} className=\"text-status-success-text\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.enabled', 'Enabled')}</span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1\">\n <BellOff size={16} className=\"text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.disabled', 'Disabled')}</span>\n </span>\n ),\n },\n {\n key: 'dmAllowed',\n header: tr('channel.dmAllowed', 'DM Allowed'),\n priority: 'secondary',\n cell: (user) =>\n user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-status-success-text\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n ),\n },\n ];\n\n // Loading state (only full-screen for initial channel load)\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || 'Channel not found'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">{channel.name} - User connections</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-text-primary\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: tr('channel.refresh', 'Refresh'),\n icon: (\n <RefreshCw\n size={16}\n className={isLoadingUsers ? 'animate-spin' : ''}\n aria-hidden=\"true\"\n />\n ),\n disabled: isLoadingUsers,\n onSelect: () => void loadUsers(currentPage),\n },\n ...(canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingUsers,\n onSelect: () => void exportUsers(),\n },\n ]\n : []),\n ]}\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<User size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.usersPurpose',\n 'The people reachable through this channel — linked via OAuth or a pairing code on platforms like Slack, Teams or WhatsApp. See who is online, whether their pairing is approved, and whether they have opted into notifications and direct messages. Use it to verify a recipient is actually connected before you target them, or to audit who can receive messages here.'\n )}\n </PagePurpose>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Emphasis — connected-users summary */}\n <EmphasisPanel className=\"flex items-center gap-3 p-4\">\n <span className=\"rounded-lg bg-accent-blue-subtle p-2\">\n <UserCheck size={18} className=\"text-accent-blue\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {total}{' '}\n {total === 1\n ? tr('channel.connectedUserSingular', 'connected user')\n : tr('channel.connectedUsersPlural', 'connected users')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.usersBannerHint',\n 'Users connected to this channel through OAuth or pairing. Notification preferences and DM authorization are managed per user.'\n )}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Users List */}\n <ResponsiveTable\n columns={userColumns}\n data={users}\n rowKey={(user) => user.id}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tr('channel.noUsersConnected', 'No users connected to this channel')}\n />\n }\n className=\"bg-surface border border-border-seam rounded-lg\"\n />\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {total > 0 && (\n <>\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * limit + 1}–{Math.min((currentPage + 1) * limit, total)}\n </span>{' '}\n {tr('channel.of', 'of')} <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={!hasPrevPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={!hasNextPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { useParams, useNavigate } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n ResponsiveTable,\n IllustratedEmptyState,\n PagePurpose,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nconst PRESENCE_CONFIG: Record<UserPresenceStatus, { label: string; color: string }> = {\n ONLINE: { label: 'Online', color: 'text-status-success-text' },\n OFFLINE: { label: 'Offline', color: 'text-text-secondary' },\n AWAY: { label: 'Away', color: 'text-status-warning-text' },\n DND: { label: 'Do Not Disturb', color: 'text-status-error-text' },\n};\n\nconst PAIRING_CONFIG: Record<\n PairingStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: {\n label: 'Pending',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n APPROVED: {\n label: 'Approved',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n EXPIRED: { label: 'Expired', color: 'text-text-secondary bg-bg-sunken', icon: XCircle },\n};\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_USER_EXPORT);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n const [currentPage, setCurrentPage] = useState(0);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(\n async (page: number = 0) => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const result = await fetchUserPlatformConnections(limit, page * limit);\n setUsers(result.items);\n setTotal(result.total);\n } finally {\n setIsLoadingUsers(false);\n }\n },\n [channelId, fetchUserPlatformConnections, limit]\n );\n\n const loadUsersRef = useRef(loadUsers);\n useEffect(() => {\n loadUsersRef.current = loadUsers;\n });\n const isFirstPageRender = useRef(true);\n // Prevents the [currentPage] effect from double-loading when limit changes\n // reset the page back to 0 (the [limit] effect already handles that load).\n const limitChangingRef = useRef(false);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n // Set the guard before resetting page so the [currentPage] effect skips\n // the redundant load that would otherwise fire when page resets to 0.\n limitChangingRef.current = true;\n setCurrentPage(0);\n void loadUsersRef.current(0).finally(() => {\n limitChangingRef.current = false;\n });\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n if (limitChangingRef.current) return;\n void Promise.resolve().then(() => loadUsersRef.current(currentPage));\n }, [currentPage]);\n\n const totalPages = Math.max(1, Math.ceil(total / limit));\n const hasPrevPage = currentPage > 0;\n const hasNextPage = currentPage < totalPages - 1;\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n const userColumns: ResponsiveTableColumn<UserPlatformConnection>[] = [\n {\n key: 'user',\n header: tr('channel.user', 'User'),\n priority: 'primary',\n cell: (user) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {user.externalDisplayName || 'Unknown'}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-text-secondary\">{user.externalUsername}</p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'platformId',\n header: tr('channel.platformId', 'Platform ID'),\n priority: 'secondary',\n cellClassName: 'text-sm font-mono text-text-secondary',\n cell: (user) => user.externalUserId,\n },\n {\n key: 'source',\n header: tr('channel.source', 'Source'),\n priority: 'secondary',\n cell: (user) => (\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-2 py-1 rounded\">\n {user.source}\n </span>\n ),\n },\n {\n key: 'status',\n header: tr('channel.status', 'Status'),\n priority: 'primary',\n cell: (user) => {\n const presenceConfig = getPresenceConfig(user.presenceStatus as UserPresenceStatus);\n return (\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>{presenceConfig.label}</span>\n </div>\n );\n },\n },\n {\n key: 'pairing',\n header: tr('channel.pairing', 'Pairing'),\n priority: 'secondary',\n cell: (user) => {\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {pairingConfig.label}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">-</span>\n );\n },\n },\n {\n key: 'notifications',\n header: tr('channel.notifications', 'Notifications'),\n priority: 'secondary',\n cell: (user) =>\n user.notificationsEnabled ? (\n <span className=\"inline-flex items-center gap-1\">\n <Bell size={16} className=\"text-status-success-text\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.enabled', 'Enabled')}</span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1\">\n <BellOff size={16} className=\"text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.disabled', 'Disabled')}</span>\n </span>\n ),\n },\n {\n key: 'dmAllowed',\n header: tr('channel.dmAllowed', 'DM Allowed'),\n priority: 'secondary',\n cell: (user) =>\n user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-status-success-text\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n ),\n },\n ];\n\n // Loading state (only full-screen for initial channel load)\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || 'Channel not found'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">{channel.name} - User connections</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-text-primary\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: tr('channel.refresh', 'Refresh'),\n icon: (\n <RefreshCw\n size={16}\n className={isLoadingUsers ? 'animate-spin' : ''}\n aria-hidden=\"true\"\n />\n ),\n disabled: isLoadingUsers,\n onSelect: () => void loadUsers(currentPage),\n },\n ...(canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingUsers,\n onSelect: () => void exportUsers(),\n },\n ]\n : []),\n ]}\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<User size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.usersPurpose',\n 'The people reachable through this channel — linked via OAuth or a pairing code on platforms like Slack, Teams or WhatsApp. See who is online, whether their pairing is approved, and whether they have opted into notifications and direct messages. Use it to verify a recipient is actually connected before you target them, or to audit who can receive messages here.'\n )}\n </PagePurpose>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Emphasis — connected-users summary */}\n <EmphasisPanel className=\"flex items-center gap-3 p-4\">\n <span className=\"rounded-lg bg-accent-blue-subtle p-2\">\n <UserCheck size={18} className=\"text-accent-blue\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {total}{' '}\n {total === 1\n ? tr('channel.connectedUserSingular', 'connected user')\n : tr('channel.connectedUsersPlural', 'connected users')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.usersBannerHint',\n 'Users connected to this channel through OAuth or pairing. Notification preferences and DM authorization are managed per user.'\n )}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Users List */}\n <ResponsiveTable\n columns={userColumns}\n data={users}\n rowKey={(user) => user.id}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tr('channel.noUsersConnected', 'No users connected to this channel')}\n />\n }\n className=\"bg-surface border border-border-seam rounded-lg\"\n />\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {total > 0 && (\n <>\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * limit + 1}–{Math.min((currentPage + 1) * limit, total)}\n </span>{' '}\n {tr('channel.of', 'of')} <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={!hasPrevPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={!hasNextPage || isLoadingUsers}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { useExportChannels as re } from "../hooks/useExport.js";
9
9
  import { useEffect as u, useMemo as d, useRef as ie, useState as f } from "react";
10
10
  import { useI18n as ae } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
11
11
  import { AlertCircle as p, AlertTriangle as m, CheckCircle2 as h, Download as oe, Eye as se, HelpCircle as g, Loader2 as _, Pencil as ce, Plus as v, Power as le, PowerOff as y, Radio as ue, RefreshCw as de, Search as fe, Trash2 as pe, XCircle as b } from "lucide-react";
12
- import { useNavigate as me } from "react-router-dom";
12
+ import { useNavigate as me } from "react-router";
13
13
  import { usePermissions as he } from "@burdenoff/fe-libs/shared/providers/shell";
14
14
  import { jsx as x, jsxs as S } from "react/jsx-runtime";
15
15
  import { CTAOverflowMenu as ge, EmphasisPanel as _e, GlassCard as C, IllustratedEmptyState as ve, PagePurpose as ye } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"ChannelsListPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelsListPage.tsx"],"sourcesContent":["/**\n * Channels List Page\n *\n * Displays a list of all channels with CRUD operations\n */\n\nimport { useEffect, useMemo, useState, useRef } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport { reportShellError, subscribeShellEvent } from '../../../utils/shellBridge';\nimport {\n Radio,\n Plus,\n Search,\n Loader2,\n AlertCircle,\n Trash2,\n RefreshCw,\n Power,\n PowerOff,\n Eye,\n Pencil,\n CheckCircle2,\n XCircle,\n AlertTriangle,\n HelpCircle,\n Download,\n} from 'lucide-react';\nimport {\n useChannels,\n type Channel,\n type ChannelPlatform,\n type ChannelStatus,\n type ConnectionHealth,\n} from '../../../hooks/useChannels';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { HEALTH_I18N, PLATFORM_I18N, STATUS_I18N, translateLabel } from '../i18n';\nimport { useExportChannels } from '../hooks/useExport';\nimport { nativeConfirm, nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\n\n// Platform display configuration\nconst PLATFORM_CONFIG: Record<ChannelPlatform, { label: string; color: string }> = {\n EMAIL: { label: 'Email', color: 'bg-accent-blue-subtle text-accent-blue' },\n SMS: { label: 'SMS', color: 'bg-status-success-bg-subtle text-status-success-text' },\n PUSH: { label: 'Push', color: 'bg-accent-purple-subtle text-accent-purple' },\n INAPP: { label: 'In-App', color: 'bg-accent-indigo-subtle text-accent-indigo' },\n WEBHOOK: { label: 'Webhook', color: 'bg-accent-orange-subtle text-accent-orange' },\n SLACK: { label: 'Slack', color: 'bg-accent-pink-subtle text-accent-pink' },\n TEAMS: { label: 'Teams', color: 'bg-accent-cyan-subtle text-accent-cyan' },\n DISCORD: { label: 'Discord', color: 'bg-accent-violet-subtle text-accent-violet' },\n WHATSAPP: { label: 'WhatsApp', color: 'bg-accent-emerald-subtle text-accent-emerald' },\n WEBCHAT: { label: 'Webchat', color: 'bg-accent-teal-subtle text-accent-teal' },\n};\n\n// Status display configuration\nconst STATUS_CONFIG: Record<\n ChannelStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n ACTIVE: {\n label: 'Active',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n INACTIVE: { label: 'Inactive', color: 'text-text-secondary bg-bg-sunken', icon: PowerOff },\n ERROR: {\n label: 'Error',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n SUSPENDED: {\n label: 'Suspended',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: AlertTriangle,\n },\n ARCHIVED: { label: 'Archived', color: 'text-text-secondary bg-bg-sunken', icon: PowerOff },\n};\n\n// Health display configuration\nconst HEALTH_CONFIG: Record<\n ConnectionHealth,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n HEALTHY: { label: 'Healthy', color: 'text-status-success-text', icon: CheckCircle2 },\n DEGRADED: { label: 'Degraded', color: 'text-status-warning-text', icon: AlertTriangle },\n UNHEALTHY: { label: 'Unhealthy', color: 'text-status-error-text', icon: XCircle },\n UNKNOWN: { label: 'Unknown', color: 'text-text-secondary', icon: HelpCircle },\n};\n\nexport function ChannelsListPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n // Channel creation is an admin-only action (channel:create). Hide the\n // affordance for users without it (per MICROFRONTEND-SPEC: {canCreateChannel && ...}).\n const canCreate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_CREATE);\n const canUpdate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_UPDATE);\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_EXPORT);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);\n const [actionLoading, setActionLoading] = useState<string | null>(null);\n const [selectedChannelIds, setSelectedChannelIds] = useState<Set<string>>(new Set());\n const [bulkActionLoading, setBulkActionLoading] = useState<string | null>(null);\n\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportChannels,\n isExporting: isExportingChannels,\n exportError: exportChannelsError,\n } = useExportChannels(organizationId ?? undefined);\n const [pageSize] = useState(12);\n const [currentPage, setCurrentPage] = useState(0);\n\n const {\n channels,\n isLoading,\n error,\n total,\n filter,\n updateFilter,\n deleteChannel,\n activateChannel,\n deactivateChannel,\n runHealthCheck,\n refetch,\n } = useChannels();\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n\n const refetchRef = useRef(refetch);\n useEffect(() => {\n refetchRef.current = refetch;\n });\n\n // Fetch the current page whenever page or page size changes\n useEffect(() => {\n void refetchRef.current({ offset: currentPage * pageSize, limit: pageSize });\n }, [currentPage, pageSize]);\n\n // Reset to page 0 when filter changes\n useEffect(() => {\n void Promise.resolve().then(() => setCurrentPage(0));\n }, [filter.search, filter.platform, filter.status]);\n\n // Cross-MFE refresh: refetch the list whenever channel/connection events\n // are emitted by other tabs or microfrontends.\n useEffect(() => {\n const events = [\n 'channel.created',\n 'channel.updated',\n 'channel.deleted',\n 'connection.created',\n 'connection.updated',\n 'connection.deleted',\n ];\n const unsubs = events.map((name) => subscribeShellEvent(name, () => void refetch()));\n return () => unsubs.forEach((u) => u());\n }, [refetch]);\n\n const handleDelete = async (id: string) => {\n void nativeImpact('medium');\n setActionLoading(id);\n try {\n await deleteChannel(id);\n void nativeNotify('success');\n setDeleteConfirm(null);\n } catch (err) {\n void nativeNotify('error');\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to delete channel' } },\n '[channel] Failed to delete channel'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleToggleStatus = async (channel: Channel) => {\n setActionLoading(channel.id);\n try {\n if (channel.status === 'ACTIVE') {\n await deactivateChannel(channel.id);\n } else {\n await activateChannel(channel.id);\n }\n } catch (err) {\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to toggle channel status' } },\n '[channel] Failed to toggle channel status'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleHealthCheck = async (id: string) => {\n setActionLoading(id);\n try {\n await runHealthCheck(id);\n } catch (err) {\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to run health check' } },\n '[channel] Failed to run health check'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const selectedChannels = useMemo(\n () => channels.filter((channel) => selectedChannelIds.has(channel.id)),\n [channels, selectedChannelIds]\n );\n const selectedActiveChannels = useMemo(\n () => selectedChannels.filter((channel) => channel.status === 'ACTIVE'),\n [selectedChannels]\n );\n const selectedInactiveChannels = useMemo(\n () => selectedChannels.filter((channel) => channel.status !== 'ACTIVE'),\n [selectedChannels]\n );\n const allVisibleSelected =\n channels.length > 0 && channels.every((channel) => selectedChannelIds.has(channel.id));\n const partiallySelected =\n channels.some((channel) => selectedChannelIds.has(channel.id)) && !allVisibleSelected;\n\n const toggleChannelSelection = (channelId: string, checked: boolean) => {\n setSelectedChannelIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(channelId);\n } else {\n next.delete(channelId);\n }\n return next;\n });\n };\n\n const toggleAllChannels = (checked: boolean) => {\n setSelectedChannelIds((prev) => {\n const next = new Set(prev);\n channels.forEach((channel) => {\n if (checked) {\n next.add(channel.id);\n } else {\n next.delete(channel.id);\n }\n });\n return next;\n });\n };\n\n const handleBulkDelete = async () => {\n if (selectedChannels.length === 0) return;\n void nativeImpact('medium');\n const message = tr(\n selectedChannels.length === 1\n ? 'channel.bulkDeleteConfirmSingle'\n : 'channel.bulkDeleteConfirmMultiple',\n selectedChannels.length === 1\n ? 'Delete 1 selected channel?'\n : `Delete ${selectedChannels.length} selected channels?`\n );\n const native = await nativeConfirm({\n title: 'Delete channels',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n setBulkActionLoading('delete');\n try {\n await Promise.allSettled(selectedChannels.map((channel) => deleteChannel(channel.id)));\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkActivate = async () => {\n if (selectedInactiveChannels.length === 0) return;\n setBulkActionLoading('activate');\n try {\n await Promise.allSettled(\n selectedInactiveChannels.map((channel) => activateChannel(channel.id))\n );\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkDeactivate = async () => {\n if (selectedActiveChannels.length === 0) return;\n setBulkActionLoading('deactivate');\n try {\n await Promise.allSettled(\n selectedActiveChannels.map((channel) => deactivateChannel(channel.id))\n );\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkHealthCheck = async () => {\n if (selectedChannels.length === 0) return;\n setBulkActionLoading('health');\n try {\n await Promise.allSettled(selectedChannels.map((channel) => runHealthCheck(channel.id)));\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const getPlatformConfig = (platform: ChannelPlatform) => {\n const config = PLATFORM_CONFIG[platform] || {\n label: platform,\n color: 'bg-bg-sunken text-text-secondary',\n };\n return {\n ...config,\n label: translateLabel(platform, PLATFORM_I18N, tr),\n };\n };\n\n const getStatusConfig = (status: ChannelStatus) => {\n const config = STATUS_CONFIG[status] || STATUS_CONFIG.INACTIVE;\n return {\n ...config,\n label: translateLabel(status, STATUS_I18N, tr),\n };\n };\n\n const getHealthConfig = (health: ConnectionHealth) => {\n const config = HEALTH_CONFIG[health] || HEALTH_CONFIG.UNKNOWN;\n return {\n ...config,\n label: translateLabel(health, HEALTH_I18N, tr),\n };\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_READ}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channels', 'Channels')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('channel.manageChannels', 'Manage your communication channels')} ({total} total)\n </p>\n </div>\n\n <div className=\"flex items-center gap-2\" data-tour=\"channels-list-export-button\">\n <CTAOverflowMenu\n primary={\n canCreate\n ? {\n label: tr('channel.createChannel', 'Create Channel'),\n icon: <Plus size={16} aria-hidden=\"true\" />,\n onSelect: () => navigate('/channels/create'),\n }\n : undefined\n }\n actions={\n canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingChannels ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingChannels,\n onSelect: () => void exportChannels(),\n },\n ]\n : []\n }\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<Radio size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.listPurpose',\n 'Every communication channel in your organization, in one browseable grid. Filter by platform or status, select several at once to activate, deactivate, health-check or delete them in bulk, and open any card to manage its connection and delivery history. Start here when you need to find a specific channel or tidy up the ones you no longer use.'\n )}\n </PagePurpose>\n\n {/* Emphasis — primary create call-to-action */}\n {canCreate && (\n <EmphasisPanel className=\"flex flex-col gap-3 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"rounded-lg bg-accent-primary/10 p-2\">\n <Plus size={18} className=\"text-accent-primary\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('channel.addAnotherChannel', 'Add another channel')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.addAnotherChannelHint',\n 'Connect a new platform — Email, SMS, Push, Slack and more — to start delivering messages through it.'\n )}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate('/channels/create')}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus size={16} aria-hidden=\"true\" />\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </EmphasisPanel>\n )}\n\n {/* Search and Filters */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n aria-label={tr('channel.searchChannels', 'Search channels...')}\n type=\"text\"\n placeholder={tr('channel.searchChannels', 'Search channels...')}\n value={filter.search || ''}\n onChange={(e) => updateFilter({ search: e.target.value })}\n className=\"w-full pl-9 pr-4 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n </div>\n\n <select\n value={filter.platform || ''}\n onChange={(e) =>\n updateFilter({ platform: (e.target.value as ChannelPlatform) || undefined })\n }\n className=\"px-3 py-2 text-sm bg-bg-surface text-text-primary border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{tr('channel.allPlatforms', 'All Platforms')}</option>\n {Object.entries(PLATFORM_CONFIG).map(([key, config]) => (\n <option key={key} value={key}>\n {config.label}\n </option>\n ))}\n </select>\n\n <select\n value={filter.status || ''}\n onChange={(e) =>\n updateFilter({ status: (e.target.value as ChannelStatus) || undefined })\n }\n className=\"px-3 py-2 text-sm bg-bg-surface text-text-primary border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{tr('channel.allStatus', 'All Status')}</option>\n {Object.entries(STATUS_CONFIG).map(([key, config]) => (\n <option key={key} value={key}>\n {config.label}\n </option>\n ))}\n </select>\n </div>\n\n {/* Export Error */}\n {exportChannelsError && (\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportChannelsError}</p>\n </div>\n )}\n\n {channels.length > 0 && (\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 border-border-seam p-4 sm:flex-row sm:items-center sm:justify-between\"\n data-tour=\"channels-list-bulk-actions\"\n >\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllChannels(event.target.checked)}\n aria-label=\"Select all channels\"\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>{tr('channel.selectAllVisible', 'Select all visible')}</span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedChannelIds.size} {tr('channel.selectedCountLabel', 'selected')}\n </span>\n {selectedChannelIds.size > 0 && (\n <button\n type=\"button\"\n onClick={() => setSelectedChannelIds(new Set())}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr('channel.clearSelection', 'Clear selection')}\n </button>\n )}\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_EXECUTE}>\n <button\n type=\"button\"\n onClick={handleBulkHealthCheck}\n disabled={selectedChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-border-subtle rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {tr('channel.healthCheck', 'Health Check')}\n {selectedChannels.length > 0 ? ` (${selectedChannels.length})` : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={handleBulkActivate}\n disabled={selectedInactiveChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-success-border/30 text-status-success-text rounded-md hover:bg-status-success-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.activateSelected', 'Activate Selected')}\n {selectedInactiveChannels.length > 0\n ? ` (${selectedInactiveChannels.length})`\n : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={handleBulkDeactivate}\n disabled={selectedActiveChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-warning-border text-status-warning-text rounded-md hover:bg-status-warning-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.deactivateSelected', 'Deactivate Selected')}\n {selectedActiveChannels.length > 0 ? ` (${selectedActiveChannels.length})` : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELETE}>\n <button\n type=\"button\"\n onClick={handleBulkDelete}\n disabled={selectedChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-error-border/30 text-status-error-text rounded-md hover:bg-status-error-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.deleteSelected', 'Delete Selected')}\n {selectedChannels.length > 0 ? ` (${selectedChannels.length})` : ''}\n </button>\n </PermissionGuard>\n </div>\n </GlassCard>\n )}\n\n {/* Error State */}\n {error && (\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {tr('channel.loadFailed', 'Failed to load channels')}\n </p>\n <p className=\"text-xs text-status-error-text\">{error}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n {tr('channel.retry', 'Retry')}\n </button>\n </div>\n )}\n\n {/* Loading State */}\n {isLoading && channels.length === 0 && (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && channels.length === 0 && (\n <IllustratedEmptyState\n illustration={\n filter.search || filter.platform || filter.status ? 'empty-search' : 'empty-data'\n }\n title={tr('channel.noChannelsFound', 'No channels found')}\n description={\n filter.search || filter.platform || filter.status\n ? tr('channel.adjustFilters', 'Try adjusting your filters to see more channels.')\n : tr(\n 'channel.createFirstChannelPrompt',\n 'Create your first communication channel to get started.'\n )\n }\n action={\n canCreate && !filter.search && !filter.platform && !filter.status ? (\n <button\n type=\"button\"\n onClick={() => navigate('/channels/create')}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors\"\n >\n <Plus size={16} />\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Channels List */}\n {channels.length > 0 && (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\" data-tour=\"channels-list-grid\">\n {channels.map((channel: Channel) => {\n const platformConfig = getPlatformConfig(channel.platform);\n const statusConfig = getStatusConfig(channel.status);\n const healthConfig = getHealthConfig(channel.connectionHealth);\n const StatusIcon = statusConfig.icon;\n const HealthIcon = healthConfig.icon;\n const isActionLoading = actionLoading === channel.id;\n\n return (\n <GlassCard\n key={channel.id}\n treatment=\"insight\"\n className={`p-4 transition-colors group ${selectedChannelIds.has(channel.id) ? 'ring-2 ring-action-primary-bg border-action-primary-bg/40' : 'border-border-seam'}`}\n >\n {/* Header */}\n <div className=\"flex items-start justify-between mb-3\">\n <div className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n checked={selectedChannelIds.has(channel.id)}\n onChange={(event) =>\n toggleChannelSelection(channel.id, event.target.checked)\n }\n aria-label={`Select channel ${channel.name}`}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <Radio size={20} className=\"text-accent-primary\" />\n <span\n className={`px-2 py-0.5 text-xs font-semibold rounded-full ${platformConfig.color}`}\n >\n {platformConfig.label}\n </span>\n </div>\n <div className=\"flex items-center gap-1\">\n <StatusIcon size={14} className={statusConfig.color.split(' ')[0]} />\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded-full ${statusConfig.color} ${\n channel.status === 'ACTIVE' ? 'motion-safe:animate-vc-glow-pulse' : ''\n }`}\n >\n {statusConfig.label}\n </span>\n </div>\n </div>\n\n {/* Content - Clickable area */}\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/channels/${channel.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`/channels/${channel.id}`);\n }\n }}\n className=\"cursor-pointer\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary mb-1 truncate\">\n {channel.name}\n </h3>\n\n {channel.description && (\n <p className=\"text-xs text-text-secondary mb-2 line-clamp-2\">\n {channel.description}\n </p>\n )}\n\n {/* Adapter */}\n {channel.adapterType && (\n <p className=\"text-xs text-text-secondary mb-2 font-mono\">\n {channel.adapterType.replace(/_/g, ' ')}\n </p>\n )}\n\n {/* Stats Row */}\n <div className=\"flex items-center gap-3 text-xs text-text-secondary mb-2\">\n <span>\n {tr('channel.priority', 'Priority')}: {channel.priority}\n </span>\n </div>\n\n {/* Health & Connection Status */}\n <div className=\"flex items-center gap-3 text-xs\">\n <div className=\"flex items-center gap-1\">\n <HealthIcon size={12} className={healthConfig.color} />\n <span className={healthConfig.color}>{healthConfig.label}</span>\n </div>\n <span>•</span>\n <span\n className={\n channel.isConnected ? 'text-status-success-text' : 'text-text-secondary'\n }\n >\n {channel.isConnected\n ? tr('channel.connected', 'Connected')\n : tr('channel.notConnected', 'Not Connected')}\n </span>\n </div>\n </div>\n\n {/* Tags */}\n {channel.tagAssignments && channel.tagAssignments.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2\" aria-label=\"Channel tags\">\n {channel.tagAssignments.slice(0, 4).map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"sm\" />\n ))}\n {channel.tagAssignments.length > 4 && (\n <span className=\"text-xs text-text-secondary\">\n +{channel.tagAssignments.length - 4}\n </span>\n )}\n </div>\n )}\n\n {/* Actions */}\n <div className=\"mt-3 pt-3 border-t border-border-subtle flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channel.id}`)}\n className=\"p-1.5 text-text-secondary hover:text-accent-primary hover:bg-accent-primary/10 rounded transition-colors cursor-pointer\"\n title={tr('channel.viewDetails', 'View Details')}\n >\n <Eye size={14} />\n </button>\n {canUpdate && (\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channel.id}/edit`)}\n className=\"p-1.5 text-text-secondary hover:text-accent-primary hover:bg-accent-primary/10 rounded transition-colors cursor-pointer\"\n title={tr('channel.editChannel', 'Edit Channel')}\n >\n <Pencil size={14} />\n </button>\n )}\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_EXECUTE}>\n <button\n type=\"button\"\n onClick={() => handleHealthCheck(channel.id)}\n disabled={isActionLoading}\n className=\"p-1.5 text-text-secondary hover:text-status-info-text hover:bg-status-info-bg-subtle rounded transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed\"\n title={tr('channel.runHealthCheck', 'Run Health Check')}\n >\n {isActionLoading ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <RefreshCw size={14} />\n )}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={() => handleToggleStatus(channel)}\n disabled={isActionLoading}\n className={`p-1.5 rounded transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed ${\n channel.status === 'ACTIVE'\n ? 'text-text-secondary hover:text-status-warning-text hover:bg-status-warning-bg-subtle'\n : 'text-text-secondary hover:text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n title={\n channel.status === 'ACTIVE'\n ? tr('channel.deactivate', 'Deactivate')\n : tr('channel.activate', 'Activate')\n }\n >\n {channel.status === 'ACTIVE' ? (\n <PowerOff size={14} />\n ) : (\n <Power size={14} />\n )}\n </button>\n </PermissionGuard>\n </div>\n\n {/* Delete */}\n {deleteConfirm === channel.id ? (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('channel.deleteQuestion', 'Delete?')}\n </span>\n <button\n type=\"button\"\n onClick={() => handleDelete(channel.id)}\n disabled={isActionLoading}\n className=\"px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors cursor-pointer disabled:cursor-not-allowed\"\n >\n {isActionLoading ? '...' : tr('channel.yes', 'Yes')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(null)}\n className=\"px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken rounded transition-colors cursor-pointer\"\n >\n {tr('channel.no', 'No')}\n </button>\n </div>\n ) : (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELETE}>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(channel.id)}\n className=\"p-1.5 text-text-secondary hover:text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors cursor-pointer\"\n title={tr('channel.deleteChannel', 'Delete Channel')}\n >\n <Trash2 size={14} />\n </button>\n </PermissionGuard>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n\n {/* Pagination */}\n {total > pageSize && (\n <div className=\"flex items-center justify-between px-1 py-2\">\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium text-text-primary\">\n {currentPage * pageSize + 1}–{Math.min((currentPage + 1) * pageSize, total)}\n </span>{' '}\n {tr('channel.of', 'of')}{' '}\n <span className=\"font-medium text-text-primary\">{total}</span>{' '}\n {tr('channel.channelsLower', 'channels')}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={currentPage === 0}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={currentPage >= totalPages - 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n </button>\n </div>\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ChannelsListPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelsListPage.tsx"],"sourcesContent":["/**\n * Channels List Page\n *\n * Displays a list of all channels with CRUD operations\n */\n\nimport { useEffect, useMemo, useState, useRef } from 'react';\nimport { useNavigate } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport { reportShellError, subscribeShellEvent } from '../../../utils/shellBridge';\nimport {\n Radio,\n Plus,\n Search,\n Loader2,\n AlertCircle,\n Trash2,\n RefreshCw,\n Power,\n PowerOff,\n Eye,\n Pencil,\n CheckCircle2,\n XCircle,\n AlertTriangle,\n HelpCircle,\n Download,\n} from 'lucide-react';\nimport {\n useChannels,\n type Channel,\n type ChannelPlatform,\n type ChannelStatus,\n type ConnectionHealth,\n} from '../../../hooks/useChannels';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { HEALTH_I18N, PLATFORM_I18N, STATUS_I18N, translateLabel } from '../i18n';\nimport { useExportChannels } from '../hooks/useExport';\nimport { nativeConfirm, nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\n\n// Platform display configuration\nconst PLATFORM_CONFIG: Record<ChannelPlatform, { label: string; color: string }> = {\n EMAIL: { label: 'Email', color: 'bg-accent-blue-subtle text-accent-blue' },\n SMS: { label: 'SMS', color: 'bg-status-success-bg-subtle text-status-success-text' },\n PUSH: { label: 'Push', color: 'bg-accent-purple-subtle text-accent-purple' },\n INAPP: { label: 'In-App', color: 'bg-accent-indigo-subtle text-accent-indigo' },\n WEBHOOK: { label: 'Webhook', color: 'bg-accent-orange-subtle text-accent-orange' },\n SLACK: { label: 'Slack', color: 'bg-accent-pink-subtle text-accent-pink' },\n TEAMS: { label: 'Teams', color: 'bg-accent-cyan-subtle text-accent-cyan' },\n DISCORD: { label: 'Discord', color: 'bg-accent-violet-subtle text-accent-violet' },\n WHATSAPP: { label: 'WhatsApp', color: 'bg-accent-emerald-subtle text-accent-emerald' },\n WEBCHAT: { label: 'Webchat', color: 'bg-accent-teal-subtle text-accent-teal' },\n};\n\n// Status display configuration\nconst STATUS_CONFIG: Record<\n ChannelStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n ACTIVE: {\n label: 'Active',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n INACTIVE: { label: 'Inactive', color: 'text-text-secondary bg-bg-sunken', icon: PowerOff },\n ERROR: {\n label: 'Error',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n SUSPENDED: {\n label: 'Suspended',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: AlertTriangle,\n },\n ARCHIVED: { label: 'Archived', color: 'text-text-secondary bg-bg-sunken', icon: PowerOff },\n};\n\n// Health display configuration\nconst HEALTH_CONFIG: Record<\n ConnectionHealth,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n HEALTHY: { label: 'Healthy', color: 'text-status-success-text', icon: CheckCircle2 },\n DEGRADED: { label: 'Degraded', color: 'text-status-warning-text', icon: AlertTriangle },\n UNHEALTHY: { label: 'Unhealthy', color: 'text-status-error-text', icon: XCircle },\n UNKNOWN: { label: 'Unknown', color: 'text-text-secondary', icon: HelpCircle },\n};\n\nexport function ChannelsListPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n // Channel creation is an admin-only action (channel:create). Hide the\n // affordance for users without it (per MICROFRONTEND-SPEC: {canCreateChannel && ...}).\n const canCreate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_CREATE);\n const canUpdate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_UPDATE);\n const canExport = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_EXPORT);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);\n const [actionLoading, setActionLoading] = useState<string | null>(null);\n const [selectedChannelIds, setSelectedChannelIds] = useState<Set<string>>(new Set());\n const [bulkActionLoading, setBulkActionLoading] = useState<string | null>(null);\n\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportChannels,\n isExporting: isExportingChannels,\n exportError: exportChannelsError,\n } = useExportChannels(organizationId ?? undefined);\n const [pageSize] = useState(12);\n const [currentPage, setCurrentPage] = useState(0);\n\n const {\n channels,\n isLoading,\n error,\n total,\n filter,\n updateFilter,\n deleteChannel,\n activateChannel,\n deactivateChannel,\n runHealthCheck,\n refetch,\n } = useChannels();\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n\n const refetchRef = useRef(refetch);\n useEffect(() => {\n refetchRef.current = refetch;\n });\n\n // Fetch the current page whenever page or page size changes\n useEffect(() => {\n void refetchRef.current({ offset: currentPage * pageSize, limit: pageSize });\n }, [currentPage, pageSize]);\n\n // Reset to page 0 when filter changes\n useEffect(() => {\n void Promise.resolve().then(() => setCurrentPage(0));\n }, [filter.search, filter.platform, filter.status]);\n\n // Cross-MFE refresh: refetch the list whenever channel/connection events\n // are emitted by other tabs or microfrontends.\n useEffect(() => {\n const events = [\n 'channel.created',\n 'channel.updated',\n 'channel.deleted',\n 'connection.created',\n 'connection.updated',\n 'connection.deleted',\n ];\n const unsubs = events.map((name) => subscribeShellEvent(name, () => void refetch()));\n return () => unsubs.forEach((u) => u());\n }, [refetch]);\n\n const handleDelete = async (id: string) => {\n void nativeImpact('medium');\n setActionLoading(id);\n try {\n await deleteChannel(id);\n void nativeNotify('success');\n setDeleteConfirm(null);\n } catch (err) {\n void nativeNotify('error');\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to delete channel' } },\n '[channel] Failed to delete channel'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleToggleStatus = async (channel: Channel) => {\n setActionLoading(channel.id);\n try {\n if (channel.status === 'ACTIVE') {\n await deactivateChannel(channel.id);\n } else {\n await activateChannel(channel.id);\n }\n } catch (err) {\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to toggle channel status' } },\n '[channel] Failed to toggle channel status'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleHealthCheck = async (id: string) => {\n setActionLoading(id);\n try {\n await runHealthCheck(id);\n } catch (err) {\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to run health check' } },\n '[channel] Failed to run health check'\n );\n } finally {\n setActionLoading(null);\n }\n };\n\n const selectedChannels = useMemo(\n () => channels.filter((channel) => selectedChannelIds.has(channel.id)),\n [channels, selectedChannelIds]\n );\n const selectedActiveChannels = useMemo(\n () => selectedChannels.filter((channel) => channel.status === 'ACTIVE'),\n [selectedChannels]\n );\n const selectedInactiveChannels = useMemo(\n () => selectedChannels.filter((channel) => channel.status !== 'ACTIVE'),\n [selectedChannels]\n );\n const allVisibleSelected =\n channels.length > 0 && channels.every((channel) => selectedChannelIds.has(channel.id));\n const partiallySelected =\n channels.some((channel) => selectedChannelIds.has(channel.id)) && !allVisibleSelected;\n\n const toggleChannelSelection = (channelId: string, checked: boolean) => {\n setSelectedChannelIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(channelId);\n } else {\n next.delete(channelId);\n }\n return next;\n });\n };\n\n const toggleAllChannels = (checked: boolean) => {\n setSelectedChannelIds((prev) => {\n const next = new Set(prev);\n channels.forEach((channel) => {\n if (checked) {\n next.add(channel.id);\n } else {\n next.delete(channel.id);\n }\n });\n return next;\n });\n };\n\n const handleBulkDelete = async () => {\n if (selectedChannels.length === 0) return;\n void nativeImpact('medium');\n const message = tr(\n selectedChannels.length === 1\n ? 'channel.bulkDeleteConfirmSingle'\n : 'channel.bulkDeleteConfirmMultiple',\n selectedChannels.length === 1\n ? 'Delete 1 selected channel?'\n : `Delete ${selectedChannels.length} selected channels?`\n );\n const native = await nativeConfirm({\n title: 'Delete channels',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n setBulkActionLoading('delete');\n try {\n await Promise.allSettled(selectedChannels.map((channel) => deleteChannel(channel.id)));\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkActivate = async () => {\n if (selectedInactiveChannels.length === 0) return;\n setBulkActionLoading('activate');\n try {\n await Promise.allSettled(\n selectedInactiveChannels.map((channel) => activateChannel(channel.id))\n );\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkDeactivate = async () => {\n if (selectedActiveChannels.length === 0) return;\n setBulkActionLoading('deactivate');\n try {\n await Promise.allSettled(\n selectedActiveChannels.map((channel) => deactivateChannel(channel.id))\n );\n setSelectedChannelIds(new Set());\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const handleBulkHealthCheck = async () => {\n if (selectedChannels.length === 0) return;\n setBulkActionLoading('health');\n try {\n await Promise.allSettled(selectedChannels.map((channel) => runHealthCheck(channel.id)));\n } finally {\n setBulkActionLoading(null);\n }\n };\n\n const getPlatformConfig = (platform: ChannelPlatform) => {\n const config = PLATFORM_CONFIG[platform] || {\n label: platform,\n color: 'bg-bg-sunken text-text-secondary',\n };\n return {\n ...config,\n label: translateLabel(platform, PLATFORM_I18N, tr),\n };\n };\n\n const getStatusConfig = (status: ChannelStatus) => {\n const config = STATUS_CONFIG[status] || STATUS_CONFIG.INACTIVE;\n return {\n ...config,\n label: translateLabel(status, STATUS_I18N, tr),\n };\n };\n\n const getHealthConfig = (health: ConnectionHealth) => {\n const config = HEALTH_CONFIG[health] || HEALTH_CONFIG.UNKNOWN;\n return {\n ...config,\n label: translateLabel(health, HEALTH_I18N, tr),\n };\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_READ}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('channel.channels', 'Channels')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('channel.manageChannels', 'Manage your communication channels')} ({total} total)\n </p>\n </div>\n\n <div className=\"flex items-center gap-2\" data-tour=\"channels-list-export-button\">\n <CTAOverflowMenu\n primary={\n canCreate\n ? {\n label: tr('channel.createChannel', 'Create Channel'),\n icon: <Plus size={16} aria-hidden=\"true\" />,\n onSelect: () => navigate('/channels/create'),\n }\n : undefined\n }\n actions={\n canExport\n ? [\n {\n label: tr('channel.export', 'Export'),\n icon: isExportingChannels ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Download size={16} aria-hidden=\"true\" />\n ),\n disabled: isExportingChannels,\n onSelect: () => void exportChannels(),\n },\n ]\n : []\n }\n />\n </div>\n </div>\n\n {/* Page purpose */}\n <PagePurpose icon={<Radio size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.listPurpose',\n 'Every communication channel in your organization, in one browseable grid. Filter by platform or status, select several at once to activate, deactivate, health-check or delete them in bulk, and open any card to manage its connection and delivery history. Start here when you need to find a specific channel or tidy up the ones you no longer use.'\n )}\n </PagePurpose>\n\n {/* Emphasis — primary create call-to-action */}\n {canCreate && (\n <EmphasisPanel className=\"flex flex-col gap-3 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"rounded-lg bg-accent-primary/10 p-2\">\n <Plus size={18} className=\"text-accent-primary\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('channel.addAnotherChannel', 'Add another channel')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.addAnotherChannelHint',\n 'Connect a new platform — Email, SMS, Push, Slack and more — to start delivering messages through it.'\n )}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate('/channels/create')}\n className=\"inline-flex shrink-0 items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus size={16} aria-hidden=\"true\" />\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </EmphasisPanel>\n )}\n\n {/* Search and Filters */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n aria-label={tr('channel.searchChannels', 'Search channels...')}\n type=\"text\"\n placeholder={tr('channel.searchChannels', 'Search channels...')}\n value={filter.search || ''}\n onChange={(e) => updateFilter({ search: e.target.value })}\n className=\"w-full pl-9 pr-4 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n </div>\n\n <select\n value={filter.platform || ''}\n onChange={(e) =>\n updateFilter({ platform: (e.target.value as ChannelPlatform) || undefined })\n }\n className=\"px-3 py-2 text-sm bg-bg-surface text-text-primary border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{tr('channel.allPlatforms', 'All Platforms')}</option>\n {Object.entries(PLATFORM_CONFIG).map(([key, config]) => (\n <option key={key} value={key}>\n {config.label}\n </option>\n ))}\n </select>\n\n <select\n value={filter.status || ''}\n onChange={(e) =>\n updateFilter({ status: (e.target.value as ChannelStatus) || undefined })\n }\n className=\"px-3 py-2 text-sm bg-bg-surface text-text-primary border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{tr('channel.allStatus', 'All Status')}</option>\n {Object.entries(STATUS_CONFIG).map(([key, config]) => (\n <option key={key} value={key}>\n {config.label}\n </option>\n ))}\n </select>\n </div>\n\n {/* Export Error */}\n {exportChannelsError && (\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportChannelsError}</p>\n </div>\n )}\n\n {channels.length > 0 && (\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 border-border-seam p-4 sm:flex-row sm:items-center sm:justify-between\"\n data-tour=\"channels-list-bulk-actions\"\n >\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllChannels(event.target.checked)}\n aria-label=\"Select all channels\"\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>{tr('channel.selectAllVisible', 'Select all visible')}</span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedChannelIds.size} {tr('channel.selectedCountLabel', 'selected')}\n </span>\n {selectedChannelIds.size > 0 && (\n <button\n type=\"button\"\n onClick={() => setSelectedChannelIds(new Set())}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr('channel.clearSelection', 'Clear selection')}\n </button>\n )}\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_EXECUTE}>\n <button\n type=\"button\"\n onClick={handleBulkHealthCheck}\n disabled={selectedChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-border-subtle rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {tr('channel.healthCheck', 'Health Check')}\n {selectedChannels.length > 0 ? ` (${selectedChannels.length})` : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={handleBulkActivate}\n disabled={selectedInactiveChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-success-border/30 text-status-success-text rounded-md hover:bg-status-success-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.activateSelected', 'Activate Selected')}\n {selectedInactiveChannels.length > 0\n ? ` (${selectedInactiveChannels.length})`\n : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={handleBulkDeactivate}\n disabled={selectedActiveChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-warning-border text-status-warning-text rounded-md hover:bg-status-warning-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.deactivateSelected', 'Deactivate Selected')}\n {selectedActiveChannels.length > 0 ? ` (${selectedActiveChannels.length})` : ''}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELETE}>\n <button\n type=\"button\"\n onClick={handleBulkDelete}\n disabled={selectedChannels.length === 0 || bulkActionLoading !== null}\n className=\"px-3 py-2 text-sm font-medium border border-status-error-border/30 text-status-error-text rounded-md hover:bg-status-error-bg-subtle transition-colors disabled:opacity-50\"\n >\n {tr('channel.deleteSelected', 'Delete Selected')}\n {selectedChannels.length > 0 ? ` (${selectedChannels.length})` : ''}\n </button>\n </PermissionGuard>\n </div>\n </GlassCard>\n )}\n\n {/* Error State */}\n {error && (\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {tr('channel.loadFailed', 'Failed to load channels')}\n </p>\n <p className=\"text-xs text-status-error-text\">{error}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n {tr('channel.retry', 'Retry')}\n </button>\n </div>\n )}\n\n {/* Loading State */}\n {isLoading && channels.length === 0 && (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && channels.length === 0 && (\n <IllustratedEmptyState\n illustration={\n filter.search || filter.platform || filter.status ? 'empty-search' : 'empty-data'\n }\n title={tr('channel.noChannelsFound', 'No channels found')}\n description={\n filter.search || filter.platform || filter.status\n ? tr('channel.adjustFilters', 'Try adjusting your filters to see more channels.')\n : tr(\n 'channel.createFirstChannelPrompt',\n 'Create your first communication channel to get started.'\n )\n }\n action={\n canCreate && !filter.search && !filter.platform && !filter.status ? (\n <button\n type=\"button\"\n onClick={() => navigate('/channels/create')}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors\"\n >\n <Plus size={16} />\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Channels List */}\n {channels.length > 0 && (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\" data-tour=\"channels-list-grid\">\n {channels.map((channel: Channel) => {\n const platformConfig = getPlatformConfig(channel.platform);\n const statusConfig = getStatusConfig(channel.status);\n const healthConfig = getHealthConfig(channel.connectionHealth);\n const StatusIcon = statusConfig.icon;\n const HealthIcon = healthConfig.icon;\n const isActionLoading = actionLoading === channel.id;\n\n return (\n <GlassCard\n key={channel.id}\n treatment=\"insight\"\n className={`p-4 transition-colors group ${selectedChannelIds.has(channel.id) ? 'ring-2 ring-action-primary-bg border-action-primary-bg/40' : 'border-border-seam'}`}\n >\n {/* Header */}\n <div className=\"flex items-start justify-between mb-3\">\n <div className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n checked={selectedChannelIds.has(channel.id)}\n onChange={(event) =>\n toggleChannelSelection(channel.id, event.target.checked)\n }\n aria-label={`Select channel ${channel.name}`}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <Radio size={20} className=\"text-accent-primary\" />\n <span\n className={`px-2 py-0.5 text-xs font-semibold rounded-full ${platformConfig.color}`}\n >\n {platformConfig.label}\n </span>\n </div>\n <div className=\"flex items-center gap-1\">\n <StatusIcon size={14} className={statusConfig.color.split(' ')[0]} />\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded-full ${statusConfig.color} ${\n channel.status === 'ACTIVE' ? 'motion-safe:animate-vc-glow-pulse' : ''\n }`}\n >\n {statusConfig.label}\n </span>\n </div>\n </div>\n\n {/* Content - Clickable area */}\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/channels/${channel.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`/channels/${channel.id}`);\n }\n }}\n className=\"cursor-pointer\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary mb-1 truncate\">\n {channel.name}\n </h3>\n\n {channel.description && (\n <p className=\"text-xs text-text-secondary mb-2 line-clamp-2\">\n {channel.description}\n </p>\n )}\n\n {/* Adapter */}\n {channel.adapterType && (\n <p className=\"text-xs text-text-secondary mb-2 font-mono\">\n {channel.adapterType.replace(/_/g, ' ')}\n </p>\n )}\n\n {/* Stats Row */}\n <div className=\"flex items-center gap-3 text-xs text-text-secondary mb-2\">\n <span>\n {tr('channel.priority', 'Priority')}: {channel.priority}\n </span>\n </div>\n\n {/* Health & Connection Status */}\n <div className=\"flex items-center gap-3 text-xs\">\n <div className=\"flex items-center gap-1\">\n <HealthIcon size={12} className={healthConfig.color} />\n <span className={healthConfig.color}>{healthConfig.label}</span>\n </div>\n <span>•</span>\n <span\n className={\n channel.isConnected ? 'text-status-success-text' : 'text-text-secondary'\n }\n >\n {channel.isConnected\n ? tr('channel.connected', 'Connected')\n : tr('channel.notConnected', 'Not Connected')}\n </span>\n </div>\n </div>\n\n {/* Tags */}\n {channel.tagAssignments && channel.tagAssignments.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2\" aria-label=\"Channel tags\">\n {channel.tagAssignments.slice(0, 4).map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"sm\" />\n ))}\n {channel.tagAssignments.length > 4 && (\n <span className=\"text-xs text-text-secondary\">\n +{channel.tagAssignments.length - 4}\n </span>\n )}\n </div>\n )}\n\n {/* Actions */}\n <div className=\"mt-3 pt-3 border-t border-border-subtle flex items-center justify-between opacity-0 group-hover:opacity-100 transition-opacity\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channel.id}`)}\n className=\"p-1.5 text-text-secondary hover:text-accent-primary hover:bg-accent-primary/10 rounded transition-colors cursor-pointer\"\n title={tr('channel.viewDetails', 'View Details')}\n >\n <Eye size={14} />\n </button>\n {canUpdate && (\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channel.id}/edit`)}\n className=\"p-1.5 text-text-secondary hover:text-accent-primary hover:bg-accent-primary/10 rounded transition-colors cursor-pointer\"\n title={tr('channel.editChannel', 'Edit Channel')}\n >\n <Pencil size={14} />\n </button>\n )}\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_EXECUTE}>\n <button\n type=\"button\"\n onClick={() => handleHealthCheck(channel.id)}\n disabled={isActionLoading}\n className=\"p-1.5 text-text-secondary hover:text-status-info-text hover:bg-status-info-bg-subtle rounded transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed\"\n title={tr('channel.runHealthCheck', 'Run Health Check')}\n >\n {isActionLoading ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <RefreshCw size={14} />\n )}\n </button>\n </PermissionGuard>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <button\n type=\"button\"\n onClick={() => handleToggleStatus(channel)}\n disabled={isActionLoading}\n className={`p-1.5 rounded transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed ${\n channel.status === 'ACTIVE'\n ? 'text-text-secondary hover:text-status-warning-text hover:bg-status-warning-bg-subtle'\n : 'text-text-secondary hover:text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n title={\n channel.status === 'ACTIVE'\n ? tr('channel.deactivate', 'Deactivate')\n : tr('channel.activate', 'Activate')\n }\n >\n {channel.status === 'ACTIVE' ? (\n <PowerOff size={14} />\n ) : (\n <Power size={14} />\n )}\n </button>\n </PermissionGuard>\n </div>\n\n {/* Delete */}\n {deleteConfirm === channel.id ? (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('channel.deleteQuestion', 'Delete?')}\n </span>\n <button\n type=\"button\"\n onClick={() => handleDelete(channel.id)}\n disabled={isActionLoading}\n className=\"px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors cursor-pointer disabled:cursor-not-allowed\"\n >\n {isActionLoading ? '...' : tr('channel.yes', 'Yes')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(null)}\n className=\"px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken rounded transition-colors cursor-pointer\"\n >\n {tr('channel.no', 'No')}\n </button>\n </div>\n ) : (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELETE}>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(channel.id)}\n className=\"p-1.5 text-text-secondary hover:text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors cursor-pointer\"\n title={tr('channel.deleteChannel', 'Delete Channel')}\n >\n <Trash2 size={14} />\n </button>\n </PermissionGuard>\n )}\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n\n {/* Pagination */}\n {total > pageSize && (\n <div className=\"flex items-center justify-between px-1 py-2\">\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium text-text-primary\">\n {currentPage * pageSize + 1}–{Math.min((currentPage + 1) * pageSize, total)}\n </span>{' '}\n {tr('channel.of', 'of')}{' '}\n <span className=\"font-medium text-text-primary\">{total}</span>{' '}\n {tr('channel.channelsLower', 'channels')}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}\n disabled={currentPage === 0}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {currentPage + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}\n disabled={currentPage >= totalPages - 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n </button>\n </div>\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { CHANNEL_PERMISSIONS as a } from "../../../constants/permissions.js";
9
9
  import { useEffect as re, useState as o } from "react";
10
10
  import { useI18n as ie } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
11
11
  import { AlertCircle as s, ArrowLeft as c, CheckCircle2 as l, Clock as u, Edit as d, Loader2 as f, RefreshCw as p, Shield as m, Trash2 as h, XCircle as g } from "lucide-react";
12
- import { useNavigate as _, useParams as v } from "react-router-dom";
12
+ import { useNavigate as _, useParams as v } from "react-router";
13
13
  import { usePermissions as ae } from "@burdenoff/fe-libs/shared/providers/shell";
14
14
  import { jsx as y, jsxs as b } from "react/jsx-runtime";
15
15
  import { CTAOverflowMenu as oe, EmphasisPanel as x, GlassCard as S, NextSteps as C, PagePurpose as w } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectionDetailsPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"sourcesContent":["/**\n * Connection Details Page\n *\n * View and manage a specific connection within a channel.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport { reportShellError } from '../../../utils/shellBridge';\nimport { nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n RefreshCw,\n Trash2,\n Edit,\n Shield,\n Clock,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n GlassCard,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useChannel } from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { getGlobalChannelClient } from '../../../graphql/global-client';\nimport {\n GetConnectionDocument,\n type GetConnectionQuery,\n} from '../../../../generated/global-operations';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\nconst HEALTH_CONFIG: Record<\n string,\n { icon: typeof CheckCircle2; color: string; bgColor: string; label: string }\n> = {\n true: {\n icon: CheckCircle2,\n color: 'text-status-success-text',\n bgColor: 'bg-status-success-bg-subtle',\n label: 'Healthy',\n },\n false: {\n icon: XCircle,\n color: 'text-status-error-text',\n bgColor: 'bg-error-500/10',\n label: 'Unhealthy',\n },\n};\n\nexport function ConnectionDetailsPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canUpdate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_UPDATE);\n const canExecute = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_EXECUTE);\n const params = useParams<{ channelId?: string; connectionId: string }>();\n const { connectionId } = params;\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n\n // Route may be /channels/connections/:connectionId (no channelId in URL).\n // Resolve channelId from getConnection when absent so downstream channel hooks work.\n const [resolvedChannelId, setResolvedChannelId] = useState<string | undefined>(params.channelId);\n const [resolveError, setResolveError] = useState<string | null>(null);\n\n useEffect(() => {\n if (resolvedChannelId || !connectionId) return;\n let cancelled = false;\n void (async () => {\n try {\n const client = getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n const result = await client.query<GetConnectionQuery>({\n query: GetConnectionDocument,\n variables: { id: connectionId },\n fetchPolicy: 'network-only',\n });\n if (cancelled) return;\n const conn = result.data?.getConnection;\n if (conn?.channelId) setResolvedChannelId(conn.channelId);\n else setResolveError('Connection not found');\n } catch (err) {\n if (!cancelled)\n setResolveError(err instanceof Error ? err.message : 'Failed to resolve connection');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [connectionId, resolvedChannelId, apiGatewayUrl, authToken, organizationId]);\n\n const channelId = resolvedChannelId;\n const { channel, isLoading, error, runHealthCheck, deleteConnection } = useChannel(channelId);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const [isDeleting, setIsDeleting] = useState(false);\n const [isRunningHealth, setIsRunningHealth] = useState(false);\n const [deleteConfirm, setDeleteConfirm] = useState(false);\n\n const connection = channel?.connection;\n\n // Run health check\n const handleRunHealthCheck = async () => {\n setIsRunningHealth(true);\n try {\n await runHealthCheck();\n } finally {\n setIsRunningHealth(false);\n }\n };\n\n // Delete connection\n const handleDelete = async () => {\n if (!connectionId) return;\n void nativeImpact('medium');\n setIsDeleting(true);\n try {\n // deleteConnection throws on failure; success is guaranteed if it returns.\n await deleteConnection(connectionId);\n void nativeNotify('success');\n navigate(`/channels/${channelId}`);\n } catch (err) {\n void nativeNotify('error');\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to delete connection' } },\n '[channel] Failed to delete connection'\n );\n } finally {\n setIsDeleting(false);\n }\n };\n\n if (isLoading || (!resolvedChannelId && !resolveError)) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 className=\"size-8 animate-spin text-text-secondary\" />\n </div>\n );\n }\n\n if (resolveError || error || !channel) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen gap-4\">\n <AlertCircle className=\"size-12 text-status-error-text\" />\n <h1 className=\"text-xl font-semibold\">\n {tr('channel.channelNotFound', 'Channel not found')}\n </h1>\n <button\n type=\"button\"\n onClick={() => navigate('../../list')}\n className=\"text-primary hover:underline\"\n >\n {tr('channel.backToChannels', 'Back to channels')}\n </button>\n </div>\n );\n }\n\n if (!connection) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen gap-4\">\n <AlertCircle className=\"size-12 text-text-secondary\" />\n <h1 className=\"text-xl font-semibold\">\n {tr('channel.noConnectionFound', 'No connection found')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.noConnectionConfigured',\n 'This channel does not have a connection configured.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"text-primary hover:underline\"\n >\n {tr('channel.backToChannel', 'Back to channel')}\n </button>\n </div>\n );\n }\n\n const health = HEALTH_CONFIG[String(connection.isHealthy)] || HEALTH_CONFIG.false;\n const HealthIcon = health.icon;\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_READ}>\n <div className=\"min-h-screen bg-bg-surface\">\n {/* Header */}\n <div className=\"border-b bg-bg-surface\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"size-5 \" />\n </button>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.connectionDetails', 'Connection Details')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {channel.name} • {channel.platform}\n </p>\n </div>\n </div>\n\n <CTAOverflowMenu\n primary={\n canUpdate\n ? {\n label: tr('channel.editChannel', 'Edit Channel'),\n icon: <Edit className=\"size-4\" aria-hidden=\"true\" />,\n onSelect: () => navigate(`/channels/${channelId}/edit`),\n }\n : undefined\n }\n actions={\n canExecute\n ? [\n {\n label: tr('channel.runHealthCheck', 'Run Health Check'),\n icon: isRunningHealth ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n ),\n disabled: isRunningHealth,\n onSelect: () => void handleRunHealthCheck(),\n },\n ]\n : []\n }\n />\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-5xl space-y-6\">\n {/* Page purpose */}\n <PagePurpose icon={<Shield size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.connectionDetailsPurpose',\n 'The authenticated link between this channel and its provider — its auth type, whether it is active, when it was last tested, and how soon its token expires. Use it to verify the channel can actually send, to spot a token that needs refreshing before it lapses, or to remove a connection you no longer need.'\n )}\n </PagePurpose>\n\n {/* Next-step guidance */}\n <NextSteps\n storageKey=\"channel-connection-detail\"\n steps={[\n ...(canExecute\n ? [\n {\n id: 'health',\n label: tr('channel.nextRunHealthCheck', 'Run a health check'),\n description: tr(\n 'channel.nextConnectionHealthDesc',\n 'Confirm the credentials still authenticate against the provider.'\n ),\n icon: <RefreshCw size={16} aria-hidden=\"true\" />,\n onClick: () => void handleRunHealthCheck(),\n },\n ]\n : []),\n ...(canUpdate\n ? [\n {\n id: 'edit',\n label: tr('channel.nextEditChannel', 'Edit the channel'),\n description: tr(\n 'channel.nextEditChannelDesc',\n 'Update credentials, status or platform metadata for this channel.'\n ),\n icon: <Edit size={16} aria-hidden=\"true\" />,\n onClick: () => navigate(`/channels/${channelId}/edit`),\n },\n ]\n : []),\n {\n id: 'back',\n label: tr('channel.nextBackToChannel', 'Back to the channel'),\n description: tr(\n 'channel.nextBackToChannelDesc',\n 'Return to the channel overview to send a test or review deliveries.'\n ),\n icon: <ArrowLeft size={16} aria-hidden=\"true\" />,\n onClick: () => navigate(`/channels/${channelId}`),\n },\n ]}\n />\n\n {/* Health Status */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div className={health.color}>\n <HealthIcon className=\"size-10 \" />\n </div>\n <div>\n <h2 className=\"text-xl font-semibold\">{health.label}</h2>\n <p className=\"text-sm text-text-secondary\">\n {connection.isHealthy\n ? tr('channel.connectionWorking', 'Connection is working properly')\n : tr('channel.connectionNotWorking', 'Connection is not working properly')}\n </p>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Connection Info */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4 lg:col-span-2\">\n <h3 className=\"font-semibold\">\n {tr('channel.connectionInformation', 'Connection Information')}\n </h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Connection ID</p>\n <p className=\"font-mono text-sm\">{connection.id}</p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Auth Type</p>\n <p className=\"flex items-center gap-2\">\n <Shield className=\"size-4 text-text-secondary\" />\n {connection.authType}\n </p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Status</p>\n <p\n className={\n connection.isActive ? 'text-status-success-text' : 'text-text-secondary'\n }\n >\n {connection.isActive\n ? tr('channel.activeStatus', 'Active')\n : tr('channel.inactiveStatus', 'Inactive')}\n </p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Token Refresh</p>\n <p\n className={\n connection.needsTokenRefresh\n ? 'text-status-warning-text'\n : 'text-text-secondary'\n }\n >\n {connection.needsTokenRefresh\n ? tr('channel.tokenRefreshRequired', 'Required soon')\n : tr('channel.tokenRefreshNotNeeded', 'Not needed')}\n </p>\n </div>\n\n {connection.lastTestedAt && (\n <div>\n <p className=\"text-sm text-text-secondary\">Last Tested</p>\n <p className=\"flex items-center gap-2\">\n <Clock className=\"size-4 text-text-secondary\" />\n {new Date(connection.lastTestedAt).toLocaleString()}\n </p>\n </div>\n )}\n\n {connection.daysUntilExpiry !== null &&\n connection.daysUntilExpiry !== undefined && (\n <div>\n <p className=\"text-sm text-text-secondary\">Days Until Token Expiry</p>\n <p\n className={\n connection.daysUntilExpiry < 30\n ? 'text-status-warning-text'\n : 'text-text-secondary'\n }\n >\n {connection.daysUntilExpiry} days\n </p>\n </div>\n )}\n </div>\n\n {connection.testStatus && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-text-secondary mb-2\">Last Test Status</p>\n <div className=\"p-3 rounded-lg bg-bg-sunken\">\n <p className=\"font-mono text-sm\">{connection.testStatus}</p>\n </div>\n </div>\n )}\n\n {connection.tagAssignments && connection.tagAssignments.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-text-secondary mb-2\">{tr('channel.tags', 'Tags')}</p>\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Connection tags\">\n {connection.tagAssignments.map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"md\" />\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4 lg:col-span-1\">\n <h3 className=\"font-semibold\">{tr('channel.actions', 'Actions')}</h3>\n\n <div className=\"flex flex-col gap-3\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.deleteConnection', 'Delete Connection')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.deleteConnectionWarning',\n 'Permanently remove this connection. The channel will become inactive.'\n )}\n </p>\n </div>\n\n {!deleteConfirm ? (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_DELETE}>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(true)}\n className=\"px-4 py-2 rounded-lg text-status-error-text border border-status-error-border hover:bg-status-error-bg/10 transition-colors flex items-center gap-2\"\n >\n <Trash2 className=\"size-4 \" />\n {tr('channel.delete', 'Delete')}\n </button>\n </PermissionGuard>\n ) : (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.confirmDelete', 'Confirm delete?')}\n </span>\n <button\n type=\"button\"\n onClick={handleDelete}\n disabled={isDeleting}\n className=\"px-4 py-2 rounded-lg bg-status-error-bg text-status-error-text hover:bg-status-error-bg/90 transition-colors flex items-center gap-2\"\n >\n {isDeleting ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Trash2 className=\"size-4 \" />\n )}\n {tr('channel.delete', 'Delete')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(false)}\n className=\"px-4 py-2 rounded-lg border hover:bg-bg-sunken transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConnectionDetailsPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"sourcesContent":["/**\n * Connection Details Page\n *\n * View and manage a specific connection within a channel.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport { reportShellError } from '../../../utils/shellBridge';\nimport { nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n RefreshCw,\n Trash2,\n Edit,\n Shield,\n Clock,\n} from 'lucide-react';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n GlassCard,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useChannel } from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { getGlobalChannelClient } from '../../../graphql/global-client';\nimport {\n GetConnectionDocument,\n type GetConnectionQuery,\n} from '../../../../generated/global-operations';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\nconst HEALTH_CONFIG: Record<\n string,\n { icon: typeof CheckCircle2; color: string; bgColor: string; label: string }\n> = {\n true: {\n icon: CheckCircle2,\n color: 'text-status-success-text',\n bgColor: 'bg-status-success-bg-subtle',\n label: 'Healthy',\n },\n false: {\n icon: XCircle,\n color: 'text-status-error-text',\n bgColor: 'bg-error-500/10',\n label: 'Unhealthy',\n },\n};\n\nexport function ConnectionDetailsPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n const canUpdate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_UPDATE);\n const canExecute = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_EXECUTE);\n const params = useParams<{ channelId?: string; connectionId: string }>();\n const { connectionId } = params;\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n\n // Route may be /channels/connections/:connectionId (no channelId in URL).\n // Resolve channelId from getConnection when absent so downstream channel hooks work.\n const [resolvedChannelId, setResolvedChannelId] = useState<string | undefined>(params.channelId);\n const [resolveError, setResolveError] = useState<string | null>(null);\n\n useEffect(() => {\n if (resolvedChannelId || !connectionId) return;\n let cancelled = false;\n void (async () => {\n try {\n const client = getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n const result = await client.query<GetConnectionQuery>({\n query: GetConnectionDocument,\n variables: { id: connectionId },\n fetchPolicy: 'network-only',\n });\n if (cancelled) return;\n const conn = result.data?.getConnection;\n if (conn?.channelId) setResolvedChannelId(conn.channelId);\n else setResolveError('Connection not found');\n } catch (err) {\n if (!cancelled)\n setResolveError(err instanceof Error ? err.message : 'Failed to resolve connection');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [connectionId, resolvedChannelId, apiGatewayUrl, authToken, organizationId]);\n\n const channelId = resolvedChannelId;\n const { channel, isLoading, error, runHealthCheck, deleteConnection } = useChannel(channelId);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const [isDeleting, setIsDeleting] = useState(false);\n const [isRunningHealth, setIsRunningHealth] = useState(false);\n const [deleteConfirm, setDeleteConfirm] = useState(false);\n\n const connection = channel?.connection;\n\n // Run health check\n const handleRunHealthCheck = async () => {\n setIsRunningHealth(true);\n try {\n await runHealthCheck();\n } finally {\n setIsRunningHealth(false);\n }\n };\n\n // Delete connection\n const handleDelete = async () => {\n if (!connectionId) return;\n void nativeImpact('medium');\n setIsDeleting(true);\n try {\n // deleteConnection throws on failure; success is guaranteed if it returns.\n await deleteConnection(connectionId);\n void nativeNotify('success');\n navigate(`/channels/${channelId}`);\n } catch (err) {\n void nativeNotify('error');\n reportShellError(\n err,\n { module: 'channel', context: { action: 'Failed to delete connection' } },\n '[channel] Failed to delete connection'\n );\n } finally {\n setIsDeleting(false);\n }\n };\n\n if (isLoading || (!resolvedChannelId && !resolveError)) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 className=\"size-8 animate-spin text-text-secondary\" />\n </div>\n );\n }\n\n if (resolveError || error || !channel) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen gap-4\">\n <AlertCircle className=\"size-12 text-status-error-text\" />\n <h1 className=\"text-xl font-semibold\">\n {tr('channel.channelNotFound', 'Channel not found')}\n </h1>\n <button\n type=\"button\"\n onClick={() => navigate('../../list')}\n className=\"text-primary hover:underline\"\n >\n {tr('channel.backToChannels', 'Back to channels')}\n </button>\n </div>\n );\n }\n\n if (!connection) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen gap-4\">\n <AlertCircle className=\"size-12 text-text-secondary\" />\n <h1 className=\"text-xl font-semibold\">\n {tr('channel.noConnectionFound', 'No connection found')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.noConnectionConfigured',\n 'This channel does not have a connection configured.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"text-primary hover:underline\"\n >\n {tr('channel.backToChannel', 'Back to channel')}\n </button>\n </div>\n );\n }\n\n const health = HEALTH_CONFIG[String(connection.isHealthy)] || HEALTH_CONFIG.false;\n const HealthIcon = health.icon;\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_READ}>\n <div className=\"min-h-screen bg-bg-surface\">\n {/* Header */}\n <div className=\"border-b bg-bg-surface\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"size-5 \" />\n </button>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.connectionDetails', 'Connection Details')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {channel.name} • {channel.platform}\n </p>\n </div>\n </div>\n\n <CTAOverflowMenu\n primary={\n canUpdate\n ? {\n label: tr('channel.editChannel', 'Edit Channel'),\n icon: <Edit className=\"size-4\" aria-hidden=\"true\" />,\n onSelect: () => navigate(`/channels/${channelId}/edit`),\n }\n : undefined\n }\n actions={\n canExecute\n ? [\n {\n label: tr('channel.runHealthCheck', 'Run Health Check'),\n icon: isRunningHealth ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n ),\n disabled: isRunningHealth,\n onSelect: () => void handleRunHealthCheck(),\n },\n ]\n : []\n }\n />\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-5xl space-y-6\">\n {/* Page purpose */}\n <PagePurpose icon={<Shield size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.connectionDetailsPurpose',\n 'The authenticated link between this channel and its provider — its auth type, whether it is active, when it was last tested, and how soon its token expires. Use it to verify the channel can actually send, to spot a token that needs refreshing before it lapses, or to remove a connection you no longer need.'\n )}\n </PagePurpose>\n\n {/* Next-step guidance */}\n <NextSteps\n storageKey=\"channel-connection-detail\"\n steps={[\n ...(canExecute\n ? [\n {\n id: 'health',\n label: tr('channel.nextRunHealthCheck', 'Run a health check'),\n description: tr(\n 'channel.nextConnectionHealthDesc',\n 'Confirm the credentials still authenticate against the provider.'\n ),\n icon: <RefreshCw size={16} aria-hidden=\"true\" />,\n onClick: () => void handleRunHealthCheck(),\n },\n ]\n : []),\n ...(canUpdate\n ? [\n {\n id: 'edit',\n label: tr('channel.nextEditChannel', 'Edit the channel'),\n description: tr(\n 'channel.nextEditChannelDesc',\n 'Update credentials, status or platform metadata for this channel.'\n ),\n icon: <Edit size={16} aria-hidden=\"true\" />,\n onClick: () => navigate(`/channels/${channelId}/edit`),\n },\n ]\n : []),\n {\n id: 'back',\n label: tr('channel.nextBackToChannel', 'Back to the channel'),\n description: tr(\n 'channel.nextBackToChannelDesc',\n 'Return to the channel overview to send a test or review deliveries.'\n ),\n icon: <ArrowLeft size={16} aria-hidden=\"true\" />,\n onClick: () => navigate(`/channels/${channelId}`),\n },\n ]}\n />\n\n {/* Health Status */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div className={health.color}>\n <HealthIcon className=\"size-10 \" />\n </div>\n <div>\n <h2 className=\"text-xl font-semibold\">{health.label}</h2>\n <p className=\"text-sm text-text-secondary\">\n {connection.isHealthy\n ? tr('channel.connectionWorking', 'Connection is working properly')\n : tr('channel.connectionNotWorking', 'Connection is not working properly')}\n </p>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Connection Info */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4 lg:col-span-2\">\n <h3 className=\"font-semibold\">\n {tr('channel.connectionInformation', 'Connection Information')}\n </h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Connection ID</p>\n <p className=\"font-mono text-sm\">{connection.id}</p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Auth Type</p>\n <p className=\"flex items-center gap-2\">\n <Shield className=\"size-4 text-text-secondary\" />\n {connection.authType}\n </p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Status</p>\n <p\n className={\n connection.isActive ? 'text-status-success-text' : 'text-text-secondary'\n }\n >\n {connection.isActive\n ? tr('channel.activeStatus', 'Active')\n : tr('channel.inactiveStatus', 'Inactive')}\n </p>\n </div>\n\n <div>\n <p className=\"text-sm text-text-secondary\">Token Refresh</p>\n <p\n className={\n connection.needsTokenRefresh\n ? 'text-status-warning-text'\n : 'text-text-secondary'\n }\n >\n {connection.needsTokenRefresh\n ? tr('channel.tokenRefreshRequired', 'Required soon')\n : tr('channel.tokenRefreshNotNeeded', 'Not needed')}\n </p>\n </div>\n\n {connection.lastTestedAt && (\n <div>\n <p className=\"text-sm text-text-secondary\">Last Tested</p>\n <p className=\"flex items-center gap-2\">\n <Clock className=\"size-4 text-text-secondary\" />\n {new Date(connection.lastTestedAt).toLocaleString()}\n </p>\n </div>\n )}\n\n {connection.daysUntilExpiry !== null &&\n connection.daysUntilExpiry !== undefined && (\n <div>\n <p className=\"text-sm text-text-secondary\">Days Until Token Expiry</p>\n <p\n className={\n connection.daysUntilExpiry < 30\n ? 'text-status-warning-text'\n : 'text-text-secondary'\n }\n >\n {connection.daysUntilExpiry} days\n </p>\n </div>\n )}\n </div>\n\n {connection.testStatus && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-text-secondary mb-2\">Last Test Status</p>\n <div className=\"p-3 rounded-lg bg-bg-sunken\">\n <p className=\"font-mono text-sm\">{connection.testStatus}</p>\n </div>\n </div>\n )}\n\n {connection.tagAssignments && connection.tagAssignments.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-text-secondary mb-2\">{tr('channel.tags', 'Tags')}</p>\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Connection tags\">\n {connection.tagAssignments.map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"md\" />\n ))}\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4 lg:col-span-1\">\n <h3 className=\"font-semibold\">{tr('channel.actions', 'Actions')}</h3>\n\n <div className=\"flex flex-col gap-3\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.deleteConnection', 'Delete Connection')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.deleteConnectionWarning',\n 'Permanently remove this connection. The channel will become inactive.'\n )}\n </p>\n </div>\n\n {!deleteConfirm ? (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_DELETE}>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(true)}\n className=\"px-4 py-2 rounded-lg text-status-error-text border border-status-error-border hover:bg-status-error-bg/10 transition-colors flex items-center gap-2\"\n >\n <Trash2 className=\"size-4 \" />\n {tr('channel.delete', 'Delete')}\n </button>\n </PermissionGuard>\n ) : (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.confirmDelete', 'Confirm delete?')}\n </span>\n <button\n type=\"button\"\n onClick={handleDelete}\n disabled={isDeleting}\n className=\"px-4 py-2 rounded-lg bg-status-error-bg text-status-error-text hover:bg-status-error-bg/90 transition-colors flex items-center gap-2\"\n >\n {isDeleting ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Trash2 className=\"size-4 \" />\n )}\n {tr('channel.delete', 'Delete')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteConfirm(false)}\n className=\"px-4 py-2 rounded-lg border hover:bg-bg-sunken transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { CHANNEL_PERMISSIONS as n } from "../../../constants/permissions.js";
4
4
  import { useEffect as r, useState as i } from "react";
5
5
  import { useI18n as a } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
6
6
  import { AlertCircle as o, ArrowLeft as s, CheckCircle2 as c, Loader2 as l, Plus as u, RefreshCw as d, Settings as f, Shield as p, XCircle as m } from "lucide-react";
7
- import { useNavigate as h } from "react-router-dom";
7
+ import { useNavigate as h } from "react-router";
8
8
  import { usePermissions as g } from "@burdenoff/fe-libs/shared/providers/shell";
9
9
  import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
10
10
  import { EmphasisPanel as b, GlassCard as x, IllustratedEmptyState as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";