@wopr-network/platform-ui-core 1.1.13 → 1.2.0
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/package.json
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
TableHeader,
|
|
37
37
|
TableRow,
|
|
38
38
|
} from "@/components/ui/table";
|
|
39
|
+
import { useIsAdminOrOwner } from "@/hooks/use-my-org-role";
|
|
39
40
|
import type { Organization, OrgMember } from "@/lib/api";
|
|
40
41
|
import {
|
|
41
42
|
changeRole,
|
|
@@ -58,6 +59,7 @@ export default function OrgPage() {
|
|
|
58
59
|
const routerRef = useRef(router);
|
|
59
60
|
routerRef.current = router;
|
|
60
61
|
const [org, setOrg] = useState<Organization | null>(null);
|
|
62
|
+
const isAdminOrOwner = useIsAdminOrOwner(org);
|
|
61
63
|
const [loading, setLoading] = useState(true);
|
|
62
64
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
63
65
|
|
|
@@ -219,82 +221,102 @@ export default function OrgPage() {
|
|
|
219
221
|
)}
|
|
220
222
|
</AnimatePresence>
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{saveMsg}
|
|
267
|
-
</motion.p>
|
|
268
|
-
)}
|
|
269
|
-
</AnimatePresence>
|
|
270
|
-
<Button type="submit" variant="terminal" className="w-fit" disabled={saving}>
|
|
271
|
-
<AnimatePresence mode="wait">
|
|
272
|
-
{saveSuccess ? (
|
|
273
|
-
<motion.span
|
|
274
|
-
key="success"
|
|
275
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
276
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
277
|
-
exit={{ opacity: 0, scale: 0.8 }}
|
|
278
|
-
className="flex items-center gap-1"
|
|
224
|
+
{isAdminOrOwner ? (
|
|
225
|
+
<Card>
|
|
226
|
+
<CardHeader>
|
|
227
|
+
<CardTitle>Organization Details</CardTitle>
|
|
228
|
+
<CardDescription>Update your organization name and billing email</CardDescription>
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardContent>
|
|
231
|
+
<form onSubmit={handleSave} className="flex flex-col gap-4">
|
|
232
|
+
<div className="flex flex-col gap-2">
|
|
233
|
+
<Label htmlFor="org-name">Organization name</Label>
|
|
234
|
+
<Input
|
|
235
|
+
id="org-name"
|
|
236
|
+
value={orgName}
|
|
237
|
+
onChange={(e) => setOrgName(e.target.value)}
|
|
238
|
+
required
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex flex-col gap-2">
|
|
242
|
+
<Label htmlFor="org-slug">Slug</Label>
|
|
243
|
+
<Input
|
|
244
|
+
id="org-slug"
|
|
245
|
+
value={orgSlug}
|
|
246
|
+
onChange={(e) => setOrgSlug(e.target.value)}
|
|
247
|
+
placeholder="my-org"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex flex-col gap-2">
|
|
251
|
+
<Label htmlFor="billing-email">Billing email</Label>
|
|
252
|
+
<Input
|
|
253
|
+
id="billing-email"
|
|
254
|
+
type="email"
|
|
255
|
+
value={billingEmail}
|
|
256
|
+
onChange={(e) => setBillingEmail(e.target.value)}
|
|
257
|
+
required
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
<AnimatePresence>
|
|
261
|
+
{saveMsg && (
|
|
262
|
+
<motion.p
|
|
263
|
+
initial={{ opacity: 0, y: -4 }}
|
|
264
|
+
animate={{ opacity: 1, y: 0 }}
|
|
265
|
+
exit={{ opacity: 0, y: -4 }}
|
|
266
|
+
transition={{ duration: 0.15 }}
|
|
267
|
+
className="text-sm text-terminal"
|
|
279
268
|
>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
</motion.span>
|
|
283
|
-
) : (
|
|
284
|
-
<motion.span
|
|
285
|
-
key="save"
|
|
286
|
-
initial={{ opacity: 0 }}
|
|
287
|
-
animate={{ opacity: 1 }}
|
|
288
|
-
exit={{ opacity: 0 }}
|
|
289
|
-
>
|
|
290
|
-
{saving ? "Saving..." : "Save changes"}
|
|
291
|
-
</motion.span>
|
|
269
|
+
{saveMsg}
|
|
270
|
+
</motion.p>
|
|
292
271
|
)}
|
|
293
272
|
</AnimatePresence>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
273
|
+
<Button type="submit" variant="terminal" className="w-fit" disabled={saving}>
|
|
274
|
+
<AnimatePresence mode="wait">
|
|
275
|
+
{saveSuccess ? (
|
|
276
|
+
<motion.span
|
|
277
|
+
key="success"
|
|
278
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
279
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
280
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
281
|
+
className="flex items-center gap-1"
|
|
282
|
+
>
|
|
283
|
+
<CheckIcon className="size-4" />
|
|
284
|
+
Saved
|
|
285
|
+
</motion.span>
|
|
286
|
+
) : (
|
|
287
|
+
<motion.span
|
|
288
|
+
key="save"
|
|
289
|
+
initial={{ opacity: 0 }}
|
|
290
|
+
animate={{ opacity: 1 }}
|
|
291
|
+
exit={{ opacity: 0 }}
|
|
292
|
+
>
|
|
293
|
+
{saving ? "Saving..." : "Save changes"}
|
|
294
|
+
</motion.span>
|
|
295
|
+
)}
|
|
296
|
+
</AnimatePresence>
|
|
297
|
+
</Button>
|
|
298
|
+
</form>
|
|
299
|
+
</CardContent>
|
|
300
|
+
</Card>
|
|
301
|
+
) : (
|
|
302
|
+
<Card>
|
|
303
|
+
<CardHeader>
|
|
304
|
+
<CardTitle>Organization Details</CardTitle>
|
|
305
|
+
<CardDescription>You are a member of this organization</CardDescription>
|
|
306
|
+
</CardHeader>
|
|
307
|
+
<CardContent>
|
|
308
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
309
|
+
<div>
|
|
310
|
+
<span className="font-medium text-muted-foreground">Name:</span> {org.name}
|
|
311
|
+
</div>
|
|
312
|
+
<div>
|
|
313
|
+
<span className="font-medium text-muted-foreground">Slug:</span>{" "}
|
|
314
|
+
{org.slug ?? <span className="text-muted-foreground italic">not set</span>}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</CardContent>
|
|
318
|
+
</Card>
|
|
319
|
+
)}
|
|
298
320
|
|
|
299
321
|
<Separator />
|
|
300
322
|
|
|
@@ -326,7 +348,7 @@ export default function OrgPage() {
|
|
|
326
348
|
{org.members.length} member{org.members.length !== 1 ? "s" : ""}
|
|
327
349
|
</p>
|
|
328
350
|
</div>
|
|
329
|
-
<InviteDialog orgId={org.id} onInvited={load} />
|
|
351
|
+
{isAdminOrOwner && <InviteDialog orgId={org.id} onInvited={load} />}
|
|
330
352
|
</div>
|
|
331
353
|
|
|
332
354
|
<div className="rounded-md border overflow-x-auto">
|
|
@@ -337,7 +359,7 @@ export default function OrgPage() {
|
|
|
337
359
|
<TableHead>Email</TableHead>
|
|
338
360
|
<TableHead>Role</TableHead>
|
|
339
361
|
<TableHead>Joined</TableHead>
|
|
340
|
-
<TableHead className="w-[160px]" />
|
|
362
|
+
{isAdminOrOwner && <TableHead className="w-[160px]" />}
|
|
341
363
|
</TableRow>
|
|
342
364
|
</TableHeader>
|
|
343
365
|
<TableBody>
|
|
@@ -355,31 +377,33 @@ export default function OrgPage() {
|
|
|
355
377
|
year: "numeric",
|
|
356
378
|
})}
|
|
357
379
|
</TableCell>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
{isAdminOrOwner && (
|
|
381
|
+
<TableCell>
|
|
382
|
+
{member.role !== "owner" && (
|
|
383
|
+
<div className="flex gap-1">
|
|
384
|
+
<ChangeRoleSelect
|
|
385
|
+
currentRole={member.role}
|
|
386
|
+
onChangeRole={(role) => handleChangeRole(member.userId, role)}
|
|
387
|
+
/>
|
|
388
|
+
<TransferDialog
|
|
389
|
+
memberName={member.name}
|
|
390
|
+
onTransfer={() => handleTransfer(member.userId)}
|
|
391
|
+
/>
|
|
392
|
+
<RemoveMemberDialog
|
|
393
|
+
memberName={member.name}
|
|
394
|
+
onRemove={() => handleRemove(member.userId)}
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</TableCell>
|
|
399
|
+
)}
|
|
376
400
|
</TableRow>
|
|
377
401
|
))}
|
|
378
402
|
</TableBody>
|
|
379
403
|
</Table>
|
|
380
404
|
</div>
|
|
381
405
|
|
|
382
|
-
{org.invites.length > 0 && (
|
|
406
|
+
{isAdminOrOwner && org.invites.length > 0 && (
|
|
383
407
|
<>
|
|
384
408
|
<Separator />
|
|
385
409
|
<div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { Organization, OrgMember } from "@/lib/api";
|
|
5
|
+
import { useSession } from "@/lib/auth-client";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derives the current user's role from an already-fetched org object.
|
|
9
|
+
* Does NOT fetch org data — avoids duplicate requests and role-flash.
|
|
10
|
+
* Pass the org from the page's own load() call.
|
|
11
|
+
*/
|
|
12
|
+
export function useMyOrgRole(org: Organization | null): OrgMember["role"] | null {
|
|
13
|
+
const { data: session } = useSession();
|
|
14
|
+
|
|
15
|
+
return useMemo(() => {
|
|
16
|
+
if (!org || !session?.user?.id) return null;
|
|
17
|
+
|
|
18
|
+
const me = org.members?.find(
|
|
19
|
+
(m: OrgMember) => m.userId === session.user.id || m.email === session.user.email,
|
|
20
|
+
);
|
|
21
|
+
return me?.role ?? null;
|
|
22
|
+
}, [org, session?.user?.id, session?.user?.email]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if the current user is an admin or owner of the given org.
|
|
27
|
+
* Pass the org from the page's own load() call.
|
|
28
|
+
*/
|
|
29
|
+
export function useIsAdminOrOwner(org: Organization | null): boolean {
|
|
30
|
+
const role = useMyOrgRole(org);
|
|
31
|
+
return role === "admin" || role === "owner";
|
|
32
|
+
}
|
package/src/lib/org-api.ts
CHANGED
|
@@ -72,3 +72,13 @@ export async function createOrganization(data: {
|
|
|
72
72
|
}): Promise<{ id: string; name: string; slug: string }> {
|
|
73
73
|
return trpcVanilla.org.createOrganization.mutate(data);
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
export async function listMyOrganizations(): Promise<
|
|
77
|
+
Array<{ id: string; name: string; slug: string; role: "owner" | "admin" | "member" }>
|
|
78
|
+
> {
|
|
79
|
+
return trpcVanilla.org.listMyOrganizations.query(undefined);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function acceptInvite(token: string): Promise<{ orgId: string; orgName: string }> {
|
|
83
|
+
return trpcVanilla.org.acceptInvite.mutate({ token });
|
|
84
|
+
}
|
package/src/lib/trpc-types.ts
CHANGED
|
@@ -157,6 +157,7 @@ type AppRouterRecord = {
|
|
|
157
157
|
transferOwnership: AnyTRPCMutationProcedure;
|
|
158
158
|
deleteOrganization: AnyTRPCMutationProcedure;
|
|
159
159
|
listMyOrganizations: AnyTRPCQueryProcedure;
|
|
160
|
+
acceptInvite: AnyTRPCMutationProcedure;
|
|
160
161
|
orgBillingBalance: AnyTRPCQueryProcedure;
|
|
161
162
|
orgMemberUsage: AnyTRPCQueryProcedure;
|
|
162
163
|
orgBillingInfo: AnyTRPCQueryProcedure;
|