@burdenoff/microfe-devportal 2026.527.1 → 2026.530.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/devportal/components/EmptyState.js +9 -23
- package/dist/devportal/components/EmptyState.js.map +1 -1
- package/dist/devportal/components/PageLayout.js +5 -2
- package/dist/devportal/components/PageLayout.js.map +1 -1
- package/dist/devportal/components/TelemetryErrorBoundary.js +37 -17
- package/dist/devportal/components/TelemetryErrorBoundary.js.map +1 -1
- package/dist/devportal/hooks/useGraphQL.js +2 -9
- package/dist/devportal/hooks/useGraphQL.js.map +1 -1
- package/dist/devportal/pages/ApiKeysPage.js +1 -1
- package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
- package/dist/devportal/pages/CreateApiKeyPage.js +78 -73
- package/dist/devportal/pages/CreateApiKeyPage.js.map +1 -1
- package/dist/devportal/pages/CreateApplicationPage.js +1 -1
- package/dist/devportal/pages/CreateApplicationPage.js.map +1 -1
- package/dist/devportal/pages/CreateOAuthClientPage.js +135 -131
- package/dist/devportal/pages/CreateOAuthClientPage.js.map +1 -1
- package/dist/devportal/pages/CreateOrganizationPage.js +1 -1
- package/dist/devportal/pages/CreateOrganizationPage.js.map +1 -1
- package/dist/devportal/pages/DashboardPage.js +36 -28
- package/dist/devportal/pages/DashboardPage.js.map +1 -1
- package/dist/devportal/pages/EditApplicationPage.js +3 -3
- package/dist/devportal/pages/EditApplicationPage.js.map +1 -1
- package/dist/devportal/pages/OAuthClientsPage.js +1 -1
- package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
- package/dist/devportal/pages/SettingsPage.js +10 -10
- package/dist/devportal/pages/SettingsPage.js.map +1 -1
- package/package.json +3 -2
|
@@ -14,8 +14,9 @@ import { useState as m } from "react";
|
|
|
14
14
|
import { useI18n as h } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
15
15
|
import { Loader2 as g, Save as _, X as v } from "lucide-react";
|
|
16
16
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
17
|
+
import { OptionalFields as x } from "@burdenoff/fe-libs/ui";
|
|
17
18
|
//#region src/devportal/pages/CreateApiKeyPage.tsx
|
|
18
|
-
var
|
|
19
|
+
var S = {
|
|
19
20
|
"30days": 720 * 60 * 60 * 1e3,
|
|
20
21
|
"90days": 2160 * 60 * 60 * 1e3,
|
|
21
22
|
"180days": 4320 * 60 * 60 * 1e3,
|
|
@@ -23,64 +24,64 @@ var x = {
|
|
|
23
24
|
"2years": 2 * 365 * 24 * 60 * 60 * 1e3,
|
|
24
25
|
never: null
|
|
25
26
|
};
|
|
26
|
-
function
|
|
27
|
-
let t =
|
|
27
|
+
function C(e) {
|
|
28
|
+
let t = S[e];
|
|
28
29
|
if (t != null) return new Date(Date.now() + t).toISOString();
|
|
29
30
|
}
|
|
30
|
-
var
|
|
31
|
-
let { t:
|
|
32
|
-
let n =
|
|
31
|
+
var w = () => {
|
|
32
|
+
let { t: S } = h(), w = (e, t) => {
|
|
33
|
+
let n = S(e);
|
|
33
34
|
return n === e ? t : n;
|
|
34
|
-
}, { navigate:
|
|
35
|
+
}, { navigate: T, basePath: E } = e(), [D, O] = m(null), [k, A] = m({}), [j, M] = m(null), [N, { loading: P }] = u(), F = t(), { data: I, loading: L } = l({ fetchPolicy: "cache-and-network" }), R = I?.currentUserApps?.apps.map((e) => ({
|
|
35
36
|
id: e.id,
|
|
36
37
|
name: e.name,
|
|
37
38
|
clientId: e.clientId
|
|
38
|
-
})) ?? [], [
|
|
39
|
+
})) ?? [], [z, B] = m({
|
|
39
40
|
appId: "",
|
|
40
41
|
name: "",
|
|
41
42
|
scopes: [],
|
|
42
43
|
expiresIn: "1year"
|
|
43
|
-
}),
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
|
|
44
|
+
}), V = (e) => {
|
|
45
|
+
T?.(`${E}${e}`);
|
|
46
|
+
}, H = () => {
|
|
47
|
+
V("/api-keys");
|
|
48
|
+
}, U = (e, t) => {
|
|
49
|
+
B((n) => ({
|
|
49
50
|
...n,
|
|
50
51
|
[e]: t
|
|
51
|
-
})),
|
|
52
|
+
})), k[e] && A((t) => ({
|
|
52
53
|
...t,
|
|
53
54
|
[e]: ""
|
|
54
|
-
})),
|
|
55
|
-
},
|
|
55
|
+
})), j && M(null);
|
|
56
|
+
}, W = () => {
|
|
56
57
|
let e = {};
|
|
57
|
-
return
|
|
58
|
-
},
|
|
59
|
-
if (e.preventDefault(),
|
|
60
|
-
|
|
58
|
+
return z.appId || (e.appId = w("devportal.selectAppError", "Please select an app to associate this key with")), z.name.trim() || (e.name = w("devportal.apiKeyNameRequired", "API key name is required")), z.scopes.length === 0 && (e.scopes = w("devportal.atLeastOneScopeRequired", "At least one scope is required")), A(e), Object.keys(e).length === 0;
|
|
59
|
+
}, G = async (e) => {
|
|
60
|
+
if (e.preventDefault(), W()) {
|
|
61
|
+
M(null);
|
|
61
62
|
try {
|
|
62
|
-
let e =
|
|
63
|
-
appId:
|
|
64
|
-
name:
|
|
65
|
-
scope:
|
|
63
|
+
let e = C(z.expiresIn), t = await p("api_key_create"), { data: n } = await N({ variables: { input: {
|
|
64
|
+
appId: z.appId,
|
|
65
|
+
name: z.name.trim(),
|
|
66
|
+
scope: z.scopes,
|
|
66
67
|
expiresAt: e,
|
|
67
68
|
...t ? { recaptchaToken: t } : {}
|
|
68
69
|
} } });
|
|
69
|
-
n?.generateAPIKey && (
|
|
70
|
+
n?.generateAPIKey && (F.emit("devportal.apikey.generated", {
|
|
70
71
|
keyId: n.generateAPIKey.keyId,
|
|
71
|
-
appId:
|
|
72
|
-
}),
|
|
72
|
+
appId: z.appId
|
|
73
|
+
}), O({
|
|
73
74
|
apiKey: n.generateAPIKey.apiKey,
|
|
74
75
|
apiSecret: n.generateAPIKey.apiSecret,
|
|
75
76
|
keyId: n.generateAPIKey.keyId
|
|
76
77
|
}));
|
|
77
78
|
} catch (e) {
|
|
78
|
-
|
|
79
|
+
M(e instanceof Error ? e.message : w("devportal.failedToGenerateApiKey", "Failed to generate API key"));
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
},
|
|
82
|
+
}, K = () => {
|
|
83
|
+
V("/api-keys");
|
|
84
|
+
}, q = [
|
|
84
85
|
{
|
|
85
86
|
value: "30days",
|
|
86
87
|
label: "30 days"
|
|
@@ -106,7 +107,7 @@ var C = () => {
|
|
|
106
107
|
label: "Never expires"
|
|
107
108
|
}
|
|
108
109
|
];
|
|
109
|
-
return
|
|
110
|
+
return D ? /* @__PURE__ */ y(n, {
|
|
110
111
|
title: "API Key Generated",
|
|
111
112
|
description: "Your new API key has been generated successfully",
|
|
112
113
|
children: /* @__PURE__ */ b("div", {
|
|
@@ -114,9 +115,9 @@ var C = () => {
|
|
|
114
115
|
children: [
|
|
115
116
|
/* @__PURE__ */ y(i, {
|
|
116
117
|
title: "API Key & Secret",
|
|
117
|
-
secret:
|
|
118
|
+
secret: D.apiSecret,
|
|
118
119
|
description: "Store this secret securely — you won't be able to see it again.",
|
|
119
|
-
onDismiss:
|
|
120
|
+
onDismiss: K
|
|
120
121
|
}),
|
|
121
122
|
/* @__PURE__ */ y(c, {
|
|
122
123
|
variant: "info",
|
|
@@ -134,7 +135,7 @@ var C = () => {
|
|
|
134
135
|
children: "Key ID:"
|
|
135
136
|
}), /* @__PURE__ */ y("code", {
|
|
136
137
|
className: "rounded bg-muted px-2 py-0.5 text-sm",
|
|
137
|
-
children:
|
|
138
|
+
children: D.keyId
|
|
138
139
|
})]
|
|
139
140
|
}), /* @__PURE__ */ b("div", {
|
|
140
141
|
className: "flex items-center gap-2",
|
|
@@ -143,7 +144,7 @@ var C = () => {
|
|
|
143
144
|
children: "API Key:"
|
|
144
145
|
}), /* @__PURE__ */ y("code", {
|
|
145
146
|
className: "rounded bg-muted px-2 py-0.5 text-sm",
|
|
146
|
-
children:
|
|
147
|
+
children: D.apiKey
|
|
147
148
|
})]
|
|
148
149
|
})]
|
|
149
150
|
}),
|
|
@@ -157,7 +158,7 @@ var C = () => {
|
|
|
157
158
|
/* @__PURE__ */ y("div", {
|
|
158
159
|
className: "flex gap-3",
|
|
159
160
|
children: /* @__PURE__ */ y("button", {
|
|
160
|
-
onClick:
|
|
161
|
+
onClick: K,
|
|
161
162
|
className: "inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors",
|
|
162
163
|
children: "Continue to API Keys"
|
|
163
164
|
})
|
|
@@ -172,27 +173,27 @@ var C = () => {
|
|
|
172
173
|
backLink: "/api-keys",
|
|
173
174
|
backLabel: "Back to API Keys",
|
|
174
175
|
children: /* @__PURE__ */ b("form", {
|
|
175
|
-
onSubmit:
|
|
176
|
+
onSubmit: G,
|
|
176
177
|
className: "max-w-3xl space-y-8",
|
|
177
178
|
children: [
|
|
178
|
-
|
|
179
|
+
j && /* @__PURE__ */ y(c, {
|
|
179
180
|
variant: "error",
|
|
180
181
|
title: "Error Generating API Key",
|
|
181
|
-
onDismiss: () =>
|
|
182
|
-
children:
|
|
182
|
+
onDismiss: () => M(null),
|
|
183
|
+
children: j
|
|
183
184
|
}),
|
|
184
185
|
/* @__PURE__ */ y(r, {
|
|
185
186
|
title: "Basic Information",
|
|
186
187
|
description: "Configure the API key settings",
|
|
187
188
|
children: /* @__PURE__ */ b("div", {
|
|
188
189
|
className: "space-y-4",
|
|
189
|
-
children: [
|
|
190
|
+
children: [L ? /* @__PURE__ */ b("div", {
|
|
190
191
|
className: "flex items-center gap-2 py-2",
|
|
191
192
|
children: [/* @__PURE__ */ y(g, { className: "h-4 w-4 animate-spin text-muted-foreground" }), /* @__PURE__ */ y("span", {
|
|
192
193
|
className: "text-sm text-muted-foreground",
|
|
193
194
|
children: "Loading apps..."
|
|
194
195
|
})]
|
|
195
|
-
}) :
|
|
196
|
+
}) : R.length === 0 ? /* @__PURE__ */ y(c, {
|
|
196
197
|
variant: "warning",
|
|
197
198
|
title: "No Apps Found",
|
|
198
199
|
children: /* @__PURE__ */ b("p", { children: [
|
|
@@ -200,31 +201,31 @@ var C = () => {
|
|
|
200
201
|
" ",
|
|
201
202
|
/* @__PURE__ */ y("button", {
|
|
202
203
|
type: "button",
|
|
203
|
-
onClick: () =>
|
|
204
|
+
onClick: () => V("/oauth/new"),
|
|
204
205
|
className: "text-primary underline hover:no-underline",
|
|
205
206
|
children: "Create an app"
|
|
206
207
|
})
|
|
207
208
|
] })
|
|
208
209
|
}) : /* @__PURE__ */ y(o, {
|
|
209
210
|
label: "App",
|
|
210
|
-
value:
|
|
211
|
-
onChange: (e) =>
|
|
211
|
+
value: z.appId,
|
|
212
|
+
onChange: (e) => U("appId", e.target.value),
|
|
212
213
|
options: [{
|
|
213
214
|
value: "",
|
|
214
215
|
label: "Select an app..."
|
|
215
|
-
}, ...
|
|
216
|
+
}, ...R.map((e) => ({
|
|
216
217
|
value: e.id,
|
|
217
218
|
label: `${e.name} (${e.clientId})`
|
|
218
219
|
}))],
|
|
219
|
-
error:
|
|
220
|
+
error: k.appId,
|
|
220
221
|
required: !0,
|
|
221
222
|
hint: "The OAuth app this API key will be associated with"
|
|
222
223
|
}), /* @__PURE__ */ y(a, {
|
|
223
224
|
label: "Key Name",
|
|
224
225
|
placeholder: "e.g., Production Backend Key",
|
|
225
|
-
value:
|
|
226
|
-
onChange: (e) =>
|
|
227
|
-
error:
|
|
226
|
+
value: z.name,
|
|
227
|
+
onChange: (e) => U("name", e.target.value),
|
|
228
|
+
error: k.name,
|
|
228
229
|
required: !0,
|
|
229
230
|
hint: "A descriptive name to identify this key"
|
|
230
231
|
})]
|
|
@@ -237,41 +238,45 @@ var C = () => {
|
|
|
237
238
|
className: "space-y-4",
|
|
238
239
|
children: /* @__PURE__ */ y(s, {
|
|
239
240
|
label: "Scopes",
|
|
240
|
-
value:
|
|
241
|
-
onChange: (e) =>
|
|
241
|
+
value: z.scopes,
|
|
242
|
+
onChange: (e) => U("scopes", e),
|
|
242
243
|
placeholder: "e.g., workspace:read, vibecontrols:write",
|
|
243
244
|
hint: "Press Enter to add each scope. At least one scope is required.",
|
|
244
|
-
error:
|
|
245
|
+
error: k.scopes
|
|
245
246
|
})
|
|
246
247
|
})
|
|
247
248
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
-
title: "
|
|
250
|
-
description: "
|
|
251
|
-
children:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
249
|
+
/* @__PURE__ */ y(x, {
|
|
250
|
+
title: "Advanced settings",
|
|
251
|
+
description: "Rate-limit overrides, allowed origins, scope tuning, and expiration.",
|
|
252
|
+
children: /* @__PURE__ */ b(r, {
|
|
253
|
+
title: "Key Expiration",
|
|
254
|
+
description: "Set when this key should expire",
|
|
255
|
+
children: [/* @__PURE__ */ y(o, {
|
|
256
|
+
label: "Expires In",
|
|
257
|
+
value: z.expiresIn,
|
|
258
|
+
onChange: (e) => U("expiresIn", e.target.value),
|
|
259
|
+
options: q,
|
|
260
|
+
hint: "Keys can be revoked at any time regardless of expiration"
|
|
261
|
+
}), z.expiresIn === "never" && /* @__PURE__ */ y(c, {
|
|
262
|
+
variant: "warning",
|
|
263
|
+
className: "mt-4",
|
|
264
|
+
children: "Non-expiring keys are a security risk. Consider setting an expiration and rotating keys periodically."
|
|
265
|
+
})]
|
|
266
|
+
})
|
|
262
267
|
}),
|
|
263
268
|
/* @__PURE__ */ b("div", {
|
|
264
269
|
className: "flex items-center justify-end gap-3 border-t pt-6",
|
|
265
270
|
children: [/* @__PURE__ */ b("button", {
|
|
266
271
|
type: "button",
|
|
267
|
-
onClick:
|
|
272
|
+
onClick: H,
|
|
268
273
|
className: "inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors",
|
|
269
274
|
children: [/* @__PURE__ */ y(v, { size: 16 }), "Cancel"]
|
|
270
275
|
}), /* @__PURE__ */ b("button", {
|
|
271
276
|
type: "submit",
|
|
272
|
-
disabled:
|
|
277
|
+
disabled: P || R.length === 0,
|
|
273
278
|
className: "inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50",
|
|
274
|
-
children: [/* @__PURE__ */ y(_, { size: 16 }),
|
|
279
|
+
children: [/* @__PURE__ */ y(_, { size: 16 }), P ? "Generating..." : "Generate API Key"]
|
|
275
280
|
})]
|
|
276
281
|
})
|
|
277
282
|
]
|
|
@@ -280,6 +285,6 @@ var C = () => {
|
|
|
280
285
|
});
|
|
281
286
|
};
|
|
282
287
|
//#endregion
|
|
283
|
-
export {
|
|
288
|
+
export { w as CreateApiKeyPage };
|
|
284
289
|
|
|
285
290
|
//# sourceMappingURL=CreateApiKeyPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateApiKeyPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApiKeyPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { InputField, SelectField, TagInput } from \"../components/FormField\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useGenerateApiKeyMutation, useCurrentUserAppsQuery } from \"../hooks\";\nimport { useDevportalEventBus } from \"../events\";\nimport { executeRecaptcha } from \"../utils/recaptcha\";\n\ninterface CreateApiKeyFormData {\n appId: string;\n name: string;\n scopes: string[];\n expiresIn: string;\n}\n\nconst EXPIRATION_DURATIONS: Record<string, number | null> = {\n \"30days\": 30 * 24 * 60 * 60 * 1000,\n \"90days\": 90 * 24 * 60 * 60 * 1000,\n \"180days\": 180 * 24 * 60 * 60 * 1000,\n \"1year\": 365 * 24 * 60 * 60 * 1000,\n \"2years\": 2 * 365 * 24 * 60 * 60 * 1000,\n never: null,\n};\n\nfunction computeExpiresAt(expiresIn: string): string | undefined {\n const duration = EXPIRATION_DURATIONS[expiresIn];\n if (duration == null) return undefined;\n return new Date(Date.now() + duration).toISOString();\n}\n\n/**\n * Page for generating a new API key via auth-svc.\n *\n * Previously created API keys through devportal-svc per application;\n * now generates keys through auth-svc's generateAPIKey mutation.\n * The user selects which OAuth app to associate the key with.\n */\nexport const CreateApiKeyPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { navigate, basePath } = useDevportal();\n const [newApiKey, setNewApiKey] = useState<{\n apiKey: string;\n apiSecret: string;\n keyId: string;\n } | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [mutationError, setMutationError] = useState<string | null>(null);\n\n const [generateApiKey, { loading: isSubmitting }] =\n useGenerateApiKeyMutation();\n const bus = useDevportalEventBus();\n\n // Fetch user's apps to populate the app selector\n const { data: appsData, loading: appsLoading } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: Array<{ id: string; name: string; clientId: string }> =\n appsData?.currentUserApps?.map(\n (app: { id: string; name: string; clientId: string }) => ({\n id: app.id,\n name: app.name,\n clientId: app.clientId,\n }),\n ) ?? [];\n\n const [formData, setFormData] = useState<CreateApiKeyFormData>({\n appId: \"\",\n name: \"\",\n scopes: [],\n expiresIn: \"1year\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const handleCancel = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const updateField = <K extends keyof CreateApiKeyFormData>(\n field: K,\n value: CreateApiKeyFormData[K],\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field]) {\n setErrors((prev) => ({ ...prev, [field]: \"\" }));\n }\n if (mutationError) {\n setMutationError(null);\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: Record<string, string> = {};\n\n if (!formData.appId) {\n newErrors.appId = tr(\n \"devportal.selectAppError\",\n \"Please select an app to associate this key with\",\n );\n }\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.apiKeyNameRequired\",\n \"API key name is required\",\n );\n }\n\n if (formData.scopes.length === 0) {\n newErrors.scopes = tr(\n \"devportal.atLeastOneScopeRequired\",\n \"At least one scope is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) return;\n\n setMutationError(null);\n\n try {\n const expiresAt = computeExpiresAt(formData.expiresIn);\n const recaptchaToken = await executeRecaptcha(\"api_key_create\");\n const input = {\n appId: formData.appId,\n name: formData.name.trim(),\n scope: formData.scopes,\n expiresAt,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n };\n\n const { data } = await generateApiKey({\n variables: {\n input,\n },\n });\n\n if (data?.generateAPIKey) {\n bus.emit(\"devportal.apikey.generated\", {\n keyId: data.generateAPIKey.keyId,\n appId: formData.appId,\n });\n setNewApiKey({\n apiKey: data.generateAPIKey.apiKey,\n apiSecret: data.generateAPIKey.apiSecret,\n keyId: data.generateAPIKey.keyId,\n });\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGenerateApiKey\",\n \"Failed to generate API key\",\n );\n setMutationError(message);\n }\n };\n\n const handleKeyDismiss = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const expirationOptions = [\n { value: \"30days\", label: \"30 days\" },\n { value: \"90days\", label: \"90 days\" },\n { value: \"180days\", label: \"180 days\" },\n { value: \"1year\", label: \"1 year\" },\n { value: \"2years\", label: \"2 years\" },\n { value: \"never\", label: \"Never expires\" },\n ];\n\n if (newApiKey) {\n return (\n <PageLayout\n title=\"API Key Generated\"\n description=\"Your new API key has been generated successfully\"\n >\n <div className=\"max-w-2xl space-y-6\">\n <NewSecretAlert\n title=\"API Key & Secret\"\n secret={newApiKey.apiSecret}\n description=\"Store this secret securely — you won't be able to see it again.\"\n onDismiss={handleKeyDismiss}\n />\n\n <Alert variant=\"info\" title=\"Usage Instructions\">\n <div className=\"mt-2 space-y-2\">\n <p>Your API key details:</p>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n Key ID:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.keyId}\n </code>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n API Key:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.apiKey}\n </code>\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Use the API key and secret to authenticate your requests.\n </p>\n </div>\n </Alert>\n\n <div className=\"flex gap-3\">\n <button\n onClick={handleKeyDismiss}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n Continue to API Keys\n </button>\n </div>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_UPDATE}>\n <PageLayout\n title=\"Generate API Key\"\n description=\"Generate a new API key for programmatic access\"\n backLink=\"/api-keys\"\n backLabel=\"Back to API Keys\"\n >\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-8\">\n {/* Mutation error */}\n {mutationError && (\n <Alert\n variant=\"error\"\n title=\"Error Generating API Key\"\n onDismiss={() => setMutationError(null)}\n >\n {mutationError}\n </Alert>\n )}\n\n {/* Basic Information */}\n <PageSection\n title=\"Basic Information\"\n description=\"Configure the API key settings\"\n >\n <div className=\"space-y-4\">\n {/* App Selector */}\n {appsLoading ? (\n <div className=\"flex items-center gap-2 py-2\">\n <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading apps...\n </span>\n </div>\n ) : apps.length === 0 ? (\n <Alert variant=\"warning\" title=\"No Apps Found\">\n <p>\n You need to create an OAuth app first before generating API\n keys.{\" \"}\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"text-primary underline hover:no-underline\"\n >\n Create an app\n </button>\n </p>\n </Alert>\n ) : (\n <SelectField\n label=\"App\"\n value={formData.appId}\n onChange={(e) => updateField(\"appId\", e.target.value)}\n options={[\n { value: \"\", label: \"Select an app...\" },\n ...apps.map((app) => ({\n value: app.id,\n label: `${app.name} (${app.clientId})`,\n })),\n ]}\n error={errors.appId}\n required\n hint=\"The OAuth app this API key will be associated with\"\n />\n )}\n\n <InputField\n label=\"Key Name\"\n placeholder=\"e.g., Production Backend Key\"\n value={formData.name}\n onChange={(e) => updateField(\"name\", e.target.value)}\n error={errors.name}\n required\n hint=\"A descriptive name to identify this key\"\n />\n </div>\n </PageSection>\n\n {/* Scopes */}\n <PageSection\n title=\"Scopes\"\n description=\"Define what this API key can access\"\n >\n <div className=\"space-y-4\">\n <TagInput\n label=\"Scopes\"\n value={formData.scopes}\n onChange={(values) => updateField(\"scopes\", values)}\n placeholder=\"e.g., workspace:read, vibecontrols:write\"\n hint=\"Press Enter to add each scope. At least one scope is required.\"\n error={errors.scopes}\n />\n </div>\n </PageSection>\n\n {/* Expiration */}\n <PageSection\n title=\"Key Expiration\"\n description=\"Set when this key should expire\"\n >\n <SelectField\n label=\"Expires In\"\n value={formData.expiresIn}\n onChange={(e) => updateField(\"expiresIn\", e.target.value)}\n options={expirationOptions}\n hint=\"Keys can be revoked at any time regardless of expiration\"\n />\n\n {formData.expiresIn === \"never\" && (\n <Alert variant=\"warning\" className=\"mt-4\">\n Non-expiring keys are a security risk. Consider setting an\n expiration and rotating keys periodically.\n </Alert>\n )}\n </PageSection>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <X size={16} />\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || apps.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n <Save size={16} />\n {isSubmitting ? \"Generating...\" : \"Generate API Key\"}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateApiKeyPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApiKeyPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { OptionalFields } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { InputField, SelectField, TagInput } from \"../components/FormField\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useGenerateApiKeyMutation, useCurrentUserAppsQuery } from \"../hooks\";\nimport { useDevportalEventBus } from \"../events\";\nimport { executeRecaptcha } from \"../utils/recaptcha\";\n\ninterface CreateApiKeyFormData {\n appId: string;\n name: string;\n scopes: string[];\n expiresIn: string;\n}\n\nconst EXPIRATION_DURATIONS: Record<string, number | null> = {\n \"30days\": 30 * 24 * 60 * 60 * 1000,\n \"90days\": 90 * 24 * 60 * 60 * 1000,\n \"180days\": 180 * 24 * 60 * 60 * 1000,\n \"1year\": 365 * 24 * 60 * 60 * 1000,\n \"2years\": 2 * 365 * 24 * 60 * 60 * 1000,\n never: null,\n};\n\nfunction computeExpiresAt(expiresIn: string): string | undefined {\n const duration = EXPIRATION_DURATIONS[expiresIn];\n if (duration == null) return undefined;\n return new Date(Date.now() + duration).toISOString();\n}\n\n/**\n * Page for generating a new API key via auth-svc.\n *\n * Previously created API keys through devportal-svc per application;\n * now generates keys through auth-svc's generateAPIKey mutation.\n * The user selects which OAuth app to associate the key with.\n */\nexport const CreateApiKeyPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { navigate, basePath } = useDevportal();\n const [newApiKey, setNewApiKey] = useState<{\n apiKey: string;\n apiSecret: string;\n keyId: string;\n } | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [mutationError, setMutationError] = useState<string | null>(null);\n\n const [generateApiKey, { loading: isSubmitting }] =\n useGenerateApiKeyMutation();\n const bus = useDevportalEventBus();\n\n // Fetch user's apps to populate the app selector\n const { data: appsData, loading: appsLoading } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: Array<{ id: string; name: string; clientId: string }> =\n appsData?.currentUserApps?.apps.map(\n (app: { id: string; name: string; clientId: string }) => ({\n id: app.id,\n name: app.name,\n clientId: app.clientId,\n }),\n ) ?? [];\n\n const [formData, setFormData] = useState<CreateApiKeyFormData>({\n appId: \"\",\n name: \"\",\n scopes: [],\n expiresIn: \"1year\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const handleCancel = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const updateField = <K extends keyof CreateApiKeyFormData>(\n field: K,\n value: CreateApiKeyFormData[K],\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field]) {\n setErrors((prev) => ({ ...prev, [field]: \"\" }));\n }\n if (mutationError) {\n setMutationError(null);\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: Record<string, string> = {};\n\n if (!formData.appId) {\n newErrors.appId = tr(\n \"devportal.selectAppError\",\n \"Please select an app to associate this key with\",\n );\n }\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.apiKeyNameRequired\",\n \"API key name is required\",\n );\n }\n\n if (formData.scopes.length === 0) {\n newErrors.scopes = tr(\n \"devportal.atLeastOneScopeRequired\",\n \"At least one scope is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) return;\n\n setMutationError(null);\n\n try {\n const expiresAt = computeExpiresAt(formData.expiresIn);\n const recaptchaToken = await executeRecaptcha(\"api_key_create\");\n const input = {\n appId: formData.appId,\n name: formData.name.trim(),\n scope: formData.scopes,\n expiresAt,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n };\n\n const { data } = await generateApiKey({\n variables: {\n input,\n },\n });\n\n if (data?.generateAPIKey) {\n bus.emit(\"devportal.apikey.generated\", {\n keyId: data.generateAPIKey.keyId,\n appId: formData.appId,\n });\n setNewApiKey({\n apiKey: data.generateAPIKey.apiKey,\n apiSecret: data.generateAPIKey.apiSecret,\n keyId: data.generateAPIKey.keyId,\n });\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGenerateApiKey\",\n \"Failed to generate API key\",\n );\n setMutationError(message);\n }\n };\n\n const handleKeyDismiss = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const expirationOptions = [\n { value: \"30days\", label: \"30 days\" },\n { value: \"90days\", label: \"90 days\" },\n { value: \"180days\", label: \"180 days\" },\n { value: \"1year\", label: \"1 year\" },\n { value: \"2years\", label: \"2 years\" },\n { value: \"never\", label: \"Never expires\" },\n ];\n\n if (newApiKey) {\n return (\n <PageLayout\n title=\"API Key Generated\"\n description=\"Your new API key has been generated successfully\"\n >\n <div className=\"max-w-2xl space-y-6\">\n <NewSecretAlert\n title=\"API Key & Secret\"\n secret={newApiKey.apiSecret}\n description=\"Store this secret securely — you won't be able to see it again.\"\n onDismiss={handleKeyDismiss}\n />\n\n <Alert variant=\"info\" title=\"Usage Instructions\">\n <div className=\"mt-2 space-y-2\">\n <p>Your API key details:</p>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n Key ID:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.keyId}\n </code>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n API Key:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.apiKey}\n </code>\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Use the API key and secret to authenticate your requests.\n </p>\n </div>\n </Alert>\n\n <div className=\"flex gap-3\">\n <button\n onClick={handleKeyDismiss}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n Continue to API Keys\n </button>\n </div>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_UPDATE}>\n <PageLayout\n title=\"Generate API Key\"\n description=\"Generate a new API key for programmatic access\"\n backLink=\"/api-keys\"\n backLabel=\"Back to API Keys\"\n >\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-8\">\n {/* Mutation error */}\n {mutationError && (\n <Alert\n variant=\"error\"\n title=\"Error Generating API Key\"\n onDismiss={() => setMutationError(null)}\n >\n {mutationError}\n </Alert>\n )}\n\n {/* Basic Information */}\n <PageSection\n title=\"Basic Information\"\n description=\"Configure the API key settings\"\n >\n <div className=\"space-y-4\">\n {/* App Selector */}\n {appsLoading ? (\n <div className=\"flex items-center gap-2 py-2\">\n <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading apps...\n </span>\n </div>\n ) : apps.length === 0 ? (\n <Alert variant=\"warning\" title=\"No Apps Found\">\n <p>\n You need to create an OAuth app first before generating API\n keys.{\" \"}\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"text-primary underline hover:no-underline\"\n >\n Create an app\n </button>\n </p>\n </Alert>\n ) : (\n <SelectField\n label=\"App\"\n value={formData.appId}\n onChange={(e) => updateField(\"appId\", e.target.value)}\n options={[\n { value: \"\", label: \"Select an app...\" },\n ...apps.map((app) => ({\n value: app.id,\n label: `${app.name} (${app.clientId})`,\n })),\n ]}\n error={errors.appId}\n required\n hint=\"The OAuth app this API key will be associated with\"\n />\n )}\n\n <InputField\n label=\"Key Name\"\n placeholder=\"e.g., Production Backend Key\"\n value={formData.name}\n onChange={(e) => updateField(\"name\", e.target.value)}\n error={errors.name}\n required\n hint=\"A descriptive name to identify this key\"\n />\n </div>\n </PageSection>\n\n {/* Scopes */}\n <PageSection\n title=\"Scopes\"\n description=\"Define what this API key can access\"\n >\n <div className=\"space-y-4\">\n <TagInput\n label=\"Scopes\"\n value={formData.scopes}\n onChange={(values) => updateField(\"scopes\", values)}\n placeholder=\"e.g., workspace:read, vibecontrols:write\"\n hint=\"Press Enter to add each scope. At least one scope is required.\"\n error={errors.scopes}\n />\n </div>\n </PageSection>\n\n {/* Advanced: rate-limit overrides / allowed origins / scope tuning / expiration — collapse the long-tail knobs */}\n <OptionalFields\n title=\"Advanced settings\"\n description=\"Rate-limit overrides, allowed origins, scope tuning, and expiration.\"\n >\n <PageSection\n title=\"Key Expiration\"\n description=\"Set when this key should expire\"\n >\n <SelectField\n label=\"Expires In\"\n value={formData.expiresIn}\n onChange={(e) => updateField(\"expiresIn\", e.target.value)}\n options={expirationOptions}\n hint=\"Keys can be revoked at any time regardless of expiration\"\n />\n\n {formData.expiresIn === \"never\" && (\n <Alert variant=\"warning\" className=\"mt-4\">\n Non-expiring keys are a security risk. Consider setting an\n expiration and rotating keys periodically.\n </Alert>\n )}\n </PageSection>\n </OptionalFields>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <X size={16} />\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || apps.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n <Save size={16} />\n {isSubmitting ? \"Generating...\" : \"Generate API Key\"}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -95,7 +95,7 @@ var C = Object.entries(i).map(([e, t]) => ({
|
|
|
95
95
|
}))], R = [{
|
|
96
96
|
value: "",
|
|
97
97
|
label: O("devportal.noLinkedAuthApp", "Link later")
|
|
98
|
-
}, ...(F?.currentUserApps ?? []).map((e) => ({
|
|
98
|
+
}, ...(F?.currentUserApps?.apps ?? []).map((e) => ({
|
|
99
99
|
value: e.id,
|
|
100
100
|
label: `${e.name} · ${e.type} · ${e.clientId}`
|
|
101
101
|
})) ?? []], [z, B] = h({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateApplicationPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApplicationPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Info } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n InputField,\n TextareaField,\n SelectField,\n TagInput,\n CheckboxField,\n} from \"../components/FormField\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalEventBus } from \"../events\";\nimport type { AppType, AppVisibility } from \"../types\";\nimport { APP_TYPE_LABELS } from \"../utils/constants\";\nimport {\n useCreateApplicationMutation,\n useGetMyOrganizationsQuery,\n} from \"../../generated/global-operations\";\nimport { useCurrentUserAppsQuery } from \"../hooks\";\n\ninterface CreateApplicationFormData {\n name: string;\n slug: string;\n shortDescription: string;\n description: string;\n appType: AppType | \"\";\n visibility: AppVisibility;\n category: string;\n tags: string[];\n keywords: string[];\n websiteUrl: string;\n documentationUrl: string;\n supportUrl: string;\n privacyPolicyUrl: string;\n termsOfServiceUrl: string;\n githubUrl: string;\n developerOrganizationId: string;\n appId: string;\n requiresApproval: boolean;\n}\n\ninterface FormErrors {\n name?: string;\n slug?: string;\n shortDescription?: string;\n appType?: string;\n category?: string;\n}\n\nconst APP_TYPE_OPTIONS = Object.entries(APP_TYPE_LABELS).map(\n ([value, label]) => ({\n value,\n label,\n }),\n);\n\nconst VISIBILITY_OPTIONS = [\n { value: \"PUBLIC\", label: \"Public - Visible to everyone\" },\n { value: \"PRIVATE\", label: \"Private - Only visible to you\" },\n { value: \"UNLISTED\", label: \"Unlisted - Accessible via direct link\" },\n { value: \"INTERNAL\", label: \"Internal - Only visible within organization\" },\n];\n\nconst CATEGORY_OPTIONS = [\n { value: \"productivity\", label: \"Productivity\" },\n { value: \"communication\", label: \"Communication\" },\n { value: \"analytics\", label: \"Analytics\" },\n { value: \"automation\", label: \"Automation\" },\n { value: \"developer-tools\", label: \"Developer Tools\" },\n { value: \"finance\", label: \"Finance\" },\n { value: \"hr\", label: \"HR & People\" },\n { value: \"marketing\", label: \"Marketing\" },\n { value: \"project-management\", label: \"Project Management\" },\n { value: \"security\", label: \"Security\" },\n { value: \"support\", label: \"Support\" },\n { value: \"other\", label: \"Other\" },\n];\n\n/**\n * Create new application page\n */\nexport const CreateApplicationPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [errors, setErrors] = useState<FormErrors>({});\n const [createApplication, { loading: isSubmitting }] =\n useCreateApplicationMutation();\n const bus = useDevportalEventBus();\n\n // Fetch real organizations\n const { data: orgsData } = useGetMyOrganizationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n const { data: authAppsData, loading: authAppsLoading } =\n useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const organizationOptions = [\n { value: \"\", label: \"Personal Account\" },\n ...(orgsData?.myDeveloperOrganizations ?? []).map(\n (org: { id: string; name: string }) => ({\n value: org.id,\n label: org.name,\n }),\n ),\n ];\n\n const authAppOptions = [\n { value: \"\", label: tr(\"devportal.noLinkedAuthApp\", \"Link later\") },\n ...((authAppsData?.currentUserApps ?? []).map(\n (authApp: {\n id: string;\n name: string;\n type: string;\n clientId: string;\n }) => ({\n value: authApp.id,\n label: `${authApp.name} · ${authApp.type} · ${authApp.clientId}`,\n }),\n ) ?? []),\n ];\n\n const [formData, setFormData] = useState<CreateApplicationFormData>({\n name: \"\",\n slug: \"\",\n shortDescription: \"\",\n description: \"\",\n appType: \"\",\n visibility: \"PRIVATE\",\n category: \"\",\n tags: [],\n keywords: [],\n websiteUrl: \"\",\n documentationUrl: \"\",\n supportUrl: \"\",\n privacyPolicyUrl: \"\",\n termsOfServiceUrl: \"\",\n githubUrl: \"\",\n developerOrganizationId: \"\",\n appId: \"\",\n requiresApproval: false,\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const generateSlug = (name: string): string => {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n };\n\n const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const name = e.target.value;\n setFormData((prev) => ({\n ...prev,\n name,\n slug: prev.slug || generateSlug(name),\n }));\n if (errors.name) {\n setErrors((prev) => ({ ...prev, name: undefined }));\n }\n };\n\n const handleChange = (\n field: keyof CreateApplicationFormData,\n value: string | string[] | boolean,\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field as keyof FormErrors]) {\n setErrors((prev) => ({ ...prev, [field]: undefined }));\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: FormErrors = {};\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.appNameRequired\",\n \"Application name is required\",\n );\n } else if (formData.name.length < 3) {\n newErrors.name = tr(\n \"devportal.nameTooShort\",\n \"Name must be at least 3 characters\",\n );\n } else if (formData.name.length > 100) {\n newErrors.name = tr(\n \"devportal.nameTooLong\",\n \"Name must be less than 100 characters\",\n );\n }\n\n if (!formData.slug.trim()) {\n newErrors.slug = tr(\"devportal.slugRequired\", \"Slug is required\");\n } else if (!/^[a-z0-9-]+$/.test(formData.slug)) {\n newErrors.slug = tr(\n \"devportal.slugInvalid\",\n \"Slug can only contain lowercase letters, numbers, and hyphens\",\n );\n }\n\n if (!formData.shortDescription.trim()) {\n newErrors.shortDescription = tr(\n \"devportal.shortDescriptionRequired\",\n \"Short description is required\",\n );\n } else if (formData.shortDescription.length > 200) {\n newErrors.shortDescription = tr(\n \"devportal.shortDescriptionTooLong\",\n \"Short description must be less than 200 characters\",\n );\n }\n\n if (!formData.appType) {\n newErrors.appType = tr(\n \"devportal.appTypeRequired\",\n \"Application type is required\",\n );\n }\n\n if (!formData.category) {\n newErrors.category = tr(\n \"devportal.categoryRequired\",\n \"Category is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) {\n return;\n }\n\n try {\n const result = await createApplication({\n variables: {\n input: {\n name: formData.name,\n slug: formData.slug || undefined,\n shortDescription: formData.shortDescription || undefined,\n description: formData.description || undefined,\n appType: formData.appType as AppType,\n visibility: formData.visibility || undefined,\n category: formData.category || undefined,\n tags: formData.tags.length > 0 ? formData.tags : undefined,\n keywords:\n formData.keywords.length > 0 ? formData.keywords : undefined,\n websiteUrl: formData.websiteUrl || undefined,\n documentationUrl: formData.documentationUrl || undefined,\n supportUrl: formData.supportUrl || undefined,\n privacyPolicyUrl: formData.privacyPolicyUrl || undefined,\n termsOfServiceUrl: formData.termsOfServiceUrl || undefined,\n developerOrganizationId:\n formData.developerOrganizationId || undefined,\n appId: formData.appId || undefined,\n githubUrl: formData.githubUrl || undefined,\n requiresApproval: formData.requiresApproval,\n iconUrl: undefined,\n },\n },\n });\n\n if (result.data?.createApplication?.id) {\n bus.emit(\"devportal.app.created\", {\n appId: result.data.createApplication.id,\n name: formData.name,\n appType: formData.appType,\n organizationId: formData.developerOrganizationId || null,\n });\n handleNavigate(`/apps/${result.data.createApplication.id}`);\n }\n } catch (error) {\n console.error(\"Failed to create application:\", error);\n }\n };\n\n const handleCancel = () => {\n handleNavigate(\"/apps\");\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_CREATE}>\n <PageLayout\n title={tr(\"devportal.createApplication\", \"Create Application\")}\n description={tr(\n \"devportal.createApplicationDescription\",\n \"Register a new application in the developer portal\",\n )}\n backLink={`${basePath}/apps`}\n backLabel={tr(\"devportal.backToApplications\", \"Back to Applications\")}\n >\n <form onSubmit={handleSubmit} className=\"space-y-8 max-w-3xl\">\n {/* Basic Information */}\n <PageSection\n title={tr(\"devportal.basicInformation\", \"Basic Information\")}\n description={tr(\n \"devportal.basicInformationDescription\",\n \"Core details about your application\",\n )}\n >\n <div className=\"space-y-4\">\n <InputField\n label={tr(\"devportal.applicationName\", \"Application Name\")}\n value={formData.name}\n onChange={handleNameChange}\n placeholder={tr(\n \"devportal.applicationNamePlaceholder\",\n \"My Awesome App\",\n )}\n required\n error={errors.name}\n hint={tr(\n \"devportal.applicationNameHint\",\n \"Choose a memorable name for your application\",\n )}\n />\n\n <InputField\n label={tr(\"devportal.slug\", \"Slug\")}\n value={formData.slug}\n onChange={(e) =>\n handleChange(\"slug\", e.target.value.toLowerCase())\n }\n placeholder={tr(\"devportal.slugPlaceholder\", \"my-awesome-app\")}\n required\n error={errors.slug}\n hint={tr(\n \"devportal.slugHint\",\n \"URL-friendly identifier (auto-generated from name)\",\n )}\n />\n\n <TextareaField\n label={tr(\"devportal.shortDescription\", \"Short Description\")}\n value={formData.shortDescription}\n onChange={(e) =>\n handleChange(\"shortDescription\", e.target.value)\n }\n placeholder={tr(\n \"devportal.shortDescriptionPlaceholder\",\n \"A brief description of what your app does\",\n )}\n required\n error={errors.shortDescription}\n hint={`${formData.shortDescription.length}/200 ${tr(\"devportal.characters\", \"characters\")}`}\n rows={2}\n />\n\n <TextareaField\n label={tr(\"devportal.fullDescription\", \"Full Description\")}\n value={formData.description}\n onChange={(e) => handleChange(\"description\", e.target.value)}\n placeholder={tr(\n \"devportal.fullDescriptionPlaceholder\",\n \"Detailed description of your application, features, and use cases...\",\n )}\n rows={6}\n hint={tr(\n \"devportal.markdownSupported\",\n \"Markdown formatting is supported\",\n )}\n />\n </div>\n </PageSection>\n\n {/* Classification */}\n <PageSection\n title={tr(\"devportal.classification\", \"Classification\")}\n description={tr(\n \"devportal.classificationDescription\",\n \"Categorize your application\",\n )}\n >\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <SelectField\n label={tr(\"devportal.applicationType\", \"Application Type\")}\n value={formData.appType}\n onChange={(e) => handleChange(\"appType\", e.target.value)}\n options={APP_TYPE_OPTIONS}\n required\n error={errors.appType}\n placeholder={tr(\"devportal.selectAppType\", \"Select app type\")}\n />\n\n <SelectField\n label={tr(\"devportal.category\", \"Category\")}\n value={formData.category}\n onChange={(e) => handleChange(\"category\", e.target.value)}\n options={CATEGORY_OPTIONS}\n required\n error={errors.category}\n placeholder={tr(\"devportal.selectCategory\", \"Select category\")}\n />\n\n <SelectField\n label={tr(\"devportal.visibility\", \"Visibility\")}\n value={formData.visibility}\n onChange={(e) =>\n handleChange(\"visibility\", e.target.value as AppVisibility)\n }\n options={VISIBILITY_OPTIONS}\n />\n\n <SelectField\n label={tr(\n \"devportal.developerOrganization\",\n \"Developer Organization\",\n )}\n value={formData.developerOrganizationId}\n onChange={(e) =>\n handleChange(\"developerOrganizationId\", e.target.value)\n }\n options={organizationOptions}\n hint={tr(\n \"devportal.developerOrganizationHint\",\n \"Optional: Associate with an organization\",\n )}\n />\n </div>\n\n <TagInput\n label={tr(\"devportal.tags\", \"Tags\")}\n value={formData.tags}\n onChange={(tags) => handleChange(\"tags\", tags)}\n placeholder={tr(\n \"devportal.addTagsPlaceholder\",\n \"Add tags (press Enter)\",\n )}\n hint={tr(\"devportal.tagsHint\", \"Help users discover your app\")}\n />\n\n <TagInput\n label={tr(\"devportal.keywords\", \"Keywords\")}\n value={formData.keywords}\n onChange={(keywords) => handleChange(\"keywords\", keywords)}\n placeholder={tr(\n \"devportal.addKeywordsPlaceholder\",\n \"Add keywords (press Enter)\",\n )}\n hint={tr(\"devportal.keywordsHint\", \"Improve search visibility\")}\n />\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.authenticationAndAuthorization\",\n \"Authentication & Authorization\",\n )}\n description={tr(\n \"devportal.authenticationAndAuthorizationDescription\",\n \"Link this marketplace app to an auth app for OAuth, client credentials, API keys, delegation, and device flows.\",\n )}\n >\n <div className=\"space-y-4\">\n <SelectField\n label={tr(\"devportal.linkedAuthApp\", \"Linked Auth App\")}\n value={formData.appId}\n onChange={(e) => handleChange(\"appId\", e.target.value)}\n options={authAppOptions}\n hint={tr(\n \"devportal.linkedAuthAppHint\",\n \"Select an existing auth app or create one first. You can still configure scopes, client credentials, app-as-user delegation, and device code in the OAuth Clients area.\",\n )}\n placeholder={\n authAppsLoading\n ? tr(\"devportal.loadingAuthApps\", \"Loading auth apps...\")\n : tr(\"devportal.selectAuthApp\", \"Select auth app\")\n }\n />\n\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <Save size={14} />\n {tr(\"devportal.createAuthAppFirst\", \"Create Auth App\")}\n </button>\n </div>\n </PageSection>\n\n {/* URLs */}\n <PageSection\n title={tr(\"devportal.links\", \"Links\")}\n description={tr(\n \"devportal.linksDescription\",\n \"External resources for your application\",\n )}\n >\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <InputField\n label={tr(\"devportal.websiteUrl\", \"Website URL\")}\n type=\"url\"\n value={formData.websiteUrl}\n onChange={(e) => handleChange(\"websiteUrl\", e.target.value)}\n placeholder=\"https://example.com\"\n />\n\n <InputField\n label={tr(\"devportal.documentationUrl\", \"Documentation URL\")}\n type=\"url\"\n value={formData.documentationUrl}\n onChange={(e) =>\n handleChange(\"documentationUrl\", e.target.value)\n }\n placeholder=\"https://docs.example.com\"\n />\n\n <InputField\n label={tr(\"devportal.supportUrl\", \"Support URL\")}\n type=\"url\"\n value={formData.supportUrl}\n onChange={(e) => handleChange(\"supportUrl\", e.target.value)}\n placeholder=\"https://support.example.com\"\n />\n\n <InputField\n label={tr(\"devportal.githubUrl\", \"GitHub URL\")}\n type=\"url\"\n value={formData.githubUrl}\n onChange={(e) => handleChange(\"githubUrl\", e.target.value)}\n placeholder=\"https://github.com/org/repo\"\n />\n\n <InputField\n label={tr(\"devportal.privacyPolicyUrl\", \"Privacy Policy URL\")}\n type=\"url\"\n value={formData.privacyPolicyUrl}\n onChange={(e) =>\n handleChange(\"privacyPolicyUrl\", e.target.value)\n }\n placeholder=\"https://example.com/privacy\"\n />\n\n <InputField\n label={tr(\n \"devportal.termsOfServiceUrl\",\n \"Terms of Service URL\",\n )}\n type=\"url\"\n value={formData.termsOfServiceUrl}\n onChange={(e) =>\n handleChange(\"termsOfServiceUrl\", e.target.value)\n }\n placeholder=\"https://example.com/terms\"\n />\n </div>\n </PageSection>\n\n {/* Settings */}\n <PageSection\n title={tr(\"devportal.settings\", \"Settings\")}\n description={tr(\n \"devportal.settingsDescription\",\n \"Additional configuration options\",\n )}\n >\n <div className=\"space-y-4\">\n <CheckboxField\n label={tr(\n \"devportal.requireWorkspaceAdminApproval\",\n \"Require workspace admin approval\",\n )}\n description={tr(\n \"devportal.requireWorkspaceAdminApprovalDescription\",\n \"When enabled, workspace admins must approve installations before users can access this app\",\n )}\n checked={formData.requiresApproval}\n onChange={(e) =>\n handleChange(\"requiresApproval\", e.target.checked)\n }\n />\n </div>\n\n {/* Info banner */}\n <div className=\"rounded-lg border border-border-subtle bg-status-info-bg-subtle p-4 flex gap-3\">\n <Info\n size={20}\n className=\"text-accent-blue flex-shrink-0 mt-0.5\"\n />\n <div className=\"text-sm text-status-info-text\">\n <p className=\"font-medium\">\n {tr(\"devportal.whatHappensNext\", \"What happens next?\")}\n </p>\n <ul className=\"mt-1 space-y-1 text-status-info-text\">\n <li>\n {tr(\n \"devportal.appCreatedInDraft\",\n \"Your app will be created in\",\n )}{\" \"}\n <strong>{tr(\"devportal.draft\", \"Draft\")}</strong>{\" \"}\n {tr(\"devportal.status\", \"status\")}\n </li>\n <li>\n {tr(\n \"devportal.canConfigureOAuthAndWebhooks\",\n \"You can configure OAuth clients, API keys, and webhooks\",\n )}\n </li>\n <li>\n {tr(\n \"devportal.submitForReviewWhenReady\",\n \"Submit for review when ready to publish to the marketplace\",\n )}\n </li>\n </ul>\n </div>\n </div>\n </PageSection>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isSubmitting}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50\"\n >\n <X size={16} />\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n {isSubmitting ? (\n <>\n <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n {tr(\"devportal.creating\", \"Creating...\")}\n </>\n ) : (\n <>\n <Save size={16} />\n {tr(\"devportal.createApplication\", \"Create Application\")}\n </>\n )}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateApplicationPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApplicationPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Info } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n InputField,\n TextareaField,\n SelectField,\n TagInput,\n CheckboxField,\n} from \"../components/FormField\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalEventBus } from \"../events\";\nimport type { AppType, AppVisibility } from \"../types\";\nimport { APP_TYPE_LABELS } from \"../utils/constants\";\nimport {\n useCreateApplicationMutation,\n useGetMyOrganizationsQuery,\n} from \"../../generated/global-operations\";\nimport { useCurrentUserAppsQuery } from \"../hooks\";\n\ninterface CreateApplicationFormData {\n name: string;\n slug: string;\n shortDescription: string;\n description: string;\n appType: AppType | \"\";\n visibility: AppVisibility;\n category: string;\n tags: string[];\n keywords: string[];\n websiteUrl: string;\n documentationUrl: string;\n supportUrl: string;\n privacyPolicyUrl: string;\n termsOfServiceUrl: string;\n githubUrl: string;\n developerOrganizationId: string;\n appId: string;\n requiresApproval: boolean;\n}\n\ninterface FormErrors {\n name?: string;\n slug?: string;\n shortDescription?: string;\n appType?: string;\n category?: string;\n}\n\nconst APP_TYPE_OPTIONS = Object.entries(APP_TYPE_LABELS).map(\n ([value, label]) => ({\n value,\n label,\n }),\n);\n\nconst VISIBILITY_OPTIONS = [\n { value: \"PUBLIC\", label: \"Public - Visible to everyone\" },\n { value: \"PRIVATE\", label: \"Private - Only visible to you\" },\n { value: \"UNLISTED\", label: \"Unlisted - Accessible via direct link\" },\n { value: \"INTERNAL\", label: \"Internal - Only visible within organization\" },\n];\n\nconst CATEGORY_OPTIONS = [\n { value: \"productivity\", label: \"Productivity\" },\n { value: \"communication\", label: \"Communication\" },\n { value: \"analytics\", label: \"Analytics\" },\n { value: \"automation\", label: \"Automation\" },\n { value: \"developer-tools\", label: \"Developer Tools\" },\n { value: \"finance\", label: \"Finance\" },\n { value: \"hr\", label: \"HR & People\" },\n { value: \"marketing\", label: \"Marketing\" },\n { value: \"project-management\", label: \"Project Management\" },\n { value: \"security\", label: \"Security\" },\n { value: \"support\", label: \"Support\" },\n { value: \"other\", label: \"Other\" },\n];\n\n/**\n * Create new application page\n */\nexport const CreateApplicationPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [errors, setErrors] = useState<FormErrors>({});\n const [createApplication, { loading: isSubmitting }] =\n useCreateApplicationMutation();\n const bus = useDevportalEventBus();\n\n // Fetch real organizations\n const { data: orgsData } = useGetMyOrganizationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n const { data: authAppsData, loading: authAppsLoading } =\n useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const organizationOptions = [\n { value: \"\", label: \"Personal Account\" },\n ...(orgsData?.myDeveloperOrganizations ?? []).map(\n (org: { id: string; name: string }) => ({\n value: org.id,\n label: org.name,\n }),\n ),\n ];\n\n const authAppOptions = [\n { value: \"\", label: tr(\"devportal.noLinkedAuthApp\", \"Link later\") },\n ...((authAppsData?.currentUserApps?.apps ?? []).map(\n (authApp: {\n id: string;\n name: string;\n type: string;\n clientId: string;\n }) => ({\n value: authApp.id,\n label: `${authApp.name} · ${authApp.type} · ${authApp.clientId}`,\n }),\n ) ?? []),\n ];\n\n const [formData, setFormData] = useState<CreateApplicationFormData>({\n name: \"\",\n slug: \"\",\n shortDescription: \"\",\n description: \"\",\n appType: \"\",\n visibility: \"PRIVATE\",\n category: \"\",\n tags: [],\n keywords: [],\n websiteUrl: \"\",\n documentationUrl: \"\",\n supportUrl: \"\",\n privacyPolicyUrl: \"\",\n termsOfServiceUrl: \"\",\n githubUrl: \"\",\n developerOrganizationId: \"\",\n appId: \"\",\n requiresApproval: false,\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const generateSlug = (name: string): string => {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n };\n\n const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const name = e.target.value;\n setFormData((prev) => ({\n ...prev,\n name,\n slug: prev.slug || generateSlug(name),\n }));\n if (errors.name) {\n setErrors((prev) => ({ ...prev, name: undefined }));\n }\n };\n\n const handleChange = (\n field: keyof CreateApplicationFormData,\n value: string | string[] | boolean,\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field as keyof FormErrors]) {\n setErrors((prev) => ({ ...prev, [field]: undefined }));\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: FormErrors = {};\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.appNameRequired\",\n \"Application name is required\",\n );\n } else if (formData.name.length < 3) {\n newErrors.name = tr(\n \"devportal.nameTooShort\",\n \"Name must be at least 3 characters\",\n );\n } else if (formData.name.length > 100) {\n newErrors.name = tr(\n \"devportal.nameTooLong\",\n \"Name must be less than 100 characters\",\n );\n }\n\n if (!formData.slug.trim()) {\n newErrors.slug = tr(\"devportal.slugRequired\", \"Slug is required\");\n } else if (!/^[a-z0-9-]+$/.test(formData.slug)) {\n newErrors.slug = tr(\n \"devportal.slugInvalid\",\n \"Slug can only contain lowercase letters, numbers, and hyphens\",\n );\n }\n\n if (!formData.shortDescription.trim()) {\n newErrors.shortDescription = tr(\n \"devportal.shortDescriptionRequired\",\n \"Short description is required\",\n );\n } else if (formData.shortDescription.length > 200) {\n newErrors.shortDescription = tr(\n \"devportal.shortDescriptionTooLong\",\n \"Short description must be less than 200 characters\",\n );\n }\n\n if (!formData.appType) {\n newErrors.appType = tr(\n \"devportal.appTypeRequired\",\n \"Application type is required\",\n );\n }\n\n if (!formData.category) {\n newErrors.category = tr(\n \"devportal.categoryRequired\",\n \"Category is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) {\n return;\n }\n\n try {\n const result = await createApplication({\n variables: {\n input: {\n name: formData.name,\n slug: formData.slug || undefined,\n shortDescription: formData.shortDescription || undefined,\n description: formData.description || undefined,\n appType: formData.appType as AppType,\n visibility: formData.visibility || undefined,\n category: formData.category || undefined,\n tags: formData.tags.length > 0 ? formData.tags : undefined,\n keywords:\n formData.keywords.length > 0 ? formData.keywords : undefined,\n websiteUrl: formData.websiteUrl || undefined,\n documentationUrl: formData.documentationUrl || undefined,\n supportUrl: formData.supportUrl || undefined,\n privacyPolicyUrl: formData.privacyPolicyUrl || undefined,\n termsOfServiceUrl: formData.termsOfServiceUrl || undefined,\n developerOrganizationId:\n formData.developerOrganizationId || undefined,\n appId: formData.appId || undefined,\n githubUrl: formData.githubUrl || undefined,\n requiresApproval: formData.requiresApproval,\n iconUrl: undefined,\n },\n },\n });\n\n if (result.data?.createApplication?.id) {\n bus.emit(\"devportal.app.created\", {\n appId: result.data.createApplication.id,\n name: formData.name,\n appType: formData.appType,\n organizationId: formData.developerOrganizationId || null,\n });\n handleNavigate(`/apps/${result.data.createApplication.id}`);\n }\n } catch (error) {\n console.error(\"Failed to create application:\", error);\n }\n };\n\n const handleCancel = () => {\n handleNavigate(\"/apps\");\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_CREATE}>\n <PageLayout\n title={tr(\"devportal.createApplication\", \"Create Application\")}\n description={tr(\n \"devportal.createApplicationDescription\",\n \"Register a new application in the developer portal\",\n )}\n backLink={`${basePath}/apps`}\n backLabel={tr(\"devportal.backToApplications\", \"Back to Applications\")}\n >\n <form onSubmit={handleSubmit} className=\"space-y-8 max-w-3xl\">\n {/* Basic Information */}\n <PageSection\n title={tr(\"devportal.basicInformation\", \"Basic Information\")}\n description={tr(\n \"devportal.basicInformationDescription\",\n \"Core details about your application\",\n )}\n >\n <div className=\"space-y-4\">\n <InputField\n label={tr(\"devportal.applicationName\", \"Application Name\")}\n value={formData.name}\n onChange={handleNameChange}\n placeholder={tr(\n \"devportal.applicationNamePlaceholder\",\n \"My Awesome App\",\n )}\n required\n error={errors.name}\n hint={tr(\n \"devportal.applicationNameHint\",\n \"Choose a memorable name for your application\",\n )}\n />\n\n <InputField\n label={tr(\"devportal.slug\", \"Slug\")}\n value={formData.slug}\n onChange={(e) =>\n handleChange(\"slug\", e.target.value.toLowerCase())\n }\n placeholder={tr(\"devportal.slugPlaceholder\", \"my-awesome-app\")}\n required\n error={errors.slug}\n hint={tr(\n \"devportal.slugHint\",\n \"URL-friendly identifier (auto-generated from name)\",\n )}\n />\n\n <TextareaField\n label={tr(\"devportal.shortDescription\", \"Short Description\")}\n value={formData.shortDescription}\n onChange={(e) =>\n handleChange(\"shortDescription\", e.target.value)\n }\n placeholder={tr(\n \"devportal.shortDescriptionPlaceholder\",\n \"A brief description of what your app does\",\n )}\n required\n error={errors.shortDescription}\n hint={`${formData.shortDescription.length}/200 ${tr(\"devportal.characters\", \"characters\")}`}\n rows={2}\n />\n\n <TextareaField\n label={tr(\"devportal.fullDescription\", \"Full Description\")}\n value={formData.description}\n onChange={(e) => handleChange(\"description\", e.target.value)}\n placeholder={tr(\n \"devportal.fullDescriptionPlaceholder\",\n \"Detailed description of your application, features, and use cases...\",\n )}\n rows={6}\n hint={tr(\n \"devportal.markdownSupported\",\n \"Markdown formatting is supported\",\n )}\n />\n </div>\n </PageSection>\n\n {/* Classification */}\n <PageSection\n title={tr(\"devportal.classification\", \"Classification\")}\n description={tr(\n \"devportal.classificationDescription\",\n \"Categorize your application\",\n )}\n >\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <SelectField\n label={tr(\"devportal.applicationType\", \"Application Type\")}\n value={formData.appType}\n onChange={(e) => handleChange(\"appType\", e.target.value)}\n options={APP_TYPE_OPTIONS}\n required\n error={errors.appType}\n placeholder={tr(\"devportal.selectAppType\", \"Select app type\")}\n />\n\n <SelectField\n label={tr(\"devportal.category\", \"Category\")}\n value={formData.category}\n onChange={(e) => handleChange(\"category\", e.target.value)}\n options={CATEGORY_OPTIONS}\n required\n error={errors.category}\n placeholder={tr(\"devportal.selectCategory\", \"Select category\")}\n />\n\n <SelectField\n label={tr(\"devportal.visibility\", \"Visibility\")}\n value={formData.visibility}\n onChange={(e) =>\n handleChange(\"visibility\", e.target.value as AppVisibility)\n }\n options={VISIBILITY_OPTIONS}\n />\n\n <SelectField\n label={tr(\n \"devportal.developerOrganization\",\n \"Developer Organization\",\n )}\n value={formData.developerOrganizationId}\n onChange={(e) =>\n handleChange(\"developerOrganizationId\", e.target.value)\n }\n options={organizationOptions}\n hint={tr(\n \"devportal.developerOrganizationHint\",\n \"Optional: Associate with an organization\",\n )}\n />\n </div>\n\n <TagInput\n label={tr(\"devportal.tags\", \"Tags\")}\n value={formData.tags}\n onChange={(tags) => handleChange(\"tags\", tags)}\n placeholder={tr(\n \"devportal.addTagsPlaceholder\",\n \"Add tags (press Enter)\",\n )}\n hint={tr(\"devportal.tagsHint\", \"Help users discover your app\")}\n />\n\n <TagInput\n label={tr(\"devportal.keywords\", \"Keywords\")}\n value={formData.keywords}\n onChange={(keywords) => handleChange(\"keywords\", keywords)}\n placeholder={tr(\n \"devportal.addKeywordsPlaceholder\",\n \"Add keywords (press Enter)\",\n )}\n hint={tr(\"devportal.keywordsHint\", \"Improve search visibility\")}\n />\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.authenticationAndAuthorization\",\n \"Authentication & Authorization\",\n )}\n description={tr(\n \"devportal.authenticationAndAuthorizationDescription\",\n \"Link this marketplace app to an auth app for OAuth, client credentials, API keys, delegation, and device flows.\",\n )}\n >\n <div className=\"space-y-4\">\n <SelectField\n label={tr(\"devportal.linkedAuthApp\", \"Linked Auth App\")}\n value={formData.appId}\n onChange={(e) => handleChange(\"appId\", e.target.value)}\n options={authAppOptions}\n hint={tr(\n \"devportal.linkedAuthAppHint\",\n \"Select an existing auth app or create one first. You can still configure scopes, client credentials, app-as-user delegation, and device code in the OAuth Clients area.\",\n )}\n placeholder={\n authAppsLoading\n ? tr(\"devportal.loadingAuthApps\", \"Loading auth apps...\")\n : tr(\"devportal.selectAuthApp\", \"Select auth app\")\n }\n />\n\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <Save size={14} />\n {tr(\"devportal.createAuthAppFirst\", \"Create Auth App\")}\n </button>\n </div>\n </PageSection>\n\n {/* URLs */}\n <PageSection\n title={tr(\"devportal.links\", \"Links\")}\n description={tr(\n \"devportal.linksDescription\",\n \"External resources for your application\",\n )}\n >\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <InputField\n label={tr(\"devportal.websiteUrl\", \"Website URL\")}\n type=\"url\"\n value={formData.websiteUrl}\n onChange={(e) => handleChange(\"websiteUrl\", e.target.value)}\n placeholder=\"https://example.com\"\n />\n\n <InputField\n label={tr(\"devportal.documentationUrl\", \"Documentation URL\")}\n type=\"url\"\n value={formData.documentationUrl}\n onChange={(e) =>\n handleChange(\"documentationUrl\", e.target.value)\n }\n placeholder=\"https://docs.example.com\"\n />\n\n <InputField\n label={tr(\"devportal.supportUrl\", \"Support URL\")}\n type=\"url\"\n value={formData.supportUrl}\n onChange={(e) => handleChange(\"supportUrl\", e.target.value)}\n placeholder=\"https://support.example.com\"\n />\n\n <InputField\n label={tr(\"devportal.githubUrl\", \"GitHub URL\")}\n type=\"url\"\n value={formData.githubUrl}\n onChange={(e) => handleChange(\"githubUrl\", e.target.value)}\n placeholder=\"https://github.com/org/repo\"\n />\n\n <InputField\n label={tr(\"devportal.privacyPolicyUrl\", \"Privacy Policy URL\")}\n type=\"url\"\n value={formData.privacyPolicyUrl}\n onChange={(e) =>\n handleChange(\"privacyPolicyUrl\", e.target.value)\n }\n placeholder=\"https://example.com/privacy\"\n />\n\n <InputField\n label={tr(\n \"devportal.termsOfServiceUrl\",\n \"Terms of Service URL\",\n )}\n type=\"url\"\n value={formData.termsOfServiceUrl}\n onChange={(e) =>\n handleChange(\"termsOfServiceUrl\", e.target.value)\n }\n placeholder=\"https://example.com/terms\"\n />\n </div>\n </PageSection>\n\n {/* Settings */}\n <PageSection\n title={tr(\"devportal.settings\", \"Settings\")}\n description={tr(\n \"devportal.settingsDescription\",\n \"Additional configuration options\",\n )}\n >\n <div className=\"space-y-4\">\n <CheckboxField\n label={tr(\n \"devportal.requireWorkspaceAdminApproval\",\n \"Require workspace admin approval\",\n )}\n description={tr(\n \"devportal.requireWorkspaceAdminApprovalDescription\",\n \"When enabled, workspace admins must approve installations before users can access this app\",\n )}\n checked={formData.requiresApproval}\n onChange={(e) =>\n handleChange(\"requiresApproval\", e.target.checked)\n }\n />\n </div>\n\n {/* Info banner */}\n <div className=\"rounded-lg border border-border-subtle bg-status-info-bg-subtle p-4 flex gap-3\">\n <Info\n size={20}\n className=\"text-accent-blue flex-shrink-0 mt-0.5\"\n />\n <div className=\"text-sm text-status-info-text\">\n <p className=\"font-medium\">\n {tr(\"devportal.whatHappensNext\", \"What happens next?\")}\n </p>\n <ul className=\"mt-1 space-y-1 text-status-info-text\">\n <li>\n {tr(\n \"devportal.appCreatedInDraft\",\n \"Your app will be created in\",\n )}{\" \"}\n <strong>{tr(\"devportal.draft\", \"Draft\")}</strong>{\" \"}\n {tr(\"devportal.status\", \"status\")}\n </li>\n <li>\n {tr(\n \"devportal.canConfigureOAuthAndWebhooks\",\n \"You can configure OAuth clients, API keys, and webhooks\",\n )}\n </li>\n <li>\n {tr(\n \"devportal.submitForReviewWhenReady\",\n \"Submit for review when ready to publish to the marketplace\",\n )}\n </li>\n </ul>\n </div>\n </div>\n </PageSection>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isSubmitting}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50\"\n >\n <X size={16} />\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n {isSubmitting ? (\n <>\n <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n {tr(\"devportal.creating\", \"Creating...\")}\n </>\n ) : (\n <>\n <Save size={16} />\n {tr(\"devportal.createApplication\", \"Create Application\")}\n </>\n )}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|