@burdenoff/microfe-channel 2026.1013.2 → 2026.1114.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/ChannelRoot.d.ts +0 -2
- package/dist/channel/ChannelRoot.js +9 -6
- package/dist/channel/ChannelRoot.js.map +1 -1
- package/dist/channel/ChannelRoutes.d.ts +1 -2
- package/dist/channel/ChannelRoutes.js +5 -6
- package/dist/channel/ChannelRoutes.js.map +1 -1
- package/dist/channel/components/AdapterCredentialsForm.d.ts +2 -4
- package/dist/channel/components/AdapterCredentialsForm.js +1 -2
- package/dist/channel/components/AdapterCredentialsForm.js.map +1 -1
- package/dist/channel/components/FeatureNotEnabled.d.ts +1 -2
- package/dist/channel/components/FeatureNotEnabled.js +1 -2
- package/dist/channel/components/FeatureNotEnabled.js.map +1 -1
- package/dist/channel/components/MarkdownEditor.d.ts +1 -2
- package/dist/channel/components/MarkdownEditor.js +2 -5
- package/dist/channel/components/MarkdownEditor.js.map +1 -1
- package/dist/channel/components/PermissionGuard.d.ts +0 -2
- package/dist/channel/components/PermissionGuard.js +1 -2
- package/dist/channel/components/PermissionGuard.js.map +1 -1
- package/dist/channel/config/adapterCredentials.d.ts +0 -2
- package/dist/channel/config/adapterCredentials.js +3 -7
- package/dist/channel/config/adapterCredentials.js.map +1 -1
- package/dist/channel/constants/permissions.d.ts +0 -1
- package/dist/channel/constants/permissions.js +1 -2
- package/dist/channel/constants/permissions.js.map +1 -1
- package/dist/channel/graphql/global-client.d.ts +0 -2
- package/dist/channel/graphql/global-client.js +5 -6
- package/dist/channel/graphql/global-client.js.map +1 -1
- package/dist/channel/hooks/useChannelOrgSettings.d.ts +0 -2
- package/dist/channel/hooks/useChannelOrgSettings.js +20 -21
- package/dist/channel/hooks/useChannelOrgSettings.js.map +1 -1
- package/dist/channel/hooks/useChannelSubscription.d.ts +0 -1
- package/dist/channel/hooks/useChannelSubscription.js +7 -12
- package/dist/channel/hooks/useChannelSubscription.js.map +1 -1
- package/dist/channel/hooks/useChannels.d.ts +20 -30
- package/dist/channel/hooks/useChannels.js +100 -101
- package/dist/channel/hooks/useChannels.js.map +1 -1
- package/dist/channel/hooks/useTr.d.ts +0 -1
- package/dist/channel/hooks/useTr.js +1 -2
- package/dist/channel/hooks/useTr.js.map +1 -1
- package/dist/channel/index.d.ts +0 -1
- package/dist/channel/index.js +3 -2
- package/dist/channel/modules/channels/hooks/useExport.d.ts +0 -2
- package/dist/channel/modules/channels/hooks/useExport.js +77 -80
- package/dist/channel/modules/channels/hooks/useExport.js.map +1 -1
- package/dist/channel/modules/channels/i18n.d.ts +0 -2
- package/dist/channel/modules/channels/i18n.js +1 -2
- package/dist/channel/modules/channels/i18n.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +10 -11
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.js +400 -401
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +8 -7
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js +5 -10
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelsListPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ChannelsListPage.js +287 -288
- package/dist/channel/modules/channels/pages/ChannelsListPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +11 -12
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionsListPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js +1 -2
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/CreateChannelPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/CreateChannelPage.js +4 -5
- package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/CreateConnectionPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/CreateConnectionPage.js +37 -38
- package/dist/channel/modules/channels/pages/CreateConnectionPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js +7 -12
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/EditChannelPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/EditChannelPage.js +105 -103
- package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/EditConnectionPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/EditConnectionPage.js +22 -23
- package/dist/channel/modules/channels/pages/EditConnectionPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/SendMessagePage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/SendMessagePage.js +180 -187
- package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.d.ts +1 -2
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js +144 -149
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js.map +1 -1
- package/dist/channel/pages/ChannelLandingPage.d.ts +1 -2
- package/dist/channel/pages/ChannelLandingPage.js +20 -21
- package/dist/channel/pages/ChannelLandingPage.js.map +1 -1
- package/dist/channel/providers/ChannelProvider.d.ts +0 -2
- package/dist/channel/providers/ChannelProvider.js +1 -2
- package/dist/channel/providers/ChannelProvider.js.map +1 -1
- package/dist/channel/types/index.d.ts +0 -2
- package/dist/channel/utils/shellBridge.d.ts +0 -1
- package/dist/channel/utils/shellBridge.js +1 -2
- package/dist/channel/utils/shellBridge.js.map +1 -1
- package/dist/dev/App.d.ts +1 -2
- package/dist/dev/main.d.ts +0 -2
- package/dist/generated/global-operations.d.ts +885 -336
- package/dist/generated/global-operations.js +2 -3
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.d.ts +2 -22
- package/dist/generated/global-types.js +1 -2
- package/dist/generated/global-types.js.map +1 -1
- package/dist/generated/wspace-operations.d.ts +4 -2
- package/dist/generated/wspace-types.d.ts +4 -24
- package/dist/index.d.ts +0 -1
- package/dist/node_modules/@apollo/client/errors/CombinedGraphQLErrors.js +1 -2
- package/dist/node_modules/@apollo/client/errors/CombinedGraphQLErrors.js.map +1 -1
- package/dist/node_modules/@apollo/client/errors/utils.js +1 -2
- package/dist/node_modules/@apollo/client/errors/utils.js.map +1 -1
- package/dist/translations/en.d.ts +0 -1
- package/dist/utils/nativeBridge.d.ts +0 -1
- package/dist/utils/nativeBridge.js +1 -2
- package/package.json +65 -43
- package/dist/channel/ChannelRoot.d.ts.map +0 -1
- package/dist/channel/ChannelRoutes.d.ts.map +0 -1
- package/dist/channel/components/AdapterCredentialsForm.d.ts.map +0 -1
- package/dist/channel/components/FeatureNotEnabled.d.ts.map +0 -1
- package/dist/channel/components/MarkdownEditor.d.ts.map +0 -1
- package/dist/channel/components/PermissionGuard.d.ts.map +0 -1
- package/dist/channel/config/adapterCredentials.d.ts.map +0 -1
- package/dist/channel/constants/permissions.d.ts.map +0 -1
- package/dist/channel/graphql/global-client.d.ts.map +0 -1
- package/dist/channel/hooks/useChannelOrgSettings.d.ts.map +0 -1
- package/dist/channel/hooks/useChannelSubscription.d.ts.map +0 -1
- package/dist/channel/hooks/useChannels.d.ts.map +0 -1
- package/dist/channel/hooks/useTr.d.ts.map +0 -1
- package/dist/channel/index.d.ts.map +0 -1
- package/dist/channel/modules/channels/hooks/useExport.d.ts.map +0 -1
- package/dist/channel/modules/channels/i18n.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ChannelsListPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/ConnectionsListPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/CreateChannelPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/CreateConnectionPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/EditChannelPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/EditConnectionPage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/SendMessagePage.d.ts.map +0 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.d.ts.map +0 -1
- package/dist/channel/pages/ChannelLandingPage.d.ts.map +0 -1
- package/dist/channel/providers/ChannelProvider.d.ts.map +0 -1
- package/dist/channel/types/index.d.ts.map +0 -1
- package/dist/channel/utils/shellBridge.d.ts.map +0 -1
- package/dist/dev/App.d.ts.map +0 -1
- package/dist/dev/main.d.ts.map +0 -1
- package/dist/generated/global-operations.d.ts.map +0 -1
- package/dist/generated/global-types.d.ts.map +0 -1
- package/dist/generated/wspace-operations.d.ts.map +0 -1
- package/dist/generated/wspace-types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/translations/en.d.ts.map +0 -1
- package/dist/utils/nativeBridge.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelSettingsPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelSettingsPage.tsx"],"sourcesContent":["/**\n * Channel Settings Page\n *\n * Global settings for the channels module.\n */\n\nimport { useState, useEffect, useRef, useMemo } from 'react';\nimport { useNavigate } from 'react-router';\nimport { useTr } from '../../../hooks/useTr';\nimport {\n ArrowLeft,\n Settings,\n Bell,\n Shield,\n Clock,\n Database,\n Save,\n Loader2,\n AlertCircle,\n CheckCircle2,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose, ShowMore } from '@burdenoff/fe-libs/ui';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useChannelOrgSettings } from '../../../hooks/useChannelOrgSettings';\nimport type { ChannelOrgSettingsInput } from '../../../hooks/useChannelOrgSettings';\n\ninterface ChannelSettings {\n defaultPriority: number;\n rateLimitPerMinute: number;\n rateLimitPerHour: number;\n healthCheckIntervalMinutes: number;\n enableAuditLogging: boolean;\n enableWebhookVerification: boolean;\n sessionTimeoutMinutes: number;\n}\n\nconst DEFAULT_SETTINGS: ChannelSettings = {\n defaultPriority: 2,\n rateLimitPerMinute: 100,\n rateLimitPerHour: 5000,\n healthCheckIntervalMinutes: 5,\n enableAuditLogging: true,\n enableWebhookVerification: true,\n sessionTimeoutMinutes: 30,\n};\n\nexport function ChannelSettingsPage() {\n const navigate = useNavigate();\n const tr = useTr();\n\n const priorityOptions = useMemo(\n () => [\n { value: 0, label: tr('channel.priorityLabel.lowest', '0 - Lowest') },\n { value: 1, label: tr('channel.priorityLabel.low', '1 - Low') },\n { value: 2, label: tr('channel.priorityLabel.normal', '2 - Normal') },\n { value: 3, label: tr('channel.priorityLabel.high', '3 - High') },\n { value: 4, label: tr('channel.priorityLabel.highest', '4 - Highest') },\n ],\n [tr]\n );\n\n const healthIntervalOptions = useMemo(\n () => [\n { value: 1, label: tr('channel.duration.oneMinute', '1 minute') },\n { value: 5, label: tr('channel.duration.fiveMinutes', '5 minutes') },\n { value: 10, label: tr('channel.duration.tenMinutes', '10 minutes') },\n { value: 15, label: tr('channel.duration.fifteenMinutes', '15 minutes') },\n { value: 30, label: tr('channel.duration.thirtyMinutes', '30 minutes') },\n { value: 60, label: tr('channel.duration.oneHour', '1 hour') },\n ],\n [tr]\n );\n\n const sessionTimeoutOptions = useMemo(\n () => [\n { value: 15, label: tr('channel.duration.fifteenMinutes', '15 minutes') },\n { value: 30, label: tr('channel.duration.thirtyMinutes', '30 minutes') },\n { value: 60, label: tr('channel.duration.oneHour', '1 hour') },\n { value: 120, label: tr('channel.duration.twoHours', '2 hours') },\n { value: 240, label: tr('channel.duration.fourHours', '4 hours') },\n ],\n [tr]\n );\n\n const {\n settings: remoteSettings,\n isLoading: isLoadingSettings,\n error: settingsLoadError,\n saveSettings,\n } = useChannelOrgSettings();\n\n const [settings, setSettings] = useState<ChannelSettings>(DEFAULT_SETTINGS);\n const [isSaving, setIsSaving] = useState(false);\n const [saveSuccess, setSaveSuccess] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const saveSuccessTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Populate / re-sync form from remote settings whenever they change.\n // This handles both the initial load and server-side normalisations returned\n // by saveSettings (e.g. clamped values), so the form always reflects the\n // true persisted state rather than potentially stale local values.\n // Defer setState to a microtask to avoid react-hooks/exhaustive-deps warnings.\n useEffect(() => {\n const timerRef = saveSuccessTimerRef;\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n const loadedOrgIdRef = useRef<string | null>(null);\n useEffect(() => {\n if (!remoteSettings) return;\n // Re-hydrate whenever the org changes *or* the updatedAt timestamp changes,\n // so server-side coercion after a save is reflected immediately.\n const changeKey = `${remoteSettings.organizationId}:${remoteSettings.updatedAt}`;\n if (changeKey === loadedOrgIdRef.current) return;\n loadedOrgIdRef.current = changeKey;\n void Promise.resolve().then(() => {\n setSettings({\n defaultPriority: remoteSettings.defaultPriority,\n rateLimitPerMinute: remoteSettings.rateLimitPerMinute,\n rateLimitPerHour: remoteSettings.rateLimitPerHour,\n healthCheckIntervalMinutes: remoteSettings.healthCheckIntervalMinutes,\n enableAuditLogging: remoteSettings.enableAuditLogging,\n enableWebhookVerification: remoteSettings.enableWebhookVerification,\n sessionTimeoutMinutes: remoteSettings.sessionTimeoutMinutes,\n });\n });\n }, [remoteSettings]);\n\n const handleSave = async () => {\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n const input: ChannelOrgSettingsInput = {\n defaultPriority: settings.defaultPriority,\n rateLimitPerMinute: settings.rateLimitPerMinute,\n rateLimitPerHour: settings.rateLimitPerHour,\n healthCheckIntervalMinutes: settings.healthCheckIntervalMinutes,\n enableAuditLogging: settings.enableAuditLogging,\n enableWebhookVerification: settings.enableWebhookVerification,\n sessionTimeoutMinutes: settings.sessionTimeoutMinutes,\n };\n // saveSettings returns null when organizationId is absent (context not\n // ready); only show success when something was actually persisted (P2).\n const saved = await saveSettings(input);\n if (saved) {\n setSaveSuccess(true);\n if (saveSuccessTimerRef.current) clearTimeout(saveSuccessTimerRef.current);\n saveSuccessTimerRef.current = setTimeout(() => setSaveSuccess(false), 3000);\n } else {\n setSaveError(\n tr(\n 'channel.settingsSaveNotPersisted',\n 'Settings could not be saved — organization context is unavailable.'\n )\n );\n }\n } catch (err: unknown) {\n const message =\n err instanceof Error\n ? err.message\n : tr('channel.settingsSaveFailed', 'Failed to save settings');\n setSaveError(message);\n } finally {\n setIsSaving(false);\n }\n };\n\n const updateSetting = <K extends keyof ChannelSettings>(key: K, value: ChannelSettings[K]) => {\n setSettings((prev) => ({ ...prev, [key]: value }));\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <div className=\"min-h-screen bg-bg-surface\">\n {/* Header */}\n <div className=\"border-b bg-bg-surface\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate('./list')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"size-5 \" />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 bg-action-primary-bg/10 rounded-lg\">\n <Settings className=\"size-5 text-primary\" />\n </div>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.channelSettings', 'Channel Settings')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.channelSettingsSubtitle',\n 'Configure global channel preferences'\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-2xl space-y-8\">\n {/* Page purpose */}\n <PagePurpose icon={<Settings size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.settingsPurpose',\n 'Organization-wide defaults that apply to every channel — how fast messages may go out (rate limits), the default priority for new channels, how often health checks run, and security policies like webhook signature verification and audit logging. Tune these to protect provider quotas, keep an audit trail, and set sensible defaults so each new channel starts configured the way you want.'\n )}\n </PagePurpose>\n\n {/* Loading state */}\n {isLoadingSettings && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span className=\"text-sm\">{tr('channel.loadingSettings', 'Loading settings…')}</span>\n </div>\n )}\n\n {/* Settings load error */}\n {settingsLoadError && (\n <div className=\"p-4 rounded-lg bg-status-error-bg-subtle border border-status-error-border flex items-center gap-3\">\n <AlertCircle className=\"size-5 text-status-error-text shrink-0\" />\n <p className=\"text-sm text-status-error-text\">\n {tr(\n 'channel.settingsLoadError',\n 'Failed to load settings — saving is disabled to prevent overwriting existing configuration.'\n )}\n </p>\n </div>\n )}\n\n {/* Alerts */}\n {saveSuccess && (\n <div className=\"p-4 rounded-lg bg-status-success-bg-subtle border border-status-success-border flex items-center gap-3\">\n <CheckCircle2 className=\"size-5 text-status-success-text shrink-0\" />\n <p className=\"text-sm text-status-success-text\">\n {tr('channel.settingsSaved', 'Settings saved successfully')}\n </p>\n </div>\n )}\n\n {saveError && (\n <div className=\"p-4 rounded-lg bg-status-error-bg/10 border border-status-error-border/20 flex items-center gap-3\">\n <AlertCircle className=\"size-5 text-status-error-text shrink-0\" />\n <p className=\"text-sm text-status-error-text\">{saveError}</p>\n </div>\n )}\n\n {/* Delivery Settings — the primary, most-impactful section */}\n <EmphasisPanel className=\"space-y-4 p-6\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.deliverySettings', 'Delivery Settings')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.defaultPriority', 'Default Priority')}\n </label>\n <select\n value={settings.defaultPriority}\n onChange={(e) => updateSetting('defaultPriority', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {priorityOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'channel.defaultPriorityHint',\n 'Priority used when creating new channels without explicit priority'\n )}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"channel-settings-rate-limit-min\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n </label>\n <input\n id=\"channel-settings-rate-limit-min\"\n aria-label={tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n type=\"number\"\n value={settings.rateLimitPerMinute}\n onChange={(e) => updateSetting('rateLimitPerMinute', Number(e.target.value))}\n min={1}\n max={1000}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"channel-settings-rate-limit-hour\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n </label>\n <input\n id=\"channel-settings-rate-limit-hour\"\n aria-label={tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n type=\"number\"\n value={settings.rateLimitPerHour}\n onChange={(e) => updateSetting('rateLimitPerHour', Number(e.target.value))}\n min={1}\n max={50000}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Health Check Settings */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.healthMonitoring', 'Health Monitoring')}\n </h2>\n </div>\n\n <div>\n <label\n htmlFor=\"channel-settings-health-interval\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.healthCheckInterval', 'Health Check Interval (minutes)')}\n </label>\n <select\n id=\"channel-settings-health-interval\"\n value={settings.healthCheckIntervalMinutes}\n onChange={(e) =>\n updateSetting('healthCheckIntervalMinutes', Number(e.target.value))\n }\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {healthIntervalOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'channel.healthCheckIntervalHint',\n 'How often to check connection health status'\n )}\n </p>\n </div>\n </GlassCard>\n\n {/* Security Settings */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">{tr('channel.security', 'Security')}</h2>\n </div>\n\n <div className=\"space-y-4\">\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-bg-sunken/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.webhookSignatureVerification', 'Webhook Signature Verification')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.webhookSignatureVerificationHint',\n 'Verify HMAC signatures on incoming webhooks'\n )}\n </p>\n </div>\n <input\n type=\"checkbox\"\n aria-label={tr(\n 'channel.webhookSignatureVerification',\n 'Webhook Signature Verification'\n )}\n checked={settings.enableWebhookVerification}\n onChange={(e) => updateSetting('enableWebhookVerification', e.target.checked)}\n className=\"size-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-bg-sunken/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">{tr('channel.auditLogging', 'Audit Logging')}</p>\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.auditLoggingHint', 'Log all channel operations for audit trail')}\n </p>\n </div>\n <input\n type=\"checkbox\"\n aria-label={tr('channel.auditLogging', 'Audit Logging')}\n checked={settings.enableAuditLogging}\n onChange={(e) => updateSetting('enableAuditLogging', e.target.checked)}\n className=\"size-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.sessionTimeout', 'Session Timeout (minutes)')}\n </label>\n <select\n value={settings.sessionTimeoutMinutes}\n onChange={(e) => updateSetting('sessionTimeoutMinutes', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {sessionTimeoutOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n </GlassCard>\n\n {/* Data Management */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.dataManagement', 'Data Management')}\n </h2>\n </div>\n\n <ShowMore\n collapsedHeight={96}\n label={tr('channel.showRetentionPolicies', 'Show retention policies')}\n >\n <div className=\"space-y-4\">\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.webhookEventRetention', 'Webhook Event Retention')}\n </p>\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.retentionDays', '{{count}} days', { count: 7 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.webhookEventRetentionHint',\n 'Incoming webhook events are automatically deleted after 7 days'\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.deliveryAttemptRetention', 'Delivery Attempt Retention')}\n </p>\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.retentionDays', '{{count}} days', { count: 7 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.deliveryAttemptRetentionHint',\n 'Successful delivery attempts are automatically deleted after 7 days'\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.failedDeliveryRetention', 'Failed Delivery Retention')}\n </p>\n <span className=\"text-sm text-status-warning-text\">\n {tr('channel.retentionDays', '{{count}} days', { count: 30 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.failedDeliveryRetentionHint',\n 'Failed delivery attempts are kept for 30 days for debugging'\n )}\n </p>\n </div>\n </div>\n </ShowMore>\n </GlassCard>\n\n {/* Save Button */}\n <div className=\"flex justify-end pt-4\">\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={isSaving || isLoadingSettings || !!settingsLoadError}\n className=\"px-6 py-3 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 flex items-center gap-2\"\n >\n {isSaving ? (\n <Loader2 className=\"size-5 animate-spin\" />\n ) : (\n <Save className=\"size-5 \" />\n )}\n {tr('channel.saveSettings', 'Save Settings')}\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChannelSettingsPage.js","names":["DEFAULT_SETTINGS","ChannelSettingsPage","navigate","useNavigate","tr","useTr","priorityOptions","useMemo","healthIntervalOptions","sessionTimeoutOptions","remoteSettings","isLoadingSettings","settingsLoadError","useChannelOrgSettings","settings","setSettings","useState","isSaving","setIsSaving","saveSuccess","setSaveSuccess","saveError","setSaveError","saveSuccessTimerRef","useRef","timerRef","loadedOrgIdRef","changeKey","handleSave","input","saveSettings","err","message","updateSetting","prev","key","value","PermissionGuard","CHANNEL_PERMISSIONS","ArrowLeft","Settings","PagePurpose","Loader2","AlertCircle","CheckCircle2","EmphasisPanel","Bell","option","GlassCard","Clock","Shield","Database","ShowMore","Save"],"sources":["../../../../../src/channel/modules/channels/pages/ChannelSettingsPage.tsx"],"sourcesContent":["/**\n * Channel Settings Page\n *\n * Global settings for the channels module.\n */\n\nimport { useState, useEffect, useRef, useMemo } from 'react';\nimport { useNavigate } from 'react-router';\nimport { useTr } from '../../../hooks/useTr';\nimport {\n ArrowLeft,\n Settings,\n Bell,\n Shield,\n Clock,\n Database,\n Save,\n Loader2,\n AlertCircle,\n CheckCircle2,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose, ShowMore } from '@burdenoff/fe-libs/ui';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useChannelOrgSettings } from '../../../hooks/useChannelOrgSettings';\nimport type { ChannelOrgSettingsInput } from '../../../hooks/useChannelOrgSettings';\n\ninterface ChannelSettings {\n defaultPriority: number;\n rateLimitPerMinute: number;\n rateLimitPerHour: number;\n healthCheckIntervalMinutes: number;\n enableAuditLogging: boolean;\n enableWebhookVerification: boolean;\n sessionTimeoutMinutes: number;\n}\n\nconst DEFAULT_SETTINGS: ChannelSettings = {\n defaultPriority: 2,\n rateLimitPerMinute: 100,\n rateLimitPerHour: 5000,\n healthCheckIntervalMinutes: 5,\n enableAuditLogging: true,\n enableWebhookVerification: true,\n sessionTimeoutMinutes: 30,\n};\n\nexport function ChannelSettingsPage() {\n const navigate = useNavigate();\n const tr = useTr();\n\n const priorityOptions = useMemo(\n () => [\n { value: 0, label: tr('channel.priorityLabel.lowest', '0 - Lowest') },\n { value: 1, label: tr('channel.priorityLabel.low', '1 - Low') },\n { value: 2, label: tr('channel.priorityLabel.normal', '2 - Normal') },\n { value: 3, label: tr('channel.priorityLabel.high', '3 - High') },\n { value: 4, label: tr('channel.priorityLabel.highest', '4 - Highest') },\n ],\n [tr]\n );\n\n const healthIntervalOptions = useMemo(\n () => [\n { value: 1, label: tr('channel.duration.oneMinute', '1 minute') },\n { value: 5, label: tr('channel.duration.fiveMinutes', '5 minutes') },\n { value: 10, label: tr('channel.duration.tenMinutes', '10 minutes') },\n { value: 15, label: tr('channel.duration.fifteenMinutes', '15 minutes') },\n { value: 30, label: tr('channel.duration.thirtyMinutes', '30 minutes') },\n { value: 60, label: tr('channel.duration.oneHour', '1 hour') },\n ],\n [tr]\n );\n\n const sessionTimeoutOptions = useMemo(\n () => [\n { value: 15, label: tr('channel.duration.fifteenMinutes', '15 minutes') },\n { value: 30, label: tr('channel.duration.thirtyMinutes', '30 minutes') },\n { value: 60, label: tr('channel.duration.oneHour', '1 hour') },\n { value: 120, label: tr('channel.duration.twoHours', '2 hours') },\n { value: 240, label: tr('channel.duration.fourHours', '4 hours') },\n ],\n [tr]\n );\n\n const {\n settings: remoteSettings,\n isLoading: isLoadingSettings,\n error: settingsLoadError,\n saveSettings,\n } = useChannelOrgSettings();\n\n const [settings, setSettings] = useState<ChannelSettings>(DEFAULT_SETTINGS);\n const [isSaving, setIsSaving] = useState(false);\n const [saveSuccess, setSaveSuccess] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const saveSuccessTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Populate / re-sync form from remote settings whenever they change.\n // This handles both the initial load and server-side normalisations returned\n // by saveSettings (e.g. clamped values), so the form always reflects the\n // true persisted state rather than potentially stale local values.\n // Defer setState to a microtask to avoid react-hooks/exhaustive-deps warnings.\n useEffect(() => {\n const timerRef = saveSuccessTimerRef;\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n const loadedOrgIdRef = useRef<string | null>(null);\n useEffect(() => {\n if (!remoteSettings) return;\n // Re-hydrate whenever the org changes *or* the updatedAt timestamp changes,\n // so server-side coercion after a save is reflected immediately.\n const changeKey = `${remoteSettings.organizationId}:${remoteSettings.updatedAt}`;\n if (changeKey === loadedOrgIdRef.current) return;\n loadedOrgIdRef.current = changeKey;\n void Promise.resolve().then(() => {\n setSettings({\n defaultPriority: remoteSettings.defaultPriority,\n rateLimitPerMinute: remoteSettings.rateLimitPerMinute,\n rateLimitPerHour: remoteSettings.rateLimitPerHour,\n healthCheckIntervalMinutes: remoteSettings.healthCheckIntervalMinutes,\n enableAuditLogging: remoteSettings.enableAuditLogging,\n enableWebhookVerification: remoteSettings.enableWebhookVerification,\n sessionTimeoutMinutes: remoteSettings.sessionTimeoutMinutes,\n });\n });\n }, [remoteSettings]);\n\n const handleSave = async () => {\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n const input: ChannelOrgSettingsInput = {\n defaultPriority: settings.defaultPriority,\n rateLimitPerMinute: settings.rateLimitPerMinute,\n rateLimitPerHour: settings.rateLimitPerHour,\n healthCheckIntervalMinutes: settings.healthCheckIntervalMinutes,\n enableAuditLogging: settings.enableAuditLogging,\n enableWebhookVerification: settings.enableWebhookVerification,\n sessionTimeoutMinutes: settings.sessionTimeoutMinutes,\n };\n // saveSettings returns null when organizationId is absent (context not\n // ready); only show success when something was actually persisted (P2).\n const saved = await saveSettings(input);\n if (saved) {\n setSaveSuccess(true);\n if (saveSuccessTimerRef.current) clearTimeout(saveSuccessTimerRef.current);\n saveSuccessTimerRef.current = setTimeout(() => setSaveSuccess(false), 3000);\n } else {\n setSaveError(\n tr(\n 'channel.settingsSaveNotPersisted',\n 'Settings could not be saved — organization context is unavailable.'\n )\n );\n }\n } catch (err: unknown) {\n const message =\n err instanceof Error\n ? err.message\n : tr('channel.settingsSaveFailed', 'Failed to save settings');\n setSaveError(message);\n } finally {\n setIsSaving(false);\n }\n };\n\n const updateSetting = <K extends keyof ChannelSettings>(key: K, value: ChannelSettings[K]) => {\n setSettings((prev) => ({ ...prev, [key]: value }));\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <div className=\"min-h-screen bg-bg-surface\">\n {/* Header */}\n <div className=\"border-b bg-bg-surface\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate('./list')}\n className=\"p-2 hover:bg-bg-sunken rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"size-5 \" />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 bg-action-primary-bg/10 rounded-lg\">\n <Settings className=\"size-5 text-primary\" />\n </div>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.channelSettings', 'Channel Settings')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.channelSettingsSubtitle',\n 'Configure global channel preferences'\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-2xl space-y-8\">\n {/* Page purpose */}\n <PagePurpose icon={<Settings size={15} aria-hidden=\"true\" />}>\n {tr(\n 'channel.settingsPurpose',\n 'Organization-wide defaults that apply to every channel — how fast messages may go out (rate limits), the default priority for new channels, how often health checks run, and security policies like webhook signature verification and audit logging. Tune these to protect provider quotas, keep an audit trail, and set sensible defaults so each new channel starts configured the way you want.'\n )}\n </PagePurpose>\n\n {/* Loading state */}\n {isLoadingSettings && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span className=\"text-sm\">{tr('channel.loadingSettings', 'Loading settings…')}</span>\n </div>\n )}\n\n {/* Settings load error */}\n {settingsLoadError && (\n <div className=\"p-4 rounded-lg bg-status-error-bg-subtle border border-status-error-border flex items-center gap-3\">\n <AlertCircle className=\"size-5 text-status-error-text shrink-0\" />\n <p className=\"text-sm text-status-error-text\">\n {tr(\n 'channel.settingsLoadError',\n 'Failed to load settings — saving is disabled to prevent overwriting existing configuration.'\n )}\n </p>\n </div>\n )}\n\n {/* Alerts */}\n {saveSuccess && (\n <div className=\"p-4 rounded-lg bg-status-success-bg-subtle border border-status-success-border flex items-center gap-3\">\n <CheckCircle2 className=\"size-5 text-status-success-text shrink-0\" />\n <p className=\"text-sm text-status-success-text\">\n {tr('channel.settingsSaved', 'Settings saved successfully')}\n </p>\n </div>\n )}\n\n {saveError && (\n <div className=\"p-4 rounded-lg bg-status-error-bg/10 border border-status-error-border/20 flex items-center gap-3\">\n <AlertCircle className=\"size-5 text-status-error-text shrink-0\" />\n <p className=\"text-sm text-status-error-text\">{saveError}</p>\n </div>\n )}\n\n {/* Delivery Settings — the primary, most-impactful section */}\n <EmphasisPanel className=\"space-y-4 p-6\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.deliverySettings', 'Delivery Settings')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.defaultPriority', 'Default Priority')}\n </label>\n <select\n value={settings.defaultPriority}\n onChange={(e) => updateSetting('defaultPriority', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {priorityOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'channel.defaultPriorityHint',\n 'Priority used when creating new channels without explicit priority'\n )}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"channel-settings-rate-limit-min\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n </label>\n <input\n id=\"channel-settings-rate-limit-min\"\n aria-label={tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n type=\"number\"\n value={settings.rateLimitPerMinute}\n onChange={(e) => updateSetting('rateLimitPerMinute', Number(e.target.value))}\n min={1}\n max={1000}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"channel-settings-rate-limit-hour\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n </label>\n <input\n id=\"channel-settings-rate-limit-hour\"\n aria-label={tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n type=\"number\"\n value={settings.rateLimitPerHour}\n onChange={(e) => updateSetting('rateLimitPerHour', Number(e.target.value))}\n min={1}\n max={50000}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Health Check Settings */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.healthMonitoring', 'Health Monitoring')}\n </h2>\n </div>\n\n <div>\n <label\n htmlFor=\"channel-settings-health-interval\"\n className=\"text-sm font-medium block mb-1\"\n >\n {tr('channel.healthCheckInterval', 'Health Check Interval (minutes)')}\n </label>\n <select\n id=\"channel-settings-health-interval\"\n value={settings.healthCheckIntervalMinutes}\n onChange={(e) =>\n updateSetting('healthCheckIntervalMinutes', Number(e.target.value))\n }\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {healthIntervalOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'channel.healthCheckIntervalHint',\n 'How often to check connection health status'\n )}\n </p>\n </div>\n </GlassCard>\n\n {/* Security Settings */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">{tr('channel.security', 'Security')}</h2>\n </div>\n\n <div className=\"space-y-4\">\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-bg-sunken/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.webhookSignatureVerification', 'Webhook Signature Verification')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.webhookSignatureVerificationHint',\n 'Verify HMAC signatures on incoming webhooks'\n )}\n </p>\n </div>\n <input\n type=\"checkbox\"\n aria-label={tr(\n 'channel.webhookSignatureVerification',\n 'Webhook Signature Verification'\n )}\n checked={settings.enableWebhookVerification}\n onChange={(e) => updateSetting('enableWebhookVerification', e.target.checked)}\n className=\"size-5 rounded border-border-subtle text-primary focus:ring-primary\"\n />\n </label>\n\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-bg-sunken/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">{tr('channel.auditLogging', 'Audit Logging')}</p>\n <p className=\"text-sm text-text-secondary\">\n {tr('channel.auditLoggingHint', 'Log all channel operations for audit trail')}\n </p>\n </div>\n <input\n type=\"checkbox\"\n aria-label={tr('channel.auditLogging', 'Audit Logging')}\n checked={settings.enableAuditLogging}\n onChange={(e) => updateSetting('enableAuditLogging', e.target.checked)}\n className=\"size-5 rounded border-border-subtle text-primary focus:ring-primary\"\n />\n </label>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.sessionTimeout', 'Session Timeout (minutes)')}\n </label>\n <select\n value={settings.sessionTimeoutMinutes}\n onChange={(e) => updateSetting('sessionTimeoutMinutes', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {sessionTimeoutOptions.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n </GlassCard>\n\n {/* Data Management */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"size-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.dataManagement', 'Data Management')}\n </h2>\n </div>\n\n <ShowMore\n collapsedHeight={96}\n label={tr('channel.showRetentionPolicies', 'Show retention policies')}\n >\n <div className=\"space-y-4\">\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.webhookEventRetention', 'Webhook Event Retention')}\n </p>\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.retentionDays', '{{count}} days', { count: 7 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.webhookEventRetentionHint',\n 'Incoming webhook events are automatically deleted after 7 days'\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.deliveryAttemptRetention', 'Delivery Attempt Retention')}\n </p>\n <span className=\"text-sm text-text-secondary\">\n {tr('channel.retentionDays', '{{count}} days', { count: 7 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.deliveryAttemptRetentionHint',\n 'Successful delivery attempts are automatically deleted after 7 days'\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.failedDeliveryRetention', 'Failed Delivery Retention')}\n </p>\n <span className=\"text-sm text-status-warning-text\">\n {tr('channel.retentionDays', '{{count}} days', { count: 30 })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'channel.failedDeliveryRetentionHint',\n 'Failed delivery attempts are kept for 30 days for debugging'\n )}\n </p>\n </div>\n </div>\n </ShowMore>\n </GlassCard>\n\n {/* Save Button */}\n <div className=\"flex justify-end pt-4\">\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={isSaving || isLoadingSettings || !!settingsLoadError}\n className=\"px-6 py-3 rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 flex items-center gap-2\"\n >\n {isSaving ? (\n <Loader2 className=\"size-5 animate-spin\" />\n ) : (\n <Save className=\"size-5 \" />\n )}\n {tr('channel.saveSettings', 'Save Settings')}\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useTr as e } from "../../../hooks/useTr.js";
|
|
2
|
-
import { useChannel as t } from "../../../
|
|
3
|
-
import { useChannel as n } from "../../../
|
|
2
|
+
import { useChannel as t } from "../../../hooks/useChannels.js";
|
|
3
|
+
import { useChannel as n } from "../../../providers/ChannelProvider.js";
|
|
4
4
|
import { PermissionGuard as r } from "../../../components/PermissionGuard.js";
|
|
5
5
|
import { CHANNEL_PERMISSIONS as i } from "../../../constants/permissions.js";
|
|
6
6
|
import { useExportChannelUsers as a } from "../hooks/useExport.js";
|
|
@@ -10,7 +10,7 @@ import { useNavigate as b, useParams as x } from "react-router";
|
|
|
10
10
|
import { usePermissions as S } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
11
11
|
import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
12
12
|
import { CTAOverflowMenu as ae, EmphasisPanel as oe, IllustratedEmptyState as se, PagePurpose as ce, ResponsiveTable as le } from "@burdenoff/fe-libs/ui";
|
|
13
|
-
|
|
13
|
+
// src/channel/modules/channels/pages/ChannelUsersPage.tsx
|
|
14
14
|
function E() {
|
|
15
15
|
let { channelId: E } = x(), D = b(), { hasPermission: O } = S(), k = O(i.CHANNEL_USER_EXPORT), A = e(), j = s(() => ({
|
|
16
16
|
ONLINE: {
|
|
@@ -69,7 +69,7 @@ function E() {
|
|
|
69
69
|
value: 100,
|
|
70
70
|
label: A("channel.resultsPerPage", "{{count}} results", { count: 100 })
|
|
71
71
|
}
|
|
72
|
-
], [A]), { organizationId: P } =
|
|
72
|
+
], [A]), { organizationId: P } = n(), { triggerExport: F, isExporting: I, exportError: L } = a(P ?? void 0, E), [R, z] = l(!0), [B, V] = l([]), [H, ue] = l(25), [U, de] = l(0), [W, G] = l(0), { channel: K, isLoading: fe, error: q, fetchUserPlatformConnections: J } = t(E), Y = ee(async (e = 0) => {
|
|
73
73
|
if (E) {
|
|
74
74
|
z(!0);
|
|
75
75
|
try {
|
|
@@ -95,11 +95,7 @@ function E() {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
}, [H]), o(() => {
|
|
98
|
-
|
|
99
|
-
Z.current = !1;
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
Q.current || Promise.resolve().then(() => X.current(W));
|
|
98
|
+
Z.current ? Z.current = !1 : Q.current || Promise.resolve().then(() => X.current(W));
|
|
103
99
|
}, [W]);
|
|
104
100
|
let $ = Math.max(1, Math.ceil(U / H)), pe = W > 0, me = W < $ - 1, he = (e) => j[e] || j.OFFLINE, ge = (e) => M[e] || M.EXPIRED, _e = [
|
|
105
101
|
{
|
|
@@ -397,7 +393,6 @@ function E() {
|
|
|
397
393
|
})
|
|
398
394
|
});
|
|
399
395
|
}
|
|
400
|
-
//#endregion
|
|
401
396
|
export { E as ChannelUsersPage };
|
|
402
397
|
|
|
403
398
|
//# sourceMappingURL=ChannelUsersPage.js.map
|
|
@@ -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, 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 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]: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
|
+
{"version":3,"file":"ChannelUsersPage.js","names":["useChannelContext","_Fragment","useChannel$1","ChannelUsersPage","useParams","navigate","useNavigate","usePermissions","canExport","hasPermission","CHANNEL_PERMISSIONS","tr","useTr","PRESENCE_CONFIG","useMemo","PAIRING_CONFIG","Clock","CheckCircle2","XCircle","resultsPerPageOptions","exportUsers","isExportingUsers","exportUsersError","useExportChannelUsers","organizationId","channelId","isLoadingUsers","setIsLoadingUsers","useState","users","setUsers","limit","setLimit","total","setTotal","currentPage","setCurrentPage","useChannel","loadUsers","useCallback","result","fetchUserPlatformConnections","page","loadUsersRef","useRef","isFirstPageRender","limitChangingRef","useEffect","totalPages","hasPrevPage","hasNextPage","getPresenceConfig","status","getPairingConfig","userColumns","User","user","presenceConfig","pairingConfig","PairingIcon","Bell","BellOff","UserCheck","isLoading","Loader2","error","channel","AlertCircle","PermissionGuard","ArrowLeft","option","CTAOverflowMenu","RefreshCw","Download","PagePurpose","EmphasisPanel","ResponsiveTable","IllustratedEmptyState","Fragment","p","ChevronLeft","ChevronRight"],"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 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]: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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"}
|