@burdenoff/microfe-channel 2026.625.1 → 2026.625.3

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 (29) hide show
  1. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +73 -76
  2. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
  3. package/dist/channel/modules/channels/pages/ChannelDetailsPage.js +407 -353
  4. package/dist/channel/modules/channels/pages/ChannelDetailsPage.js.map +1 -1
  5. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +93 -87
  6. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
  7. package/dist/channel/modules/channels/pages/ChannelUsersPage.js +119 -96
  8. package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
  9. package/dist/channel/modules/channels/pages/ChannelsListPage.js +219 -196
  10. package/dist/channel/modules/channels/pages/ChannelsListPage.js.map +1 -1
  11. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +277 -232
  12. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
  13. package/dist/channel/modules/channels/pages/ConnectionsListPage.js +112 -91
  14. package/dist/channel/modules/channels/pages/ConnectionsListPage.js.map +1 -1
  15. package/dist/channel/modules/channels/pages/CreateChannelPage.js +28 -22
  16. package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
  17. package/dist/channel/modules/channels/pages/CreateConnectionPage.js +72 -65
  18. package/dist/channel/modules/channels/pages/CreateConnectionPage.js.map +1 -1
  19. package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js +101 -93
  20. package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js.map +1 -1
  21. package/dist/channel/modules/channels/pages/EditChannelPage.js +154 -148
  22. package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
  23. package/dist/channel/modules/channels/pages/EditConnectionPage.js +69 -62
  24. package/dist/channel/modules/channels/pages/EditConnectionPage.js.map +1 -1
  25. package/dist/channel/modules/channels/pages/SendMessagePage.js +131 -124
  26. package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
  27. package/dist/channel/modules/channels/pages/WebhookEventsPage.js +152 -129
  28. package/dist/channel/modules/channels/pages/WebhookEventsPage.js.map +1 -1
  29. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectionsListPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ConnectionsListPage.tsx"],"sourcesContent":["/**\n * Connections List Page\n *\n * Displays all connections across channels with their status.\n */\n\nimport { useState, useEffect } 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 {\n ArrowLeft,\n Plus,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n RefreshCw,\n Settings,\n Link,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useChannels } from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\nconst HEALTH_CONFIG: Record<string, { icon: typeof CheckCircle2; color: string; label: string }> = {\n true: { icon: CheckCircle2, color: 'text-status-success-text', label: 'Healthy' },\n false: { icon: XCircle, color: 'text-status-error-text', label: 'Unhealthy' },\n};\n\nexport function ConnectionsListPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n // Creating connections is admin-only (channel-connection:create) — hide otherwise.\n const canConnectionCreate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_CREATE);\n const { channels, isLoading, error, refetch } = useChannels();\n\n // Poll for connection status updates every 60 s.\n // connectionStatusChanged subscription is scoped to a single channelId;\n // this page shows all connections, so polling is used instead.\n useEffect(() => {\n const timer = setInterval(() => {\n void refetch();\n }, 60_000);\n return () => clearInterval(timer);\n }, [refetch]);\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 // Filter state\n const [healthFilter, setHealthFilter] = useState<string>('all');\n const [platformFilter, setPlatformFilter] = useState<string>('all');\n const pageSize = 10;\n const [currentPage, setCurrentPage] = useState(0);\n\n // Get all connections from channels (single-pass; avoids filter().map() double traversal)\n const connections = channels.reduce<\n Array<{\n channelId: string;\n channelName: string;\n platform: (typeof channels)[number]['platform'];\n adapterType: (typeof channels)[number]['adapterType'];\n channelStatus: (typeof channels)[number]['status'];\n connectionId: string;\n isActive: boolean;\n isHealthy: boolean;\n needsTokenRefresh: boolean;\n lastTestedAt: string | null | undefined;\n authType: NonNullable<(typeof channels)[number]['connection']>['authType'];\n tagAssignments: NonNullable<\n NonNullable<(typeof channels)[number]['connection']>['tagAssignments']\n >;\n }>\n >((acc, ch) => {\n const conn = ch.connection;\n if (!conn) return acc;\n acc.push({\n channelId: ch.id,\n channelName: ch.name,\n platform: ch.platform,\n adapterType: ch.adapterType,\n channelStatus: ch.status,\n connectionId: conn.id,\n isActive: conn.isActive,\n isHealthy: conn.isHealthy,\n needsTokenRefresh: conn.needsTokenRefresh,\n lastTestedAt: conn.lastTestedAt,\n authType: conn.authType,\n tagAssignments: conn.tagAssignments ?? [],\n });\n return acc;\n }, []);\n\n // Get unique platforms\n const platforms = [...new Set(connections.map((c) => c.platform))];\n\n // Apply filters\n const filteredConnections = connections.filter((conn) => {\n if (healthFilter === 'healthy' && !conn.isHealthy) return false;\n if (healthFilter === 'unhealthy' && conn.isHealthy) return false;\n if (platformFilter !== 'all' && conn.platform !== platformFilter) return false;\n return true;\n });\n\n const totalPages = Math.max(1, Math.ceil(filteredConnections.length / pageSize));\n const pagedConnections = filteredConnections.slice(\n currentPage * pageSize,\n (currentPage + 1) * pageSize\n );\n\n if (isLoading) {\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 (error) {\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.loadConnectionsFailed', 'Failed to load connections')}\n </h1>\n <p className=\"text-sm text-text-secondary\">{error}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr('channel.retry', 'Retry')}\n </button>\n </div>\n );\n }\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\" data-tour=\"channels-connections-header\">\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('./list')}\n aria-label={tr('channel.backToChannels', 'Back to channels')}\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\">{tr('channel.connections', 'Connections')}</h1>\n <p className=\"text-sm text-text-secondary\">\n {filteredConnections.length} connection\n {filteredConnections.length !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n {canConnectionCreate && (\n <button\n type=\"button\"\n onClick={() => navigate('./create')}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors flex items-center gap-2\"\n >\n <Plus className=\"size-4 \" />\n {tr('channel.newConnection', 'New Connection')}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"container mx-auto px-6 py-4\" data-tour=\"channels-connections-filters\">\n <div className=\"flex items-center gap-4\">\n <select\n value={platformFilter}\n onChange={(e) => setPlatformFilter(e.target.value)}\n className=\"px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">{tr('channel.allPlatforms', 'All Platforms')}</option>\n {platforms.map((p) => (\n <option key={p} value={p}>\n {p}\n </option>\n ))}\n </select>\n\n <select\n value={healthFilter}\n onChange={(e) => setHealthFilter(e.target.value)}\n className=\"px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">{tr('channel.allStatus', 'All Status')}</option>\n <option value=\"healthy\">{tr('channel.healthy', 'Healthy')}</option>\n <option value=\"unhealthy\">{tr('channel.unhealthy', 'Unhealthy')}</option>\n </select>\n\n <button\n type=\"button\"\n onClick={() => refetch()}\n aria-label={tr('channel.refresh', 'Refresh')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <RefreshCw className=\"size-5 \" aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n\n {/* Connections List */}\n <div className=\"container mx-auto px-6 pb-8\">\n {filteredConnections.length === 0 ? (\n <div className=\"text-center py-12\">\n <Link className=\"size-12 mx-auto text-text-secondary mb-4\" />\n <h2 className=\"text-lg font-semibold mb-2\">\n {tr('channel.noConnectionsFound', 'No connections found')}\n </h2>\n <p className=\"text-sm text-text-secondary mb-4\">\n {channels.length === 0\n ? tr('channel.createChannelFirst', 'Create a channel first to add connections')\n : tr('channel.noConnectionsMatchFilter', 'No connections match your filters')}\n </p>\n {channels.length === 0 ? (\n canConnectionCreate ? (\n <button\n type=\"button\"\n onClick={() => navigate('./create')}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr('channel.createConnection', 'Create Connection')}\n </button>\n ) : null\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n setHealthFilter('all');\n setPlatformFilter('all');\n }}\n className=\"px-4 py-2 rounded-lg border hover:bg-bg-sunken\"\n >\n {tr('channel.clearFilters', 'Clear Filters')}\n </button>\n )}\n </div>\n ) : (\n <>\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam rounded-lg p-0 divide-y divide-[var(--color-border-seam)] overflow-hidden\"\n >\n {pagedConnections.map((conn) => {\n const health = HEALTH_CONFIG[String(conn.isHealthy)] || HEALTH_CONFIG.false;\n const HealthIcon = health.icon;\n\n return (\n <div\n key={conn.connectionId}\n role=\"button\"\n tabIndex={0}\n className=\"p-4 hover:bg-bg-sunken/50 transition-colors cursor-pointer\"\n onClick={() => navigate(`./${conn.channelId}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`./${conn.channelId}`);\n }\n }}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div className={health.color} aria-label={health.label}>\n <HealthIcon className=\"size-5 \" aria-hidden=\"true\" />\n </div>\n <div>\n <p className=\"font-medium\">{conn.channelName}</p>\n <p className=\"text-sm text-text-secondary\">\n {conn.platform} • {conn.adapterType} • {conn.authType}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <div className=\"text-right\">\n <p className=\"text-sm\">\n {conn.channelStatus === 'ACTIVE' ? (\n <span className=\"text-status-success-text\">\n {tr('channel.activeStatus', 'Active')}\n </span>\n ) : conn.channelStatus === 'INACTIVE' ? (\n <span className=\"text-text-secondary\">\n {tr('channel.inactiveStatus', 'Inactive')}\n </span>\n ) : conn.channelStatus === 'ERROR' ? (\n <span className=\"text-status-error-text\">\n {tr('channel.errorStatus', 'Error')}\n </span>\n ) : conn.channelStatus === 'SUSPENDED' ? (\n <span className=\"text-status-warning-text\">\n {tr('channel.suspendedStatus', 'Suspended')}\n </span>\n ) : conn.channelStatus === 'ARCHIVED' ? (\n <span className=\"text-text-secondary\">\n {tr('channel.archivedStatus', 'Archived')}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr('channel.inactiveStatus', 'Inactive')}\n </span>\n )}\n </p>\n {conn.lastTestedAt && (\n <p className=\"text-xs text-text-secondary\">\n Last tested: {new Date(conn.lastTestedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n\n {conn.needsTokenRefresh && (\n <span className=\"px-2 py-1 text-xs bg-status-warning-bg-subtle text-status-warning-text rounded\">\n Token Expires\n </span>\n )}\n\n {conn.tagAssignments.length > 0 && (\n <div className=\"flex flex-wrap gap-1\" aria-label=\"Connection tags\">\n {conn.tagAssignments.slice(0, 3).map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"sm\" />\n ))}\n {conn.tagAssignments.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{conn.tagAssignments.length - 3}\n </span>\n )}\n </div>\n )}\n\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/channels/${conn.channelId}`);\n }}\n aria-label={tr('channel.viewChannelSettings', 'View channel settings')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <Settings className=\"size-4 \" />\n </button>\n </div>\n </div>\n </div>\n );\n })}\n </GlassCard>\n {/* Pagination */}\n {filteredConnections.length > pageSize && (\n <div className=\"flex items-center justify-between px-4 py-3 border border-border-seam rounded-lg mt-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * pageSize + 1}–\n {Math.min((currentPage + 1) * pageSize, filteredConnections.length)}\n </span>{' '}\n {tr('channel.of', 'of')}{' '}\n <span className=\"font-medium\">{filteredConnections.length}</span>\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 </>\n )}\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConnectionsListPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ConnectionsListPage.tsx"],"sourcesContent":["/**\n * Connections List Page\n *\n * Displays all connections across channels with their status.\n */\n\nimport { useState, useEffect } 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 {\n ArrowLeft,\n Plus,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n RefreshCw,\n Settings,\n Shield,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useChannels } from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\nconst HEALTH_CONFIG: Record<string, { icon: typeof CheckCircle2; color: string; label: string }> = {\n true: { icon: CheckCircle2, color: 'text-status-success-text', label: 'Healthy' },\n false: { icon: XCircle, color: 'text-status-error-text', label: 'Unhealthy' },\n};\n\nexport function ConnectionsListPage() {\n const navigate = useNavigate();\n const { hasPermission } = usePermissions();\n // Creating connections is admin-only (channel-connection:create) — hide otherwise.\n const canConnectionCreate = hasPermission(CHANNEL_PERMISSIONS.CHANNEL_CONNECTION_CREATE);\n const { channels, isLoading, error, refetch } = useChannels();\n\n // Poll for connection status updates every 60 s.\n // connectionStatusChanged subscription is scoped to a single channelId;\n // this page shows all connections, so polling is used instead.\n useEffect(() => {\n const timer = setInterval(() => {\n void refetch();\n }, 60_000);\n return () => clearInterval(timer);\n }, [refetch]);\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 // Filter state\n const [healthFilter, setHealthFilter] = useState<string>('all');\n const [platformFilter, setPlatformFilter] = useState<string>('all');\n const pageSize = 10;\n const [currentPage, setCurrentPage] = useState(0);\n\n // Get all connections from channels (single-pass; avoids filter().map() double traversal)\n const connections = channels.reduce<\n Array<{\n channelId: string;\n channelName: string;\n platform: (typeof channels)[number]['platform'];\n adapterType: (typeof channels)[number]['adapterType'];\n channelStatus: (typeof channels)[number]['status'];\n connectionId: string;\n isActive: boolean;\n isHealthy: boolean;\n needsTokenRefresh: boolean;\n lastTestedAt: string | null | undefined;\n authType: NonNullable<(typeof channels)[number]['connection']>['authType'];\n tagAssignments: NonNullable<\n NonNullable<(typeof channels)[number]['connection']>['tagAssignments']\n >;\n }>\n >((acc, ch) => {\n const conn = ch.connection;\n if (!conn) return acc;\n acc.push({\n channelId: ch.id,\n channelName: ch.name,\n platform: ch.platform,\n adapterType: ch.adapterType,\n channelStatus: ch.status,\n connectionId: conn.id,\n isActive: conn.isActive,\n isHealthy: conn.isHealthy,\n needsTokenRefresh: conn.needsTokenRefresh,\n lastTestedAt: conn.lastTestedAt,\n authType: conn.authType,\n tagAssignments: conn.tagAssignments ?? [],\n });\n return acc;\n }, []);\n\n // Get unique platforms\n const platforms = [...new Set(connections.map((c) => c.platform))];\n\n // Connections that are unhealthy or have a token expiring soon — the single\n // most important thing to act on, surfaced in the page's emphasis zone.\n const connectionsNeedingAttention = connections.filter(\n (c) => !c.isHealthy || c.needsTokenRefresh\n ).length;\n\n // Apply filters\n const filteredConnections = connections.filter((conn) => {\n if (healthFilter === 'healthy' && !conn.isHealthy) return false;\n if (healthFilter === 'unhealthy' && conn.isHealthy) return false;\n if (platformFilter !== 'all' && conn.platform !== platformFilter) return false;\n return true;\n });\n\n const totalPages = Math.max(1, Math.ceil(filteredConnections.length / pageSize));\n const pagedConnections = filteredConnections.slice(\n currentPage * pageSize,\n (currentPage + 1) * pageSize\n );\n\n if (isLoading) {\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 (error) {\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.loadConnectionsFailed', 'Failed to load connections')}\n </h1>\n <p className=\"text-sm text-text-secondary\">{error}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr('channel.retry', 'Retry')}\n </button>\n </div>\n );\n }\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\" data-tour=\"channels-connections-header\">\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('./list')}\n aria-label={tr('channel.backToChannels', 'Back to channels')}\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\">{tr('channel.connections', 'Connections')}</h1>\n <p className=\"text-sm text-text-secondary\">\n {filteredConnections.length} connection\n {filteredConnections.length !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n {canConnectionCreate && (\n <button\n type=\"button\"\n onClick={() => navigate('./create')}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors flex items-center gap-2\"\n >\n <Plus className=\"size-4 \" />\n {tr('channel.newConnection', 'New Connection')}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Page purpose + emphasis */}\n <div className=\"container mx-auto space-y-4 px-6 pt-4\">\n <PagePurpose icon={<Shield size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.connectionsPurpose',\n \"A connection is the authenticated link — the OAuth grant, API key or credentials — that lets a channel actually reach its provider. This page lists every channel's connection with its live health and token status. Use it to confirm channels are authenticated and to catch tokens that are about to expire before deliveries start failing.\"\n )}\n </PagePurpose>\n {connectionsNeedingAttention > 0 && (\n <EmphasisPanel className=\"flex items-center gap-3 p-4 shadow-[var(--shadow-glow)]\">\n <span className=\"rounded-lg bg-status-warning-bg-subtle p-2\">\n <AlertCircle size={18} className=\"text-status-warning-text\" aria-hidden=\"true\" />\n </span>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {connectionsNeedingAttention}{' '}\n {connectionsNeedingAttention === 1\n ? tr('channel.connectionNeedsAttention', 'connection needs attention')\n : tr('channel.connectionsNeedAttention', 'connections need attention')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.connectionsNeedAttentionHint',\n 'These connections are unhealthy or have tokens expiring soon. Refresh or re-authenticate them to keep deliveries flowing.'\n )}\n </p>\n </div>\n </EmphasisPanel>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"container mx-auto px-6 py-4\" data-tour=\"channels-connections-filters\">\n <div className=\"flex items-center gap-4\">\n <select\n value={platformFilter}\n onChange={(e) => setPlatformFilter(e.target.value)}\n className=\"px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">{tr('channel.allPlatforms', 'All Platforms')}</option>\n {platforms.map((p) => (\n <option key={p} value={p}>\n {p}\n </option>\n ))}\n </select>\n\n <select\n value={healthFilter}\n onChange={(e) => setHealthFilter(e.target.value)}\n className=\"px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">{tr('channel.allStatus', 'All Status')}</option>\n <option value=\"healthy\">{tr('channel.healthy', 'Healthy')}</option>\n <option value=\"unhealthy\">{tr('channel.unhealthy', 'Unhealthy')}</option>\n </select>\n\n <button\n type=\"button\"\n onClick={() => refetch()}\n aria-label={tr('channel.refresh', 'Refresh')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <RefreshCw className=\"size-5 \" aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n\n {/* Connections List */}\n <div className=\"container mx-auto px-6 pb-8\">\n {filteredConnections.length === 0 ? (\n <IllustratedEmptyState\n illustration={channels.length === 0 ? 'empty-data' : 'empty-search'}\n title={tr('channel.noConnectionsFound', 'No connections found')}\n description={\n channels.length === 0\n ? tr('channel.createChannelFirst', 'Create a channel first to add connections')\n : tr('channel.noConnectionsMatchFilter', 'No connections match your filters')\n }\n action={\n channels.length === 0 ? (\n canConnectionCreate ? (\n <button\n type=\"button\"\n onClick={() => navigate('./create')}\n className=\"px-4 py-2 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr('channel.createConnection', 'Create Connection')}\n </button>\n ) : undefined\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n setHealthFilter('all');\n setPlatformFilter('all');\n }}\n className=\"px-4 py-2 rounded-lg border hover:bg-bg-sunken\"\n >\n {tr('channel.clearFilters', 'Clear Filters')}\n </button>\n )\n }\n />\n ) : (\n <>\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam rounded-lg p-0 divide-y divide-[var(--color-border-seam)] overflow-hidden\"\n >\n {pagedConnections.map((conn) => {\n const health = HEALTH_CONFIG[String(conn.isHealthy)] || HEALTH_CONFIG.false;\n const HealthIcon = health.icon;\n\n return (\n <div\n key={conn.connectionId}\n role=\"button\"\n tabIndex={0}\n className=\"p-4 hover:bg-bg-sunken/50 transition-colors cursor-pointer\"\n onClick={() => navigate(`./${conn.channelId}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`./${conn.channelId}`);\n }\n }}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div\n className={`${health.color} ${\n conn.isHealthy ? 'motion-safe:animate-vc-glow-pulse' : ''\n }`}\n aria-label={health.label}\n >\n <HealthIcon className=\"size-5 \" aria-hidden=\"true\" />\n </div>\n <div>\n <p className=\"font-medium\">{conn.channelName}</p>\n <p className=\"text-sm text-text-secondary\">\n {conn.platform} • {conn.adapterType} • {conn.authType}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <div className=\"text-right\">\n <p className=\"text-sm\">\n {conn.channelStatus === 'ACTIVE' ? (\n <span className=\"text-status-success-text\">\n {tr('channel.activeStatus', 'Active')}\n </span>\n ) : conn.channelStatus === 'INACTIVE' ? (\n <span className=\"text-text-secondary\">\n {tr('channel.inactiveStatus', 'Inactive')}\n </span>\n ) : conn.channelStatus === 'ERROR' ? (\n <span className=\"text-status-error-text\">\n {tr('channel.errorStatus', 'Error')}\n </span>\n ) : conn.channelStatus === 'SUSPENDED' ? (\n <span className=\"text-status-warning-text\">\n {tr('channel.suspendedStatus', 'Suspended')}\n </span>\n ) : conn.channelStatus === 'ARCHIVED' ? (\n <span className=\"text-text-secondary\">\n {tr('channel.archivedStatus', 'Archived')}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr('channel.inactiveStatus', 'Inactive')}\n </span>\n )}\n </p>\n {conn.lastTestedAt && (\n <p className=\"text-xs text-text-secondary\">\n Last tested: {new Date(conn.lastTestedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n\n {conn.needsTokenRefresh && (\n <span className=\"px-2 py-1 text-xs bg-status-warning-bg-subtle text-status-warning-text rounded\">\n Token Expires\n </span>\n )}\n\n {conn.tagAssignments.length > 0 && (\n <div className=\"flex flex-wrap gap-1\" aria-label=\"Connection tags\">\n {conn.tagAssignments.slice(0, 3).map((a) => (\n <TagBadge key={a.id} id={a.tagId} label={a.tagId} size=\"sm\" />\n ))}\n {conn.tagAssignments.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{conn.tagAssignments.length - 3}\n </span>\n )}\n </div>\n )}\n\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/channels/${conn.channelId}`);\n }}\n aria-label={tr('channel.viewChannelSettings', 'View channel settings')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <Settings className=\"size-4 \" />\n </button>\n </div>\n </div>\n </div>\n );\n })}\n </GlassCard>\n {/* Pagination */}\n {filteredConnections.length > pageSize && (\n <div className=\"flex items-center justify-between px-4 py-3 border border-border-seam rounded-lg mt-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.showing', 'Showing')}{' '}\n <span className=\"font-medium\">\n {currentPage * pageSize + 1}–\n {Math.min((currentPage + 1) * pageSize, filteredConnections.length)}\n </span>{' '}\n {tr('channel.of', 'of')}{' '}\n <span className=\"font-medium\">{filteredConnections.length}</span>\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 </>\n )}\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -7,9 +7,9 @@ import { useI18n as a } from "@burdenoff/fe-libs/shared/providers/shell/I18nProv
