@burdenoff/microfe-admin 2026.626.1 → 2026.720.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/admin/AdminRoot.js +2 -2
  2. package/dist/admin/AdminRoot.js.map +1 -1
  3. package/dist/admin/AdminRoutes.js +130 -78
  4. package/dist/admin/AdminRoutes.js.map +1 -1
  5. package/dist/admin/components/ConfirmDialog.js.map +1 -1
  6. package/dist/admin/components/DataTable.js.map +1 -1
  7. package/dist/admin/components/MarkdownEditor.js +116 -0
  8. package/dist/admin/components/MarkdownEditor.js.map +1 -0
  9. package/dist/admin/components/index.js +1 -0
  10. package/dist/admin/constants/featureFlags.js +12 -0
  11. package/dist/admin/constants/featureFlags.js.map +1 -0
  12. package/dist/admin/pages/AuditLogsPage.js.map +1 -1
  13. package/dist/admin/pages/CommunicationsPage.js.map +1 -1
  14. package/dist/admin/pages/ContentPagesPage.js +197 -197
  15. package/dist/admin/pages/ContentPagesPage.js.map +1 -1
  16. package/dist/admin/pages/DashboardPage.js +5 -5
  17. package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
  18. package/dist/admin/pages/ReferralsPage.js.map +1 -1
  19. package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
  20. package/dist/admin/pages/SREPage.js.map +1 -1
  21. package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
  22. package/dist/admin/pages/SettingsPage.js +6 -6
  23. package/dist/admin/pages/SettingsPage.js.map +1 -1
  24. package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
  25. package/dist/admin/pages/TenantDetailPage.js +169 -157
  26. package/dist/admin/pages/TenantDetailPage.js.map +1 -1
  27. package/dist/admin/pages/TenantFormRoutePage.js +75 -74
  28. package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
  29. package/dist/admin/pages/TenantsListPage.js +169 -157
  30. package/dist/admin/pages/TenantsListPage.js.map +1 -1
  31. package/dist/admin/pages/UsersListPage.js +5 -5
  32. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canManageTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canManageTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canManageTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup ? 'text-status-success-text' : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled ? 'text-status-success-text' : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired ? 'text-status-success-text' : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canManageTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canManageTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantMembersPanel } from '../components/TenantMembersPanel';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { PagePurpose, NextSteps, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const managementFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const detailFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.detail, true);\n const editFeatureEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.edit, true);\n const pageFeatureEnabled = managementFeatureEnabled && detailFeatureEnabled;\n const canWriteTenants = canManageTenants && editFeatureEnabled;\n const canLifecycleTenants = canManageTenants && managementFeatureEnabled;\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n if (!permissionsLoading && !canViewTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {null}\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={tr('admin.tenantDetail.featureDisabledTitle', 'Tenant management unavailable')}\n description={tr(\n 'admin.tenantDetail.featureDisabled',\n 'This feature is currently disabled.'\n )}\n >\n {null}\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canWriteTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canLifecycleTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-status-warning-text\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-status-success-text\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'admin.tenantDetail.purpose',\n 'Everything about a single tenant in one place — their profile, the people inside it, and their single sign-on setup. From here you manage that one customer account: review who has access, adjust their SSO, and suspend or reactivate them when needed.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey={`admin-tenant-detail`}\n steps={[\n {\n id: 'edit-tenant',\n label: tr('admin.tenantDetail.nextSteps.edit', 'Edit tenant details'),\n description: tr(\n 'admin.tenantDetail.nextSteps.editDesc',\n 'Update the name, domain or plan for this tenant.'\n ),\n onClick: () => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`)),\n done: !canWriteTenants,\n },\n {\n id: 'review-members',\n label: tr('admin.tenantDetail.nextSteps.members', 'Review members'),\n description: tr(\n 'admin.tenantDetail.nextSteps.membersDesc',\n 'Check who has access inside this tenant.'\n ),\n href: '#tenant-members',\n },\n {\n id: 'configure-sso',\n label: tr('admin.tenantDetail.nextSteps.sso', 'Configure SSO'),\n description: tr(\n 'admin.tenantDetail.nextSteps.ssoDesc',\n 'Set up single sign-on so their team logs in with your IdP.'\n ),\n href: '#tenant-sso',\n },\n ]}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Overview — the primary emphasis zone for this page. */}\n <EmphasisPanel className=\"p-0\">\n <Card className=\"border-0 bg-transparent shadow-none hover:border-0\">\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-text-primary\">{tenant.name}</h2>\n <p className=\"text-sm text-text-secondary\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-action-primary-bg/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <div>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n </EmphasisPanel>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired\n ? 'text-status-success-text'\n : 'text-text-secondary'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <div id=\"tenant-sso\">\n <TenantSsoPanel tenantId={tenant.id} canManage={canWriteTenants} />\n </div>\n\n {/* Tenant Members (BOFF-2652) — gates createOrganization on DEDICATED tenants */}\n <div id=\"tenant-members\">\n <TenantMembersPanel\n tenantId={tenant.id}\n tenantType={tenant.type ?? ''}\n canManage={canWriteTenants}\n />\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -11,30 +11,31 @@ import { joinPath as c } from "../utils/navigation.js";
11
11
  import { mapTenantToUi as l } from "../utils/mappers.js";
12
12
  import { useCreateTenantMutation as u, useGetTenantQuery as d, useUpdateTenantMutation as f } from "../../generated/global-operations.js";
13
13
  import { emitTenantLifecycle as p } from "../utils/tenantEvents.js";
14
- import { useCallback as m, useMemo as h } from "react";
15
- import { ArrowLeft as g, Loader2 as _ } from "lucide-react";
16
- import { useNavigate as v, useParams as y } from "react-router-dom";
17
- import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
18
- import { useFeatureFlag as C } from "@burdenoff/fe-libs/shared/feature-flags";
19
- import { PagePurpose as w } from "@burdenoff/fe-libs/ui";
14
+ import { TENANT_FEATURE_FLAGS as m } from "../constants/featureFlags.js";
15
+ import { useCallback as h, useMemo as g } from "react";
16
+ import { ArrowLeft as _, Loader2 as v } from "lucide-react";
17
+ import { useNavigate as y, useParams as b } from "react-router-dom";
18
+ import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
19
+ import { useFeatureFlag as w } from "@burdenoff/fe-libs/shared/feature-flags";
20
20
  import { useI18n as T } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
21
- import { useEventBus as E } from "@burdenoff/fe-libs/shared/events";
21
+ import { PagePurpose as E } from "@burdenoff/fe-libs/ui";
22
+ import { useEventBus as D } from "@burdenoff/fe-libs/shared/events";
22
23
  //#region src/admin/pages/TenantFormRoutePage.tsx
23
- function D({ mode: D }) {
24
- let { tenantId: O } = y(), k = v(), { basePath: A = "" } = t(), j = e(), { t: M } = T(), { canManageTenants: N, isLoading: P } = n(), F = D === "edit", I = C(F ? "admin.tenant.edit" : "admin.tenant.create", !0), L = E(), R = m((e, t, n) => a(M, e, t, n), [M]), z = c(A, "tenants"), B = F && O ? c(A, `tenants/${O}`) : z, { data: V, loading: H, error: U } = d({
25
- variables: { id: O ?? "" },
26
- skip: !F || !O || P || !N || !I,
24
+ function O({ mode: O }) {
25
+ let { tenantId: k } = b(), A = y(), { basePath: j = "" } = t(), M = e(), { t: N } = T(), { canManageTenants: P, isLoading: F } = n(), I = O === "edit", L = I ? m.edit : m.create, R = w(m.management, !0), z = w(L, !0), B = R && z, V = D(), H = h((e, t, n) => a(N, e, t, n), [N]), U = c(j, "tenants"), W = I && k ? c(j, `tenants/${k}`) : U, { data: G, loading: K, error: q } = d({
26
+ variables: { id: k ?? "" },
27
+ skip: !I || !k || F || !P || !B,
27
28
  fetchPolicy: "cache-and-network"
28
- }), [W] = u(), [G] = f(), K = h(() => V?.tenant ? l(V.tenant) : void 0, [V?.tenant]), q = m(() => {
29
- k(B);
30
- }, [k, B]), J = m(async (e) => {
31
- if (F) {
32
- if (!O) return;
29
+ }), [J] = u(), [Y] = f(), X = g(() => G?.tenant ? l(G.tenant) : void 0, [G?.tenant]), Z = h(() => {
30
+ A(W);
31
+ }, [A, W]), Q = h(async (e) => {
32
+ if (I) {
33
+ if (!k) return;
33
34
  try {
34
- p(L, "tenant.tenant.updated", {
35
- tenantId: O,
36
- tenantSlug: (await G({ variables: {
37
- id: O,
35
+ p(V, "tenant.tenant.updated", {
36
+ tenantId: k,
37
+ tenantSlug: (await Y({ variables: {
38
+ id: k,
38
39
  input: {
39
40
  name: e.name,
40
41
  customDomain: e.domain,
@@ -44,15 +45,15 @@ function D({ mode: D }) {
44
45
  plan: e.plan
45
46
  }
46
47
  }
47
- } })).data?.updateTenant?.slug ?? K?.slug
48
- }), j.success(R("admin.tenants.toast.updatedTitle", "Tenant updated"), R("admin.tenants.toast.updatedDescription", "{{name}} has been updated.", { name: e.name ?? "" })), k(c(A, `tenants/${O}`));
48
+ } })).data?.updateTenant?.slug ?? X?.slug
49
+ }), M.success(H("admin.tenants.toast.updatedTitle", "Tenant updated"), H("admin.tenants.toast.updatedDescription", "{{name}} has been updated.", { name: e.name ?? "" })), A(c(j, `tenants/${k}`));
49
50
  } catch (e) {
50
- throw j.error(R("admin.tenants.toast.updateFailedTitle", "Update failed"), e instanceof Error ? e.message : ""), e;
51
+ throw M.error(H("admin.tenants.toast.updateFailedTitle", "Update failed"), e instanceof Error ? e.message : ""), e;
51
52
  }
52
53
  return;
53
54
  }
54
55
  try {
55
- let t = (await W({ variables: { input: {
56
+ let t = (await J({ variables: { input: {
56
57
  name: e.name,
57
58
  slug: e.slug,
58
59
  customDomain: e.domain,
@@ -62,79 +63,79 @@ function D({ mode: D }) {
62
63
  plan: e.plan
63
64
  }
64
65
  } } })).data?.createTenant;
65
- if (!t?.id) throw Error(R("admin.tenants.toast.createNoRecord", "Tenant create returned no record. Please retry."));
66
- p(L, "tenant.tenant.created", {
66
+ if (!t?.id) throw Error(H("admin.tenants.toast.createNoRecord", "Tenant create returned no record. Please retry."));
67
+ p(V, "tenant.tenant.created", {
67
68
  tenantId: t.id,
68
69
  tenantSlug: t.slug
69
- }), j.success(R("admin.tenants.toast.createdTitle", "Tenant created"), R("admin.tenants.toast.createdDescription", "{{name}} has been created.", { name: e.name ?? "" })), k(c(A, `tenants/${t.id}`));
70
+ }), M.success(H("admin.tenants.toast.createdTitle", "Tenant created"), H("admin.tenants.toast.createdDescription", "{{name}} has been created.", { name: e.name ?? "" })), A(c(j, `tenants/${t.id}`));
70
71
  } catch (e) {
71
- throw j.error(R("admin.tenants.toast.createFailedTitle", "Create failed"), e instanceof Error ? e.message : ""), e;
72
+ throw M.error(H("admin.tenants.toast.createFailedTitle", "Create failed"), e instanceof Error ? e.message : ""), e;
72
73
  }
73
74
  }, [
75
+ j,
76
+ J,
77
+ V,
78
+ I,
74
79
  A,
75
- W,
76
- L,
77
- F,
80
+ X?.slug,
78
81
  k,
79
- K?.slug,
80
- O,
81
- z,
82
- j,
83
- R,
84
- G
82
+ U,
83
+ M,
84
+ H,
85
+ Y
85
86
  ]);
86
- return !P && !N ? /* @__PURE__ */ x(s, {
87
+ return !F && !P ? /* @__PURE__ */ S(s, {
87
88
  allowed: !1,
88
89
  isLoading: !1,
89
- pageTitle: F ? R("admin.tenantForm.editTitle", "Edit Tenant") : R("admin.tenantForm.createTitle", "Create Tenant"),
90
- denyMessage: R("admin.tenantForm.permissionDenied", "You do not have permission to manage tenants."),
91
- children: /* @__PURE__ */ x(b, {})
92
- }) : I ? /* @__PURE__ */ x(s, {
93
- allowed: N,
94
- isLoading: P,
95
- pageTitle: F ? R("admin.tenantForm.editTitle", "Edit Tenant") : R("admin.tenantForm.createTitle", "Create Tenant"),
96
- denyMessage: R("admin.tenantForm.denyMessage", "You don't have permission to manage tenants."),
97
- children: F && H ? /* @__PURE__ */ x(i, {
98
- title: R("admin.tenantForm.loadingTitle", "Loading tenant..."),
99
- children: /* @__PURE__ */ x("div", {
90
+ pageTitle: I ? H("admin.tenantForm.editTitle", "Edit Tenant") : H("admin.tenantForm.createTitle", "Create Tenant"),
91
+ denyMessage: H("admin.tenantForm.permissionDenied", "You do not have permission to manage tenants."),
92
+ children: /* @__PURE__ */ S(x, {})
93
+ }) : B ? /* @__PURE__ */ S(s, {
94
+ allowed: P,
95
+ isLoading: F,
96
+ pageTitle: I ? H("admin.tenantForm.editTitle", "Edit Tenant") : H("admin.tenantForm.createTitle", "Create Tenant"),
97
+ denyMessage: H("admin.tenantForm.denyMessage", "You don't have permission to manage tenants."),
98
+ children: I && K ? /* @__PURE__ */ S(i, {
99
+ title: H("admin.tenantForm.loadingTitle", "Loading tenant..."),
100
+ children: /* @__PURE__ */ S("div", {
100
101
  className: "flex h-64 items-center justify-center",
101
- children: /* @__PURE__ */ x(_, { className: "h-8 w-8 animate-spin text-text-secondary" })
102
+ children: /* @__PURE__ */ S(v, { className: "h-8 w-8 animate-spin text-text-secondary" })
102
103
  })
103
- }) : F && (U || !K) ? /* @__PURE__ */ S(i, {
104
- title: R("admin.tenantForm.notFoundTitle", "Tenant Not Found"),
105
- description: R("admin.tenantForm.notFoundDescription", "The tenant you want to edit could not be found."),
106
- children: [U && /* @__PURE__ */ x("div", {
104
+ }) : I && (q || !X) ? /* @__PURE__ */ C(i, {
105
+ title: H("admin.tenantForm.notFoundTitle", "Tenant Not Found"),
106
+ description: H("admin.tenantForm.notFoundDescription", "The tenant you want to edit could not be found."),
107
+ children: [q && /* @__PURE__ */ S("div", {
107
108
  className: "mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
108
- children: U.message
109
- }), /* @__PURE__ */ S(r, {
109
+ children: q.message
110
+ }), /* @__PURE__ */ C(r, {
110
111
  variant: "outline",
111
- onClick: () => k(z),
112
- children: [/* @__PURE__ */ x(g, { className: "mr-2 h-4 w-4" }), R("admin.tenantForm.backToTenants", "Back to Tenants")]
112
+ onClick: () => A(U),
113
+ children: [/* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4" }), H("admin.tenantForm.backToTenants", "Back to Tenants")]
113
114
  })]
114
- }) : /* @__PURE__ */ S(i, {
115
- title: F ? R("admin.tenantForm.editTitle", "Edit Tenant") : R("admin.tenantForm.createTitle", "Create Tenant"),
116
- description: F ? R("admin.tenantForm.editDescription", "Update tenant configuration and metadata.") : R("admin.tenantForm.createDescription", "Add a new tenant to the platform."),
117
- children: [/* @__PURE__ */ x(w, {
115
+ }) : /* @__PURE__ */ C(i, {
116
+ title: I ? H("admin.tenantForm.editTitle", "Edit Tenant") : H("admin.tenantForm.createTitle", "Create Tenant"),
117
+ description: I ? H("admin.tenantForm.editDescription", "Update tenant configuration and metadata.") : H("admin.tenantForm.createDescription", "Add a new tenant to the platform."),
118
+ children: [/* @__PURE__ */ S(E, {
118
119
  className: "mb-6",
119
- children: F ? R("admin.tenantForm.editPurpose", "Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.") : R("admin.tenantForm.createPurpose", "Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.")
120
- }), /* @__PURE__ */ x(o, {
120
+ children: I ? H("admin.tenantForm.editPurpose", "Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.") : H("admin.tenantForm.createPurpose", "Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.")
121
+ }), /* @__PURE__ */ S(o, {
121
122
  open: !0,
122
- onClose: q,
123
- tenant: K,
124
- onSave: J
123
+ onClose: Z,
124
+ tenant: X,
125
+ onSave: Q
125
126
  })]
126
127
  })
127
- }) : /* @__PURE__ */ x(i, {
128
- title: F ? R("admin.tenantForm.editTitle", "Edit Tenant") : R("admin.tenantForm.createTitle", "Create Tenant"),
129
- description: R("admin.tenantForm.featureDisabled", "This feature is currently disabled."),
130
- children: /* @__PURE__ */ S(r, {
128
+ }) : /* @__PURE__ */ S(i, {
129
+ title: I ? H("admin.tenantForm.editTitle", "Edit Tenant") : H("admin.tenantForm.createTitle", "Create Tenant"),
130
+ description: H("admin.tenantForm.featureDisabled", "This feature is currently disabled."),
131
+ children: /* @__PURE__ */ C(r, {
131
132
  variant: "outline",
132
- onClick: () => k(z),
133
- children: [/* @__PURE__ */ x(g, { className: "mr-2 h-4 w-4" }), R("admin.tenantForm.backToTenants", "Back to Tenants")]
133
+ onClick: () => A(U),
134
+ children: [/* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4" }), H("admin.tenantForm.backToTenants", "Back to Tenants")]
134
135
  })
135
136
  });
136
137
  }
137
138
  //#endregion
138
- export { D as TenantFormRoutePage };
139
+ export { O as TenantFormRoutePage };
139
140
 
140
141
  //# sourceMappingURL=TenantFormRoutePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TenantFormRoutePage.js","names":[],"sources":["../../../src/admin/pages/TenantFormRoutePage.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport {\n useCreateTenantMutation,\n useGetTenantQuery,\n useUpdateTenantMutation,\n} from '../../generated/global-operations';\nimport { Button, PageLayout, PermissionGuard, TenantForm, useToast } from '../components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { joinPath } from '../utils/navigation';\nimport { tWithFallback } from '../utils/i18n';\nimport { mapTenantToUi } from '../utils/mappers';\n\ntype TenantFormRoutePageProps = {\n mode: 'create' | 'edit';\n};\n\nexport function TenantFormRoutePage({ mode }: TenantFormRoutePageProps) {\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const { canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n const isEdit = mode === 'edit';\n // Page-level feature flags. Default-allow keeps prior behavior intact when\n // the flag is missing; turning the flag OFF in OpenBao/feature-flag-svc\n // disables the corresponding route without a code deploy.\n const flagKey = isEdit ? 'admin.tenant.edit' : 'admin.tenant.create';\n const flagEnabled = useFeatureFlag(flagKey, true);\n const events = useEventBus();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const tenantsPath = joinPath(basePath, 'tenants');\n const closePath = isEdit && tenantId ? joinPath(basePath, `tenants/${tenantId}`) : tenantsPath;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId ?? '' },\n // Also skip when the page is feature-flag-disabled: the early return\n // below renders the \"feature disabled\" panel instead of the form, so a\n // network round-trip would be wasted.\n skip: !isEdit || !tenantId || permissionsLoading || !canManageTenants || !flagEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [createTenant] = useCreateTenantMutation();\n const [updateTenant] = useUpdateTenantMutation();\n\n // Memoize the mapped tenant so its object identity is stable across\n // renders. mapTenantToUi otherwise produces a new object on every render,\n // forcing downstream memoization (TenantForm.defaultValues, reset effects)\n // to fan out unnecessarily.\n const tenant = useMemo(\n () => (data?.tenant ? mapTenantToUi(data.tenant) : undefined),\n [data?.tenant]\n );\n\n const handleClose = useCallback(() => {\n navigate(closePath);\n }, [navigate, closePath]);\n\n const handleSave = useCallback(\n async (input: CreateTenantInput | UpdateTenantInput) => {\n if (isEdit) {\n if (!tenantId) return;\n try {\n const updated = await updateTenant({\n variables: {\n id: tenantId,\n input: {\n name: input.name,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n // The update mutation does not accept slug, so emit the slug from\n // the persisted record (mutation response, falling back to the\n // already-loaded tenant) rather than the user's form input — the\n // form's slug field is read-only for edits, but a defensive emit\n // protects listeners against any future change.\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId,\n tenantSlug: updated.data?.updateTenant?.slug ?? tenant?.slug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${tenantId}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.updateFailedTitle', 'Update failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so TenantForm.onSubmit's try/catch sees the rejection\n // and keeps the form open instead of calling onClose() — losing\n // the user's edits would be a worse outcome than a duplicate\n // surfacing of the same error.\n throw err;\n }\n return;\n }\n\n try {\n const result = await createTenant({\n variables: {\n input: {\n name: input.name!,\n slug: input.slug!,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const created = result.data?.createTenant;\n if (!created?.id) {\n // A resolved mutation without a createdTenant record is a\n // degenerate response (federation hiccup, partial schema, etc.).\n // Treat as failure so the user is not shown a misleading success\n // toast and the form stays open with their input intact.\n throw new Error(\n tr(\n 'admin.tenants.toast.createNoRecord',\n 'Tenant create returned no record. Please retry.'\n )\n );\n }\n // Slug from the server-confirmed record so listeners cannot see a\n // form value that was rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: created.id,\n tenantSlug: created.slug,\n });\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${created.id}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so the form stays open on failure (see edit branch).\n throw err;\n }\n },\n [\n basePath,\n createTenant,\n events,\n isEdit,\n navigate,\n tenant?.slug,\n tenantId,\n tenantsPath,\n toast,\n tr,\n updateTenant,\n ]\n );\n\n // Permission denials win over feature-flag denials: a user without\n // canManageTenants should see \"permission denied\" rather than learning\n // whether the feature flag is on/off (minor information disclosure).\n if (!permissionsLoading && !canManageTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.permissionDenied',\n 'You do not have permission to manage tenants.'\n )}\n >\n <></>\n </PermissionGuard>\n );\n }\n\n if (!flagEnabled) {\n return (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={tr('admin.tenantForm.featureDisabled', 'This feature is currently disabled.')}\n >\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canManageTenants}\n isLoading={permissionsLoading}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.denyMessage',\n \"You don't have permission to manage tenants.\"\n )}\n >\n {isEdit && loading ? (\n <PageLayout title={tr('admin.tenantForm.loadingTitle', 'Loading tenant...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : isEdit && (error || !tenant) ? (\n <PageLayout\n title={tr('admin.tenantForm.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantForm.notFoundDescription',\n 'The tenant you want to edit could not be found.'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={\n isEdit\n ? tr('admin.tenantForm.editDescription', 'Update tenant configuration and metadata.')\n : tr('admin.tenantForm.createDescription', 'Add a new tenant to the platform.')\n }\n >\n <PagePurpose className=\"mb-6\">\n {isEdit\n ? tr(\n 'admin.tenantForm.editPurpose',\n 'Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.'\n )\n : tr(\n 'admin.tenantForm.createPurpose',\n 'Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.'\n )}\n </PagePurpose>\n <TenantForm open={true} onClose={handleClose} tenant={tenant} onSave={handleSave} />\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantFormRoutePage.js","names":[],"sources":["../../../src/admin/pages/TenantFormRoutePage.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useFeatureFlag } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\nimport {\n useCreateTenantMutation,\n useGetTenantQuery,\n useUpdateTenantMutation,\n} from '../../generated/global-operations';\nimport { Button, PageLayout, PermissionGuard, TenantForm, useToast } from '../components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport type { CreateTenantInput, UpdateTenantInput } from '../schemas';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { joinPath } from '../utils/navigation';\nimport { tWithFallback } from '../utils/i18n';\nimport { mapTenantToUi } from '../utils/mappers';\nimport { TENANT_FEATURE_FLAGS } from '../constants/featureFlags';\n\ntype TenantFormRoutePageProps = {\n mode: 'create' | 'edit';\n};\n\nexport function TenantFormRoutePage({ mode }: TenantFormRoutePageProps) {\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const { canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n const isEdit = mode === 'edit';\n // Page-level feature flags. Default-allow keeps prior behavior intact when\n // the flag is missing; turning the flag OFF in OpenBao/feature-flag-svc\n // disables the corresponding route without a code deploy.\n const flagKey = isEdit ? TENANT_FEATURE_FLAGS.edit : TENANT_FEATURE_FLAGS.create;\n const managementFlagEnabled = useFeatureFlag(TENANT_FEATURE_FLAGS.management, true);\n const flagEnabled = useFeatureFlag(flagKey, true);\n const pageFeatureEnabled = managementFlagEnabled && flagEnabled;\n const events = useEventBus();\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params),\n [t]\n );\n\n const tenantsPath = joinPath(basePath, 'tenants');\n const closePath = isEdit && tenantId ? joinPath(basePath, `tenants/${tenantId}`) : tenantsPath;\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId ?? '' },\n // Also skip when the page is feature-flag-disabled: the early return\n // below renders the \"feature disabled\" panel instead of the form, so a\n // network round-trip would be wasted.\n skip: !isEdit || !tenantId || permissionsLoading || !canManageTenants || !pageFeatureEnabled,\n fetchPolicy: 'cache-and-network',\n });\n\n const [createTenant] = useCreateTenantMutation();\n const [updateTenant] = useUpdateTenantMutation();\n\n // Memoize the mapped tenant so its object identity is stable across\n // renders. mapTenantToUi otherwise produces a new object on every render,\n // forcing downstream memoization (TenantForm.defaultValues, reset effects)\n // to fan out unnecessarily.\n const tenant = useMemo(\n () => (data?.tenant ? mapTenantToUi(data.tenant) : undefined),\n [data?.tenant]\n );\n\n const handleClose = useCallback(() => {\n navigate(closePath);\n }, [navigate, closePath]);\n\n const handleSave = useCallback(\n async (input: CreateTenantInput | UpdateTenantInput) => {\n if (isEdit) {\n if (!tenantId) return;\n try {\n const updated = await updateTenant({\n variables: {\n id: tenantId,\n input: {\n name: input.name,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n // The update mutation does not accept slug, so emit the slug from\n // the persisted record (mutation response, falling back to the\n // already-loaded tenant) rather than the user's form input — the\n // form's slug field is read-only for edits, but a defensive emit\n // protects listeners against any future change.\n emitTenantLifecycle(events, 'tenant.tenant.updated', {\n tenantId,\n tenantSlug: updated.data?.updateTenant?.slug ?? tenant?.slug,\n });\n toast.success(\n tr('admin.tenants.toast.updatedTitle', 'Tenant updated'),\n tr('admin.tenants.toast.updatedDescription', '{{name}} has been updated.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${tenantId}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.updateFailedTitle', 'Update failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so TenantForm.onSubmit's try/catch sees the rejection\n // and keeps the form open instead of calling onClose() — losing\n // the user's edits would be a worse outcome than a duplicate\n // surfacing of the same error.\n throw err;\n }\n return;\n }\n\n try {\n const result = await createTenant({\n variables: {\n input: {\n name: input.name!,\n slug: input.slug!,\n customDomain: input.domain,\n metadata: {\n ownerEmail: input.ownerEmail,\n billingEmail: input.billingEmail,\n plan: input.plan,\n } as Record<string, unknown>,\n },\n },\n });\n const created = result.data?.createTenant;\n if (!created?.id) {\n // A resolved mutation without a createdTenant record is a\n // degenerate response (federation hiccup, partial schema, etc.).\n // Treat as failure so the user is not shown a misleading success\n // toast and the form stays open with their input intact.\n throw new Error(\n tr(\n 'admin.tenants.toast.createNoRecord',\n 'Tenant create returned no record. Please retry.'\n )\n );\n }\n // Slug from the server-confirmed record so listeners cannot see a\n // form value that was rejected (e.g. uniqueness collision).\n emitTenantLifecycle(events, 'tenant.tenant.created', {\n tenantId: created.id,\n tenantSlug: created.slug,\n });\n toast.success(\n tr('admin.tenants.toast.createdTitle', 'Tenant created'),\n tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', {\n name: input.name ?? '',\n })\n );\n navigate(joinPath(basePath, `tenants/${created.id}`));\n } catch (err) {\n toast.error(\n tr('admin.tenants.toast.createFailedTitle', 'Create failed'),\n err instanceof Error ? err.message : ''\n );\n // Rethrow so the form stays open on failure (see edit branch).\n throw err;\n }\n },\n [\n basePath,\n createTenant,\n events,\n isEdit,\n navigate,\n tenant?.slug,\n tenantId,\n tenantsPath,\n toast,\n tr,\n updateTenant,\n ]\n );\n\n // Permission denials win over feature-flag denials: a user without\n // canManageTenants should see \"permission denied\" rather than learning\n // whether the feature flag is on/off (minor information disclosure).\n if (!permissionsLoading && !canManageTenants) {\n return (\n <PermissionGuard\n allowed={false}\n isLoading={false}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.permissionDenied',\n 'You do not have permission to manage tenants.'\n )}\n >\n <></>\n </PermissionGuard>\n );\n }\n\n if (!pageFeatureEnabled) {\n return (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={tr('admin.tenantForm.featureDisabled', 'This feature is currently disabled.')}\n >\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard\n allowed={canManageTenants}\n isLoading={permissionsLoading}\n pageTitle={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n denyMessage={tr(\n 'admin.tenantForm.denyMessage',\n \"You don't have permission to manage tenants.\"\n )}\n >\n {isEdit && loading ? (\n <PageLayout title={tr('admin.tenantForm.loadingTitle', 'Loading tenant...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n ) : isEdit && (error || !tenant) ? (\n <PageLayout\n title={tr('admin.tenantForm.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantForm.notFoundDescription',\n 'The tenant you want to edit could not be found.'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(tenantsPath)}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantForm.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={\n isEdit\n ? tr('admin.tenantForm.editTitle', 'Edit Tenant')\n : tr('admin.tenantForm.createTitle', 'Create Tenant')\n }\n description={\n isEdit\n ? tr('admin.tenantForm.editDescription', 'Update tenant configuration and metadata.')\n : tr('admin.tenantForm.createDescription', 'Add a new tenant to the platform.')\n }\n >\n <PagePurpose className=\"mb-6\">\n {isEdit\n ? tr(\n 'admin.tenantForm.editPurpose',\n 'Update this tenant’s details — its display name, custom domain, owner and plan. Changes here affect how the customer account is identified and billed across the platform.'\n )\n : tr(\n 'admin.tenantForm.createPurpose',\n 'Provision a brand-new customer account. Give it a name and URL slug, set the owner and starting plan, and the platform spins up an isolated tenant ready for its users.'\n )}\n </PagePurpose>\n <TenantForm open={true} onClose={handleClose} tenant={tenant} onSave={handleSave} />\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}