@burdenoff/microfe-devportal 2026.713.2 → 2026.713.4
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/pages/AdminReviewPage.js +73 -59
- package/dist/devportal/pages/AdminReviewPage.js.map +1 -1
- package/dist/devportal/pages/ApplicationVersionsPage.js +379 -361
- package/dist/devportal/pages/ApplicationVersionsPage.js.map +1 -1
- package/dist/devportal/pages/StoreSubmissionPage.js +254 -206
- package/dist/devportal/pages/StoreSubmissionPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,15 +24,15 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
24
24
|
className: "mt-2 max-h-48 overflow-auto rounded-md bg-bg-sunken p-3 text-xs font-mono whitespace-pre-wrap break-all",
|
|
25
25
|
children: JSON.stringify(e, null, 2)
|
|
26
26
|
})] });
|
|
27
|
-
}, M = ({ app: t, onApprove: n, onReject: r, processing: a }) => {
|
|
28
|
-
let { navigate:
|
|
29
|
-
let n =
|
|
27
|
+
}, M = ({ app: t, onApprove: n, onReject: r, processing: a, hasPendingVersions: o }) => {
|
|
28
|
+
let { navigate: s, basePath: c } = e(), { t: l } = g(), u = (e, t) => {
|
|
29
|
+
let n = l(e);
|
|
30
30
|
return n === e ? t : n;
|
|
31
|
-
}, [
|
|
31
|
+
}, [d, f] = h(!1), [_, x] = h(""), w = m(p.APP_APPROVE), D = m(p.APP_REJECT), { data: M } = i({
|
|
32
32
|
variables: { id: t.id },
|
|
33
|
-
skip: !
|
|
33
|
+
skip: !d,
|
|
34
34
|
fetchPolicy: "cache-first"
|
|
35
|
-
}),
|
|
35
|
+
}), N = M?.application?.versions?.edges?.[0]?.node;
|
|
36
36
|
return /* @__PURE__ */ A("div", {
|
|
37
37
|
className: "rounded-card border border-border-seam bg-bg-surface overflow-hidden",
|
|
38
38
|
children: [/* @__PURE__ */ A("div", {
|
|
@@ -71,19 +71,19 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
71
71
|
}),
|
|
72
72
|
/* @__PURE__ */ k("p", {
|
|
73
73
|
className: "text-sm text-text-secondary truncate",
|
|
74
|
-
children: t.shortDescription ?? t.description ??
|
|
74
|
+
children: t.shortDescription ?? t.description ?? u("devportal.noDescription", "No description")
|
|
75
75
|
}),
|
|
76
76
|
/* @__PURE__ */ A("p", {
|
|
77
77
|
className: "text-xs text-text-secondary mt-0.5",
|
|
78
78
|
children: [
|
|
79
|
-
|
|
79
|
+
u("devportal.submitted", "Submitted"),
|
|
80
80
|
":",
|
|
81
81
|
" ",
|
|
82
82
|
t.submittedAt ? new Date(t.submittedAt).toLocaleDateString(void 0, {
|
|
83
83
|
year: "numeric",
|
|
84
84
|
month: "short",
|
|
85
85
|
day: "numeric"
|
|
86
|
-
}) :
|
|
86
|
+
}) : u("devportal.unknown", "Unknown")
|
|
87
87
|
]
|
|
88
88
|
})
|
|
89
89
|
]
|
|
@@ -91,25 +91,38 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
91
91
|
/* @__PURE__ */ A("div", {
|
|
92
92
|
className: "flex items-center gap-2 flex-shrink-0",
|
|
93
93
|
children: [/* @__PURE__ */ A("button", {
|
|
94
|
-
onClick: () =>
|
|
94
|
+
onClick: () => s?.(`${c}/apps/${t.id}`),
|
|
95
95
|
className: "inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken",
|
|
96
|
-
title:
|
|
97
|
-
children: [/* @__PURE__ */ k(C, { size: 14 }),
|
|
96
|
+
title: u("devportal.viewAppDetails", "View app details"),
|
|
97
|
+
children: [/* @__PURE__ */ k(C, { size: 14 }), u("devportal.view", "View")]
|
|
98
98
|
}), /* @__PURE__ */ A("button", {
|
|
99
|
-
onClick: () =>
|
|
99
|
+
onClick: () => f((e) => !e),
|
|
100
100
|
className: "inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken",
|
|
101
|
-
children: [k(
|
|
101
|
+
children: [k(d ? b : y, { size: 14 }), u("devportal.review", "Review")]
|
|
102
102
|
})]
|
|
103
103
|
})
|
|
104
104
|
]
|
|
105
|
-
}),
|
|
105
|
+
}), d && /* @__PURE__ */ A("div", {
|
|
106
106
|
className: "border-t bg-bg-sunken/30 p-4 space-y-4",
|
|
107
107
|
children: [
|
|
108
|
+
o && /* @__PURE__ */ A("div", {
|
|
109
|
+
className: "flex items-start gap-2 rounded-md border border-status-warning-bg bg-status-warning-bg-subtle px-3 py-2.5 text-xs text-status-warning-text",
|
|
110
|
+
children: [/* @__PURE__ */ k(T, {
|
|
111
|
+
size: 14,
|
|
112
|
+
className: "mt-0.5 shrink-0",
|
|
113
|
+
"aria-hidden": "true"
|
|
114
|
+
}), /* @__PURE__ */ A("span", { children: [
|
|
115
|
+
"This app has a pending version. Approve the version in the",
|
|
116
|
+
" ",
|
|
117
|
+
/* @__PURE__ */ k("strong", { children: "Version Updates" }),
|
|
118
|
+
" section below before approving the app — the version must be live before the app can go public."
|
|
119
|
+
] })]
|
|
120
|
+
}),
|
|
108
121
|
t.reviewNotes && /* @__PURE__ */ A("div", {
|
|
109
122
|
className: "rounded-md border bg-bg-surface p-3",
|
|
110
123
|
children: [/* @__PURE__ */ k("p", {
|
|
111
124
|
className: "text-xs font-medium text-text-secondary mb-1",
|
|
112
|
-
children:
|
|
125
|
+
children: u("devportal.developerNotes", "Developer Notes")
|
|
113
126
|
}), /* @__PURE__ */ k("p", {
|
|
114
127
|
className: "text-sm",
|
|
115
128
|
children: t.reviewNotes
|
|
@@ -121,7 +134,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
121
134
|
/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ A("span", {
|
|
122
135
|
className: "text-text-secondary",
|
|
123
136
|
children: [
|
|
124
|
-
|
|
137
|
+
u("devportal.category", "Category"),
|
|
125
138
|
":",
|
|
126
139
|
" "
|
|
127
140
|
]
|
|
@@ -129,7 +142,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
129
142
|
/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ A("span", {
|
|
130
143
|
className: "text-text-secondary",
|
|
131
144
|
children: [
|
|
132
|
-
|
|
145
|
+
u("devportal.pricing", "Pricing"),
|
|
133
146
|
":",
|
|
134
147
|
" "
|
|
135
148
|
]
|
|
@@ -137,7 +150,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
137
150
|
/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ A("span", {
|
|
138
151
|
className: "text-text-secondary",
|
|
139
152
|
children: [
|
|
140
|
-
|
|
153
|
+
u("devportal.website", "Website"),
|
|
141
154
|
":",
|
|
142
155
|
" "
|
|
143
156
|
]
|
|
@@ -146,12 +159,12 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
146
159
|
target: "_blank",
|
|
147
160
|
rel: "noopener noreferrer",
|
|
148
161
|
className: "text-primary underline hover:no-underline",
|
|
149
|
-
children:
|
|
162
|
+
children: u("devportal.link", "Link")
|
|
150
163
|
}) : /* @__PURE__ */ k("span", { children: "—" })] }),
|
|
151
164
|
/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ A("span", {
|
|
152
165
|
className: "text-text-secondary",
|
|
153
166
|
children: [
|
|
154
|
-
|
|
167
|
+
u("devportal.privacyPolicy", "Privacy Policy"),
|
|
155
168
|
":",
|
|
156
169
|
" "
|
|
157
170
|
]
|
|
@@ -160,73 +173,73 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
160
173
|
target: "_blank",
|
|
161
174
|
rel: "noopener noreferrer",
|
|
162
175
|
className: "text-primary underline hover:no-underline",
|
|
163
|
-
children:
|
|
176
|
+
children: u("devportal.link", "Link")
|
|
164
177
|
}) : /* @__PURE__ */ k("span", {
|
|
165
178
|
className: "text-status-error-text",
|
|
166
|
-
children:
|
|
179
|
+
children: u("devportal.missing", "Missing")
|
|
167
180
|
})] })
|
|
168
181
|
]
|
|
169
182
|
}),
|
|
170
|
-
|
|
183
|
+
N && /* @__PURE__ */ A("div", {
|
|
171
184
|
className: "rounded-md border bg-bg-surface p-3 space-y-3",
|
|
172
185
|
children: [
|
|
173
186
|
/* @__PURE__ */ A("p", {
|
|
174
187
|
className: "text-xs font-medium text-text-secondary",
|
|
175
188
|
children: [
|
|
176
|
-
|
|
189
|
+
u("devportal.latestVersion", "Latest Version"),
|
|
177
190
|
":",
|
|
178
191
|
" ",
|
|
179
192
|
/* @__PURE__ */ k("span", {
|
|
180
193
|
className: "font-mono text-text-primary",
|
|
181
|
-
children:
|
|
194
|
+
children: N.version
|
|
182
195
|
})
|
|
183
196
|
]
|
|
184
197
|
}),
|
|
185
198
|
/* @__PURE__ */ k("div", {
|
|
186
199
|
className: "flex flex-wrap gap-2",
|
|
187
|
-
children:
|
|
188
|
-
href:
|
|
200
|
+
children: N.packageUrl && /* @__PURE__ */ A("a", {
|
|
201
|
+
href: N.packageUrl,
|
|
189
202
|
download: !0,
|
|
190
203
|
target: "_blank",
|
|
191
204
|
rel: "noopener noreferrer",
|
|
192
205
|
className: "inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken",
|
|
193
206
|
children: [
|
|
194
207
|
/* @__PURE__ */ k(S, { size: 14 }),
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
u("devportal.downloadPackage", "Download Package"),
|
|
209
|
+
N.packageSize && /* @__PURE__ */ A("span", {
|
|
197
210
|
className: "text-text-secondary text-xs ml-1",
|
|
198
211
|
children: [
|
|
199
212
|
"(",
|
|
200
|
-
|
|
213
|
+
N.packageSize,
|
|
201
214
|
")"
|
|
202
215
|
]
|
|
203
216
|
})
|
|
204
217
|
]
|
|
205
218
|
})
|
|
206
219
|
}),
|
|
207
|
-
|
|
208
|
-
manifest:
|
|
209
|
-
tr:
|
|
220
|
+
N.manifest && /* @__PURE__ */ k(j, {
|
|
221
|
+
manifest: N.manifest,
|
|
222
|
+
tr: u
|
|
210
223
|
})
|
|
211
224
|
]
|
|
212
225
|
}),
|
|
213
226
|
/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ k("label", {
|
|
214
227
|
className: "text-sm font-medium",
|
|
215
|
-
children:
|
|
228
|
+
children: u("devportal.reviewNotes", "Review Notes")
|
|
216
229
|
}), /* @__PURE__ */ k("textarea", {
|
|
217
|
-
value:
|
|
218
|
-
onChange: (e) =>
|
|
219
|
-
placeholder:
|
|
230
|
+
value: _,
|
|
231
|
+
onChange: (e) => x(e.target.value),
|
|
232
|
+
placeholder: u("devportal.reviewNotesPlaceholder", "Add feedback for the developer (required for rejection)..."),
|
|
220
233
|
rows: 3,
|
|
221
234
|
className: "mt-1 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none"
|
|
222
235
|
})] }),
|
|
223
236
|
/* @__PURE__ */ A("div", {
|
|
224
237
|
className: "flex gap-3",
|
|
225
238
|
children: [/* @__PURE__ */ A("button", {
|
|
226
|
-
onClick: () => n(t.id,
|
|
227
|
-
disabled: a || !
|
|
228
|
-
"aria-disabled": a || !
|
|
229
|
-
title:
|
|
239
|
+
onClick: () => n(t.id, _),
|
|
240
|
+
disabled: a || !w.allowed,
|
|
241
|
+
"aria-disabled": a || !w.allowed,
|
|
242
|
+
title: w.allowed ? void 0 : u("devportal.common.noPermission", "You do not have permission to perform this action"),
|
|
230
243
|
className: "inline-flex items-center gap-2 rounded-md bg-status-success-bg px-4 py-2 text-sm font-medium text-white hover:bg-status-success-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
|
|
231
244
|
children: [a ? /* @__PURE__ */ k(E, {
|
|
232
245
|
size: 14,
|
|
@@ -235,13 +248,13 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
235
248
|
}) : /* @__PURE__ */ k(v, {
|
|
236
249
|
size: 14,
|
|
237
250
|
"aria-hidden": "true"
|
|
238
|
-
}),
|
|
251
|
+
}), u("devportal.approve", "Approve")]
|
|
239
252
|
}), /* @__PURE__ */ A("button", {
|
|
240
|
-
onClick: () => r(t.id,
|
|
241
|
-
disabled: a || !
|
|
242
|
-
"aria-disabled": a || !
|
|
253
|
+
onClick: () => r(t.id, _),
|
|
254
|
+
disabled: a || !_.trim() || !D.allowed,
|
|
255
|
+
"aria-disabled": a || !_.trim() || !D.allowed,
|
|
243
256
|
className: "inline-flex items-center gap-2 rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
|
|
244
|
-
title:
|
|
257
|
+
title: D.allowed ? _.trim() ? void 0 : u("devportal.notesRequiredForRejection", "Notes required for rejection") : u("devportal.common.noPermission", "You do not have permission to perform this action"),
|
|
245
258
|
children: [a ? /* @__PURE__ */ k(E, {
|
|
246
259
|
size: 14,
|
|
247
260
|
className: "animate-spin",
|
|
@@ -249,7 +262,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
249
262
|
}) : /* @__PURE__ */ k(O, {
|
|
250
263
|
size: 14,
|
|
251
264
|
"aria-hidden": "true"
|
|
252
|
-
}),
|
|
265
|
+
}), u("devportal.reject", "Reject")]
|
|
253
266
|
})]
|
|
254
267
|
})
|
|
255
268
|
]
|
|
@@ -440,7 +453,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
440
453
|
}), { data: W, refetch: G } = o({
|
|
441
454
|
variables: { limit: 100 },
|
|
442
455
|
fetchPolicy: "cache-and-network"
|
|
443
|
-
}), [K] = n(), [q] = s(), [J] = r(), [Y] = c(), X = B?.applications?.edges?.map((e) => e.node) ?? [], Z = W?.applicationsWithPendingVersions?.edges?.map((e) => e.node) ?? [], Q = m ? X.filter((e) => e.name.toLowerCase().includes(m.toLowerCase()) || (e.shortDescription ?? "").toLowerCase().includes(m.toLowerCase())) : X,
|
|
456
|
+
}), [K] = n(), [q] = s(), [J] = r(), [Y] = c(), X = B?.applications?.edges?.map((e) => e.node) ?? [], Z = W?.applicationsWithPendingVersions?.edges?.map((e) => e.node) ?? [], Q = new Set(Z.map((e) => e.id)), $ = m ? X.filter((e) => e.name.toLowerCase().includes(m.toLowerCase()) || (e.shortDescription ?? "").toLowerCase().includes(m.toLowerCase())) : X, ee = async (e, t) => {
|
|
444
457
|
I(e), j(null), F(null);
|
|
445
458
|
try {
|
|
446
459
|
let n = await K({ variables: {
|
|
@@ -454,7 +467,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
454
467
|
} finally {
|
|
455
468
|
L(e);
|
|
456
469
|
}
|
|
457
|
-
},
|
|
470
|
+
}, te = async (e, t) => {
|
|
458
471
|
if (t.trim()) {
|
|
459
472
|
I(e), j(null), F(null);
|
|
460
473
|
try {
|
|
@@ -470,7 +483,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
470
483
|
L(e);
|
|
471
484
|
}
|
|
472
485
|
}
|
|
473
|
-
},
|
|
486
|
+
}, ne = async (e, t, n) => {
|
|
474
487
|
R(e), j(null), F(null);
|
|
475
488
|
try {
|
|
476
489
|
let t = await J({ variables: {
|
|
@@ -484,7 +497,7 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
484
497
|
} finally {
|
|
485
498
|
z(e);
|
|
486
499
|
}
|
|
487
|
-
},
|
|
500
|
+
}, re = async (e, t, n) => {
|
|
488
501
|
if (n.trim()) {
|
|
489
502
|
R(e), j(null), F(null);
|
|
490
503
|
try {
|
|
@@ -603,18 +616,19 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
603
616
|
}),
|
|
604
617
|
/* @__PURE__ */ k(d, {
|
|
605
618
|
title: i("devportal.newAppSubmissions", "New App Submissions"),
|
|
606
|
-
children:
|
|
619
|
+
children: $.length === 0 ? /* @__PURE__ */ k(t, {
|
|
607
620
|
illustration: "empty-data",
|
|
608
621
|
icon: /* @__PURE__ */ k(v, { size: 32 }),
|
|
609
622
|
title: i("devportal.noAppsPendingReview", "No apps pending review"),
|
|
610
623
|
description: m ? i("devportal.noAppsMatchSearch", "No apps match your search") : i("devportal.allApplicationsReviewed", "All submitted applications have been reviewed")
|
|
611
624
|
}) : /* @__PURE__ */ k("div", {
|
|
612
625
|
className: "space-y-3",
|
|
613
|
-
children:
|
|
626
|
+
children: $.map((e) => /* @__PURE__ */ k(M, {
|
|
614
627
|
app: e,
|
|
615
|
-
onApprove:
|
|
616
|
-
onReject:
|
|
617
|
-
processing: b.has(e.id)
|
|
628
|
+
onApprove: ee,
|
|
629
|
+
onReject: te,
|
|
630
|
+
processing: b.has(e.id),
|
|
631
|
+
hasPendingVersions: Q.has(e.id)
|
|
618
632
|
}, e.id))
|
|
619
633
|
})
|
|
620
634
|
}),
|
|
@@ -629,8 +643,8 @@ var j = ({ manifest: e, tr: t }) => {
|
|
|
629
643
|
className: "space-y-3",
|
|
630
644
|
children: Z.map((e) => /* @__PURE__ */ k(N, {
|
|
631
645
|
app: e,
|
|
632
|
-
onApproveVersion:
|
|
633
|
-
onRejectVersion:
|
|
646
|
+
onApproveVersion: ne,
|
|
647
|
+
onRejectVersion: re,
|
|
634
648
|
processingVersionIds: C
|
|
635
649
|
}, e.id))
|
|
636
650
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminReviewPage.js","names":[],"sources":["../../../src/devportal/pages/AdminReviewPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n CheckCircle,\n XCircle,\n Clock,\n Search,\n Loader2,\n AlertCircle,\n Eye,\n ChevronDown,\n ChevronUp,\n Download,\n FileJson,\n GitBranch,\n} from \"lucide-react\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport {\n useGetApplicationsQuery,\n useApproveApplicationMutation,\n useRejectApplicationMutation,\n useGetApplicationForReviewQuery,\n useGetApplicationsWithPendingVersionsQuery,\n useApproveApplicationVersionMutation,\n useRejectApplicationVersionMutation,\n getErrorMessage,\n useDevportalPermission,\n type ApplicationFieldsFragment,\n type ApplicationVersionFieldsFragment,\n type GetApplicationsWithPendingVersionsQuery,\n} from \"../hooks\";\n\ntype AppWithPendingVersions = NonNullable<\n GetApplicationsWithPendingVersionsQuery[\"applicationsWithPendingVersions\"][\"edges\"][number][\"node\"]\n>;\n\n// ============================================================================\n// Manifest Viewer\n// ============================================================================\n\ninterface ManifestViewerProps {\n manifest: Record<string, unknown>;\n tr: (key: string, fallback: string) => string;\n}\n\nconst ManifestViewer: FC<ManifestViewerProps> = ({ manifest, tr }) => {\n const [open, setOpen] = useState(false);\n return (\n <div>\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary hover:text-text-primary\"\n >\n <FileJson size={13} />\n {open\n ? tr(\"devportal.hideManifest\", \"Hide manifest\")\n : tr(\"devportal.viewManifest\", \"View manifest.json\")}\n </button>\n {open && (\n <pre className=\"mt-2 max-h-48 overflow-auto rounded-md bg-bg-sunken p-3 text-xs font-mono whitespace-pre-wrap break-all\">\n {JSON.stringify(manifest, null, 2)}\n </pre>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Review Row Component\n// ============================================================================\n\ninterface ReviewRowProps {\n app: ApplicationFieldsFragment;\n onApprove: (id: string, notes: string) => void;\n onReject: (id: string, notes: string) => void;\n processing: boolean;\n}\n\nconst ReviewRow: FC<ReviewRowProps> = ({\n app,\n onApprove,\n onReject,\n processing,\n}) => {\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 [expanded, setExpanded] = useState(false);\n const [reviewNotes, setReviewNotes] = useState(\"\");\n const approvePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_APPROVE);\n const rejectPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_REJECT);\n\n const { data: versionData } = useGetApplicationForReviewQuery({\n variables: { id: app.id },\n skip: !expanded,\n fetchPolicy: \"cache-first\",\n });\n const latestVersion = versionData?.application?.versions?.edges?.[0]?.node;\n\n return (\n <div className=\"rounded-card border border-border-seam bg-bg-surface overflow-hidden\">\n {/* Header row */}\n <div className=\"flex items-center gap-4 p-4\">\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover flex-shrink-0\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <span className=\"text-lg font-bold text-text-secondary\">\n {app.name.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n <span className=\"inline-flex items-center rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n {app.status}\n </span>\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {app.appType.replace(/_/g, \" \")}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription ??\n app.description ??\n tr(\"devportal.noDescription\", \"No description\")}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {tr(\"devportal.submitted\", \"Submitted\")}:{\" \"}\n {app.submittedAt\n ? new Date(app.submittedAt).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })\n : tr(\"devportal.unknown\", \"Unknown\")}\n </p>\n </div>\n\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <button\n onClick={() => navigate?.(`${basePath}/apps/${app.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n title={tr(\"devportal.viewAppDetails\", \"View app details\")}\n >\n <Eye size={14} />\n {tr(\"devportal.view\", \"View\")}\n </button>\n <button\n onClick={() => setExpanded((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}\n {tr(\"devportal.review\", \"Review\")}\n </button>\n </div>\n </div>\n\n {/* Expanded review panel */}\n {expanded && (\n <div className=\"border-t bg-bg-sunken/30 p-4 space-y-4\">\n {/* Submission notes from developer */}\n {app.reviewNotes && (\n <div className=\"rounded-md border bg-bg-surface p-3\">\n <p className=\"text-xs font-medium text-text-secondary mb-1\">\n {tr(\"devportal.developerNotes\", \"Developer Notes\")}\n </p>\n <p className=\"text-sm\">{app.reviewNotes}</p>\n </div>\n )}\n\n {/* App details summary */}\n <div className=\"grid grid-cols-2 gap-3 text-sm\">\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.category\", \"Category\")}:{\" \"}\n </span>\n <span>{app.category ?? \"—\"}</span>\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.pricing\", \"Pricing\")}:{\" \"}\n </span>\n <span>{app.pricingType ?? \"FREE\"}</span>\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.website\", \"Website\")}:{\" \"}\n </span>\n {app.websiteUrl ? (\n <a\n href={app.websiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary underline hover:no-underline\"\n >\n {tr(\"devportal.link\", \"Link\")}\n </a>\n ) : (\n <span>—</span>\n )}\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.privacyPolicy\", \"Privacy Policy\")}:{\" \"}\n </span>\n {app.privacyPolicyUrl ? (\n <a\n href={app.privacyPolicyUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary underline hover:no-underline\"\n >\n {tr(\"devportal.link\", \"Link\")}\n </a>\n ) : (\n <span className=\"text-status-error-text\">\n {tr(\"devportal.missing\", \"Missing\")}\n </span>\n )}\n </div>\n </div>\n\n {/* Latest version package for review */}\n {latestVersion && (\n <div className=\"rounded-md border bg-bg-surface p-3 space-y-3\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {tr(\"devportal.latestVersion\", \"Latest Version\")}:{\" \"}\n <span className=\"font-mono text-text-primary\">\n {latestVersion.version}\n </span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {latestVersion.packageUrl && (\n <a\n href={latestVersion.packageUrl}\n download\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Download size={14} />\n {tr(\"devportal.downloadPackage\", \"Download Package\")}\n {latestVersion.packageSize && (\n <span className=\"text-text-secondary text-xs ml-1\">\n ({latestVersion.packageSize})\n </span>\n )}\n </a>\n )}\n </div>\n {latestVersion.manifest && (\n <ManifestViewer\n manifest={latestVersion.manifest as Record<string, unknown>}\n tr={tr}\n />\n )}\n </div>\n )}\n\n {/* Reviewer notes input */}\n <div>\n <label className=\"text-sm font-medium\">\n {tr(\"devportal.reviewNotes\", \"Review Notes\")}\n </label>\n <textarea\n value={reviewNotes}\n onChange={(e) => setReviewNotes(e.target.value)}\n placeholder={tr(\n \"devportal.reviewNotesPlaceholder\",\n \"Add feedback for the developer (required for rejection)...\",\n )}\n rows={3}\n className=\"mt-1 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n />\n </div>\n\n {/* Action buttons */}\n <div className=\"flex gap-3\">\n <button\n onClick={() => onApprove(app.id, reviewNotes)}\n disabled={processing || !approvePerm.allowed}\n aria-disabled={processing || !approvePerm.allowed}\n title={\n !approvePerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-status-success-bg px-4 py-2 text-sm font-medium text-white hover:bg-status-success-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {processing ? (\n <Loader2\n size={14}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n ) : (\n <CheckCircle size={14} aria-hidden=\"true\" />\n )}\n {tr(\"devportal.approve\", \"Approve\")}\n </button>\n <button\n onClick={() => onReject(app.id, reviewNotes)}\n disabled={\n processing || !reviewNotes.trim() || !rejectPerm.allowed\n }\n aria-disabled={\n processing || !reviewNotes.trim() || !rejectPerm.allowed\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n title={\n !rejectPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : !reviewNotes.trim()\n ? tr(\n \"devportal.notesRequiredForRejection\",\n \"Notes required for rejection\",\n )\n : undefined\n }\n >\n {processing ? (\n <Loader2\n size={14}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n ) : (\n <XCircle size={14} aria-hidden=\"true\" />\n )}\n {tr(\"devportal.reject\", \"Reject\")}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Version Review Row Component\n// ============================================================================\n\ninterface VersionReviewRowProps {\n app: AppWithPendingVersions;\n onApproveVersion: (versionId: string, appId: string, notes: string) => void;\n onRejectVersion: (versionId: string, appId: string, reason: string) => void;\n processingVersionIds: Set<string>;\n}\n\nconst VersionReviewRow: FC<VersionReviewRowProps> = ({\n app,\n onApproveVersion,\n onRejectVersion,\n processingVersionIds,\n}) => {\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 [expanded, setExpanded] = useState(false);\n\n const pendingVersions = app.versions.edges\n .map((e) => e.node)\n .filter((v) => v.status === \"pending_review\");\n\n if (pendingVersions.length === 0) return null;\n\n return (\n <div className=\"rounded-card border border-border-seam bg-bg-surface overflow-hidden\">\n <div className=\"flex items-center gap-4 p-4\">\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover flex-shrink-0\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <span className=\"text-lg font-bold text-text-secondary\">\n {app.name.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n <span className=\"inline-flex items-center rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-xs font-medium text-status-info-text\">\n {pendingVersions.length} version\n {pendingVersions.length !== 1 ? \"s\" : \"\"} pending\n </span>\n </div>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription ??\n app.description ??\n tr(\"devportal.noDescription\", \"No description\")}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <button\n onClick={() => navigate?.(`${basePath}/apps/${app.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Eye size={14} />\n {tr(\"devportal.view\", \"View\")}\n </button>\n <button\n onClick={() => setExpanded((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}\n {tr(\"devportal.review\", \"Review\")}\n </button>\n </div>\n </div>\n\n {expanded && (\n <div className=\"border-t bg-bg-sunken/30 p-4 space-y-4\">\n {pendingVersions.map((version) => (\n <VersionCard\n key={version.id}\n version={version}\n appId={app.id}\n onApprove={onApproveVersion}\n onReject={onRejectVersion}\n processing={processingVersionIds.has(version.id)}\n tr={tr}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\ntype PendingVersion =\n AppWithPendingVersions[\"versions\"][\"edges\"][number][\"node\"];\n\ninterface VersionCardProps {\n version: PendingVersion;\n appId: string;\n onApprove: (versionId: string, appId: string, notes: string) => void;\n onReject: (versionId: string, appId: string, reason: string) => void;\n processing: boolean;\n tr: (key: string, fallback: string) => string;\n}\n\nconst VersionCard: FC<VersionCardProps> = ({\n version,\n appId,\n onApprove,\n onReject,\n processing,\n tr,\n}) => {\n const [notes, setNotes] = useState(\"\");\n const approvePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_APPROVE);\n const rejectPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_REJECT);\n\n return (\n <div className=\"rounded-md border bg-bg-surface p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <GitBranch size={14} className=\"text-text-secondary flex-shrink-0\" />\n <span className=\"font-mono font-semibold text-sm\">\n {version.version}\n </span>\n {version.isLatest && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n Latest\n </span>\n )}\n <span className=\"text-xs text-text-secondary ml-auto\">\n {tr(\"devportal.submitted\", \"Submitted\")}:{\" \"}\n {new Date(version.createdAt).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })}\n </span>\n </div>\n\n {version.releaseNotes && (\n <p className=\"text-sm text-text-secondary\">{version.releaseNotes}</p>\n )}\n\n <div className=\"flex flex-wrap gap-2\">\n {version.packageUrl && (\n <a\n href={version.packageUrl}\n download\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Download size={14} />\n {tr(\"devportal.downloadPackage\", \"Download Package\")}\n {version.packageSize && (\n <span className=\"text-text-secondary text-xs ml-1\">\n ({version.packageSize})\n </span>\n )}\n </a>\n )}\n </div>\n\n {version.manifest && (\n <ManifestViewer\n manifest={version.manifest as Record<string, unknown>}\n tr={tr}\n />\n )}\n\n <div>\n <label className=\"text-sm font-medium\">\n {tr(\"devportal.reviewNotes\", \"Review Notes\")}\n </label>\n <textarea\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr(\n \"devportal.reviewNotesPlaceholder\",\n \"Add feedback for the developer (required for rejection)...\",\n )}\n rows={2}\n className=\"mt-1 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n />\n </div>\n\n <div className=\"flex gap-3\">\n <button\n onClick={() => onApprove(version.id, appId, notes)}\n disabled={processing || !approvePerm.allowed}\n aria-disabled={processing || !approvePerm.allowed}\n className=\"inline-flex items-center gap-2 rounded-md bg-status-success-bg px-4 py-2 text-sm font-medium text-white hover:bg-status-success-bg/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {processing ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <CheckCircle size={14} />\n )}\n {tr(\"devportal.approveVersion\", \"Approve Version\")}\n </button>\n <button\n onClick={() => onReject(version.id, appId, notes)}\n disabled={processing || !notes.trim() || !rejectPerm.allowed}\n aria-disabled={processing || !notes.trim() || !rejectPerm.allowed}\n className=\"inline-flex items-center gap-2 rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {processing ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <XCircle size={14} />\n )}\n {tr(\"devportal.rejectVersion\", \"Reject Version\")}\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Admin Review Page\n// ============================================================================\n\nexport const AdminReviewPage: 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 [searchQuery, setSearchQuery] = useState(\"\");\n const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());\n const [processingVersionIds, setProcessingVersionIds] = useState<Set<string>>(\n new Set(),\n );\n const [actionError, setActionError] = useState<string | null>(null);\n const [actionSuccess, setActionSuccess] = useState<string | null>(null);\n\n const addProcessing = (id: string) =>\n setProcessingIds((prev) => new Set(prev).add(id));\n const removeProcessing = (id: string) =>\n setProcessingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n const addProcessingVersion = (id: string) =>\n setProcessingVersionIds((prev) => new Set(prev).add(id));\n const removeProcessingVersion = (id: string) =>\n setProcessingVersionIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const { data, loading, error, refetch } = useGetApplicationsQuery({\n variables: { status: \"SUBMITTED\" as const, limit: 100 },\n fetchPolicy: \"cache-and-network\",\n });\n\n const { data: pendingVersionData, refetch: refetchPendingVersions } =\n useGetApplicationsWithPendingVersionsQuery({\n variables: { limit: 100 },\n fetchPolicy: \"cache-and-network\",\n });\n\n const [approveApplication] = useApproveApplicationMutation();\n const [rejectApplication] = useRejectApplicationMutation();\n const [approveApplicationVersion] = useApproveApplicationVersionMutation();\n const [rejectApplicationVersion] = useRejectApplicationVersionMutation();\n\n const apps =\n data?.applications?.edges?.map(\n (edge: { node: ApplicationFieldsFragment }) => edge.node,\n ) ?? [];\n\n const appsWithPendingVersions =\n pendingVersionData?.applicationsWithPendingVersions?.edges?.map(\n (edge: { node: AppWithPendingVersions }) => edge.node,\n ) ?? [];\n\n const filteredApps = searchQuery\n ? apps.filter(\n (app: ApplicationFieldsFragment) =>\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (app.shortDescription ?? \"\")\n .toLowerCase()\n .includes(searchQuery.toLowerCase()),\n )\n : apps;\n\n const handleApprove = async (id: string, notes: string) => {\n addProcessing(id);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await approveApplication({\n variables: { id, notes: notes || undefined },\n });\n if (result.error) {\n throw result.error;\n }\n setActionSuccess(\n tr(\n \"devportal.applicationApprovedSuccess\",\n \"Application approved successfully.\",\n ),\n );\n await refetch();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToApproveApplication\",\n \"Failed to approve application\",\n ),\n );\n } finally {\n removeProcessing(id);\n }\n };\n\n const handleReject = async (id: string, notes: string) => {\n if (!notes.trim()) return;\n addProcessing(id);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await rejectApplication({ variables: { id, notes } });\n if (result.error) {\n throw result.error;\n }\n setActionSuccess(\n tr(\"devportal.applicationRejected\", \"Application rejected.\"),\n );\n await refetch();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToRejectApplication\",\n \"Failed to reject application\",\n ),\n );\n } finally {\n removeProcessing(id);\n }\n };\n\n const handleApproveVersion = async (\n versionId: string,\n _appId: string,\n notes: string,\n ) => {\n addProcessingVersion(versionId);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await approveApplicationVersion({\n variables: { id: versionId, notes: notes || undefined },\n });\n if (result.error) throw result.error;\n setActionSuccess(\n tr(\n \"devportal.versionApprovedSuccess\",\n \"Version approved and published.\",\n ),\n );\n await refetchPendingVersions();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\"devportal.failedToApproveVersion\", \"Failed to approve version\"),\n );\n } finally {\n removeProcessingVersion(versionId);\n }\n };\n\n const handleRejectVersion = async (\n versionId: string,\n _appId: string,\n reason: string,\n ) => {\n if (!reason.trim()) return;\n addProcessingVersion(versionId);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await rejectApplicationVersion({\n variables: { id: versionId, reason },\n });\n if (result.error) throw result.error;\n setActionSuccess(\n tr(\n \"devportal.versionRejectedSuccess\",\n \"Version rejected and returned to draft.\",\n ),\n );\n await refetchPendingVersions();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\"devportal.failedToRejectVersion\", \"Failed to reject version\"),\n );\n } finally {\n removeProcessingVersion(versionId);\n }\n };\n\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueDescription\",\n \"Review submitted applications for approval\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueDescription\",\n \"Review submitted applications for approval\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <p className=\"mt-2 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.retry\", \"Retry\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.REVIEW_MANAGE}>\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueFullDescription\",\n \"Review submitted applications for approval or rejection\",\n )}\n purpose={tr(\n \"devportal.appReviewQueuePurpose\",\n \"The moderation queue for apps developers have submitted to the marketplace. Open each submission to check its listing, permissions, and behaviour against the guidelines, then approve it to go live or reject it with feedback.\",\n )}\n actions={\n <button\n onClick={() => {\n void refetch();\n void refetchPendingVersions();\n }}\n className=\"rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {tr(\"devportal.refresh\", \"Refresh\")}\n </button>\n }\n >\n {/* Stats bar */}\n <div className=\"flex items-center gap-6 rounded-card border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2\">\n <Clock size={18} className=\"text-status-warning-text\" />\n <span className=\"text-sm font-medium\">\n {apps.length} {tr(\"devportal.pendingReview\", \"pending review\")}\n </span>\n </div>\n {appsWithPendingVersions.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <GitBranch size={18} className=\"text-status-info-text\" />\n <span className=\"text-sm font-medium\">\n {appsWithPendingVersions.length}{\" \"}\n {tr(\"devportal.withPendingVersions\", \"with pending versions\")}\n </span>\n </div>\n )}\n {(processingIds.size > 0 || processingVersionIds.size > 0) && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"devportal.processing\", \"Processing...\")}\n </div>\n )}\n </div>\n\n {/* Feedback messages */}\n {actionSuccess && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-subtle bg-status-success-bg-subtle px-4 py-3 text-sm text-status-success-text\">\n <CheckCircle size={16} />\n {actionSuccess}\n </div>\n )}\n {actionError && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-error-border/50 bg-status-error-bg/10 px-4 py-3 text-sm text-status-error-text\">\n <AlertCircle size={16} />\n {actionError}\n </div>\n )}\n\n {/* Search */}\n {apps.length > 0 && (\n <div className=\"relative max-w-sm\">\n <Search\n size={14}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchSubmittedApps\",\n \"Search submitted apps...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n )}\n\n {/* New app submissions */}\n <PageSection\n title={tr(\"devportal.newAppSubmissions\", \"New App Submissions\")}\n >\n {filteredApps.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<CheckCircle size={32} />}\n title={tr(\n \"devportal.noAppsPendingReview\",\n \"No apps pending review\",\n )}\n description={\n searchQuery\n ? tr(\n \"devportal.noAppsMatchSearch\",\n \"No apps match your search\",\n )\n : tr(\n \"devportal.allApplicationsReviewed\",\n \"All submitted applications have been reviewed\",\n )\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredApps.map((app: ApplicationFieldsFragment) => (\n <ReviewRow\n key={app.id}\n app={app}\n onApprove={handleApprove}\n onReject={handleReject}\n processing={processingIds.has(app.id)}\n />\n ))}\n </div>\n )}\n </PageSection>\n\n {/* Version updates for published apps */}\n <PageSection\n title={tr(\n \"devportal.versionUpdates\",\n \"Version Updates (Published Apps)\",\n )}\n >\n {appsWithPendingVersions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<GitBranch size={32} />}\n title={tr(\n \"devportal.noPendingVersions\",\n \"No pending version updates\",\n )}\n description={tr(\n \"devportal.allVersionsReviewed\",\n \"All version updates from published apps have been reviewed\",\n )}\n />\n ) : (\n <div className=\"space-y-3\">\n {appsWithPendingVersions.map((app: AppWithPendingVersions) => (\n <VersionReviewRow\n key={app.id}\n app={app}\n onApproveVersion={handleApproveVersion}\n onRejectVersion={handleRejectVersion}\n processingVersionIds={processingVersionIds}\n />\n ))}\n </div>\n )}\n </PageSection>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminReviewPage.js","names":[],"sources":["../../../src/devportal/pages/AdminReviewPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n CheckCircle,\n XCircle,\n Clock,\n Search,\n Loader2,\n AlertCircle,\n Eye,\n ChevronDown,\n ChevronUp,\n Download,\n FileJson,\n GitBranch,\n} from \"lucide-react\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport {\n useGetApplicationsQuery,\n useApproveApplicationMutation,\n useRejectApplicationMutation,\n useGetApplicationForReviewQuery,\n useGetApplicationsWithPendingVersionsQuery,\n useApproveApplicationVersionMutation,\n useRejectApplicationVersionMutation,\n getErrorMessage,\n useDevportalPermission,\n type ApplicationFieldsFragment,\n type ApplicationVersionFieldsFragment,\n type GetApplicationsWithPendingVersionsQuery,\n} from \"../hooks\";\n\ntype AppWithPendingVersions = NonNullable<\n GetApplicationsWithPendingVersionsQuery[\"applicationsWithPendingVersions\"][\"edges\"][number][\"node\"]\n>;\n\n// ============================================================================\n// Manifest Viewer\n// ============================================================================\n\ninterface ManifestViewerProps {\n manifest: Record<string, unknown>;\n tr: (key: string, fallback: string) => string;\n}\n\nconst ManifestViewer: FC<ManifestViewerProps> = ({ manifest, tr }) => {\n const [open, setOpen] = useState(false);\n return (\n <div>\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary hover:text-text-primary\"\n >\n <FileJson size={13} />\n {open\n ? tr(\"devportal.hideManifest\", \"Hide manifest\")\n : tr(\"devportal.viewManifest\", \"View manifest.json\")}\n </button>\n {open && (\n <pre className=\"mt-2 max-h-48 overflow-auto rounded-md bg-bg-sunken p-3 text-xs font-mono whitespace-pre-wrap break-all\">\n {JSON.stringify(manifest, null, 2)}\n </pre>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Review Row Component\n// ============================================================================\n\ninterface ReviewRowProps {\n app: ApplicationFieldsFragment;\n onApprove: (id: string, notes: string) => void;\n onReject: (id: string, notes: string) => void;\n processing: boolean;\n hasPendingVersions: boolean;\n}\n\nconst ReviewRow: FC<ReviewRowProps> = ({\n app,\n onApprove,\n onReject,\n processing,\n hasPendingVersions,\n}) => {\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 [expanded, setExpanded] = useState(false);\n const [reviewNotes, setReviewNotes] = useState(\"\");\n const approvePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_APPROVE);\n const rejectPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_REJECT);\n\n const { data: versionData } = useGetApplicationForReviewQuery({\n variables: { id: app.id },\n skip: !expanded,\n fetchPolicy: \"cache-first\",\n });\n const latestVersion = versionData?.application?.versions?.edges?.[0]?.node;\n\n return (\n <div className=\"rounded-card border border-border-seam bg-bg-surface overflow-hidden\">\n {/* Header row */}\n <div className=\"flex items-center gap-4 p-4\">\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover flex-shrink-0\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <span className=\"text-lg font-bold text-text-secondary\">\n {app.name.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n <span className=\"inline-flex items-center rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n {app.status}\n </span>\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {app.appType.replace(/_/g, \" \")}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription ??\n app.description ??\n tr(\"devportal.noDescription\", \"No description\")}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {tr(\"devportal.submitted\", \"Submitted\")}:{\" \"}\n {app.submittedAt\n ? new Date(app.submittedAt).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })\n : tr(\"devportal.unknown\", \"Unknown\")}\n </p>\n </div>\n\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <button\n onClick={() => navigate?.(`${basePath}/apps/${app.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n title={tr(\"devportal.viewAppDetails\", \"View app details\")}\n >\n <Eye size={14} />\n {tr(\"devportal.view\", \"View\")}\n </button>\n <button\n onClick={() => setExpanded((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}\n {tr(\"devportal.review\", \"Review\")}\n </button>\n </div>\n </div>\n\n {/* Expanded review panel */}\n {expanded && (\n <div className=\"border-t bg-bg-sunken/30 p-4 space-y-4\">\n {/* Version approval hint — shown when this app also has pending versions */}\n {hasPendingVersions && (\n <div className=\"flex items-start gap-2 rounded-md border border-status-warning-bg bg-status-warning-bg-subtle px-3 py-2.5 text-xs text-status-warning-text\">\n <GitBranch\n size={14}\n className=\"mt-0.5 shrink-0\"\n aria-hidden=\"true\"\n />\n <span>\n This app has a pending version. Approve the version in the{\" \"}\n <strong>Version Updates</strong> section below before approving\n the app — the version must be live before the app can go public.\n </span>\n </div>\n )}\n\n {/* Submission notes from developer */}\n {app.reviewNotes && (\n <div className=\"rounded-md border bg-bg-surface p-3\">\n <p className=\"text-xs font-medium text-text-secondary mb-1\">\n {tr(\"devportal.developerNotes\", \"Developer Notes\")}\n </p>\n <p className=\"text-sm\">{app.reviewNotes}</p>\n </div>\n )}\n\n {/* App details summary */}\n <div className=\"grid grid-cols-2 gap-3 text-sm\">\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.category\", \"Category\")}:{\" \"}\n </span>\n <span>{app.category ?? \"—\"}</span>\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.pricing\", \"Pricing\")}:{\" \"}\n </span>\n <span>{app.pricingType ?? \"FREE\"}</span>\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.website\", \"Website\")}:{\" \"}\n </span>\n {app.websiteUrl ? (\n <a\n href={app.websiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary underline hover:no-underline\"\n >\n {tr(\"devportal.link\", \"Link\")}\n </a>\n ) : (\n <span>—</span>\n )}\n </div>\n <div>\n <span className=\"text-text-secondary\">\n {tr(\"devportal.privacyPolicy\", \"Privacy Policy\")}:{\" \"}\n </span>\n {app.privacyPolicyUrl ? (\n <a\n href={app.privacyPolicyUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary underline hover:no-underline\"\n >\n {tr(\"devportal.link\", \"Link\")}\n </a>\n ) : (\n <span className=\"text-status-error-text\">\n {tr(\"devportal.missing\", \"Missing\")}\n </span>\n )}\n </div>\n </div>\n\n {/* Latest version package for review */}\n {latestVersion && (\n <div className=\"rounded-md border bg-bg-surface p-3 space-y-3\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {tr(\"devportal.latestVersion\", \"Latest Version\")}:{\" \"}\n <span className=\"font-mono text-text-primary\">\n {latestVersion.version}\n </span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {latestVersion.packageUrl && (\n <a\n href={latestVersion.packageUrl}\n download\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Download size={14} />\n {tr(\"devportal.downloadPackage\", \"Download Package\")}\n {latestVersion.packageSize && (\n <span className=\"text-text-secondary text-xs ml-1\">\n ({latestVersion.packageSize})\n </span>\n )}\n </a>\n )}\n </div>\n {latestVersion.manifest && (\n <ManifestViewer\n manifest={latestVersion.manifest as Record<string, unknown>}\n tr={tr}\n />\n )}\n </div>\n )}\n\n {/* Reviewer notes input */}\n <div>\n <label className=\"text-sm font-medium\">\n {tr(\"devportal.reviewNotes\", \"Review Notes\")}\n </label>\n <textarea\n value={reviewNotes}\n onChange={(e) => setReviewNotes(e.target.value)}\n placeholder={tr(\n \"devportal.reviewNotesPlaceholder\",\n \"Add feedback for the developer (required for rejection)...\",\n )}\n rows={3}\n className=\"mt-1 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n />\n </div>\n\n {/* Action buttons */}\n <div className=\"flex gap-3\">\n <button\n onClick={() => onApprove(app.id, reviewNotes)}\n disabled={processing || !approvePerm.allowed}\n aria-disabled={processing || !approvePerm.allowed}\n title={\n !approvePerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-status-success-bg px-4 py-2 text-sm font-medium text-white hover:bg-status-success-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {processing ? (\n <Loader2\n size={14}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n ) : (\n <CheckCircle size={14} aria-hidden=\"true\" />\n )}\n {tr(\"devportal.approve\", \"Approve\")}\n </button>\n <button\n onClick={() => onReject(app.id, reviewNotes)}\n disabled={\n processing || !reviewNotes.trim() || !rejectPerm.allowed\n }\n aria-disabled={\n processing || !reviewNotes.trim() || !rejectPerm.allowed\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n title={\n !rejectPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : !reviewNotes.trim()\n ? tr(\n \"devportal.notesRequiredForRejection\",\n \"Notes required for rejection\",\n )\n : undefined\n }\n >\n {processing ? (\n <Loader2\n size={14}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n ) : (\n <XCircle size={14} aria-hidden=\"true\" />\n )}\n {tr(\"devportal.reject\", \"Reject\")}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Version Review Row Component\n// ============================================================================\n\ninterface VersionReviewRowProps {\n app: AppWithPendingVersions;\n onApproveVersion: (versionId: string, appId: string, notes: string) => void;\n onRejectVersion: (versionId: string, appId: string, reason: string) => void;\n processingVersionIds: Set<string>;\n}\n\nconst VersionReviewRow: FC<VersionReviewRowProps> = ({\n app,\n onApproveVersion,\n onRejectVersion,\n processingVersionIds,\n}) => {\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 [expanded, setExpanded] = useState(false);\n\n const pendingVersions = app.versions.edges\n .map((e) => e.node)\n .filter((v) => v.status === \"pending_review\");\n\n if (pendingVersions.length === 0) return null;\n\n return (\n <div className=\"rounded-card border border-border-seam bg-bg-surface overflow-hidden\">\n <div className=\"flex items-center gap-4 p-4\">\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt={app.name}\n className=\"h-12 w-12 rounded-lg object-cover flex-shrink-0\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <span className=\"text-lg font-bold text-text-secondary\">\n {app.name.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold truncate\">{app.name}</h3>\n <span className=\"inline-flex items-center rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-xs font-medium text-status-info-text\">\n {pendingVersions.length} version\n {pendingVersions.length !== 1 ? \"s\" : \"\"} pending\n </span>\n </div>\n <p className=\"text-sm text-text-secondary truncate\">\n {app.shortDescription ??\n app.description ??\n tr(\"devportal.noDescription\", \"No description\")}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <button\n onClick={() => navigate?.(`${basePath}/apps/${app.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Eye size={14} />\n {tr(\"devportal.view\", \"View\")}\n </button>\n <button\n onClick={() => setExpanded((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}\n {tr(\"devportal.review\", \"Review\")}\n </button>\n </div>\n </div>\n\n {expanded && (\n <div className=\"border-t bg-bg-sunken/30 p-4 space-y-4\">\n {pendingVersions.map((version) => (\n <VersionCard\n key={version.id}\n version={version}\n appId={app.id}\n onApprove={onApproveVersion}\n onReject={onRejectVersion}\n processing={processingVersionIds.has(version.id)}\n tr={tr}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\ntype PendingVersion =\n AppWithPendingVersions[\"versions\"][\"edges\"][number][\"node\"];\n\ninterface VersionCardProps {\n version: PendingVersion;\n appId: string;\n onApprove: (versionId: string, appId: string, notes: string) => void;\n onReject: (versionId: string, appId: string, reason: string) => void;\n processing: boolean;\n tr: (key: string, fallback: string) => string;\n}\n\nconst VersionCard: FC<VersionCardProps> = ({\n version,\n appId,\n onApprove,\n onReject,\n processing,\n tr,\n}) => {\n const [notes, setNotes] = useState(\"\");\n const approvePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_APPROVE);\n const rejectPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_REJECT);\n\n return (\n <div className=\"rounded-md border bg-bg-surface p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <GitBranch size={14} className=\"text-text-secondary flex-shrink-0\" />\n <span className=\"font-mono font-semibold text-sm\">\n {version.version}\n </span>\n {version.isLatest && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n Latest\n </span>\n )}\n <span className=\"text-xs text-text-secondary ml-auto\">\n {tr(\"devportal.submitted\", \"Submitted\")}:{\" \"}\n {new Date(version.createdAt).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })}\n </span>\n </div>\n\n {version.releaseNotes && (\n <p className=\"text-sm text-text-secondary\">{version.releaseNotes}</p>\n )}\n\n <div className=\"flex flex-wrap gap-2\">\n {version.packageUrl && (\n <a\n href={version.packageUrl}\n download\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n <Download size={14} />\n {tr(\"devportal.downloadPackage\", \"Download Package\")}\n {version.packageSize && (\n <span className=\"text-text-secondary text-xs ml-1\">\n ({version.packageSize})\n </span>\n )}\n </a>\n )}\n </div>\n\n {version.manifest && (\n <ManifestViewer\n manifest={version.manifest as Record<string, unknown>}\n tr={tr}\n />\n )}\n\n <div>\n <label className=\"text-sm font-medium\">\n {tr(\"devportal.reviewNotes\", \"Review Notes\")}\n </label>\n <textarea\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr(\n \"devportal.reviewNotesPlaceholder\",\n \"Add feedback for the developer (required for rejection)...\",\n )}\n rows={2}\n className=\"mt-1 w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm ring-offset-background placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n />\n </div>\n\n <div className=\"flex gap-3\">\n <button\n onClick={() => onApprove(version.id, appId, notes)}\n disabled={processing || !approvePerm.allowed}\n aria-disabled={processing || !approvePerm.allowed}\n className=\"inline-flex items-center gap-2 rounded-md bg-status-success-bg px-4 py-2 text-sm font-medium text-white hover:bg-status-success-bg/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {processing ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <CheckCircle size={14} />\n )}\n {tr(\"devportal.approveVersion\", \"Approve Version\")}\n </button>\n <button\n onClick={() => onReject(version.id, appId, notes)}\n disabled={processing || !notes.trim() || !rejectPerm.allowed}\n aria-disabled={processing || !notes.trim() || !rejectPerm.allowed}\n className=\"inline-flex items-center gap-2 rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {processing ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <XCircle size={14} />\n )}\n {tr(\"devportal.rejectVersion\", \"Reject Version\")}\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Admin Review Page\n// ============================================================================\n\nexport const AdminReviewPage: 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 [searchQuery, setSearchQuery] = useState(\"\");\n const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());\n const [processingVersionIds, setProcessingVersionIds] = useState<Set<string>>(\n new Set(),\n );\n const [actionError, setActionError] = useState<string | null>(null);\n const [actionSuccess, setActionSuccess] = useState<string | null>(null);\n\n const addProcessing = (id: string) =>\n setProcessingIds((prev) => new Set(prev).add(id));\n const removeProcessing = (id: string) =>\n setProcessingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n const addProcessingVersion = (id: string) =>\n setProcessingVersionIds((prev) => new Set(prev).add(id));\n const removeProcessingVersion = (id: string) =>\n setProcessingVersionIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const { data, loading, error, refetch } = useGetApplicationsQuery({\n variables: { status: \"SUBMITTED\" as const, limit: 100 },\n fetchPolicy: \"cache-and-network\",\n });\n\n const { data: pendingVersionData, refetch: refetchPendingVersions } =\n useGetApplicationsWithPendingVersionsQuery({\n variables: { limit: 100 },\n fetchPolicy: \"cache-and-network\",\n });\n\n const [approveApplication] = useApproveApplicationMutation();\n const [rejectApplication] = useRejectApplicationMutation();\n const [approveApplicationVersion] = useApproveApplicationVersionMutation();\n const [rejectApplicationVersion] = useRejectApplicationVersionMutation();\n\n const apps =\n data?.applications?.edges?.map(\n (edge: { node: ApplicationFieldsFragment }) => edge.node,\n ) ?? [];\n\n const appsWithPendingVersions =\n pendingVersionData?.applicationsWithPendingVersions?.edges?.map(\n (edge: { node: AppWithPendingVersions }) => edge.node,\n ) ?? [];\n\n const pendingVersionAppIds = new Set(\n appsWithPendingVersions.map((a: AppWithPendingVersions) => a.id),\n );\n\n const filteredApps = searchQuery\n ? apps.filter(\n (app: ApplicationFieldsFragment) =>\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (app.shortDescription ?? \"\")\n .toLowerCase()\n .includes(searchQuery.toLowerCase()),\n )\n : apps;\n\n const handleApprove = async (id: string, notes: string) => {\n addProcessing(id);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await approveApplication({\n variables: { id, notes: notes || undefined },\n });\n if (result.error) {\n throw result.error;\n }\n setActionSuccess(\n tr(\n \"devportal.applicationApprovedSuccess\",\n \"Application approved successfully.\",\n ),\n );\n await refetch();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToApproveApplication\",\n \"Failed to approve application\",\n ),\n );\n } finally {\n removeProcessing(id);\n }\n };\n\n const handleReject = async (id: string, notes: string) => {\n if (!notes.trim()) return;\n addProcessing(id);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await rejectApplication({ variables: { id, notes } });\n if (result.error) {\n throw result.error;\n }\n setActionSuccess(\n tr(\"devportal.applicationRejected\", \"Application rejected.\"),\n );\n await refetch();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToRejectApplication\",\n \"Failed to reject application\",\n ),\n );\n } finally {\n removeProcessing(id);\n }\n };\n\n const handleApproveVersion = async (\n versionId: string,\n _appId: string,\n notes: string,\n ) => {\n addProcessingVersion(versionId);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await approveApplicationVersion({\n variables: { id: versionId, notes: notes || undefined },\n });\n if (result.error) throw result.error;\n setActionSuccess(\n tr(\n \"devportal.versionApprovedSuccess\",\n \"Version approved and published.\",\n ),\n );\n await refetchPendingVersions();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\"devportal.failedToApproveVersion\", \"Failed to approve version\"),\n );\n } finally {\n removeProcessingVersion(versionId);\n }\n };\n\n const handleRejectVersion = async (\n versionId: string,\n _appId: string,\n reason: string,\n ) => {\n if (!reason.trim()) return;\n addProcessingVersion(versionId);\n setActionError(null);\n setActionSuccess(null);\n try {\n const result = await rejectApplicationVersion({\n variables: { id: versionId, reason },\n });\n if (result.error) throw result.error;\n setActionSuccess(\n tr(\n \"devportal.versionRejectedSuccess\",\n \"Version rejected and returned to draft.\",\n ),\n );\n await refetchPendingVersions();\n } catch (err) {\n setActionError(\n err instanceof Error\n ? err.message\n : tr(\"devportal.failedToRejectVersion\", \"Failed to reject version\"),\n );\n } finally {\n removeProcessingVersion(versionId);\n }\n };\n\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueDescription\",\n \"Review submitted applications for approval\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueDescription\",\n \"Review submitted applications for approval\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <p className=\"mt-2 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.retry\", \"Retry\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.REVIEW_MANAGE}>\n <PageLayout\n title={tr(\"devportal.appReviewQueue\", \"App Review Queue\")}\n description={tr(\n \"devportal.appReviewQueueFullDescription\",\n \"Review submitted applications for approval or rejection\",\n )}\n purpose={tr(\n \"devportal.appReviewQueuePurpose\",\n \"The moderation queue for apps developers have submitted to the marketplace. Open each submission to check its listing, permissions, and behaviour against the guidelines, then approve it to go live or reject it with feedback.\",\n )}\n actions={\n <button\n onClick={() => {\n void refetch();\n void refetchPendingVersions();\n }}\n className=\"rounded-md border px-3 py-1.5 text-sm hover:bg-bg-sunken\"\n >\n {tr(\"devportal.refresh\", \"Refresh\")}\n </button>\n }\n >\n {/* Stats bar */}\n <div className=\"flex items-center gap-6 rounded-card border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2\">\n <Clock size={18} className=\"text-status-warning-text\" />\n <span className=\"text-sm font-medium\">\n {apps.length} {tr(\"devportal.pendingReview\", \"pending review\")}\n </span>\n </div>\n {appsWithPendingVersions.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <GitBranch size={18} className=\"text-status-info-text\" />\n <span className=\"text-sm font-medium\">\n {appsWithPendingVersions.length}{\" \"}\n {tr(\"devportal.withPendingVersions\", \"with pending versions\")}\n </span>\n </div>\n )}\n {(processingIds.size > 0 || processingVersionIds.size > 0) && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"devportal.processing\", \"Processing...\")}\n </div>\n )}\n </div>\n\n {/* Feedback messages */}\n {actionSuccess && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-subtle bg-status-success-bg-subtle px-4 py-3 text-sm text-status-success-text\">\n <CheckCircle size={16} />\n {actionSuccess}\n </div>\n )}\n {actionError && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-error-border/50 bg-status-error-bg/10 px-4 py-3 text-sm text-status-error-text\">\n <AlertCircle size={16} />\n {actionError}\n </div>\n )}\n\n {/* Search */}\n {apps.length > 0 && (\n <div className=\"relative max-w-sm\">\n <Search\n size={14}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchSubmittedApps\",\n \"Search submitted apps...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n )}\n\n {/* New app submissions */}\n <PageSection\n title={tr(\"devportal.newAppSubmissions\", \"New App Submissions\")}\n >\n {filteredApps.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<CheckCircle size={32} />}\n title={tr(\n \"devportal.noAppsPendingReview\",\n \"No apps pending review\",\n )}\n description={\n searchQuery\n ? tr(\n \"devportal.noAppsMatchSearch\",\n \"No apps match your search\",\n )\n : tr(\n \"devportal.allApplicationsReviewed\",\n \"All submitted applications have been reviewed\",\n )\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredApps.map((app: ApplicationFieldsFragment) => (\n <ReviewRow\n key={app.id}\n app={app}\n onApprove={handleApprove}\n onReject={handleReject}\n processing={processingIds.has(app.id)}\n hasPendingVersions={pendingVersionAppIds.has(app.id)}\n />\n ))}\n </div>\n )}\n </PageSection>\n\n {/* Version updates for published apps */}\n <PageSection\n title={tr(\n \"devportal.versionUpdates\",\n \"Version Updates (Published Apps)\",\n )}\n >\n {appsWithPendingVersions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<GitBranch size={32} />}\n title={tr(\n \"devportal.noPendingVersions\",\n \"No pending version updates\",\n )}\n description={tr(\n \"devportal.allVersionsReviewed\",\n \"All version updates from published apps have been reviewed\",\n )}\n />\n ) : (\n <div className=\"space-y-3\">\n {appsWithPendingVersions.map((app: AppWithPendingVersions) => (\n <VersionReviewRow\n key={app.id}\n app={app}\n onApproveVersion={handleApproveVersion}\n onRejectVersion={handleRejectVersion}\n processingVersionIds={processingVersionIds}\n />\n ))}\n </div>\n )}\n </PageSection>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAkDA,IAAM,KAA2C,EAAE,aAAU,YAAS;CACpE,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM;AACvC,QACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,UAAD;EACE,MAAK;EACL,eAAe,GAAS,MAAM,CAAC,EAAE;EACjC,WAAU;YAHZ,CAKE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA,EACrB,IACG,EAAG,0BAA0B,gBAAgB,GAC7C,EAAG,0BAA0B,qBAAqB,CAC/C;KACR,KACC,kBAAC,OAAD;EAAK,WAAU;YACZ,KAAK,UAAU,GAAU,MAAM,EAAE;EAC9B,CAAA,CAEJ,EAAA,CAAA;GAgBJ,KAAiC,EACrC,QACA,cACA,aACA,eACA,4BACI;CACJ,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAE9B,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,IAAc,EAAuB,EAAsB,YAAY,EACvE,IAAa,EAAuB,EAAsB,WAAW,EAErE,EAAE,MAAM,MAAgB,EAAgC;EAC5D,WAAW,EAAE,IAAI,EAAI,IAAI;EACzB,MAAM,CAAC;EACP,aAAa;EACd,CAAC,EACI,IAAgB,GAAa,aAAa,UAAU,QAAQ,IAAI;AAEtE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,EAAI,UACH,kBAAC,OAAD;KACE,KAAK,EAAI;KACT,KAAK,EAAI;KACT,WAAU;KACV,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAI,KAAK,OAAO,EAAE,CAAC,aAAa;MAC5B,CAAA;KACH,CAAA;IAGR,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBAA0B,EAAI;SAAU,CAAA;QACtD,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAI;SACA,CAAA;QACP,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAI,QAAQ,QAAQ,MAAM,IAAI;SAC1B,CAAA;QACH;;MACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAI,oBACH,EAAI,eACJ,EAAG,2BAA2B,iBAAiB;OAC/C,CAAA;MACJ,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAG,uBAAuB,YAAY;QAAC;QAAE;QACzC,EAAI,cACD,IAAI,KAAK,EAAI,YAAY,CAAC,mBAAmB,KAAA,GAAW;SACtD,MAAM;SACN,OAAO;SACP,KAAK;SACN,CAAC,GACF,EAAG,qBAAqB,UAAU;QACpC;;MACA;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,eAAe,IAAW,GAAG,EAAS,QAAQ,EAAI,KAAK;MACvD,WAAU;MACV,OAAO,EAAG,4BAA4B,mBAAmB;gBAH3D,CAKE,kBAAC,GAAD,EAAK,MAAM,IAAM,CAAA,EAChB,EAAG,kBAAkB,OAAO,CACtB;SACT,kBAAC,UAAD;MACE,eAAe,GAAa,MAAM,CAAC,EAAE;MACrC,WAAU;gBAFZ,CAIc,EAAX,IAAY,IAA0B,GAA3B,EAAW,MAAM,IAAM,CAA4B,EAC9D,EAAG,oBAAoB,SAAS,CAC1B;QACL;;IACF;MAGL,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEG,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,MAAM;MACN,WAAU;MACV,eAAY;MACZ,CAAA,EACF,kBAAC,QAAD,EAAA,UAAA;MAAM;MACuD;MAC3D,kBAAC,UAAD,EAAA,UAAQ,mBAAwB,CAAA;;MAE3B,EAAA,CAAA,CACH;;IAIP,EAAI,eACH,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,4BAA4B,kBAAkB;MAChD,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAI;MAAgB,CAAA,CACxC;;IAIR,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAG,sBAAsB,WAAW;QAAC;QAAE;QACnC;UACP,kBAAC,QAAD,EAAA,UAAO,EAAI,YAAY,KAAW,CAAA,CAC9B,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAG,qBAAqB,UAAU;QAAC;QAAE;QACjC;UACP,kBAAC,QAAD,EAAA,UAAO,EAAI,eAAe,QAAc,CAAA,CACpC,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAG,qBAAqB,UAAU;QAAC;QAAE;QACjC;UACN,EAAI,aACH,kBAAC,KAAD;OACE,MAAM,EAAI;OACV,QAAO;OACP,KAAI;OACJ,WAAU;iBAET,EAAG,kBAAkB,OAAO;OAC3B,CAAA,GAEJ,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA,CAEZ,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAG,2BAA2B,iBAAiB;QAAC;QAAE;QAC9C;UACN,EAAI,mBACH,kBAAC,KAAD;OACE,MAAM,EAAI;OACV,QAAO;OACP,KAAI;OACJ,WAAU;iBAET,EAAG,kBAAkB,OAAO;OAC3B,CAAA,GAEJ,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,qBAAqB,UAAU;OAC9B,CAAA,CAEL,EAAA,CAAA;MACF;;IAGL,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAG,2BAA2B,iBAAiB;QAAC;QAAE;QACnD,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAc;SACV,CAAA;QACL;;MACJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,cACb,kBAAC,KAAD;QACE,MAAM,EAAc;QACpB,UAAA;QACA,QAAO;QACP,KAAI;QACJ,WAAU;kBALZ;SAOE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;SACrB,EAAG,6BAA6B,mBAAmB;SACnD,EAAc,eACb,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WAAmD;WAC/C,EAAc;WAAY;WACvB;;SAEP;;OAEF,CAAA;MACL,EAAc,YACb,kBAAC,GAAD;OACE,UAAU,EAAc;OACpB;OACJ,CAAA;MAEA;;IAIR,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,WAAU;eACd,EAAG,yBAAyB,eAAe;KACtC,CAAA,EACR,kBAAC,YAAD;KACE,OAAO;KACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;KAC/C,aAAa,EACX,oCACA,6DACD;KACD,MAAM;KACN,WAAU;KACV,CAAA,CACE,EAAA,CAAA;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,eAAe,EAAU,EAAI,IAAI,EAAY;MAC7C,UAAU,KAAc,CAAC,EAAY;MACrC,iBAAe,KAAc,CAAC,EAAY;MAC1C,OACG,EAAY,UAKT,KAAA,IAJA,EACE,iCACA,oDACD;MAGP,WAAU;gBAZZ,CAcG,IACC,kBAAC,GAAD;OACE,MAAM;OACN,WAAU;OACV,eAAY;OACZ,CAAA,GAEF,kBAAC,GAAD;OAAa,MAAM;OAAI,eAAY;OAAS,CAAA,EAE7C,EAAG,qBAAqB,UAAU,CAC5B;SACT,kBAAC,UAAD;MACE,eAAe,EAAS,EAAI,IAAI,EAAY;MAC5C,UACE,KAAc,CAAC,EAAY,MAAM,IAAI,CAAC,EAAW;MAEnD,iBACE,KAAc,CAAC,EAAY,MAAM,IAAI,CAAC,EAAW;MAEnD,WAAU;MACV,OACG,EAAW,UAKP,EAAY,MAAM,GAKjB,KAAA,IAJA,EACE,uCACA,+BACD,GARH,EACE,iCACA,oDACD;gBAdT,CAuBG,IACC,kBAAC,GAAD;OACE,MAAM;OACN,WAAU;OACV,eAAY;OACZ,CAAA,GAEF,kBAAC,GAAD;OAAS,MAAM;OAAI,eAAY;OAAS,CAAA,EAEzC,EAAG,oBAAoB,SAAS,CAC1B;QACL;;IACF;KAEJ;;GAeJ,KAA+C,EACnD,QACA,qBACA,oBACA,8BACI;CACJ,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAE9B,CAAC,GAAU,KAAe,EAAS,GAAM,EAEzC,IAAkB,EAAI,SAAS,MAClC,KAAK,MAAM,EAAE,KAAK,CAClB,QAAQ,MAAM,EAAE,WAAW,iBAAiB;AAI/C,QAFI,EAAgB,WAAW,IAAU,OAGvC,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,EAAI,UACH,kBAAC,OAAD;KACE,KAAK,EAAI;KACT,KAAK,EAAI;KACT,WAAU;KACV,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAI,KAAK,OAAO,EAAE,CAAC,aAAa;MAC5B,CAAA;KACH,CAAA;IAER,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA0B,EAAI;OAAU,CAAA,EACtD,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAgB;QAAO;QACvB,EAAgB,WAAW,IAAU,KAAN;QAAS;QACpC;SACH;SACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAI,oBACH,EAAI,eACJ,EAAG,2BAA2B,iBAAiB;MAC/C,CAAA,CACA;;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,eAAe,IAAW,GAAG,EAAS,QAAQ,EAAI,KAAK;MACvD,WAAU;gBAFZ,CAIE,kBAAC,GAAD,EAAK,MAAM,IAAM,CAAA,EAChB,EAAG,kBAAkB,OAAO,CACtB;SACT,kBAAC,UAAD;MACE,eAAe,GAAa,MAAM,CAAC,EAAE;MACrC,WAAU;gBAFZ,CAIc,EAAX,IAAY,IAA0B,GAA3B,EAAW,MAAM,IAAM,CAA4B,EAC9D,EAAG,oBAAoB,SAAS,CAC1B;QACL;;IACF;MAEL,KACC,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAgB,KAAK,MACpB,kBAAC,GAAD;IAEW;IACT,OAAO,EAAI;IACX,WAAW;IACX,UAAU;IACV,YAAY,EAAqB,IAAI,EAAQ,GAAG;IAC5C;IACJ,EAPK,EAAQ,GAOb,CACF;GACE,CAAA,CAEJ;;GAgBJ,KAAqC,EACzC,YACA,UACA,cACA,aACA,eACA,YACI;CACJ,IAAM,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,IAAc,EAAuB,EAAsB,YAAY,EACvE,IAAa,EAAuB,EAAsB,WAAW;AAE3E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,MAAM;MAAI,WAAU;MAAsC,CAAA;KACrE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAQ;MACJ,CAAA;KACN,EAAQ,YACP,kBAAC,QAAD;MAAM,WAAU;gBAA6F;MAEtG,CAAA;KAET,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,EAAG,uBAAuB,YAAY;OAAC;OAAE;OACzC,IAAI,KAAK,EAAQ,UAAU,CAAC,mBAAmB,KAAA,GAAW;QACzD,MAAM;QACN,OAAO;QACP,KAAK;QACN,CAAC;OACG;;KACH;;GAEL,EAAQ,gBACP,kBAAC,KAAD;IAAG,WAAU;cAA+B,EAAQ;IAAiB,CAAA;GAGvE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,cACP,kBAAC,KAAD;KACE,MAAM,EAAQ;KACd,UAAA;KACA,QAAO;KACP,KAAI;KACJ,WAAU;eALZ;MAOE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;MACrB,EAAG,6BAA6B,mBAAmB;MACnD,EAAQ,eACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QAAmD;QAC/C,EAAQ;QAAY;QACjB;;MAEP;;IAEF,CAAA;GAEL,EAAQ,YACP,kBAAC,GAAD;IACE,UAAU,EAAQ;IACd;IACJ,CAAA;GAGJ,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IAAO,WAAU;cACd,EAAG,yBAAyB,eAAe;IACtC,CAAA,EACR,kBAAC,YAAD;IACE,OAAO;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;IACzC,aAAa,EACX,oCACA,6DACD;IACD,MAAM;IACN,WAAU;IACV,CAAA,CACE,EAAA,CAAA;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,eAAe,EAAU,EAAQ,IAAI,GAAO,EAAM;KAClD,UAAU,KAAc,CAAC,EAAY;KACrC,iBAAe,KAAc,CAAC,EAAY;KAC1C,WAAU;eAJZ,CAMG,IACC,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAE9C,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EAE1B,EAAG,4BAA4B,kBAAkB,CAC3C;QACT,kBAAC,UAAD;KACE,eAAe,EAAS,EAAQ,IAAI,GAAO,EAAM;KACjD,UAAU,KAAc,CAAC,EAAM,MAAM,IAAI,CAAC,EAAW;KACrD,iBAAe,KAAc,CAAC,EAAM,MAAM,IAAI,CAAC,EAAW;KAC1D,WAAU;eAJZ,CAMG,IACC,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAE9C,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA,EAEtB,EAAG,2BAA2B,iBAAiB,CACzC;OACL;;GACF;;GAQG,UAA4B;CACvC,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAE9B,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAe,KAAoB,kBAAsB,IAAI,KAAK,CAAC,EACpE,CAAC,GAAsB,KAA2B,kBACtD,IAAI,KAAK,CACV,EACK,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAe,KAAoB,EAAwB,KAAK,EAEjE,KAAiB,MACrB,GAAkB,MAAS,IAAI,IAAI,EAAK,CAAC,IAAI,EAAG,CAAC,EAC7C,KAAoB,MACxB,GAAkB,MAAS;EACzB,IAAM,IAAO,IAAI,IAAI,EAAK;AAE1B,SADA,EAAK,OAAO,EAAG,EACR;GACP,EACE,KAAwB,MAC5B,GAAyB,MAAS,IAAI,IAAI,EAAK,CAAC,IAAI,EAAG,CAAC,EACpD,KAA2B,MAC/B,GAAyB,MAAS;EAChC,IAAM,IAAO,IAAI,IAAI,EAAK;AAE1B,SADA,EAAK,OAAO,EAAG,EACR;GACP,EAEE,EAAE,SAAM,YAAS,UAAO,eAAY,EAAwB;EAChE,WAAW;GAAE,QAAQ;GAAsB,OAAO;GAAK;EACvD,aAAa;EACd,CAAC,EAEI,EAAE,MAAM,GAAoB,SAAS,MACzC,EAA2C;EACzC,WAAW,EAAE,OAAO,KAAK;EACzB,aAAa;EACd,CAAC,EAEE,CAAC,KAAsB,GAA+B,EACtD,CAAC,KAAqB,GAA8B,EACpD,CAAC,KAA6B,GAAsC,EACpE,CAAC,KAA4B,GAAqC,EAElE,IACJ,GAAM,cAAc,OAAO,KACxB,MAA8C,EAAK,KACrD,IAAI,EAAE,EAEH,IACJ,GAAoB,iCAAiC,OAAO,KACzD,MAA2C,EAAK,KAClD,IAAI,EAAE,EAEH,IAAuB,IAAI,IAC/B,EAAwB,KAAK,MAA8B,EAAE,GAAG,CACjE,EAEK,IAAe,IACjB,EAAK,QACF,MACC,EAAI,KAAK,aAAa,CAAC,SAAS,EAAY,aAAa,CAAC,KACzD,EAAI,oBAAoB,IACtB,aAAa,CACb,SAAS,EAAY,aAAa,CAAC,CACzC,GACD,GAEE,KAAgB,OAAO,GAAY,MAAkB;AAGzD,EAFA,EAAc,EAAG,EACjB,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,MAAI;GACF,IAAM,IAAS,MAAM,EAAmB,EACtC,WAAW;IAAE;IAAI,OAAO,KAAS,KAAA;IAAW,EAC7C,CAAC;AACF,OAAI,EAAO,MACT,OAAM,EAAO;AAQf,GANA,EACE,EACE,wCACA,qCACD,CACF,EACD,MAAM,GAAS;WACR,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EACE,wCACA,gCACD,CACN;YACO;AACR,KAAiB,EAAG;;IAIlB,KAAe,OAAO,GAAY,MAAkB;AACnD,QAAM,MAAM,EAGjB;GAFA,EAAc,EAAG,EACjB,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,OAAI;IACF,IAAM,IAAS,MAAM,EAAkB,EAAE,WAAW;KAAE;KAAI;KAAO,EAAE,CAAC;AACpE,QAAI,EAAO,MACT,OAAM,EAAO;AAKf,IAHA,EACE,EAAG,iCAAiC,wBAAwB,CAC7D,EACD,MAAM,GAAS;YACR,GAAK;AACZ,MACE,aAAe,QACX,EAAI,UACJ,EACE,uCACA,+BACD,CACN;aACO;AACR,MAAiB,EAAG;;;IAIlB,KAAuB,OAC3B,GACA,GACA,MACG;AAGH,EAFA,EAAqB,EAAU,EAC/B,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,MAAI;GACF,IAAM,IAAS,MAAM,EAA0B,EAC7C,WAAW;IAAE,IAAI;IAAW,OAAO,KAAS,KAAA;IAAW,EACxD,CAAC;AACF,OAAI,EAAO,MAAO,OAAM,EAAO;AAO/B,GANA,EACE,EACE,oCACA,kCACD,CACF,EACD,MAAM,GAAwB;WACvB,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,oCAAoC,4BAA4B,CACxE;YACO;AACR,KAAwB,EAAU;;IAIhC,KAAsB,OAC1B,GACA,GACA,MACG;AACE,QAAO,MAAM,EAGlB;GAFA,EAAqB,EAAU,EAC/B,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,OAAI;IACF,IAAM,IAAS,MAAM,EAAyB,EAC5C,WAAW;KAAE,IAAI;KAAW;KAAQ,EACrC,CAAC;AACF,QAAI,EAAO,MAAO,OAAM,EAAO;AAO/B,IANA,EACE,EACE,oCACA,0CACD,CACF,EACD,MAAM,GAAwB;YACvB,GAAK;AACZ,MACE,aAAe,QACX,EAAI,UACJ,EAAG,mCAAmC,2BAA2B,CACtE;aACO;AACR,MAAwB,EAAU;;;;AA6CtC,QAzCI,KAAW,CAAC,IAEZ,kBAAC,GAAD;EACE,OAAO,EAAG,4BAA4B,mBAAmB;EACzD,aAAa,EACX,uCACA,6CACD;YAED,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,EAAS,WAAU,4CAA6C,CAAA;GAC5D,CAAA;EACK,CAAA,GAIb,IAEA,kBAAC,GAAD;EACE,OAAO,EAAG,4BAA4B,mBAAmB;EACzD,aAAa,EACX,uCACA,6CACD;YAED,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD,EAAa,WAAU,0CAA2C,CAAA;IAClE,kBAAC,KAAD;KAAG,WAAU;eACV,EAAgB,EAAM;KACrB,CAAA;IACJ,kBAAC,UAAD;KACE,eAAe,GAAS;KACxB,WAAU;eAET,EAAG,mBAAmB,QAAQ;KACxB,CAAA;IACL;;EACK,CAAA,GAKf,kBAAC,GAAD;EAAiB,YAAY,EAAsB;YACjD,kBAAC,GAAD;GACE,OAAO,EAAG,4BAA4B,mBAAmB;GACzD,aAAa,EACX,2CACA,0DACD;GACD,SAAS,EACP,mCACA,mOACD;GACD,SACE,kBAAC,UAAD;IACE,eAAe;AAER,KADA,GAAS,EACT,GAAwB;;IAE/B,WAAU;cAET,EAAG,qBAAqB,UAAU;IAC5B,CAAA;aAnBb;IAuBE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,MAAM;QAAI,WAAU;QAA6B,CAAA,EACxD,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAK;SAAO;SAAE,EAAG,2BAA2B,iBAAiB;SACzD;UACH;;MACL,EAAwB,SAAS,KAChC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAW,MAAM;QAAI,WAAU;QAA0B,CAAA,EACzD,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAwB;SAAQ;SAChC,EAAG,iCAAiC,wBAAwB;SACxD;UACH;;OAEN,EAAc,OAAO,KAAK,EAAqB,OAAO,MACtD,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAS,MAAM;QAAI,WAAU;QAAiB,CAAA,EAC7C,EAAG,wBAAwB,gBAAgB,CACxC;;MAEJ;;IAGL,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EACxB,EACG;;IAEP,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EACxB,EACG;;IAIP,EAAK,SAAS,KACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,MAAM;MACN,WAAU;MACV,CAAA,EACF,kBAAC,SAAD;MACE,MAAK;MACL,aAAa,EACX,iCACA,2BACD;MACD,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;MAC/C,WAAU;MACV,CAAA,CACE;;IAIR,kBAAC,GAAD;KACE,OAAO,EAAG,+BAA+B,sBAAsB;eAE9D,EAAa,WAAW,IACvB,kBAAC,GAAD;MACE,cAAa;MACb,MAAM,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA;MAC/B,OAAO,EACL,iCACA,yBACD;MACD,aACE,IACI,EACE,+BACA,4BACD,GACD,EACE,qCACA,gDACD;MAEP,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAa,KAAK,MACjB,kBAAC,GAAD;OAEO;OACL,WAAW;OACX,UAAU;OACV,YAAY,EAAc,IAAI,EAAI,GAAG;OACrC,oBAAoB,EAAqB,IAAI,EAAI,GAAG;OACpD,EANK,EAAI,GAMT,CACF;MACE,CAAA;KAEI,CAAA;IAGd,kBAAC,GAAD;KACE,OAAO,EACL,4BACA,mCACD;eAEA,EAAwB,WAAW,IAClC,kBAAC,GAAD;MACE,cAAa;MACb,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;MAC7B,OAAO,EACL,+BACA,6BACD;MACD,aAAa,EACX,iCACA,6DACD;MACD,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAwB,KAAK,MAC5B,kBAAC,GAAD;OAEO;OACL,kBAAkB;OAClB,iBAAiB;OACK;OACtB,EALK,EAAI,GAKT,CACF;MACE,CAAA;KAEI,CAAA;IACH;;EACG,CAAA"}
|