@burdenoff/microfe-channel 2026.805.1 → 2026.830.2

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 (50) hide show
  1. package/dist/channel/ChannelRoot.d.ts.map +1 -1
  2. package/dist/channel/ChannelRoot.js +32 -35
  3. package/dist/channel/ChannelRoot.js.map +1 -1
  4. package/dist/channel/components/AdapterCredentialsForm.d.ts.map +1 -1
  5. package/dist/channel/components/AdapterCredentialsForm.js +52 -58
  6. package/dist/channel/components/AdapterCredentialsForm.js.map +1 -1
  7. package/dist/channel/components/FeatureNotEnabled.d.ts.map +1 -1
  8. package/dist/channel/components/FeatureNotEnabled.js +27 -22
  9. package/dist/channel/components/FeatureNotEnabled.js.map +1 -1
  10. package/dist/channel/components/MarkdownEditor.d.ts.map +1 -1
  11. package/dist/channel/components/MarkdownEditor.js +78 -77
  12. package/dist/channel/components/MarkdownEditor.js.map +1 -1
  13. package/dist/channel/components/PermissionGuard.d.ts.map +1 -1
  14. package/dist/channel/components/PermissionGuard.js +6 -9
  15. package/dist/channel/components/PermissionGuard.js.map +1 -1
  16. package/dist/channel/hooks/useTr.d.ts +17 -0
  17. package/dist/channel/hooks/useTr.d.ts.map +1 -0
  18. package/dist/channel/hooks/useTr.js +14 -0
  19. package/dist/channel/hooks/useTr.js.map +1 -0
  20. package/dist/channel/modules/channels/i18n.js +24 -24
  21. package/dist/channel/modules/channels/i18n.js.map +1 -1
  22. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +2 -2
  23. package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
  24. package/dist/channel/modules/channels/pages/ChannelSettingsPage.d.ts.map +1 -1
  25. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +232 -226
  26. package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
  27. package/dist/channel/modules/channels/pages/ChannelUsersPage.d.ts.map +1 -1
  28. package/dist/channel/modules/channels/pages/ChannelUsersPage.js +152 -146
  29. package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
  30. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.d.ts.map +1 -1
  31. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +112 -114
  32. package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
  33. package/dist/channel/modules/channels/pages/CreateChannelPage.d.ts.map +1 -1
  34. package/dist/channel/modules/channels/pages/CreateChannelPage.js +300 -269
  35. package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
  36. package/dist/channel/modules/channels/pages/CreateConnectionPage.d.ts.map +1 -1
  37. package/dist/channel/modules/channels/pages/CreateConnectionPage.js +108 -102
  38. package/dist/channel/modules/channels/pages/CreateConnectionPage.js.map +1 -1
  39. package/dist/channel/modules/channels/pages/EditChannelPage.d.ts.map +1 -1
  40. package/dist/channel/modules/channels/pages/EditChannelPage.js +384 -343
  41. package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
  42. package/dist/channel/modules/channels/pages/SendMessagePage.d.ts.map +1 -1
  43. package/dist/channel/modules/channels/pages/SendMessagePage.js +170 -156
  44. package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
  45. package/dist/channel/pages/ChannelLandingPage.d.ts.map +1 -1
  46. package/dist/channel/pages/ChannelLandingPage.js +47 -50
  47. package/dist/channel/pages/ChannelLandingPage.js.map +1 -1
  48. package/dist/translations/en.d.ts +271 -4
  49. package/dist/translations/en.d.ts.map +1 -1
  50. package/package.json +1 -1
