@burdenoff/microfe-workspaces 2026.616.1 → 2026.624.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/components/CreateWorkspaceDialog.js +5 -5
- package/dist/components/CreateWorkspaceDialog.js.map +1 -1
- package/dist/components/InviteMemberDialog.js +11 -11
- package/dist/components/InviteMemberDialog.js.map +1 -1
- package/dist/components/ProjectCard.js +99 -76
- package/dist/components/ProjectCard.js.map +1 -1
- package/dist/components/ProjectMemberPickerDialog.js +14 -14
- package/dist/components/ProjectMemberPickerDialog.js.map +1 -1
- package/dist/components/WorkspaceCard.js +3 -3
- package/dist/components/WorkspaceCard.js.map +1 -1
- package/dist/components/WorkspacesErrorBoundary.js +9 -9
- package/dist/components/WorkspacesErrorBoundary.js.map +1 -1
- package/dist/components/tags/ProjectTagsWidget.js +8 -8
- package/dist/components/tags/ProjectTagsWidget.js.map +1 -1
- package/dist/pages/ActivityListPage.js +12 -12
- package/dist/pages/ActivityListPage.js.map +1 -1
- package/dist/pages/CreateWorkspacePage.js +24 -24
- package/dist/pages/CreateWorkspacePage.js.map +1 -1
- package/dist/pages/InviteAcceptancePage.js +46 -46
- package/dist/pages/InviteAcceptancePage.js.map +1 -1
- package/dist/pages/MembersListPage.js +27 -27
- package/dist/pages/MembersListPage.js.map +1 -1
- package/dist/pages/PendingInvitesPage.js +23 -23
- package/dist/pages/PendingInvitesPage.js.map +1 -1
- package/dist/pages/ProjectDetailPage.js +192 -181
- package/dist/pages/ProjectDetailPage.js.map +1 -1
- package/dist/pages/ProjectMembersPage.js +15 -15
- package/dist/pages/ProjectMembersPage.js.map +1 -1
- package/dist/pages/ProjectsListPage.js +16 -16
- package/dist/pages/ProjectsListPage.js.map +1 -1
- package/dist/pages/WorkspaceDetailPage.js +16 -16
- package/dist/pages/WorkspaceDetailPage.js.map +1 -1
- package/dist/pages/WorkspaceInvitePage.js +51 -51
- package/dist/pages/WorkspaceInvitePage.js.map +1 -1
- package/dist/pages/WorkspaceSettingsPage.js +2 -2
- package/dist/pages/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/WorkspacesListPage.js +8 -8
- package/dist/pages/WorkspacesListPage.js.map +1 -1
- package/dist/pages/dashboard/DashboardCustomizer.js +3 -3
- package/dist/pages/dashboard/DashboardCustomizer.js.map +1 -1
- package/dist/pages/dashboard/DashboardGrid.js +2 -2
- package/dist/pages/dashboard/DashboardGrid.js.map +1 -1
- package/dist/pages/dashboard/DashboardPage.js +5 -5
- package/dist/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/pages/dashboard/WidgetWrapper.js +6 -6
- package/dist/pages/dashboard/WidgetWrapper.js.map +1 -1
- package/dist/pages/dashboard/widgets/AgentsWidget.js +6 -6
- package/dist/pages/dashboard/widgets/AgentsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/GroupsWidget.js +3 -3
- package/dist/pages/dashboard/widgets/GroupsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/MembersWidget.js +3 -3
- package/dist/pages/dashboard/widgets/MembersWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/ProjectsWidget.js +2 -2
- package/dist/pages/dashboard/widgets/ProjectsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js +3 -3
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/SessionsWidget.js +4 -4
- package/dist/pages/dashboard/widgets/SessionsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TagCloudWidget.js +2 -2
- package/dist/pages/dashboard/widgets/TagCloudWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TipsWidget.js +3 -3
- package/dist/pages/dashboard/widgets/TipsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js +2 -2
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/VibesWidget.js +2 -2
- package/dist/pages/dashboard/widgets/VibesWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WelcomeWidget.js +3 -3
- package/dist/pages/dashboard/widgets/WelcomeWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js +4 -4
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js +2 -2
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityListPage.js","names":[],"sources":["../../src/pages/ActivityListPage.tsx"],"sourcesContent":["import { Activity } from 'lucide-react';\nimport { useParams } from 'react-router-dom';\nimport { Card, CardContent, CardHeader, CardTitle, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { useWorkspaceActivity } from '../hooks/useWorkspaceActivity';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { formatDate } from '../utils/formatters';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nexport function ActivityListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.activity', { workspaceId, requireParams: true });\n const { data: activities, isLoading, error } = useWorkspaceActivity(workspaceId);\n const { canViewActivity, isLoading: permissionsLoading } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <Skeleton className=\"h-64\" />\n </div>\n );\n }\n\n if (!canViewActivity) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'activityList.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-
|
|
1
|
+
{"version":3,"file":"ActivityListPage.js","names":[],"sources":["../../src/pages/ActivityListPage.tsx"],"sourcesContent":["import { Activity } from 'lucide-react';\nimport { useParams } from 'react-router-dom';\nimport { Card, CardContent, CardHeader, CardTitle, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { useWorkspaceActivity } from '../hooks/useWorkspaceActivity';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { formatDate } from '../utils/formatters';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\nexport function ActivityListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.activity', { workspaceId, requireParams: true });\n const { data: activities, isLoading, error } = useWorkspaceActivity(workspaceId);\n const { canViewActivity, isLoading: permissionsLoading } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <Skeleton className=\"h-64\" />\n </div>\n );\n }\n\n if (!canViewActivity) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'activityList.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'activityList.accessDeniedDescription',\n \"You don't have permission to view activity.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'activityList.title', 'Activity')}\n </h1>\n <p className=\"text-text-secondary\">\n {tWithFallback(t, 'activityList.description', 'Track workspace activity')}\n </p>\n </div>\n\n {isLoading ? (\n <Card>\n <CardContent className=\"py-8 text-sm text-text-secondary\">\n {tWithFallback(t, 'activityList.loading', 'Loading activity…')}\n </CardContent>\n </Card>\n ) : error ? (\n <Card>\n <CardContent className=\"py-8 text-sm text-status-error-text\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'activityList.loadError', 'Failed to load activity')}\n </CardContent>\n </Card>\n ) : (activities?.length ?? 0) === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-bg-sunken p-6 mb-4\">\n <Activity size={48} className=\"text-text-secondary\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'activityList.emptyTitle', 'No recent activity')}\n </h3>\n <p className=\"text-text-secondary max-w-sm\">\n {tWithFallback(\n t,\n 'activityList.emptyDescription',\n 'Recent workspace actions will appear here as your team creates, updates, and manages work.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {activities?.map((item) => (\n <Card key={item.id}>\n <CardHeader className=\"pb-2\">\n <CardTitle className=\"text-base\">{item.description || item.action}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-1 text-sm text-text-secondary\">\n <p>\n {tWithFallback(t, 'activityList.labelAction', 'Action')}:{' '}\n <span className=\"text-text-primary\">{item.action}</span>\n </p>\n {item.resourceType && (\n <p>\n {tWithFallback(t, 'activityList.labelResource', 'Resource')}:{' '}\n <span className=\"text-text-primary\">{item.resourceType}</span>\n </p>\n )}\n <p>\n {tWithFallback(t, 'activityList.labelWhen', 'When')}:{' '}\n <span className=\"text-text-primary\">{formatDate(item.timestamp)}</span>\n </p>\n <p>\n {tWithFallback(t, 'activityList.labelActor', 'Actor')}:{' '}\n <span className=\"break-all text-text-primary\">{item.userId}</span>\n </p>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -65,7 +65,7 @@ function E() {
|
|
|
65
65
|
className: "p-6",
|
|
66
66
|
children: [/* @__PURE__ */ d(v, {
|
|
67
67
|
to: `${P}/list`,
|
|
68
|
-
className: "inline-flex items-center gap-2 text-
|
|
68
|
+
className: "inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors mb-6",
|
|
69
69
|
children: [/* @__PURE__ */ u(l, { size: 20 }), t(E, "createWorkspace.backToWorkspaces", "Back to Workspaces")]
|
|
70
70
|
}), /* @__PURE__ */ u(x, {
|
|
71
71
|
variant: "card",
|
|
@@ -82,7 +82,7 @@ function E() {
|
|
|
82
82
|
children: [
|
|
83
83
|
/* @__PURE__ */ d(v, {
|
|
84
84
|
to: `${P}/list`,
|
|
85
|
-
className: "inline-flex items-center gap-2 text-
|
|
85
|
+
className: "inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors",
|
|
86
86
|
children: [/* @__PURE__ */ u(l, { size: 20 }), t(E, "createWorkspace.backToWorkspaces", "Back to Workspaces")]
|
|
87
87
|
}),
|
|
88
88
|
re && /* @__PURE__ */ u("div", {
|
|
@@ -99,7 +99,7 @@ function E() {
|
|
|
99
99
|
className: "max-w-2xl mx-auto",
|
|
100
100
|
children: [/* @__PURE__ */ d(h, { children: [/* @__PURE__ */ u(g, { children: t(E, "createWorkspace.title", "Create New Workspace") }), /* @__PURE__ */ u(ne, { children: t(E, "createWorkspace.subtitle", "Set up a new workspace for your team") })] }), /* @__PURE__ */ d(m, { children: [Z && /* @__PURE__ */ d("div", {
|
|
101
101
|
role: "alert",
|
|
102
|
-
className: "mb-4 flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 p-3 text-sm text-
|
|
102
|
+
className: "mb-4 flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 p-3 text-sm text-text-primary",
|
|
103
103
|
children: [/* @__PURE__ */ u(c, {
|
|
104
104
|
size: 16,
|
|
105
105
|
className: "mt-0.5 shrink-0 text-warning"
|
|
@@ -113,21 +113,21 @@ function E() {
|
|
|
113
113
|
children: [
|
|
114
114
|
/* @__PURE__ */ u("label", {
|
|
115
115
|
htmlFor: "workspace-name",
|
|
116
|
-
className: "text-sm font-medium text-
|
|
116
|
+
className: "text-sm font-medium text-text-primary",
|
|
117
117
|
children: t(E, "createWorkspace.nameLabel", "Workspace Name *")
|
|
118
118
|
}),
|
|
119
119
|
/* @__PURE__ */ u("input", {
|
|
120
120
|
id: "workspace-name",
|
|
121
121
|
placeholder: t(E, "createWorkspace.namePlaceholder", "My Workspace"),
|
|
122
|
-
className: "h-10 w-full rounded-md border border-
|
|
122
|
+
className: "h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
123
123
|
...K.register("name")
|
|
124
124
|
}),
|
|
125
125
|
/* @__PURE__ */ u("p", {
|
|
126
|
-
className: "text-xs text-
|
|
126
|
+
className: "text-xs text-text-secondary",
|
|
127
127
|
children: t(E, "createWorkspace.nameHint", "A descriptive name for your workspace")
|
|
128
128
|
}),
|
|
129
129
|
K.formState.errors.name?.message && /* @__PURE__ */ u("p", {
|
|
130
|
-
className: "text-sm text-
|
|
130
|
+
className: "text-sm text-status-error-text",
|
|
131
131
|
children: K.formState.errors.name.message
|
|
132
132
|
})
|
|
133
133
|
]
|
|
@@ -137,21 +137,21 @@ function E() {
|
|
|
137
137
|
children: [
|
|
138
138
|
/* @__PURE__ */ u("label", {
|
|
139
139
|
htmlFor: "workspace-slug",
|
|
140
|
-
className: "text-sm font-medium text-
|
|
140
|
+
className: "text-sm font-medium text-text-primary",
|
|
141
141
|
children: t(E, "createWorkspace.slugLabel", "Slug *")
|
|
142
142
|
}),
|
|
143
143
|
/* @__PURE__ */ u("input", {
|
|
144
144
|
id: "workspace-slug",
|
|
145
145
|
placeholder: t(E, "createWorkspace.slugPlaceholder", "my-workspace"),
|
|
146
|
-
className: "h-10 w-full rounded-md border border-
|
|
146
|
+
className: "h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
147
147
|
...K.register("slug")
|
|
148
148
|
}),
|
|
149
149
|
/* @__PURE__ */ u("p", {
|
|
150
|
-
className: "text-xs text-
|
|
150
|
+
className: "text-xs text-text-secondary",
|
|
151
151
|
children: t(E, "createWorkspace.slugHint", "URL-friendly identifier (auto-generated, but you can edit it)")
|
|
152
152
|
}),
|
|
153
153
|
K.formState.errors.slug?.message && /* @__PURE__ */ u("p", {
|
|
154
|
-
className: "text-sm text-
|
|
154
|
+
className: "text-sm text-status-error-text",
|
|
155
155
|
children: K.formState.errors.slug.message
|
|
156
156
|
})
|
|
157
157
|
]
|
|
@@ -161,18 +161,18 @@ function E() {
|
|
|
161
161
|
children: [
|
|
162
162
|
/* @__PURE__ */ u("label", {
|
|
163
163
|
htmlFor: "workspace-tenant",
|
|
164
|
-
className: "text-sm font-medium text-
|
|
164
|
+
className: "text-sm font-medium text-text-primary",
|
|
165
165
|
children: t(E, "createWorkspace.tenantLabel", "Tenant *")
|
|
166
166
|
}),
|
|
167
167
|
R ? /* @__PURE__ */ u(_, { className: "h-10 w-full" }) : z ? /* @__PURE__ */ d("div", {
|
|
168
|
-
className: "flex items-center gap-2 text-sm text-
|
|
168
|
+
className: "flex items-center gap-2 text-sm text-status-error-text",
|
|
169
169
|
children: [/* @__PURE__ */ u(c, { size: 16 }), t(E, "createWorkspace.failedToLoadTenants", "Failed to load tenants")]
|
|
170
170
|
}) : U.length === 0 ? /* @__PURE__ */ u("div", {
|
|
171
|
-
className: "text-sm text-
|
|
171
|
+
className: "text-sm text-text-secondary",
|
|
172
172
|
children: t(E, "createWorkspace.noTenantsAvailable", "No tenants available. Contact your administrator.")
|
|
173
173
|
}) : /* @__PURE__ */ d("select", {
|
|
174
174
|
id: "workspace-tenant",
|
|
175
|
-
className: "h-10 w-full rounded-md border border-
|
|
175
|
+
className: "h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
176
176
|
...K.register("tenantId"),
|
|
177
177
|
children: [/* @__PURE__ */ u("option", {
|
|
178
178
|
value: "",
|
|
@@ -183,11 +183,11 @@ function E() {
|
|
|
183
183
|
}, e.id))]
|
|
184
184
|
}),
|
|
185
185
|
/* @__PURE__ */ u("p", {
|
|
186
|
-
className: "text-xs text-
|
|
186
|
+
className: "text-xs text-text-secondary",
|
|
187
187
|
children: t(E, "createWorkspace.tenantHint", "Select which tenant this workspace belongs to")
|
|
188
188
|
}),
|
|
189
189
|
K.formState.errors.tenantId?.message && /* @__PURE__ */ u("p", {
|
|
190
|
-
className: "text-sm text-
|
|
190
|
+
className: "text-sm text-status-error-text",
|
|
191
191
|
children: K.formState.errors.tenantId.message
|
|
192
192
|
})
|
|
193
193
|
]
|
|
@@ -197,18 +197,18 @@ function E() {
|
|
|
197
197
|
children: [
|
|
198
198
|
/* @__PURE__ */ u("label", {
|
|
199
199
|
htmlFor: "workspace-organization",
|
|
200
|
-
className: "text-sm font-medium text-
|
|
200
|
+
className: "text-sm font-medium text-text-primary",
|
|
201
201
|
children: t(E, "createWorkspace.organizationLabel", "Organization *")
|
|
202
202
|
}),
|
|
203
203
|
V || X ? /* @__PURE__ */ u(_, { className: "h-10 w-full" }) : H ? /* @__PURE__ */ d("div", {
|
|
204
|
-
className: "flex items-center gap-2 text-sm text-
|
|
204
|
+
className: "flex items-center gap-2 text-sm text-status-error-text",
|
|
205
205
|
children: [/* @__PURE__ */ u(c, { size: 16 }), t(E, "createWorkspace.failedToLoadOrganizations", "Failed to load organizations")]
|
|
206
206
|
}) : q ? $.length === 0 ? /* @__PURE__ */ u("div", {
|
|
207
|
-
className: "text-sm text-
|
|
207
|
+
className: "text-sm text-text-secondary",
|
|
208
208
|
children: t(E, "createWorkspace.noOrganizationsFound", "No organizations found for this tenant")
|
|
209
209
|
}) : /* @__PURE__ */ d("select", {
|
|
210
210
|
id: "workspace-organization",
|
|
211
|
-
className: "h-10 w-full rounded-md border border-
|
|
211
|
+
className: "h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
212
212
|
disabled: !q,
|
|
213
213
|
...K.register("organizationId"),
|
|
214
214
|
children: [/* @__PURE__ */ u("option", {
|
|
@@ -219,15 +219,15 @@ function E() {
|
|
|
219
219
|
children: e.name
|
|
220
220
|
}, e.id))]
|
|
221
221
|
}) : /* @__PURE__ */ u("div", {
|
|
222
|
-
className: "text-sm text-
|
|
222
|
+
className: "text-sm text-text-secondary",
|
|
223
223
|
children: t(E, "createWorkspace.selectTenantFirst", "Please select a tenant first")
|
|
224
224
|
}),
|
|
225
225
|
/* @__PURE__ */ u("p", {
|
|
226
|
-
className: "text-xs text-
|
|
226
|
+
className: "text-xs text-text-secondary",
|
|
227
227
|
children: t(E, "createWorkspace.organizationHint", "Select which organization owns this workspace")
|
|
228
228
|
}),
|
|
229
229
|
K.formState.errors.organizationId?.message && /* @__PURE__ */ u("p", {
|
|
230
|
-
className: "text-sm text-
|
|
230
|
+
className: "text-sm text-status-error-text",
|
|
231
231
|
children: K.formState.errors.organizationId.message
|
|
232
232
|
})
|
|
233
233
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateWorkspacePage.js","names":[],"sources":["../../src/pages/CreateWorkspacePage.tsx"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { ArrowLeft, AlertCircle } from 'lucide-react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport {\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useForm, z } from '@burdenoff/fe-libs/form';\nimport { AccessDenied, UpgradeInlineBanner } from '@burdenoff/fe-libs/shared/components';\nimport { useUpgradePrompt } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useCreateWorkspace } from '../hooks/useWorkspaceMutations';\nimport { useMyTenants, useMyOrganizations } from '../hooks/useTenants';\nimport { useOrgsWithPermission } from '../hooks/useOrgsWithPermission';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { generateSlug } from '../utils/slug';\nimport { tWithFallback } from '../utils/i18n';\n\ntype CreateWorkspaceFormData = {\n name: string;\n slug: string;\n tenantId: string;\n organizationId: string;\n};\n\nexport function CreateWorkspacePage() {\n useWorkspacesPageView('workspaces.create');\n const { t } = useI18n();\n const {\n isFree,\n approachingLimits,\n plan: planSnapshot,\n } = useUpgradePrompt({ resource: 'workspaces' });\n const workspacesUsage = approachingLimits[0];\n const atWorkspaceCap =\n !!workspacesUsage &&\n typeof workspacesUsage.limit === 'number' &&\n workspacesUsage.current >= workspacesUsage.limit;\n const showUpgradeBanner = isFree && !!workspacesUsage;\n const createWorkspaceSchema = useMemo(\n () =>\n z.object({\n name: z\n .string()\n .min(\n 2,\n tWithFallback(\n t,\n 'createWorkspace.validation.nameMin',\n 'Name must be at least 2 characters'\n )\n )\n .max(100),\n slug: z\n .string()\n .min(\n 2,\n tWithFallback(\n t,\n 'createWorkspace.validation.slugMin',\n 'Slug must be at least 2 characters'\n )\n )\n .max(50)\n .regex(\n /^[a-z0-9-]+$/,\n tWithFallback(\n t,\n 'createWorkspace.validation.slugFormat',\n 'Slug can only contain lowercase letters, numbers, and hyphens'\n )\n ),\n tenantId: z\n .string()\n .min(\n 1,\n tWithFallback(t, 'createWorkspace.validation.tenantRequired', 'Please select a tenant')\n ),\n organizationId: z\n .string()\n .min(\n 1,\n tWithFallback(\n t,\n 'createWorkspace.validation.organizationRequired',\n 'Please select an organization'\n )\n ),\n }),\n [t]\n );\n\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { mutate: createWorkspace, isPending } = useCreateWorkspace();\n const { data: tenantsData, isLoading: isLoadingTenants, error: tenantsError } = useMyTenants();\n const {\n data: organizationsData,\n isLoading: isLoadingOrganizations,\n error: organizationsError,\n } = useMyOrganizations();\n\n const tenants = useMemo(() => tenantsData?.items ?? [], [tenantsData]);\n const allOrganizations = useMemo(() => organizationsData?.items ?? [], [organizationsData]);\n\n const defaultTenantId = useMemo(() => {\n const sharedTenant = tenants.find(\n (tenant) => tenant.type === 'SHARED' || tenant.slug === 'burdenoff-shared'\n );\n return sharedTenant?.id ?? tenants[0]?.id ?? '';\n }, [tenants]);\n\n const form = useForm<CreateWorkspaceFormData>({\n schema: createWorkspaceSchema,\n defaultValues: {\n name: '',\n slug: '',\n tenantId: '',\n organizationId: '',\n },\n });\n\n const selectedTenantId = form.watch('tenantId');\n const workspaceName = form.watch('name');\n\n // Check workspace:create permission across ALL the user's orgs up front\n // (bulk-checked in a single rbac round-trip). This is the authoritative\n // gate — only orgs where the user can actually create a workspace make\n // it into the dropdown, and the page can decisively render the \"no\n // permissions anywhere\" state once the check resolves.\n const allOrgIds = useMemo(() => allOrganizations.map((o) => o.id), [allOrganizations]);\n const {\n allowedIds: workspaceCreateAllowedOrgIds,\n loading: isCheckingOrgPermissions,\n isError: orgPermissionsHadError,\n } = useOrgsWithPermission(allOrgIds, 'workspace', 'create');\n\n // Filter organizations by selected tenant AND permission. When the rbac\n // permission lookup errored we deliberately skip the permission filter\n // (see `orgPermissionsHadError` block below) — the gateway @rbac\n // directive on `createWorkspace` is the authoritative gate, so it's\n // safer to let the user pick from the full tenant list and hit a 403 on\n // submit than to lock them out of a flow they actually have access to.\n const tenantOrganizations = useMemo(() => {\n if (!selectedTenantId) return [];\n return allOrganizations.filter((org) => org.tenantId === selectedTenantId);\n }, [selectedTenantId, allOrganizations]);\n const filteredOrganizations = useMemo(() => {\n if (orgPermissionsHadError) return tenantOrganizations;\n return tenantOrganizations.filter((org) => workspaceCreateAllowedOrgIds.has(org.id));\n }, [tenantOrganizations, workspaceCreateAllowedOrgIds, orgPermissionsHadError]);\n\n // Auto-generate slug from name (clear when name is empty)\n useEffect(() => {\n form.setValue('slug', workspaceName ? generateSlug(workspaceName) : '');\n }, [workspaceName, form]);\n\n // Reset organizationId when tenant changes\n useEffect(() => {\n form.setValue('organizationId', '');\n }, [selectedTenantId, form]);\n\n useEffect(() => {\n if (!form.getValues('tenantId') && defaultTenantId) {\n form.setValue('tenantId', defaultTenantId, {\n shouldDirty: false,\n shouldTouch: false,\n });\n }\n }, [defaultTenantId, form]);\n\n useEffect(() => {\n const currentOrganizationId = form.getValues('organizationId');\n const hasCurrentOrganization = filteredOrganizations.some(\n (organization) => organization.id === currentOrganizationId\n );\n\n if (!hasCurrentOrganization && filteredOrganizations[0]?.id) {\n form.setValue('organizationId', filteredOrganizations[0].id, {\n shouldDirty: false,\n shouldTouch: false,\n });\n }\n }, [filteredOrganizations, form]);\n\n const onSubmit = (data: CreateWorkspaceFormData) => {\n createWorkspace(\n {\n name: data.name,\n slug: data.slug,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n },\n {\n onSuccess: (newWorkspace) => {\n // Navigate to the new workspace\n navigate(`${basePath}/${newWorkspace.id}`);\n },\n }\n );\n };\n\n // `workspace:create` is an org-scoped permission. Page-level `PermissionGate`\n // resolved at the outer shell scope and incorrectly denied users who *do*\n // have the permission inside at least one of their orgs. The authoritative\n // gate now lives on the org dropdown (only orgs where the user has\n // workspace:create are listed via useOrgsWithPermission) and on the gateway\n // @rbac directive enforcing the createWorkspace mutation. We still want a\n // clear \"you can't create anywhere\" empty state — once tenants and org\n // permission lookups have settled, if no org is eligible we render it.\n // The AccessDenied empty-state should ONLY render when we know with\n // certainty that the user has no creatable org. That requires:\n // 1. tenants + organizations queries both succeeded (otherwise an\n // empty `allOrganizations` is a data-fetch failure, not an authz\n // failure — masking it as \"permission denied\" hides the real\n // error and offers the user no path to retry),\n // 2. the rbac permission lookup also succeeded (errors there fall\n // through to the unfiltered form via the warning banner below),\n // 3. and even then, only when the user has at least one org but none\n // of them allow workspace:create.\n // If `allOrganizations` is empty because the user genuinely belongs to\n // no orgs, the inner \"no organizations found for this tenant\" message\n // is a better surface than an authz-denied page.\n const hasAnyCreatableOrg = workspaceCreateAllowedOrgIds.size > 0;\n const stillResolvingPermissions =\n isLoadingTenants || isLoadingOrganizations || isCheckingOrgPermissions;\n const orgDataLoadedSuccessfully =\n !tenantsError && !organizationsError && allOrganizations.length > 0;\n if (\n !stillResolvingPermissions &&\n !orgPermissionsHadError &&\n orgDataLoadedSuccessfully &&\n !hasAnyCreatableOrg\n ) {\n return (\n <div className=\"p-6\">\n <Link\n to={`${basePath}/list`}\n className=\"inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-6\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'createWorkspace.backToWorkspaces', 'Back to Workspaces')}\n </Link>\n <AccessDenied\n variant=\"card\"\n title={tWithFallback(t, 'createWorkspace.permissionRequiredTitle', 'Permission Required')}\n description={tWithFallback(\n t,\n 'createWorkspace.permissionRequiredDescription',\n \"You don't have permission to create workspaces in any of your organizations.\"\n )}\n details={tWithFallback(\n t,\n 'createWorkspace.permissionRequiredDetails',\n 'Required: workspace:create'\n )}\n primaryAction={{\n label: tWithFallback(t, 'createWorkspace.goBack', 'Go Back'),\n onClick: () => navigate(`${basePath}/list`),\n }}\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Back link */}\n <Link\n to={`${basePath}/list`}\n className=\"inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'createWorkspace.backToWorkspaces', 'Back to Workspaces')}\n </Link>\n\n {showUpgradeBanner && (\n <div className=\"max-w-2xl mx-auto w-full\">\n <UpgradeInlineBanner\n resource=\"workspaces\"\n current={workspacesUsage.current}\n limit={workspacesUsage.limit ?? undefined}\n severity={atWorkspaceCap ? 'critical' : undefined}\n message={\n atWorkspaceCap\n ? tWithFallback(\n t,\n 'createWorkspace.upgradeCapMessage',\n `You've used all workspaces in the ${planSnapshot?.planName ?? 'free'} plan.`\n )\n : tWithFallback(\n t,\n 'createWorkspace.upgradeApproachingMessage',\n 'Upgrade to create more workspaces.'\n )\n }\n />\n </div>\n )}\n\n {/* Form card */}\n <Card className=\"max-w-2xl mx-auto\">\n <CardHeader>\n <CardTitle>{tWithFallback(t, 'createWorkspace.title', 'Create New Workspace')}</CardTitle>\n <CardDescription>\n {tWithFallback(t, 'createWorkspace.subtitle', 'Set up a new workspace for your team')}\n </CardDescription>\n </CardHeader>\n <CardContent>\n {orgPermissionsHadError && (\n <div\n role=\"alert\"\n className=\"mb-4 flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 p-3 text-sm text-foreground\"\n >\n <AlertCircle size={16} className=\"mt-0.5 shrink-0 text-warning\" />\n <span>\n {tWithFallback(\n t,\n 'createWorkspace.permissionCheckUnavailable',\n \"We couldn't verify your workspace-create permissions just now, so the organization list isn't filtered. If you submit and don't have access, the request will be rejected and you can retry.\"\n )}\n </span>\n </div>\n )}\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-name\" className=\"text-sm font-medium text-foreground\">\n {tWithFallback(t, 'createWorkspace.nameLabel', 'Workspace Name *')}\n </label>\n <input\n id=\"workspace-name\"\n placeholder={tWithFallback(t, 'createWorkspace.namePlaceholder', 'My Workspace')}\n className=\"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground\"\n {...form.register('name')}\n />\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.nameHint',\n 'A descriptive name for your workspace'\n )}\n </p>\n {form.formState.errors.name?.message && (\n <p className=\"text-sm text-destructive\">{form.formState.errors.name.message}</p>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-slug\" className=\"text-sm font-medium text-foreground\">\n {tWithFallback(t, 'createWorkspace.slugLabel', 'Slug *')}\n </label>\n <input\n id=\"workspace-slug\"\n placeholder={tWithFallback(t, 'createWorkspace.slugPlaceholder', 'my-workspace')}\n className=\"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground\"\n {...form.register('slug')}\n />\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.slugHint',\n 'URL-friendly identifier (auto-generated, but you can edit it)'\n )}\n </p>\n {form.formState.errors.slug?.message && (\n <p className=\"text-sm text-destructive\">{form.formState.errors.slug.message}</p>\n )}\n </div>\n\n {/* Tenant picker — shown only when the user has more than one\n tenant to choose between. With a single shared tenant there is\n nothing to choose, so it is hidden and auto-selected. The\n loading / error / empty branches still render (length !== 1). */}\n {tenants.length !== 1 ? (\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-tenant\" className=\"text-sm font-medium text-foreground\">\n {tWithFallback(t, 'createWorkspace.tenantLabel', 'Tenant *')}\n </label>\n {isLoadingTenants ? (\n <Skeleton className=\"h-10 w-full\" />\n ) : tenantsError ? (\n <div className=\"flex items-center gap-2 text-sm text-destructive\">\n <AlertCircle size={16} />\n {tWithFallback(\n t,\n 'createWorkspace.failedToLoadTenants',\n 'Failed to load tenants'\n )}\n </div>\n ) : tenants.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.noTenantsAvailable',\n 'No tenants available. Contact your administrator.'\n )}\n </div>\n ) : (\n <select\n id=\"workspace-tenant\"\n className=\"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground\"\n {...form.register('tenantId')}\n >\n <option value=\"\">\n {tWithFallback(\n t,\n 'createWorkspace.selectTenantPlaceholder',\n 'Select a tenant'\n )}\n </option>\n {tenants.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name}\n </option>\n ))}\n </select>\n )}\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.tenantHint',\n 'Select which tenant this workspace belongs to'\n )}\n </p>\n {form.formState.errors.tenantId?.message && (\n <p className=\"text-sm text-destructive\">\n {form.formState.errors.tenantId.message}\n </p>\n )}\n </div>\n ) : null}\n\n <div className=\"space-y-2\">\n <label\n htmlFor=\"workspace-organization\"\n className=\"text-sm font-medium text-foreground\"\n >\n {tWithFallback(t, 'createWorkspace.organizationLabel', 'Organization *')}\n </label>\n {isLoadingOrganizations || isCheckingOrgPermissions ? (\n // While the permission check is in flight,\n // `workspaceCreateAllowedOrgIds` is empty so\n // `filteredOrganizations` is also empty. Render the same\n // skeleton we use for the org-load itself instead of the\n // \"no organizations found\" empty-state — otherwise a slow\n // rbac response briefly looks like an authorization\n // failure for users who DO have workspace:create.\n <Skeleton className=\"h-10 w-full\" />\n ) : organizationsError ? (\n <div className=\"flex items-center gap-2 text-sm text-destructive\">\n <AlertCircle size={16} />\n {tWithFallback(\n t,\n 'createWorkspace.failedToLoadOrganizations',\n 'Failed to load organizations'\n )}\n </div>\n ) : !selectedTenantId ? (\n <div className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.selectTenantFirst',\n 'Please select a tenant first'\n )}\n </div>\n ) : filteredOrganizations.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.noOrganizationsFound',\n 'No organizations found for this tenant'\n )}\n </div>\n ) : (\n <select\n id=\"workspace-organization\"\n className=\"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground\"\n disabled={!selectedTenantId}\n {...form.register('organizationId')}\n >\n <option value=\"\">\n {tWithFallback(\n t,\n 'createWorkspace.selectOrganizationPlaceholder',\n 'Select an organization'\n )}\n </option>\n {filteredOrganizations.map((org) => (\n <option key={org.id} value={org.id}>\n {org.name}\n </option>\n ))}\n </select>\n )}\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'createWorkspace.organizationHint',\n 'Select which organization owns this workspace'\n )}\n </p>\n {form.formState.errors.organizationId?.message && (\n <p className=\"text-sm text-destructive\">\n {form.formState.errors.organizationId.message}\n </p>\n )}\n </div>\n\n <div className=\"flex justify-end gap-3 pt-4\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`${basePath}/list`)}\n disabled={isPending}\n >\n {tWithFallback(t, 'createWorkspace.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"submit\"\n disabled={\n isPending ||\n isLoadingTenants ||\n isLoadingOrganizations ||\n isCheckingOrgPermissions ||\n atWorkspaceCap\n }\n title={\n atWorkspaceCap\n ? tWithFallback(\n t,\n 'createWorkspace.disabledAtCap',\n 'Workspace limit reached — upgrade your plan to create more.'\n )\n : undefined\n }\n >\n {isPending\n ? tWithFallback(t, 'createWorkspace.creating', 'Creating...')\n : tWithFallback(t, 'createWorkspace.submit', 'Create Workspace')}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateWorkspacePage.js","names":[],"sources":["../../src/pages/CreateWorkspacePage.tsx"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { ArrowLeft, AlertCircle } from 'lucide-react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport {\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useForm, z } from '@burdenoff/fe-libs/form';\nimport { AccessDenied, UpgradeInlineBanner } from '@burdenoff/fe-libs/shared/components';\nimport { useUpgradePrompt } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useCreateWorkspace } from '../hooks/useWorkspaceMutations';\nimport { useMyTenants, useMyOrganizations } from '../hooks/useTenants';\nimport { useOrgsWithPermission } from '../hooks/useOrgsWithPermission';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { generateSlug } from '../utils/slug';\nimport { tWithFallback } from '../utils/i18n';\n\ntype CreateWorkspaceFormData = {\n name: string;\n slug: string;\n tenantId: string;\n organizationId: string;\n};\n\nexport function CreateWorkspacePage() {\n useWorkspacesPageView('workspaces.create');\n const { t } = useI18n();\n const {\n isFree,\n approachingLimits,\n plan: planSnapshot,\n } = useUpgradePrompt({ resource: 'workspaces' });\n const workspacesUsage = approachingLimits[0];\n const atWorkspaceCap =\n !!workspacesUsage &&\n typeof workspacesUsage.limit === 'number' &&\n workspacesUsage.current >= workspacesUsage.limit;\n const showUpgradeBanner = isFree && !!workspacesUsage;\n const createWorkspaceSchema = useMemo(\n () =>\n z.object({\n name: z\n .string()\n .min(\n 2,\n tWithFallback(\n t,\n 'createWorkspace.validation.nameMin',\n 'Name must be at least 2 characters'\n )\n )\n .max(100),\n slug: z\n .string()\n .min(\n 2,\n tWithFallback(\n t,\n 'createWorkspace.validation.slugMin',\n 'Slug must be at least 2 characters'\n )\n )\n .max(50)\n .regex(\n /^[a-z0-9-]+$/,\n tWithFallback(\n t,\n 'createWorkspace.validation.slugFormat',\n 'Slug can only contain lowercase letters, numbers, and hyphens'\n )\n ),\n tenantId: z\n .string()\n .min(\n 1,\n tWithFallback(t, 'createWorkspace.validation.tenantRequired', 'Please select a tenant')\n ),\n organizationId: z\n .string()\n .min(\n 1,\n tWithFallback(\n t,\n 'createWorkspace.validation.organizationRequired',\n 'Please select an organization'\n )\n ),\n }),\n [t]\n );\n\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { mutate: createWorkspace, isPending } = useCreateWorkspace();\n const { data: tenantsData, isLoading: isLoadingTenants, error: tenantsError } = useMyTenants();\n const {\n data: organizationsData,\n isLoading: isLoadingOrganizations,\n error: organizationsError,\n } = useMyOrganizations();\n\n const tenants = useMemo(() => tenantsData?.items ?? [], [tenantsData]);\n const allOrganizations = useMemo(() => organizationsData?.items ?? [], [organizationsData]);\n\n const defaultTenantId = useMemo(() => {\n const sharedTenant = tenants.find(\n (tenant) => tenant.type === 'SHARED' || tenant.slug === 'burdenoff-shared'\n );\n return sharedTenant?.id ?? tenants[0]?.id ?? '';\n }, [tenants]);\n\n const form = useForm<CreateWorkspaceFormData>({\n schema: createWorkspaceSchema,\n defaultValues: {\n name: '',\n slug: '',\n tenantId: '',\n organizationId: '',\n },\n });\n\n const selectedTenantId = form.watch('tenantId');\n const workspaceName = form.watch('name');\n\n // Check workspace:create permission across ALL the user's orgs up front\n // (bulk-checked in a single rbac round-trip). This is the authoritative\n // gate — only orgs where the user can actually create a workspace make\n // it into the dropdown, and the page can decisively render the \"no\n // permissions anywhere\" state once the check resolves.\n const allOrgIds = useMemo(() => allOrganizations.map((o) => o.id), [allOrganizations]);\n const {\n allowedIds: workspaceCreateAllowedOrgIds,\n loading: isCheckingOrgPermissions,\n isError: orgPermissionsHadError,\n } = useOrgsWithPermission(allOrgIds, 'workspace', 'create');\n\n // Filter organizations by selected tenant AND permission. When the rbac\n // permission lookup errored we deliberately skip the permission filter\n // (see `orgPermissionsHadError` block below) — the gateway @rbac\n // directive on `createWorkspace` is the authoritative gate, so it's\n // safer to let the user pick from the full tenant list and hit a 403 on\n // submit than to lock them out of a flow they actually have access to.\n const tenantOrganizations = useMemo(() => {\n if (!selectedTenantId) return [];\n return allOrganizations.filter((org) => org.tenantId === selectedTenantId);\n }, [selectedTenantId, allOrganizations]);\n const filteredOrganizations = useMemo(() => {\n if (orgPermissionsHadError) return tenantOrganizations;\n return tenantOrganizations.filter((org) => workspaceCreateAllowedOrgIds.has(org.id));\n }, [tenantOrganizations, workspaceCreateAllowedOrgIds, orgPermissionsHadError]);\n\n // Auto-generate slug from name (clear when name is empty)\n useEffect(() => {\n form.setValue('slug', workspaceName ? generateSlug(workspaceName) : '');\n }, [workspaceName, form]);\n\n // Reset organizationId when tenant changes\n useEffect(() => {\n form.setValue('organizationId', '');\n }, [selectedTenantId, form]);\n\n useEffect(() => {\n if (!form.getValues('tenantId') && defaultTenantId) {\n form.setValue('tenantId', defaultTenantId, {\n shouldDirty: false,\n shouldTouch: false,\n });\n }\n }, [defaultTenantId, form]);\n\n useEffect(() => {\n const currentOrganizationId = form.getValues('organizationId');\n const hasCurrentOrganization = filteredOrganizations.some(\n (organization) => organization.id === currentOrganizationId\n );\n\n if (!hasCurrentOrganization && filteredOrganizations[0]?.id) {\n form.setValue('organizationId', filteredOrganizations[0].id, {\n shouldDirty: false,\n shouldTouch: false,\n });\n }\n }, [filteredOrganizations, form]);\n\n const onSubmit = (data: CreateWorkspaceFormData) => {\n createWorkspace(\n {\n name: data.name,\n slug: data.slug,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n },\n {\n onSuccess: (newWorkspace) => {\n // Navigate to the new workspace\n navigate(`${basePath}/${newWorkspace.id}`);\n },\n }\n );\n };\n\n // `workspace:create` is an org-scoped permission. Page-level `PermissionGate`\n // resolved at the outer shell scope and incorrectly denied users who *do*\n // have the permission inside at least one of their orgs. The authoritative\n // gate now lives on the org dropdown (only orgs where the user has\n // workspace:create are listed via useOrgsWithPermission) and on the gateway\n // @rbac directive enforcing the createWorkspace mutation. We still want a\n // clear \"you can't create anywhere\" empty state — once tenants and org\n // permission lookups have settled, if no org is eligible we render it.\n // The AccessDenied empty-state should ONLY render when we know with\n // certainty that the user has no creatable org. That requires:\n // 1. tenants + organizations queries both succeeded (otherwise an\n // empty `allOrganizations` is a data-fetch failure, not an authz\n // failure — masking it as \"permission denied\" hides the real\n // error and offers the user no path to retry),\n // 2. the rbac permission lookup also succeeded (errors there fall\n // through to the unfiltered form via the warning banner below),\n // 3. and even then, only when the user has at least one org but none\n // of them allow workspace:create.\n // If `allOrganizations` is empty because the user genuinely belongs to\n // no orgs, the inner \"no organizations found for this tenant\" message\n // is a better surface than an authz-denied page.\n const hasAnyCreatableOrg = workspaceCreateAllowedOrgIds.size > 0;\n const stillResolvingPermissions =\n isLoadingTenants || isLoadingOrganizations || isCheckingOrgPermissions;\n const orgDataLoadedSuccessfully =\n !tenantsError && !organizationsError && allOrganizations.length > 0;\n if (\n !stillResolvingPermissions &&\n !orgPermissionsHadError &&\n orgDataLoadedSuccessfully &&\n !hasAnyCreatableOrg\n ) {\n return (\n <div className=\"p-6\">\n <Link\n to={`${basePath}/list`}\n className=\"inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors mb-6\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'createWorkspace.backToWorkspaces', 'Back to Workspaces')}\n </Link>\n <AccessDenied\n variant=\"card\"\n title={tWithFallback(t, 'createWorkspace.permissionRequiredTitle', 'Permission Required')}\n description={tWithFallback(\n t,\n 'createWorkspace.permissionRequiredDescription',\n \"You don't have permission to create workspaces in any of your organizations.\"\n )}\n details={tWithFallback(\n t,\n 'createWorkspace.permissionRequiredDetails',\n 'Required: workspace:create'\n )}\n primaryAction={{\n label: tWithFallback(t, 'createWorkspace.goBack', 'Go Back'),\n onClick: () => navigate(`${basePath}/list`),\n }}\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Back link */}\n <Link\n to={`${basePath}/list`}\n className=\"inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'createWorkspace.backToWorkspaces', 'Back to Workspaces')}\n </Link>\n\n {showUpgradeBanner && (\n <div className=\"max-w-2xl mx-auto w-full\">\n <UpgradeInlineBanner\n resource=\"workspaces\"\n current={workspacesUsage.current}\n limit={workspacesUsage.limit ?? undefined}\n severity={atWorkspaceCap ? 'critical' : undefined}\n message={\n atWorkspaceCap\n ? tWithFallback(\n t,\n 'createWorkspace.upgradeCapMessage',\n `You've used all workspaces in the ${planSnapshot?.planName ?? 'free'} plan.`\n )\n : tWithFallback(\n t,\n 'createWorkspace.upgradeApproachingMessage',\n 'Upgrade to create more workspaces.'\n )\n }\n />\n </div>\n )}\n\n {/* Form card */}\n <Card className=\"max-w-2xl mx-auto\">\n <CardHeader>\n <CardTitle>{tWithFallback(t, 'createWorkspace.title', 'Create New Workspace')}</CardTitle>\n <CardDescription>\n {tWithFallback(t, 'createWorkspace.subtitle', 'Set up a new workspace for your team')}\n </CardDescription>\n </CardHeader>\n <CardContent>\n {orgPermissionsHadError && (\n <div\n role=\"alert\"\n className=\"mb-4 flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 p-3 text-sm text-text-primary\"\n >\n <AlertCircle size={16} className=\"mt-0.5 shrink-0 text-warning\" />\n <span>\n {tWithFallback(\n t,\n 'createWorkspace.permissionCheckUnavailable',\n \"We couldn't verify your workspace-create permissions just now, so the organization list isn't filtered. If you submit and don't have access, the request will be rejected and you can retry.\"\n )}\n </span>\n </div>\n )}\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-name\" className=\"text-sm font-medium text-text-primary\">\n {tWithFallback(t, 'createWorkspace.nameLabel', 'Workspace Name *')}\n </label>\n <input\n id=\"workspace-name\"\n placeholder={tWithFallback(t, 'createWorkspace.namePlaceholder', 'My Workspace')}\n className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n {...form.register('name')}\n />\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.nameHint',\n 'A descriptive name for your workspace'\n )}\n </p>\n {form.formState.errors.name?.message && (\n <p className=\"text-sm text-status-error-text\">{form.formState.errors.name.message}</p>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-slug\" className=\"text-sm font-medium text-text-primary\">\n {tWithFallback(t, 'createWorkspace.slugLabel', 'Slug *')}\n </label>\n <input\n id=\"workspace-slug\"\n placeholder={tWithFallback(t, 'createWorkspace.slugPlaceholder', 'my-workspace')}\n className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n {...form.register('slug')}\n />\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.slugHint',\n 'URL-friendly identifier (auto-generated, but you can edit it)'\n )}\n </p>\n {form.formState.errors.slug?.message && (\n <p className=\"text-sm text-status-error-text\">{form.formState.errors.slug.message}</p>\n )}\n </div>\n\n {/* Tenant picker — shown only when the user has more than one\n tenant to choose between. With a single shared tenant there is\n nothing to choose, so it is hidden and auto-selected. The\n loading / error / empty branches still render (length !== 1). */}\n {tenants.length !== 1 ? (\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-tenant\" className=\"text-sm font-medium text-text-primary\">\n {tWithFallback(t, 'createWorkspace.tenantLabel', 'Tenant *')}\n </label>\n {isLoadingTenants ? (\n <Skeleton className=\"h-10 w-full\" />\n ) : tenantsError ? (\n <div className=\"flex items-center gap-2 text-sm text-status-error-text\">\n <AlertCircle size={16} />\n {tWithFallback(\n t,\n 'createWorkspace.failedToLoadTenants',\n 'Failed to load tenants'\n )}\n </div>\n ) : tenants.length === 0 ? (\n <div className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.noTenantsAvailable',\n 'No tenants available. Contact your administrator.'\n )}\n </div>\n ) : (\n <select\n id=\"workspace-tenant\"\n className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n {...form.register('tenantId')}\n >\n <option value=\"\">\n {tWithFallback(\n t,\n 'createWorkspace.selectTenantPlaceholder',\n 'Select a tenant'\n )}\n </option>\n {tenants.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name}\n </option>\n ))}\n </select>\n )}\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.tenantHint',\n 'Select which tenant this workspace belongs to'\n )}\n </p>\n {form.formState.errors.tenantId?.message && (\n <p className=\"text-sm text-status-error-text\">\n {form.formState.errors.tenantId.message}\n </p>\n )}\n </div>\n ) : null}\n\n <div className=\"space-y-2\">\n <label\n htmlFor=\"workspace-organization\"\n className=\"text-sm font-medium text-text-primary\"\n >\n {tWithFallback(t, 'createWorkspace.organizationLabel', 'Organization *')}\n </label>\n {isLoadingOrganizations || isCheckingOrgPermissions ? (\n // While the permission check is in flight,\n // `workspaceCreateAllowedOrgIds` is empty so\n // `filteredOrganizations` is also empty. Render the same\n // skeleton we use for the org-load itself instead of the\n // \"no organizations found\" empty-state — otherwise a slow\n // rbac response briefly looks like an authorization\n // failure for users who DO have workspace:create.\n <Skeleton className=\"h-10 w-full\" />\n ) : organizationsError ? (\n <div className=\"flex items-center gap-2 text-sm text-status-error-text\">\n <AlertCircle size={16} />\n {tWithFallback(\n t,\n 'createWorkspace.failedToLoadOrganizations',\n 'Failed to load organizations'\n )}\n </div>\n ) : !selectedTenantId ? (\n <div className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.selectTenantFirst',\n 'Please select a tenant first'\n )}\n </div>\n ) : filteredOrganizations.length === 0 ? (\n <div className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.noOrganizationsFound',\n 'No organizations found for this tenant'\n )}\n </div>\n ) : (\n <select\n id=\"workspace-organization\"\n className=\"h-10 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n disabled={!selectedTenantId}\n {...form.register('organizationId')}\n >\n <option value=\"\">\n {tWithFallback(\n t,\n 'createWorkspace.selectOrganizationPlaceholder',\n 'Select an organization'\n )}\n </option>\n {filteredOrganizations.map((org) => (\n <option key={org.id} value={org.id}>\n {org.name}\n </option>\n ))}\n </select>\n )}\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'createWorkspace.organizationHint',\n 'Select which organization owns this workspace'\n )}\n </p>\n {form.formState.errors.organizationId?.message && (\n <p className=\"text-sm text-status-error-text\">\n {form.formState.errors.organizationId.message}\n </p>\n )}\n </div>\n\n <div className=\"flex justify-end gap-3 pt-4\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`${basePath}/list`)}\n disabled={isPending}\n >\n {tWithFallback(t, 'createWorkspace.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"submit\"\n disabled={\n isPending ||\n isLoadingTenants ||\n isLoadingOrganizations ||\n isCheckingOrgPermissions ||\n atWorkspaceCap\n }\n title={\n atWorkspaceCap\n ? tWithFallback(\n t,\n 'createWorkspace.disabledAtCap',\n 'Workspace limit reached — upgrade your plan to create more.'\n )\n : undefined\n }\n >\n {isPending\n ? tWithFallback(t, 'createWorkspace.creating', 'Creating...')\n : tWithFallback(t, 'createWorkspace.submit', 'Create Workspace')}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|