7
7
  import { AlertCircle as o, ArrowLeft as s, Bell as c, CheckCircle2 as l, Globe as u, Hash as d, Inbox as f, Loader2 as p, Mail as m, MessageCircle as h, MessageSquare as g, Radio as _, Save as v, Send as y, Smartphone as b, Users as x } from "lucide-react";
8
8
  import { useNavigate as S } from "react-router-dom";
9
9
  import { jsx as C, jsxs as w } from "react/jsx-runtime";
10
- import { GlassCard as T, OptionalFields as E } from "@burdenoff/fe-libs/ui";
10
+ import { EmphasisPanel as T, GlassCard as E, OptionalFields as D, PagePurpose as O } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/channel/modules/channels/pages/CreateChannelPage.tsx
12
- var D = [
12
+ var k = [
13
13
  {
14
14
  value: "EMAIL",
15
15
  label: "Email",
@@ -127,11 +127,11 @@ var D = [
127
127
  }]
128
128
  }
129
129
  ];
130
- function O() {
130
+ function A() {
131
131
  let c = S(), { organizationId: u } = e(), { createChannel: d } = t(), { t: f } = a(), m = (e, t) => {
132
132
  let n = f(e);
133
133
  return n === e ? t : n;
134
- }, [h, g] = i(!1), [y, b] = i(null), [x, O] = i({
134
+ }, [h, g] = i(!1), [y, b] = i(null), [x, A] = i({
135
135
  name: "",
136
136
  platform: "EMAIL",
137
137
  description: "",
@@ -139,8 +139,8 @@ function O() {
139
139
  status: "ACTIVE",
140
140
  priority: 100,
141
141
  platformConfig: ""
142
- }), k = D.find((e) => e.value === x.platform)?.adapters || [], A = D.find((e) => e.value === x.platform), j = (e) => {
143
- O((t) => ({
142
+ }), j = k.find((e) => e.value === x.platform)?.adapters || [], M = k.find((e) => e.value === x.platform), N = (e) => {
143
+ A((t) => ({
144
144
  ...t,
145
145
  platform: e,
146
146
  adapterType: ""
@@ -172,6 +172,13 @@ function O() {
172
172
  })] })]
173
173
  })
174
174
  }),
175
+ /* @__PURE__ */ C(O, {
176
+ icon: /* @__PURE__ */ C(_, {
177
+ size: 15,
178
+ "aria-hidden": "true"
179
+ }),
180
+ children: m("channel.createChannelPurpose", "Set up a new way to reach people — for example a transactional email sender, an SMS gateway, a Slack workspace or a push provider. Name it, pick the platform and provider, and you will get a channel you can then authenticate and send through. Create one channel per provider account you want messages to flow through.")
181
+ }),
175
182
  y && /* @__PURE__ */ w("div", {
176
183
  className: "flex items-center gap-3 p-3 bg-error-500/10 border border-status-error-border/30 rounded-lg",
177
184
  children: [/* @__PURE__ */ C(o, {
@@ -223,8 +230,7 @@ function O() {
223
230
  /* @__PURE__ */ w("div", {
224
231
  className: "grid gap-5 lg:grid-cols-2",
225
232
  children: [/* @__PURE__ */ w(T, {
226
- treatment: "plain",
227
- className: "p-5 border-border-seam space-y-4",
233
+ className: "p-5 space-y-4",
228
234
  children: [
229
235
  /* @__PURE__ */ C("h2", {
230
236
  className: "text-sm font-semibold text-text-primary uppercase tracking-wider",
@@ -244,7 +250,7 @@ function O() {
244
250
  "aria-label": m("channel.namePlaceholder", "e.g., Primary Email, SMS Alerts"),
245
251
  type: "text",
246
252
  value: x.name,
247
- onChange: (e) => O((t) => ({
253
+ onChange: (e) => A((t) => ({
248
254
  ...t,
249
255
  name: e.target.value
250
256
  })),
@@ -257,7 +263,7 @@ function O() {
257
263
  }), /* @__PURE__ */ C("textarea", {
258
264
  "aria-label": m("channel.descriptionPlaceholder", "Optional description of this channel's purpose"),
259
265
  value: x.description,
260
- onChange: (e) => O((t) => ({
266
+ onChange: (e) => A((t) => ({
261
267
  ...t,
262
268
  description: e.target.value
263
269
  })),
@@ -266,7 +272,7 @@ function O() {
266
272
  placeholder: m("channel.descriptionPlaceholder", "Optional description of this channel's purpose")
267
273
  })] })
268
274
  ]
269
- }), /* @__PURE__ */ w(T, {
275
+ }), /* @__PURE__ */ w(E, {
270
276
  treatment: "plain",
271
277
  className: "p-5 border-border-seam space-y-4",
272
278
  children: [
@@ -281,7 +287,7 @@ function O() {
281
287
  }),
282
288
  /* @__PURE__ */ w("select", {
283
289
  value: x.adapterType,
284
- onChange: (e) => O((t) => ({
290
+ onChange: (e) => A((t) => ({
285
291
  ...t,
286
292
  adapterType: e.target.value
287
293
  })),
@@ -289,14 +295,14 @@ function O() {
289
295
  children: [/* @__PURE__ */ C("option", {
290
296
  value: "",
291
297
  children: m("channel.selectProvider", "Select a provider (optional)")
292
- }), k.map((e) => /* @__PURE__ */ C("option", {
298
+ }), j.map((e) => /* @__PURE__ */ C("option", {
293
299
  value: e.value,
294
300
  children: e.label
295
301
  }, e.value))]
296
302
  }),
297
303
  /* @__PURE__ */ C("p", {
298
304
  className: "mt-1 text-[11px] text-text-secondary",
299
- children: A ? m("channel.providersAvailableForPlatform", `${k.length} providers available for ${A.label}`) : m("channel.selectPlatformFirst", "Select a platform first")
305
+ children: M ? m("channel.providersAvailableForPlatform", `${j.length} providers available for ${M.label}`) : m("channel.selectPlatformFirst", "Select a platform first")
300
306
  })
301
307
  ] }),
302
308
  /* @__PURE__ */ w("div", {
@@ -308,7 +314,7 @@ function O() {
308
314
  }),
309
315
  /* @__PURE__ */ w("select", {
310
316
  value: x.status,
311
- onChange: (e) => O((t) => ({
317
+ onChange: (e) => A((t) => ({
312
318
  ...t,
313
319
  status: e.target.value
314
320
  })),
@@ -336,7 +342,7 @@ function O() {
336
342
  "aria-label": m("channel.priority", "Priority"),
337
343
  type: "number",
338
344
  value: x.priority,
339
- onChange: (e) => O((t) => ({
345
+ onChange: (e) => A((t) => ({
340
346
  ...t,
341
347
  priority: parseInt(e.target.value) || 0
342
348
  })),
@@ -353,7 +359,7 @@ function O() {
353
359
  ]
354
360
  })]
355
361
  }),
356
- /* @__PURE__ */ w(T, {
362
+ /* @__PURE__ */ w(E, {
357
363
  treatment: "plain",
358
364
  className: "p-5 border-border-seam space-y-4",
359
365
  children: [/* @__PURE__ */ w("h2", {
@@ -368,11 +374,11 @@ function O() {
368
374
  ]
369
375
  }), /* @__PURE__ */ C("div", {
370
376
  className: "grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5",
371
- children: D.map((e) => {
377
+ children: k.map((e) => {
372
378
  let t = e.icon, n = x.platform === e.value;
373
379
  return /* @__PURE__ */ w("button", {
374
380
  type: "button",
375
- onClick: () => j(e.value),
381
+ onClick: () => N(e.value),
376
382
  className: `relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${n ? `${e.color} border-current ring-1 ring-current` : "border-border-subtle hover:bg-surface-hover text-text-secondary hover:text-text-primary"}`,
377
383
  children: [
378
384
  n && /* @__PURE__ */ C(l, {
@@ -393,7 +399,7 @@ function O() {
393
399
  })
394
400
  })]
395
401
  }),
396
- /* @__PURE__ */ C(E, {
402
+ /* @__PURE__ */ C(D, {
397
403
  title: m("channel.advancedOptions", "Advanced settings"),
398
404
  children: /* @__PURE__ */ C("div", {
399
405
  className: "grid gap-4 lg:grid-cols-2",
@@ -412,7 +418,7 @@ function O() {
412
418
  /* @__PURE__ */ C("textarea", {
413
419
  "aria-label": m("channel.platformConfig", "Platform Config"),
414
420
  value: x.platformConfig,
415
- onChange: (e) => O((t) => ({
421
+ onChange: (e) => A((t) => ({
416
422
  ...t,
417
423
  platformConfig: e.target.value
418
424
  })),
@@ -451,6 +457,6 @@ function O() {
451
457
  });
452
458
  }
453
459
  //#endregion
454
- export { O as CreateChannelPage };
460
+ export { A as CreateChannelPage };
455
461
 
456
462
  //# sourceMappingURL=CreateChannelPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CreateChannelPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/CreateChannelPage.tsx"],"sourcesContent":["/**\n * Create Channel Page\n *\n * Form fields match the exact CreateChannelInput from global-channel-svc:\n * name (required), platform (required), organizationId (auto), description,\n * adapterType, status, priority, platformConfig\n *\n * organizationId is injected from shell context — not shown in form.\n * Credentials belong to createConnection mutation — not part of channel creation.\n */\n\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { GlassCard, OptionalFields } from '@burdenoff/fe-libs/ui';\nimport {\n Radio,\n ArrowLeft,\n Save,\n Loader2,\n AlertCircle,\n Mail,\n Smartphone,\n Bell,\n Inbox,\n Globe,\n Hash,\n Users,\n MessageCircle,\n Send,\n MessageSquare,\n CheckCircle2,\n} from 'lucide-react';\nimport { useChannel } from '../../../providers/ChannelProvider';\nimport {\n useChannels,\n type ChannelPlatform,\n type AdapterType,\n type ChannelStatus,\n type CreateChannelInput,\n} from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\n// ============================================================================\n// PLATFORM CONFIG\n// ============================================================================\n\n// NOTE: AdapterType values MUST match the backend Prisma schema exactly.\n// See: global-channel-svc/prisma/schema/schema.prisma (enum AdapterType)\nconst PLATFORM_OPTIONS: {\n value: ChannelPlatform;\n label: string;\n description: string;\n icon: typeof Mail;\n color: string;\n adapters: { value: AdapterType; label: string }[];\n}[] = [\n {\n value: 'EMAIL',\n label: 'Email',\n description: 'Send emails via various providers',\n icon: Mail,\n color: 'text-accent-blue bg-accent-blue-subtle border-accent-blue-subtle',\n adapters: [\n { value: 'AWS_SES', label: 'AWS SES' },\n { value: 'AZURE_ACS', label: 'Azure Communication Services' },\n ],\n },\n {\n value: 'SMS',\n label: 'SMS',\n description: 'Send text messages',\n icon: Smartphone,\n color: 'text-status-success-text bg-status-success-bg-subtle border-status-success-bg-subtle',\n adapters: [\n { value: 'AWS_SNS', label: 'AWS SNS' },\n { value: 'TWILIO_SMS', label: 'Twilio SMS' },\n ],\n },\n {\n value: 'PUSH',\n label: 'Push',\n description: 'Mobile & web push',\n icon: Bell,\n color: 'text-accent-purple bg-accent-purple-subtle border-accent-purple-subtle',\n adapters: [{ value: 'FCM', label: 'Firebase Cloud Messaging' }],\n },\n {\n value: 'INAPP',\n label: 'In-App',\n description: 'In-app notifications',\n icon: Inbox,\n color: 'text-accent-indigo bg-accent-indigo-subtle border-accent-indigo-subtle',\n adapters: [{ value: 'INAPP_NATIVE', label: 'Native In-App' }],\n },\n {\n value: 'SLACK',\n label: 'Slack',\n description: 'Workspace notifications',\n icon: Hash,\n color: 'text-accent-pink bg-accent-pink-subtle border-accent-pink-subtle',\n adapters: [{ value: 'SLACK_BOT', label: 'Slack Bot' }],\n },\n {\n value: 'TEAMS',\n label: 'Teams',\n description: 'Teams notifications',\n icon: Users,\n color: 'text-accent-cyan bg-accent-cyan-subtle border-accent-cyan-subtle',\n adapters: [{ value: 'TEAMS_BOT', label: 'Teams Bot' }],\n },\n {\n value: 'DISCORD',\n label: 'Discord',\n description: 'Server notifications',\n icon: MessageCircle,\n color: 'text-accent-violet bg-accent-violet-subtle border-accent-violet-subtle',\n adapters: [{ value: 'DISCORD_BOT', label: 'Discord Bot' }],\n },\n {\n value: 'WHATSAPP',\n label: 'WhatsApp',\n description: 'Business messaging',\n icon: Send,\n color: 'text-accent-emerald bg-accent-emerald-subtle border-accent-emerald-subtle',\n adapters: [\n { value: 'WHATSAPP_CLOUD', label: 'WhatsApp Cloud API' },\n { value: 'WHATSAPP_TWILIO', label: 'WhatsApp via Twilio' },\n ],\n },\n {\n value: 'WEBCHAT',\n label: 'Webchat',\n description: 'Chat widgets',\n icon: MessageSquare,\n color: 'text-accent-teal bg-accent-teal-subtle border-accent-teal-subtle',\n adapters: [], // No WEBCHAT adapters in backend Prisma schema yet\n },\n {\n value: 'WEBHOOK',\n label: 'Webhook',\n description: 'HTTP webhooks',\n icon: Globe,\n color: 'text-accent-orange bg-accent-orange-subtle border-accent-orange-subtle',\n adapters: [{ value: 'WEBHOOK_HTTP', label: 'HTTP Webhook' }],\n },\n];\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function CreateChannelPage() {\n const navigate = useNavigate();\n const { organizationId } = useChannel();\n const { createChannel } = useChannels();\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 [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Form state — matches CreateChannelInput exactly\n const [formData, setFormData] = useState<{\n name: string; // required\n platform: ChannelPlatform; // required\n description: string; // optional\n adapterType: AdapterType | ''; // required (auto-selected based on platform)\n status: ChannelStatus; // optional — default ACTIVE on server\n priority: number; // optional — default 100 on server\n platformConfig: string; // optional — JSON string\n }>({\n name: '',\n platform: 'EMAIL',\n description: '',\n adapterType: 'AWS_SES', // Default to AWS SES for Email platform\n status: 'ACTIVE',\n priority: 100,\n platformConfig: '',\n });\n\n const availableAdapters =\n PLATFORM_OPTIONS.find((p) => p.value === formData.platform)?.adapters || [];\n\n const selectedPlatform = PLATFORM_OPTIONS.find((p) => p.value === formData.platform);\n\n const handlePlatformChange = (platform: ChannelPlatform) => {\n setFormData((prev) => ({ ...prev, platform, adapterType: '' }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!formData.name.trim()) {\n setError(tr('channel.validation.nameRequired', 'Channel name is required'));\n return;\n }\n\n if (!organizationId) {\n setError(\n tr(\n 'channel.validation.organizationRequired',\n 'Organization context is required. Please ensure an organization is selected.'\n )\n );\n return;\n }\n\n // Validate platformConfig JSON if provided\n let parsedPlatformConfig: Record<string, unknown> | undefined;\n if (formData.platformConfig.trim()) {\n try {\n parsedPlatformConfig = JSON.parse(formData.platformConfig);\n } catch {\n setError(tr('channel.validation.platformConfigJson', 'Platform Config must be valid JSON'));\n return;\n }\n }\n\n setIsSubmitting(true);\n try {\n const input: CreateChannelInput = {\n name: formData.name.trim(),\n platform: formData.platform,\n organizationId,\n description: formData.description.trim() || undefined,\n adapterType: (formData.adapterType as AdapterType) || undefined,\n status: formData.status,\n priority: formData.priority,\n platformConfig: parsedPlatformConfig,\n };\n\n const result = await createChannel(input);\n if (result) {\n navigate(`../${result.id}`);\n } else {\n setError(tr('channel.createFailed', 'Failed to create channel'));\n }\n } catch (err: unknown) {\n setError(\n err instanceof Error ? err.message : tr('channel.createFailed', 'Failed to create channel')\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // ── Render ────────────────────────────────────────────────────────────\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CREATE}>\n <div className=\"p-3 sm:p-6 space-y-6 max-w-5xl mx-auto\">\n {/* ── Header ───────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-surface-hover rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl font-bold text-text-primary flex items-center gap-2\">\n <Radio size={22} className=\"text-accent-primary\" />\n {tr('channel.createChannel', 'Create Channel')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {tr('channel.createChannelSubtitle', 'Set up a new communication channel')}\n </p>\n </div>\n </div>\n </div>\n\n {/* ── Error ────────────────────────────────────────────────────── */}\n {error && (\n <div className=\"flex items-center gap-3 p-3 bg-error-500/10 border border-status-error-border/30 rounded-lg\">\n <AlertCircle size={16} className=\"text-status-error-text shrink-0\" />\n <p className=\"text-sm text-error-400\">{error}</p>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* ── Row 1: Name + Description | Status + Provider (2-col) ─── */}\n <div className=\"grid gap-5 lg:grid-cols-2\">\n {/* Left: Name & Description */}\n <GlassCard treatment=\"plain\" className=\"p-5 border-border-seam space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.basicInformation', 'Basic Information')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.name', 'Name')} <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 placeholder:text-text-secondary\"\n placeholder={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.description', 'Description')}\n </label>\n <textarea\n aria-label={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n value={formData.description}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, description: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none placeholder:text-text-secondary\"\n placeholder={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n />\n </div>\n </GlassCard>\n\n {/* Right: Status + Provider / Adapter */}\n <GlassCard treatment=\"plain\" className=\"p-5 border-border-seam space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.providerStatus', 'Provider & Status')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.providerAdapter', 'Provider / Adapter')}\n </label>\n <select\n value={formData.adapterType}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n adapterType: e.target.value as AdapterType | '',\n }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary\"\n >\n <option value=\"\">\n {tr('channel.selectProvider', 'Select a provider (optional)')}\n </option>\n {availableAdapters.map((a) => (\n <option key={a.value} value={a.value}>\n {a.label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {selectedPlatform\n ? tr(\n 'channel.providersAvailableForPlatform',\n `${availableAdapters.length} providers available for ${selectedPlatform.label}`\n )\n : tr('channel.selectPlatformFirst', 'Select a platform first')}\n </p>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.status', 'Status')}\n </label>\n <select\n value={formData.status}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, status: e.target.value as ChannelStatus }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary\"\n >\n <option value=\"ACTIVE\">{tr('channel.status.active', 'Active')}</option>\n <option value=\"INACTIVE\">{tr('channel.status.inactive', 'Inactive')}</option>\n </select>\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {formData.status === 'ACTIVE'\n ? tr('channel.statusHint.active', 'Ready to send immediately')\n : tr('channel.statusHint.inactive', 'Configure before activating')}\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"create-channel-priority\"\n className=\"block text-sm font-medium text-text-primary mb-1.5\"\n >\n {tr('channel.priority', 'Priority')}\n </label>\n <input\n id=\"create-channel-priority\"\n aria-label={tr('channel.priority', 'Priority')}\n type=\"number\"\n value={formData.priority}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n priority: parseInt(e.target.value) || 0,\n }))\n }\n min={0}\n max={100}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50\"\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {tr('channel.priorityHint', 'Lower = higher priority (0-100)')}\n </p>\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Row 2: Platform selection (full width) ─────────────────── */}\n <GlassCard treatment=\"plain\" className=\"p-5 border-border-seam space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.platform', 'Platform')} <span className=\"text-status-error-text\">*</span>\n </h2>\n\n <div className=\"grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\">\n {PLATFORM_OPTIONS.map((platform) => {\n const Icon = platform.icon;\n const isSelected = formData.platform === platform.value;\n return (\n <button\n key={platform.value}\n type=\"button\"\n onClick={() => handlePlatformChange(platform.value)}\n className={`relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${\n isSelected\n ? `${platform.color} border-current ring-1 ring-current`\n : 'border-border-subtle hover:bg-surface-hover text-text-secondary hover:text-text-primary'\n }`}\n >\n {isSelected && (\n <CheckCircle2 size={14} className=\"absolute top-1.5 right-1.5 text-current\" />\n )}\n <Icon size={20} />\n <span className=\"text-xs font-semibold\">{platform.label}</span>\n <span className=\"text-[10px] leading-tight opacity-70\">\n {platform.description}\n </span>\n </button>\n );\n })}\n </div>\n </GlassCard>\n\n {/* ── Row 3: Advanced (collapsible) — webhook config / delivery / retry policy (platformConfig) */}\n <OptionalFields title={tr('channel.advancedOptions', 'Advanced settings')}>\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.platformConfig', 'Platform Config')}{' '}\n <span className=\"text-[10px] text-text-secondary\">(JSON)</span>\n </label>\n <textarea\n aria-label={tr('channel.platformConfig', 'Platform Config')}\n value={formData.platformConfig}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, platformConfig: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none font-mono placeholder:text-text-secondary\"\n placeholder={tr(\n 'channel.platformConfigPlaceholder',\n '{\"fromEmail\": \"noreply@example.com\"}'\n )}\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {tr('channel.platformConfigHint', 'Platform-specific configuration as JSON')}\n </p>\n </div>\n </div>\n </OptionalFields>\n\n {/* ── Actions ────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between pt-2\">\n <button\n type=\"button\"\n onClick={() => navigate('/channels/list')}\n className=\"px-4 py-2 text-sm text-text-secondary hover:text-text-primary hover:bg-surface-hover border border-border-subtle rounded-lg transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || !formData.name.trim()}\n className=\"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? <Loader2 size={16} className=\"animate-spin\" /> : <Save size={16} />}\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </div>\n </form>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateChannelPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/CreateChannelPage.tsx"],"sourcesContent":["/**\n * Create Channel Page\n *\n * Form fields match the exact CreateChannelInput from global-channel-svc:\n * name (required), platform (required), organizationId (auto), description,\n * adapterType, status, priority, platformConfig\n *\n * organizationId is injected from shell context — not shown in form.\n * Credentials belong to createConnection mutation — not part of channel creation.\n */\n\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { EmphasisPanel, GlassCard, OptionalFields, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n Radio,\n ArrowLeft,\n Save,\n Loader2,\n AlertCircle,\n Mail,\n Smartphone,\n Bell,\n Inbox,\n Globe,\n Hash,\n Users,\n MessageCircle,\n Send,\n MessageSquare,\n CheckCircle2,\n} from 'lucide-react';\nimport { useChannel } from '../../../providers/ChannelProvider';\nimport {\n useChannels,\n type ChannelPlatform,\n type AdapterType,\n type ChannelStatus,\n type CreateChannelInput,\n} from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\n// ============================================================================\n// PLATFORM CONFIG\n// ============================================================================\n\n// NOTE: AdapterType values MUST match the backend Prisma schema exactly.\n// See: global-channel-svc/prisma/schema/schema.prisma (enum AdapterType)\nconst PLATFORM_OPTIONS: {\n value: ChannelPlatform;\n label: string;\n description: string;\n icon: typeof Mail;\n color: string;\n adapters: { value: AdapterType; label: string }[];\n}[] = [\n {\n value: 'EMAIL',\n label: 'Email',\n description: 'Send emails via various providers',\n icon: Mail,\n color: 'text-accent-blue bg-accent-blue-subtle border-accent-blue-subtle',\n adapters: [\n { value: 'AWS_SES', label: 'AWS SES' },\n { value: 'AZURE_ACS', label: 'Azure Communication Services' },\n ],\n },\n {\n value: 'SMS',\n label: 'SMS',\n description: 'Send text messages',\n icon: Smartphone,\n color: 'text-status-success-text bg-status-success-bg-subtle border-status-success-bg-subtle',\n adapters: [\n { value: 'AWS_SNS', label: 'AWS SNS' },\n { value: 'TWILIO_SMS', label: 'Twilio SMS' },\n ],\n },\n {\n value: 'PUSH',\n label: 'Push',\n description: 'Mobile & web push',\n icon: Bell,\n color: 'text-accent-purple bg-accent-purple-subtle border-accent-purple-subtle',\n adapters: [{ value: 'FCM', label: 'Firebase Cloud Messaging' }],\n },\n {\n value: 'INAPP',\n label: 'In-App',\n description: 'In-app notifications',\n icon: Inbox,\n color: 'text-accent-indigo bg-accent-indigo-subtle border-accent-indigo-subtle',\n adapters: [{ value: 'INAPP_NATIVE', label: 'Native In-App' }],\n },\n {\n value: 'SLACK',\n label: 'Slack',\n description: 'Workspace notifications',\n icon: Hash,\n color: 'text-accent-pink bg-accent-pink-subtle border-accent-pink-subtle',\n adapters: [{ value: 'SLACK_BOT', label: 'Slack Bot' }],\n },\n {\n value: 'TEAMS',\n label: 'Teams',\n description: 'Teams notifications',\n icon: Users,\n color: 'text-accent-cyan bg-accent-cyan-subtle border-accent-cyan-subtle',\n adapters: [{ value: 'TEAMS_BOT', label: 'Teams Bot' }],\n },\n {\n value: 'DISCORD',\n label: 'Discord',\n description: 'Server notifications',\n icon: MessageCircle,\n color: 'text-accent-violet bg-accent-violet-subtle border-accent-violet-subtle',\n adapters: [{ value: 'DISCORD_BOT', label: 'Discord Bot' }],\n },\n {\n value: 'WHATSAPP',\n label: 'WhatsApp',\n description: 'Business messaging',\n icon: Send,\n color: 'text-accent-emerald bg-accent-emerald-subtle border-accent-emerald-subtle',\n adapters: [\n { value: 'WHATSAPP_CLOUD', label: 'WhatsApp Cloud API' },\n { value: 'WHATSAPP_TWILIO', label: 'WhatsApp via Twilio' },\n ],\n },\n {\n value: 'WEBCHAT',\n label: 'Webchat',\n description: 'Chat widgets',\n icon: MessageSquare,\n color: 'text-accent-teal bg-accent-teal-subtle border-accent-teal-subtle',\n adapters: [], // No WEBCHAT adapters in backend Prisma schema yet\n },\n {\n value: 'WEBHOOK',\n label: 'Webhook',\n description: 'HTTP webhooks',\n icon: Globe,\n color: 'text-accent-orange bg-accent-orange-subtle border-accent-orange-subtle',\n adapters: [{ value: 'WEBHOOK_HTTP', label: 'HTTP Webhook' }],\n },\n];\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function CreateChannelPage() {\n const navigate = useNavigate();\n const { organizationId } = useChannel();\n const { createChannel } = useChannels();\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 [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Form state — matches CreateChannelInput exactly\n const [formData, setFormData] = useState<{\n name: string; // required\n platform: ChannelPlatform; // required\n description: string; // optional\n adapterType: AdapterType | ''; // required (auto-selected based on platform)\n status: ChannelStatus; // optional — default ACTIVE on server\n priority: number; // optional — default 100 on server\n platformConfig: string; // optional — JSON string\n }>({\n name: '',\n platform: 'EMAIL',\n description: '',\n adapterType: 'AWS_SES', // Default to AWS SES for Email platform\n status: 'ACTIVE',\n priority: 100,\n platformConfig: '',\n });\n\n const availableAdapters =\n PLATFORM_OPTIONS.find((p) => p.value === formData.platform)?.adapters || [];\n\n const selectedPlatform = PLATFORM_OPTIONS.find((p) => p.value === formData.platform);\n\n const handlePlatformChange = (platform: ChannelPlatform) => {\n setFormData((prev) => ({ ...prev, platform, adapterType: '' }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!formData.name.trim()) {\n setError(tr('channel.validation.nameRequired', 'Channel name is required'));\n return;\n }\n\n if (!organizationId) {\n setError(\n tr(\n 'channel.validation.organizationRequired',\n 'Organization context is required. Please ensure an organization is selected.'\n )\n );\n return;\n }\n\n // Validate platformConfig JSON if provided\n let parsedPlatformConfig: Record<string, unknown> | undefined;\n if (formData.platformConfig.trim()) {\n try {\n parsedPlatformConfig = JSON.parse(formData.platformConfig);\n } catch {\n setError(tr('channel.validation.platformConfigJson', 'Platform Config must be valid JSON'));\n return;\n }\n }\n\n setIsSubmitting(true);\n try {\n const input: CreateChannelInput = {\n name: formData.name.trim(),\n platform: formData.platform,\n organizationId,\n description: formData.description.trim() || undefined,\n adapterType: (formData.adapterType as AdapterType) || undefined,\n status: formData.status,\n priority: formData.priority,\n platformConfig: parsedPlatformConfig,\n };\n\n const result = await createChannel(input);\n if (result) {\n navigate(`../${result.id}`);\n } else {\n setError(tr('channel.createFailed', 'Failed to create channel'));\n }\n } catch (err: unknown) {\n setError(\n err instanceof Error ? err.message : tr('channel.createFailed', 'Failed to create channel')\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // ── Render ────────────────────────────────────────────────────────────\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CREATE}>\n <div className=\"p-3 sm:p-6 space-y-6 max-w-5xl mx-auto\">\n {/* ── Header ───────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n className=\"p-2 text-text-secondary hover:text-text-primary hover:bg-surface-hover rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl font-bold text-text-primary flex items-center gap-2\">\n <Radio size={22} className=\"text-accent-primary\" />\n {tr('channel.createChannel', 'Create Channel')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {tr('channel.createChannelSubtitle', 'Set up a new communication channel')}\n </p>\n </div>\n </div>\n </div>\n\n {/* ── Page purpose ─────────────────────────────────────────────── */}\n <PagePurpose icon={<Radio size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.createChannelPurpose',\n 'Set up a new way to reach people — for example a transactional email sender, an SMS gateway, a Slack workspace or a push provider. Name it, pick the platform and provider, and you will get a channel you can then authenticate and send through. Create one channel per provider account you want messages to flow through.'\n )}\n </PagePurpose>\n\n {/* ── Error ────────────────────────────────────────────────────── */}\n {error && (\n <div className=\"flex items-center gap-3 p-3 bg-error-500/10 border border-status-error-border/30 rounded-lg\">\n <AlertCircle size={16} className=\"text-status-error-text shrink-0\" />\n <p className=\"text-sm text-error-400\">{error}</p>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* ── Row 1: Name + Description | Status + Provider (2-col) ─── */}\n <div className=\"grid gap-5 lg:grid-cols-2\">\n {/* Left: Name & Description — the primary identity of the channel */}\n <EmphasisPanel className=\"p-5 space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.basicInformation', 'Basic Information')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.name', 'Name')} <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 placeholder:text-text-secondary\"\n placeholder={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.description', 'Description')}\n </label>\n <textarea\n aria-label={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n value={formData.description}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, description: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none placeholder:text-text-secondary\"\n placeholder={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n />\n </div>\n </EmphasisPanel>\n\n {/* Right: Status + Provider / Adapter */}\n <GlassCard treatment=\"plain\" className=\"p-5 border-border-seam space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.providerStatus', 'Provider & Status')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.providerAdapter', 'Provider / Adapter')}\n </label>\n <select\n value={formData.adapterType}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n adapterType: e.target.value as AdapterType | '',\n }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary\"\n >\n <option value=\"\">\n {tr('channel.selectProvider', 'Select a provider (optional)')}\n </option>\n {availableAdapters.map((a) => (\n <option key={a.value} value={a.value}>\n {a.label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {selectedPlatform\n ? tr(\n 'channel.providersAvailableForPlatform',\n `${availableAdapters.length} providers available for ${selectedPlatform.label}`\n )\n : tr('channel.selectPlatformFirst', 'Select a platform first')}\n </p>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.status', 'Status')}\n </label>\n <select\n value={formData.status}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, status: e.target.value as ChannelStatus }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary\"\n >\n <option value=\"ACTIVE\">{tr('channel.status.active', 'Active')}</option>\n <option value=\"INACTIVE\">{tr('channel.status.inactive', 'Inactive')}</option>\n </select>\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {formData.status === 'ACTIVE'\n ? tr('channel.statusHint.active', 'Ready to send immediately')\n : tr('channel.statusHint.inactive', 'Configure before activating')}\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"create-channel-priority\"\n className=\"block text-sm font-medium text-text-primary mb-1.5\"\n >\n {tr('channel.priority', 'Priority')}\n </label>\n <input\n id=\"create-channel-priority\"\n aria-label={tr('channel.priority', 'Priority')}\n type=\"number\"\n value={formData.priority}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n priority: parseInt(e.target.value) || 0,\n }))\n }\n min={0}\n max={100}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50\"\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {tr('channel.priorityHint', 'Lower = higher priority (0-100)')}\n </p>\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Row 2: Platform selection (full width) ─────────────────── */}\n <GlassCard treatment=\"plain\" className=\"p-5 border-border-seam space-y-4\">\n <h2 className=\"text-sm font-semibold text-text-primary uppercase tracking-wider\">\n {tr('channel.platform', 'Platform')} <span className=\"text-status-error-text\">*</span>\n </h2>\n\n <div className=\"grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\">\n {PLATFORM_OPTIONS.map((platform) => {\n const Icon = platform.icon;\n const isSelected = formData.platform === platform.value;\n return (\n <button\n key={platform.value}\n type=\"button\"\n onClick={() => handlePlatformChange(platform.value)}\n className={`relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${\n isSelected\n ? `${platform.color} border-current ring-1 ring-current`\n : 'border-border-subtle hover:bg-surface-hover text-text-secondary hover:text-text-primary'\n }`}\n >\n {isSelected && (\n <CheckCircle2 size={14} className=\"absolute top-1.5 right-1.5 text-current\" />\n )}\n <Icon size={20} />\n <span className=\"text-xs font-semibold\">{platform.label}</span>\n <span className=\"text-[10px] leading-tight opacity-70\">\n {platform.description}\n </span>\n </button>\n );\n })}\n </div>\n </GlassCard>\n\n {/* ── Row 3: Advanced (collapsible) — webhook config / delivery / retry policy (platformConfig) */}\n <OptionalFields title={tr('channel.advancedOptions', 'Advanced settings')}>\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1.5\">\n {tr('channel.platformConfig', 'Platform Config')}{' '}\n <span className=\"text-[10px] text-text-secondary\">(JSON)</span>\n </label>\n <textarea\n aria-label={tr('channel.platformConfig', 'Platform Config')}\n value={formData.platformConfig}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, platformConfig: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none font-mono placeholder:text-text-secondary\"\n placeholder={tr(\n 'channel.platformConfigPlaceholder',\n '{\"fromEmail\": \"noreply@example.com\"}'\n )}\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n {tr('channel.platformConfigHint', 'Platform-specific configuration as JSON')}\n </p>\n </div>\n </div>\n </OptionalFields>\n\n {/* ── Actions ────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between pt-2\">\n <button\n type=\"button\"\n onClick={() => navigate('/channels/list')}\n className=\"px-4 py-2 text-sm text-text-secondary hover:text-text-primary hover:bg-surface-hover border border-border-subtle rounded-lg transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || !formData.name.trim()}\n className=\"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? <Loader2 size={16} className=\"animate-spin\" /> : <Save size={16} />}\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </div>\n </form>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}