@@ -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';\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, useMemo } from 'react';\nimport { useParams, useNavigate } from 'react-router';\nimport { useTr } from '../../../hooks/useTr';\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\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 tr = useTr();\n\n const PRESENCE_CONFIG: Record<\n UserPresenceStatus,\n { labelKey: string; labelFallback: string; color: string }\n > = useMemo(\n () => ({\n ONLINE: {\n labelKey: 'channel.presence.online',\n labelFallback: 'Online',\n color: 'text-status-success-text',\n },\n OFFLINE: {\n labelKey: 'channel.presence.offline',\n labelFallback: 'Offline',\n color: 'text-text-secondary',\n },\n AWAY: {\n labelKey: 'channel.presence.away',\n labelFallback: 'Away',\n color: 'text-status-warning-text',\n },\n DND: {\n labelKey: 'channel.presence.dnd',\n labelFallback: 'Do Not Disturb',\n color: 'text-status-error-text',\n },\n }),\n []\n );\n\n const PAIRING_CONFIG: Record<\n PairingStatus,\n { labelKey: string; labelFallback: string; color: string; icon: typeof CheckCircle2 }\n > = useMemo(\n () => ({\n PENDING: {\n labelKey: 'channel.pairingLabel.pending',\n labelFallback: 'Pending',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n APPROVED: {\n labelKey: 'channel.pairingLabel.approved',\n labelFallback: 'Approved',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n EXPIRED: {\n labelKey: 'channel.pairingLabel.expired',\n labelFallback: 'Expired',\n color: 'text-text-secondary bg-bg-sunken',\n icon: XCircle,\n },\n }),\n []\n );\n\n const resultsPerPageOptions = useMemo(\n () => [\n { value: 10, label: tr('channel.resultsPerPage', '{{count}} results', { count: 10 }) },\n { value: 25, label: tr('channel.resultsPerPage', '{{count}} results', { count: 25 }) },\n { value: 50, label: tr('channel.resultsPerPage', '{{count}} results', { count: 50 }) },\n { value: 100, label: tr('channel.resultsPerPage', '{{count}} results', { count: 100 }) },\n ],\n [tr]\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 || tr('channel.unknownUser', '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}`}>\n {tr(presenceConfig.labelKey, presenceConfig.labelFallback)}\n </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 {tr(pairingConfig.labelKey, pairingConfig.labelFallback)}\n </span>\n ) : (\n <span className=\"text-xs text-text-secondary\">{tr('channel.noPairing', '-')}</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 || tr('channel.channelNotFound', '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 {tr('channel.goBack', '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\">\n {tr('channel.usersPageSubtitle', '{{channelName}} - User connections', {\n channelName: channel.name,\n })}\n </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 {resultsPerPageOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\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 +1 @@
1
- {"version":3,"file":"ConnectionDetailsPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAwDH,wBAAgB,qBAAqB,gCA2apC"}
1
+ {"version":3,"file":"ConnectionDetailsPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ConnectionDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAsCH,wBAAgB,qBAAqB,gCAodpC"}
@@ -1,43 +1,29 @@
1
- import { useChannel as e } from "../../../providers/ChannelProvider.js";
2
- import { nativeImpact as t, nativeNotify as n } from "../../../../utils/nativeBridge.js";
3
- import { reportShellError as ee } from "../../../utils/shellBridge.js";
4
- import { getGlobalChannelClient as te } from "../../../graphql/global-client.js";
5
- import { GetConnectionDocument as r } from "../../../../generated/global-operations.js";
6
- import { useChannel as ne } from "../../../hooks/useChannels.js";
7
- import { PermissionGuard as i } from "../../../components/PermissionGuard.js";
8
- import { CHANNEL_PERMISSIONS as a } from "../../../constants/permissions.js";
9
- import { useEffect as re, useState as o } from "react";
10
- import { useI18n as ie } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
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";
13
- import { usePermissions as ae } from "@burdenoff/fe-libs/shared/providers/shell";
1
+ import { useTr as e } from "../../../hooks/useTr.js";
2
+ import { useChannel as t } from "../../../providers/ChannelProvider.js";
3
+ import { nativeImpact as n, nativeNotify as r } from "../../../../utils/nativeBridge.js";
4
+ import { reportShellError as i } from "../../../utils/shellBridge.js";
5
+ import { getGlobalChannelClient as a } from "../../../graphql/global-client.js";
6
+ import { GetConnectionDocument as ee } from "../../../../generated/global-operations.js";
7
+ import { useChannel as te } from "../../../hooks/useChannels.js";
8
+ import { PermissionGuard as o } from "../../../components/PermissionGuard.js";
9
+ import { CHANNEL_PERMISSIONS as s } from "../../../constants/permissions.js";
10
+ import { useEffect as ne, useMemo as c, useState as l } from "react";
11
+ import { AlertCircle as u, ArrowLeft as d, CheckCircle2 as re, Clock as ie, Edit as f, Loader2 as p, RefreshCw as m, Shield as h, Trash2 as g, XCircle as _ } from "lucide-react";
12
+ import { useNavigate as v, useParams as ae } from "react-router";
13
+ import { usePermissions as oe } from "@burdenoff/fe-libs/shared/providers/shell";
14
14
  import { jsx as y, jsxs as b } from "react/jsx-runtime";
15
- import { CTAOverflowMenu as oe, EmphasisPanel as x, GlassCard as S, NextSteps as C, PagePurpose as w } from "@burdenoff/fe-libs/ui";
16
- import { TagBadge as T } from "@burdenoff/fe-libs/tag";
15
+ import { CTAOverflowMenu as se, EmphasisPanel as ce, GlassCard as x, NextSteps as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
16
+ import { TagBadge as w } from "@burdenoff/fe-libs/tag";
17
17
  //#region src/channel/modules/channels/pages/ConnectionDetailsPage.tsx
18
- var E = {
19
- true: {
20
- icon: l,
21
- color: "text-status-success-text",
22
- bgColor: "bg-status-success-bg-subtle",
23
- label: "Healthy"
24
- },
25
- false: {
26
- icon: g,
27
- color: "text-status-error-text",
28
- bgColor: "bg-error-500/10",
29
- label: "Unhealthy"
30
- }
31
- };
32
- function D() {
33
- let l = _(), { hasPermission: g } = ae(), D = g(a.CHANNEL_UPDATE), O = g(a.CHANNEL_EXECUTE), k = v(), { connectionId: A } = k, { authToken: j, apiGatewayUrl: M, organizationId: N } = e(), [P, F] = o(k.channelId), [I, L] = o(null);
34
- re(() => {
18
+ function T() {
19
+ let T = v(), { hasPermission: E } = oe(), D = E(s.CHANNEL_UPDATE), O = E(s.CHANNEL_EXECUTE), k = ae(), { connectionId: A } = k, { authToken: j, apiGatewayUrl: M, organizationId: N } = t(), [P, F] = l(k.channelId), [I, L] = l(null);
20
+ ne(() => {
35
21
  if (P || !A) return;
36
22
  let e = !1;
37
23
  return (async () => {
38
24
  try {
39
- let t = await te(M, j, N).query({
40
- query: r,
25
+ let t = await a(M, j, N).query({
26
+ query: ee,
41
27
  variables: { id: A },
42
28
  fetchPolicy: "network-only"
43
29
  });
@@ -57,23 +43,35 @@ function D() {
57
43
  j,
58
44
  N
59
45
  ]);
60
- let R = P, { channel: z, isLoading: B, error: V, runHealthCheck: H, deleteConnection: U } = ne(R), { t: W } = ie(), G = (e, t) => {
61
- let n = W(e);
62
- return n === e ? t : n;
63
- }, [K, q] = o(!1), [J, Y] = o(!1), [se, X] = o(!1), Z = z?.connection, Q = async () => {
46
+ let R = P, { channel: z, isLoading: B, error: V, runHealthCheck: H, deleteConnection: U } = te(R), W = e(), G = c(() => ({
47
+ true: {
48
+ icon: re,
49
+ color: "text-status-success-text",
50
+ bgColor: "bg-status-success-bg-subtle",
51
+ labelKey: "channel.healthLabel.healthy",
52
+ labelFallback: "Healthy"
53
+ },
54
+ false: {
55
+ icon: _,
56
+ color: "text-status-error-text",
57
+ bgColor: "bg-error-500/10",
58
+ labelKey: "channel.healthLabel.unhealthy",
59
+ labelFallback: "Unhealthy"
60
+ }
61
+ }), []), [K, q] = l(!1), [J, Y] = l(!1), [le, X] = l(!1), Z = z?.connection, Q = async () => {
64
62
  Y(!0);
65
63
  try {
66
64
  await H();
67
65
  } finally {
68
66
  Y(!1);
69
67
  }
70
- }, ce = async () => {
68
+ }, ue = async () => {
71
69
  if (A) {
72
- t("medium"), q(!0);
70
+ n("medium"), q(!0);
73
71
  try {
74
- await U(A), n("success"), l(`/channels/${R}`);
72
+ await U(A), r("success"), T(`/channels/${R}`);
75
73
  } catch (e) {
76
- n("error"), ee(e, {
74
+ r("error"), i(e, {
77
75
  module: "channel",
78
76
  context: { action: "Failed to delete connection" }
79
77
  }, "[channel] Failed to delete connection");
@@ -84,47 +82,47 @@ function D() {
84
82
  };
85
83
  if (B || !P && !I) return /* @__PURE__ */ y("div", {
86
84
  className: "flex items-center justify-center min-h-screen",
87
- children: /* @__PURE__ */ y(f, { className: "size-8 animate-spin text-text-secondary" })
85
+ children: /* @__PURE__ */ y(p, { className: "size-8 animate-spin text-text-secondary" })
88
86
  });
89
87
  if (I || V || !z) return /* @__PURE__ */ b("div", {
90
88
  className: "flex flex-col items-center justify-center min-h-screen gap-4",
91
89
  children: [
92
- /* @__PURE__ */ y(s, { className: "size-12 text-status-error-text" }),
90
+ /* @__PURE__ */ y(u, { className: "size-12 text-status-error-text" }),
93
91
  /* @__PURE__ */ y("h1", {
94
92
  className: "text-xl font-semibold",
95
- children: G("channel.channelNotFound", "Channel not found")
93
+ children: W("channel.channelNotFound", "Channel not found")
96
94
  }),
97
95
  /* @__PURE__ */ y("button", {
98
96
  type: "button",
99
- onClick: () => l("../../list"),
97
+ onClick: () => T("../../list"),
100
98
  className: "text-primary hover:underline",
101
- children: G("channel.backToChannels", "Back to channels")
99
+ children: W("channel.backToChannels", "Back to channels")
102
100
  })
103
101
  ]
104
102
  });
105
103
  if (!Z) return /* @__PURE__ */ b("div", {
106
104
  className: "flex flex-col items-center justify-center min-h-screen gap-4",
107
105
  children: [
108
- /* @__PURE__ */ y(s, { className: "size-12 text-text-secondary" }),
106
+ /* @__PURE__ */ y(u, { className: "size-12 text-text-secondary" }),
109
107
  /* @__PURE__ */ y("h1", {
110
108
  className: "text-xl font-semibold",
111
- children: G("channel.noConnectionFound", "No connection found")
109
+ children: W("channel.noConnectionFound", "No connection found")
112
110
  }),
113
111
  /* @__PURE__ */ y("p", {
114
112
  className: "text-sm text-text-secondary",
115
- children: G("channel.noConnectionConfigured", "This channel does not have a connection configured.")
113
+ children: W("channel.noConnectionConfigured", "This channel does not have a connection configured.")
116
114
  }),
117
115
  /* @__PURE__ */ y("button", {
118
116
  type: "button",
119
- onClick: () => l(`/channels/${R}`),
117
+ onClick: () => T(`/channels/${R}`),
120
118
  className: "text-primary hover:underline",
121
- children: G("channel.backToChannel", "Back to channel")
119
+ children: W("channel.backToChannel", "Back to channel")
122
120
  })
123
121
  ]
124
122
  });
125
- let $ = E[String(Z.isHealthy)] || E.false, le = $.icon;
126
- return /* @__PURE__ */ y(i, {
127
- permission: a.CHANNEL_CONNECTION_READ,
123
+ let $ = G[String(Z.isHealthy)] || G.false, de = $.icon;
124
+ return /* @__PURE__ */ y(o, {
125
+ permission: s.CHANNEL_CONNECTION_READ,
128
126
  children: /* @__PURE__ */ b("div", {
129
127
  className: "min-h-screen bg-bg-surface",
130
128
  children: [/* @__PURE__ */ y("div", {
@@ -137,12 +135,12 @@ function D() {
137
135
  className: "flex items-center gap-4",
138
136
  children: [/* @__PURE__ */ y("button", {
139
137
  type: "button",
140
- onClick: () => l(`/channels/${R}`),
138
+ onClick: () => T(`/channels/${R}`),
141
139
  className: "p-2 hover:bg-bg-sunken rounded-lg transition-colors",
142
- children: /* @__PURE__ */ y(c, { className: "size-5 " })
140
+ children: /* @__PURE__ */ y(d, { className: "size-5 " })
143
141
  }), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h1", {
144
142
  className: "text-2xl font-bold",
145
- children: G("channel.connectionDetails", "Connection Details")
143
+ children: W("channel.connectionDetails", "Connection Details")
146
144
  }), /* @__PURE__ */ b("p", {
147
145
  className: "text-sm text-text-secondary",
148
146
  children: [
@@ -151,21 +149,21 @@ function D() {
151
149
  z.platform
152
150
  ]
153
151
  })] })]
154
- }), /* @__PURE__ */ y(oe, {
152
+ }), /* @__PURE__ */ y(se, {
155
153
  primary: D ? {
156
- label: G("channel.editChannel", "Edit Channel"),
157
- icon: /* @__PURE__ */ y(d, {
154
+ label: W("channel.editChannel", "Edit Channel"),
155
+ icon: /* @__PURE__ */ y(f, {
158
156
  className: "size-4",
159
157
  "aria-hidden": "true"
160
158
  }),
161
- onSelect: () => l(`/channels/${R}/edit`)
159
+ onSelect: () => T(`/channels/${R}/edit`)
162
160
  } : void 0,
163
161
  actions: O ? [{
164
- label: G("channel.runHealthCheck", "Run Health Check"),
165
- icon: J ? /* @__PURE__ */ y(f, {
162
+ label: W("channel.runHealthCheck", "Run Health Check"),
163
+ icon: J ? /* @__PURE__ */ y(p, {
166
164
  className: "size-4 animate-spin",
167
165
  "aria-hidden": "true"
168
- }) : /* @__PURE__ */ y(p, {
166
+ }) : /* @__PURE__ */ y(m, {
169
167
  className: "size-4",
170
168
  "aria-hidden": "true"
171
169
  }),
@@ -178,21 +176,21 @@ function D() {
178
176
  }), /* @__PURE__ */ b("div", {
179
177
  className: "container mx-auto px-6 py-8 max-w-5xl space-y-6",
180
178
  children: [
181
- /* @__PURE__ */ y(w, {
182
- icon: /* @__PURE__ */ y(m, {
179
+ /* @__PURE__ */ y(C, {
180
+ icon: /* @__PURE__ */ y(h, {
183
181
  size: 15,
184
182
  "aria-hidden": "true"
185
183
  }),
186
- children: G("channel.connectionDetailsPurpose", "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.")
184
+ children: W("channel.connectionDetailsPurpose", "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.")
187
185
  }),
188
- /* @__PURE__ */ y(C, {
186
+ /* @__PURE__ */ y(S, {
189
187
  storageKey: "channel-connection-detail",
190
188
  steps: [
191
189
  ...O ? [{
192
190
  id: "health",
193
- label: G("channel.nextRunHealthCheck", "Run a health check"),
194
- description: G("channel.nextConnectionHealthDesc", "Confirm the credentials still authenticate against the provider."),
195
- icon: /* @__PURE__ */ y(p, {
191
+ label: W("channel.nextRunHealthCheck", "Run a health check"),
192
+ description: W("channel.nextConnectionHealthDesc", "Confirm the credentials still authenticate against the provider."),
193
+ icon: /* @__PURE__ */ y(m, {
196
194
  size: 16,
197
195
  "aria-hidden": "true"
198
196
  }),
@@ -200,27 +198,27 @@ function D() {
200
198
  }] : [],
201
199
  ...D ? [{
202
200
  id: "edit",
203
- label: G("channel.nextEditChannel", "Edit the channel"),
204
- description: G("channel.nextEditChannelDesc", "Update credentials, status or platform metadata for this channel."),
205
- icon: /* @__PURE__ */ y(d, {
201
+ label: W("channel.nextEditChannel", "Edit the channel"),
202
+ description: W("channel.nextEditChannelDesc", "Update credentials, status or platform metadata for this channel."),
203
+ icon: /* @__PURE__ */ y(f, {
206
204
  size: 16,
207
205
  "aria-hidden": "true"
208
206
  }),
209
- onClick: () => l(`/channels/${R}/edit`)
207
+ onClick: () => T(`/channels/${R}/edit`)
210
208
  }] : [],
211
209
  {
212
210
  id: "back",
213
- label: G("channel.nextBackToChannel", "Back to the channel"),
214
- description: G("channel.nextBackToChannelDesc", "Return to the channel overview to send a test or review deliveries."),
215
- icon: /* @__PURE__ */ y(c, {
211
+ label: W("channel.nextBackToChannel", "Back to the channel"),
212
+ description: W("channel.nextBackToChannelDesc", "Return to the channel overview to send a test or review deliveries."),
213
+ icon: /* @__PURE__ */ y(d, {
216
214
  size: 16,
217
215
  "aria-hidden": "true"
218
216
  }),
219
- onClick: () => l(`/channels/${R}`)
217
+ onClick: () => T(`/channels/${R}`)
220
218
  }
221
219
  ]
222
220
  }),
223
- /* @__PURE__ */ y(x, {
221
+ /* @__PURE__ */ y(ce, {
224
222
  className: "p-6",
225
223
  children: /* @__PURE__ */ y("div", {
226
224
  className: "flex items-center justify-between",
@@ -228,71 +226,71 @@ function D() {
228
226
  className: "flex items-center gap-4",
229
227
  children: [/* @__PURE__ */ y("div", {
230
228
  className: $.color,
231
- children: /* @__PURE__ */ y(le, { className: "size-10 " })
229
+ children: /* @__PURE__ */ y(de, { className: "size-10 " })
232
230
  }), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
233
231
  className: "text-xl font-semibold",
234
- children: $.label
232
+ children: W($.labelKey, $.labelFallback)
235
233
  }), /* @__PURE__ */ y("p", {
236
234
  className: "text-sm text-text-secondary",
237
- children: Z.isHealthy ? G("channel.connectionWorking", "Connection is working properly") : G("channel.connectionNotWorking", "Connection is not working properly")
235
+ children: Z.isHealthy ? W("channel.connectionWorking", "Connection is working properly") : W("channel.connectionNotWorking", "Connection is not working properly")
238
236
  })] })]
239
237
  })
240
238
  })
241
239
  }),
242
240
  /* @__PURE__ */ b("div", {
243
241
  className: "grid gap-6 lg:grid-cols-3",
244
- children: [/* @__PURE__ */ b(S, {
242
+ children: [/* @__PURE__ */ b(x, {
245
243
  treatment: "plain",
246
244
  className: "border-border-seam p-6 space-y-4 lg:col-span-2",
247
245
  children: [
248
246
  /* @__PURE__ */ y("h3", {
249
247
  className: "font-semibold",
250
- children: G("channel.connectionInformation", "Connection Information")
248
+ children: W("channel.connectionInformation", "Connection Information")
251
249
  }),
252
250
  /* @__PURE__ */ b("div", {
253
251
  className: "grid grid-cols-2 gap-4",
254
252
  children: [
255
253
  /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
256
254
  className: "text-sm text-text-secondary",
257
- children: "Connection ID"
255
+ children: W("connection.connectionId", "Connection ID")
258
256
  }), /* @__PURE__ */ y("p", {
259
257
  className: "font-mono text-sm",
260
258
  children: Z.id
261
259
  })] }),
262
260
  /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
263
261
  className: "text-sm text-text-secondary",
264
- children: "Auth Type"
262
+ children: W("channel.authType", "Auth Type")
265
263
  }), /* @__PURE__ */ b("p", {
266
264
  className: "flex items-center gap-2",
267
- children: [/* @__PURE__ */ y(m, { className: "size-4 text-text-secondary" }), Z.authType]
265
+ children: [/* @__PURE__ */ y(h, { className: "size-4 text-text-secondary" }), Z.authType]
268
266
  })] }),
269
267
  /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
270
268
  className: "text-sm text-text-secondary",
271
- children: "Status"
269
+ children: W("channel.status", "Status")
272
270
  }), /* @__PURE__ */ y("p", {
273
271
  className: Z.isActive ? "text-status-success-text" : "text-text-secondary",
274
- children: Z.isActive ? G("channel.activeStatus", "Active") : G("channel.inactiveStatus", "Inactive")
272
+ children: Z.isActive ? W("channel.activeStatus", "Active") : W("channel.inactiveStatus", "Inactive")
275
273
  })] }),
276
274
  /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
277
275
  className: "text-sm text-text-secondary",
278
- children: "Token Refresh"
276
+ children: W("connection.tokenRefresh", "Token Refresh")
279
277
  }), /* @__PURE__ */ y("p", {
280
278
  className: Z.needsTokenRefresh ? "text-status-warning-text" : "text-text-secondary",
281
- children: Z.needsTokenRefresh ? G("channel.tokenRefreshRequired", "Required soon") : G("channel.tokenRefreshNotNeeded", "Not needed")
279
+ children: Z.needsTokenRefresh ? W("channel.tokenRefreshRequired", "Required soon") : W("channel.tokenRefreshNotNeeded", "Not needed")
282
280
  })] }),
283
281
  Z.lastTestedAt && /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
284
282
  className: "text-sm text-text-secondary",
285
- children: "Last Tested"
283
+ children: W("channel.lastTested", "Last Tested")
286
284
  }), /* @__PURE__ */ b("p", {
287
285
  className: "flex items-center gap-2",
288
- children: [/* @__PURE__ */ y(u, { className: "size-4 text-text-secondary" }), new Date(Z.lastTestedAt).toLocaleString()]
286
+ children: [/* @__PURE__ */ y(ie, { className: "size-4 text-text-secondary" }), new Date(Z.lastTestedAt).toLocaleString()]
289
287
  })] }),
290
288
  Z.daysUntilExpiry !== null && Z.daysUntilExpiry !== void 0 && /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
291
289
  className: "text-sm text-text-secondary",
292
- children: "Days Until Token Expiry"
293
- }), /* @__PURE__ */ b("p", {
290
+ children: W("connection.daysUntilTokenExpiry", "Days Until Token Expiry")
291
+ }), /* @__PURE__ */ y("p", {
294
292
  className: Z.daysUntilExpiry < 30 ? "text-status-warning-text" : "text-text-secondary",
295
- children: [Z.daysUntilExpiry, " days"]
293
+ children: W("channel.duration.days", "{{count}} days", { count: Z.daysUntilExpiry })
296
294
  })] })
297
295
  ]
298
296
  }),
@@ -300,7 +298,7 @@ function D() {
300
298
  className: "pt-4 border-t",
301
299
  children: [/* @__PURE__ */ y("p", {
302
300
  className: "text-sm text-text-secondary mb-2",
303
- children: "Last Test Status"
301
+ children: W("connection.lastTestStatus", "Last Test Status")
304
302
  }), /* @__PURE__ */ y("div", {
305
303
  className: "p-3 rounded-lg bg-bg-sunken",
306
304
  children: /* @__PURE__ */ y("p", {
@@ -313,11 +311,11 @@ function D() {
313
311
  className: "pt-4 border-t",
314
312
  children: [/* @__PURE__ */ y("p", {
315
313
  className: "text-sm text-text-secondary mb-2",
316
- children: G("channel.tags", "Tags")
314
+ children: W("channel.tags", "Tags")
317
315
  }), /* @__PURE__ */ y("div", {
318
316
  className: "flex flex-wrap gap-1.5",
319
317
  "aria-label": "Connection tags",
320
- children: Z.tagAssignments.map((e) => /* @__PURE__ */ y(T, {
318
+ children: Z.tagAssignments.map((e) => /* @__PURE__ */ y(w, {
321
319
  id: e.tagId,
322
320
  label: e.tagId,
323
321
  size: "md"
@@ -325,48 +323,48 @@ function D() {
325
323
  })]
326
324
  })
327
325
  ]
328
- }), /* @__PURE__ */ b(S, {
326
+ }), /* @__PURE__ */ b(x, {
329
327
  treatment: "plain",
330
328
  className: "border-border-seam p-6 space-y-4 lg:col-span-1",
331
329
  children: [/* @__PURE__ */ y("h3", {
332
330
  className: "font-semibold",
333
- children: G("channel.actions", "Actions")
331
+ children: W("channel.actions", "Actions")
334
332
  }), /* @__PURE__ */ b("div", {
335
333
  className: "flex flex-col gap-3",
336
334
  children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
337
335
  className: "font-medium",
338
- children: G("channel.deleteConnection", "Delete Connection")
336
+ children: W("channel.deleteConnection", "Delete Connection")
339
337
  }), /* @__PURE__ */ y("p", {
340
338
  className: "text-sm text-text-secondary",
341
- children: G("channel.deleteConnectionWarning", "Permanently remove this connection. The channel will become inactive.")
342
- })] }), se ? /* @__PURE__ */ b("div", {
339
+ children: W("channel.deleteConnectionWarning", "Permanently remove this connection. The channel will become inactive.")
340
+ })] }), le ? /* @__PURE__ */ b("div", {
343
341
  className: "flex items-center gap-2",
344
342
  children: [
345
343
  /* @__PURE__ */ y("span", {
346
344
  className: "text-sm text-text-secondary",
347
- children: G("channel.confirmDelete", "Confirm delete?")
345
+ children: W("channel.confirmDelete", "Confirm delete?")
348
346
  }),
349
347
  /* @__PURE__ */ b("button", {
350
348
  type: "button",
351
- onClick: ce,
349
+ onClick: ue,
352
350
  disabled: K,
353
351
  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",
354
- children: [K ? /* @__PURE__ */ y(f, { className: "size-4 animate-spin" }) : /* @__PURE__ */ y(h, { className: "size-4 " }), G("channel.delete", "Delete")]
352
+ children: [K ? /* @__PURE__ */ y(p, { className: "size-4 animate-spin" }) : /* @__PURE__ */ y(g, { className: "size-4 " }), W("channel.delete", "Delete")]
355
353
  }),
356
354
  /* @__PURE__ */ y("button", {
357
355
  type: "button",
358
356
  onClick: () => X(!1),
359
357
  className: "px-4 py-2 rounded-lg border hover:bg-bg-sunken transition-colors",
360
- children: G("channel.cancel", "Cancel")
358
+ children: W("channel.cancel", "Cancel")
361
359
  })
362
360
  ]
363
- }) : /* @__PURE__ */ y(i, {
364
- permission: a.CHANNEL_CONNECTION_DELETE,
361
+ }) : /* @__PURE__ */ y(o, {
362
+ permission: s.CHANNEL_CONNECTION_DELETE,
365
363
  children: /* @__PURE__ */ b("button", {
366
364
  type: "button",
367
365
  onClick: () => X(!0),
368
366
  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",
369
- children: [/* @__PURE__ */ y(h, { className: "size-4 " }), G("channel.delete", "Delete")]
367
+ children: [/* @__PURE__ */ y(g, { className: "size-4 " }), W("channel.delete", "Delete")]
370
368
  })
371
369
  })]
372
370
  })]
@@ -378,6 +376,6 @@ function D() {
378
376
  });
379
377
  }
380
378
  //#endregion
381
- export { D as ConnectionDetailsPage };
379
+ export { T as ConnectionDetailsPage };
382
380
 
383
381
  //# sourceMappingURL=ConnectionDetailsPage.js.map