@simple-auth-kit/cli 1.3.2 → 1.4.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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/registry/admin-apps/nextjs/shared/.husky/commit-msg +1 -0
  3. package/registry/admin-apps/nextjs/shared/.husky/pre-commit +1 -0
  4. package/registry/admin-apps/nextjs/shared/.lintstagedrc.json +4 -0
  5. package/registry/admin-apps/nextjs/shared/.prettierrc.json +3 -0
  6. package/registry/admin-apps/nextjs/shared/commitlint.config.js +5 -0
  7. package/registry/admin-apps/nextjs/shared/eslint.config.js +28 -0
  8. package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
  9. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
  10. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
  11. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
  12. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
  13. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
  14. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
  15. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
  16. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
  17. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
  18. package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
  19. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
  20. package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
  21. package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
  22. package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
  23. package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
  24. package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
  25. package/registry/admin-apps/react/shared/eslint.config.js +38 -0
  26. package/registry/admin-apps/react/variants/base/package.json +15 -2
  27. package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
  28. package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
  29. package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
  30. package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
  31. package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
  32. package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
  33. package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
  34. package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
  35. package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
  36. package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
  37. package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
  38. package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
  39. package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
  40. package/registry/combos/express-drizzle/eslint.config.js +18 -0
  41. package/registry/combos/express-drizzle/package.json +5 -1
  42. package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
  43. package/registry/combos/express-prisma/eslint.config.js +18 -0
  44. package/registry/combos/express-prisma/package.json +5 -1
  45. package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
  46. package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
  47. package/registry/combos/nestjs-drizzle/package.json +5 -1
  48. package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
  49. package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
  50. package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
  51. package/registry/combos/nestjs-prisma/package.json +5 -1
  52. package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
  53. package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
  54. package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
  55. package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
  56. package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
  57. package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
  58. package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
  59. package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
  60. package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
  61. package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
  62. package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
  63. package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
  64. package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
  65. package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
  66. package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
  67. package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
  68. package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
  69. package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
  70. package/registry/mobile-apps/expo/variants/base/package.json +12 -3
  71. package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
  72. package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
  73. package/registry.json +13 -36
  74. package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
  75. package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
@@ -3,8 +3,11 @@
3
3
  import { useAbility } from "@casl/react";
4
4
  import Link from "next/link";
5
5
  import { useParams, useRouter } from "next/navigation";
6
- import { useCallback, useEffect, useState } from "react";
7
- import { AuthApiError, type LanguageSummary } from "@simple-auth-kit/auth-client";
6
+ import { startTransition, useCallback, useEffect, useState } from "react";
7
+ import {
8
+ AuthApiError,
9
+ type LanguageSummary,
10
+ } from "@simple-auth-kit/auth-client";
8
11
  import { format } from "date-fns";
9
12
  import { PencilIcon } from "lucide-react";
10
13
  import { toast } from "sonner";
@@ -12,7 +15,13 @@ import { Breadcrumb } from "@/components/breadcrumb";
12
15
  import { PermissionRequired } from "@/components/permission-required";
13
16
  import { Badge } from "@/components/ui/badge";
14
17
  import { Button, buttonVariants } from "@/components/ui/button";
15
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
18
+ import {
19
+ Card,
20
+ CardContent,
21
+ CardDescription,
22
+ CardHeader,
23
+ CardTitle,
24
+ } from "@/components/ui/card";
16
25
  import {
17
26
  Dialog,
18
27
  DialogContent,
@@ -22,7 +31,12 @@ import {
22
31
  DialogTitle,
23
32
  DialogTrigger,
24
33
  } from "@/components/ui/dialog";
25
- import { PERMISSIONS, hasPermission, missingPermissionHint, type AppAbility } from "@/lib/ability";
34
+ import {
35
+ PERMISSIONS,
36
+ hasPermission,
37
+ missingPermissionHint,
38
+ type AppAbility,
39
+ } from "@/lib/ability";
26
40
  import { authClient } from "@/lib/auth-client";
27
41
 
28
42
  function apiErrorMessage(err: unknown, fallback: string): string {
@@ -65,20 +79,35 @@ export default function LanguageDetailPage() {
65
79
 
66
80
  useEffect(() => {
67
81
  if (!canRead) return;
68
- void load();
82
+ startTransition(() => void load());
69
83
  }, [canRead, load]);
70
84
 
71
- if (!canRead) return <PermissionRequired permission={PERMISSIONS.languagesRead} what="Language details" />;
85
+ if (!canRead)
86
+ return (
87
+ <PermissionRequired
88
+ permission={PERMISSIONS.languagesRead}
89
+ what="Language details"
90
+ />
91
+ );
72
92
 
73
93
  async function toggleActive() {
74
94
  if (!language) return;
75
95
  try {
76
96
  if (language.isActive) await authClient.deactivateLanguage(id);
77
97
  else await authClient.activateLanguage(id);
78
- setLanguage((prev) => (prev ? { ...prev, isActive: !prev.isActive } : prev));
79
- toast.success(language.isActive ? "Language deactivated." : "Language activated.");
98
+ setLanguage((prev) =>
99
+ prev ? { ...prev, isActive: !prev.isActive } : prev,
100
+ );
101
+ toast.success(
102
+ language.isActive ? "Language deactivated." : "Language activated.",
103
+ );
80
104
  } catch (err) {
81
- toast.error(apiErrorMessage(err, "Couldn't change this language's status. Try again."));
105
+ toast.error(
106
+ apiErrorMessage(
107
+ err,
108
+ "Couldn't change this language's status. Try again.",
109
+ ),
110
+ );
82
111
  }
83
112
  }
84
113
 
@@ -89,7 +118,9 @@ export default function LanguageDetailPage() {
89
118
  toast.success("Language deleted.");
90
119
  router.push("/languages");
91
120
  } catch (err) {
92
- toast.error(apiErrorMessage(err, "Couldn't delete this language. Try again."));
121
+ toast.error(
122
+ apiErrorMessage(err, "Couldn't delete this language. Try again."),
123
+ );
93
124
  setDeleting(false);
94
125
  setDeleteOpen(false);
95
126
  }
@@ -97,9 +128,16 @@ export default function LanguageDetailPage() {
97
128
 
98
129
  return (
99
130
  <div className="flex flex-col gap-4">
100
- <Breadcrumb items={[{ title: "Languages", href: "/languages" }, { title: language?.name ?? "Details", href: `/languages/${id}` }]} />
131
+ <Breadcrumb
132
+ items={[
133
+ { title: "Languages", href: "/languages" },
134
+ { title: language?.name ?? "Details", href: `/languages/${id}` },
135
+ ]}
136
+ />
101
137
 
102
- {loading && !language && <p className="text-sm text-muted-foreground">Loading…</p>}
138
+ {loading && !language && (
139
+ <p className="text-sm text-muted-foreground">Loading…</p>
140
+ )}
103
141
 
104
142
  {language && (
105
143
  <>
@@ -108,16 +146,26 @@ export default function LanguageDetailPage() {
108
146
  <div>
109
147
  <CardTitle>{language.name}</CardTitle>
110
148
  <CardDescription>
111
- Created: {format(new Date(language.createdAt), "dd MMM yyyy")} · Updated: {format(new Date(language.updatedAt), "dd MMM yyyy")}
149
+ Created: {format(new Date(language.createdAt), "dd MMM yyyy")}{" "}
150
+ · Updated:{" "}
151
+ {format(new Date(language.updatedAt), "dd MMM yyyy")}
112
152
  </CardDescription>
113
153
  </div>
114
154
  <div className="flex items-center gap-1.5">
115
- {language.isDefault && <Badge variant="secondary">Default</Badge>}
116
- <Badge variant={language.isActive ? "success" : "destructive"}>{language.isActive ? "Active" : "Inactive"}</Badge>
155
+ {language.isDefault && (
156
+ <Badge variant="secondary">Default</Badge>
157
+ )}
158
+ <Badge variant={language.isActive ? "success" : "destructive"}>
159
+ {language.isActive ? "Active" : "Inactive"}
160
+ </Badge>
117
161
  <Link
118
162
  href={`/languages/${id}/edit`}
119
163
  className={buttonVariants({ variant: "outline", size: "sm" })}
120
- title={canManage ? undefined : missingPermissionHint(PERMISSIONS.languagesManage)}
164
+ title={
165
+ canManage
166
+ ? undefined
167
+ : missingPermissionHint(PERMISSIONS.languagesManage)
168
+ }
121
169
  aria-disabled={!canManage}
122
170
  onClick={(e) => !canManage && e.preventDefault()}
123
171
  >
@@ -131,7 +179,12 @@ export default function LanguageDetailPage() {
131
179
  {field("Name", language.name)}
132
180
  {field("Native name", language.nativeName)}
133
181
  {field("Code", language.code)}
134
- {field("Direction", language.direction === "rtl" ? "Right to left (RTL)" : "Left to right (LTR)")}
182
+ {field(
183
+ "Direction",
184
+ language.direction === "rtl"
185
+ ? "Right to left (RTL)"
186
+ : "Left to right (LTR)",
187
+ )}
135
188
  {field("Default language", language.isDefault ? "Yes" : "No")}
136
189
  </div>
137
190
  </CardContent>
@@ -145,7 +198,11 @@ export default function LanguageDetailPage() {
145
198
  <Button
146
199
  variant={language.isActive ? "destructive" : "outline"}
147
200
  disabled={!canStatus}
148
- title={canStatus ? undefined : missingPermissionHint(PERMISSIONS.languagesStatus)}
201
+ title={
202
+ canStatus
203
+ ? undefined
204
+ : missingPermissionHint(PERMISSIONS.languagesStatus)
205
+ }
149
206
  onClick={() => void toggleActive()}
150
207
  >
151
208
  {language.isActive ? "Deactivate" : "Activate"}
@@ -153,7 +210,15 @@ export default function LanguageDetailPage() {
153
210
 
154
211
  <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
155
212
  <DialogTrigger asChild>
156
- <Button variant="destructive" disabled={!canManage} title={canManage ? undefined : missingPermissionHint(PERMISSIONS.languagesManage)}>
213
+ <Button
214
+ variant="destructive"
215
+ disabled={!canManage}
216
+ title={
217
+ canManage
218
+ ? undefined
219
+ : missingPermissionHint(PERMISSIONS.languagesManage)
220
+ }
221
+ >
157
222
  Delete language
158
223
  </Button>
159
224
  </DialogTrigger>
@@ -161,14 +226,24 @@ export default function LanguageDetailPage() {
161
226
  <DialogHeader>
162
227
  <DialogTitle>Delete {language.name}?</DialogTitle>
163
228
  <DialogDescription>
164
- This soft-deletes the language: it stops appearing in listings and pickers, but the row is kept for audit purposes.
229
+ This soft-deletes the language: it stops appearing in
230
+ listings and pickers, but the row is kept for audit
231
+ purposes.
165
232
  </DialogDescription>
166
233
  </DialogHeader>
167
234
  <DialogFooter>
168
- <Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
235
+ <Button
236
+ variant="outline"
237
+ onClick={() => setDeleteOpen(false)}
238
+ disabled={deleting}
239
+ >
169
240
  Cancel
170
241
  </Button>
171
- <Button variant="destructive" onClick={() => void handleDelete()} disabled={deleting}>
242
+ <Button
243
+ variant="destructive"
244
+ onClick={() => void handleDelete()}
245
+ disabled={deleting}
246
+ >
172
247
  {deleting ? "Deleting…" : "Delete language"}
173
248
  </Button>
174
249
  </DialogFooter>
@@ -3,18 +3,36 @@
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { useAbility } from "@casl/react";
5
5
  import { useParams, useRouter } from "next/navigation";
6
- import { useEffect, useMemo, useState } from "react";
6
+ import { startTransition, useEffect, useMemo, useState } from "react";
7
7
  import { useForm } from "react-hook-form";
8
- import type { DefinePermissionInput, PermissionSummary } from "@simple-auth-kit/auth-client";
8
+ import type {
9
+ DefinePermissionInput,
10
+ PermissionSummary,
11
+ } from "@simple-auth-kit/auth-client";
9
12
  import { toast } from "sonner";
10
13
  import { Breadcrumb } from "@/components/breadcrumb";
11
14
  import { FormErrorAlert } from "@/components/form-error-alert";
12
15
  import { PermissionRequired } from "@/components/permission-required";
13
16
  import { Button } from "@/components/ui/button";
14
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardFooter,
22
+ CardHeader,
23
+ CardTitle,
24
+ } from "@/components/ui/card";
15
25
  import { Checkbox } from "@/components/ui/checkbox";
16
26
  import { Combobox, type ComboboxOptions } from "@/components/ui/combobox";
17
- import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
27
+ import {
28
+ Form,
29
+ FormControl,
30
+ FormDescription,
31
+ FormField,
32
+ FormItem,
33
+ FormLabel,
34
+ FormMessage,
35
+ } from "@/components/ui/form";
18
36
  import { Heading } from "@/components/ui/heading";
19
37
  import { Input } from "@/components/ui/input";
20
38
  import { Separator } from "@/components/ui/separator";
@@ -49,37 +67,48 @@ export default function EditPermissionPage() {
49
67
 
50
68
  useEffect(() => {
51
69
  if (!canDefine) return;
52
- setLoading(true);
53
- // No single-permission endpoint on the client — load the catalog and find by id.
54
- authClient
55
- .listPermissions()
56
- .then((catalog) => {
57
- setPermissions(catalog);
58
- const permission = catalog.find((p) => p.id === id) ?? null;
59
- setEditing(permission);
60
- if (permission) {
61
- form.reset({
62
- slug: permission.slug,
63
- displayName: permission.displayName,
64
- description: permission.description ?? "",
65
- group: permission.group,
66
- groupOrder: permission.groupOrder,
67
- order: permission.order,
68
- isActive: permission.isActive,
69
- });
70
- } else {
71
- toast.error("Couldn't find this permission.");
72
- }
73
- })
74
- .catch((err) => toast.error(errorMessage(err)))
75
- .finally(() => setLoading(false));
70
+ startTransition(() => {
71
+ setLoading(true);
72
+ // No single-permission endpoint on the client — load the catalog and find by id.
73
+ authClient
74
+ .listPermissions()
75
+ .then((catalog) => {
76
+ setPermissions(catalog);
77
+ const permission = catalog.find((p) => p.id === id) ?? null;
78
+ setEditing(permission);
79
+ if (permission) {
80
+ form.reset({
81
+ slug: permission.slug,
82
+ displayName: permission.displayName,
83
+ description: permission.description ?? "",
84
+ group: permission.group,
85
+ groupOrder: permission.groupOrder,
86
+ order: permission.order,
87
+ isActive: permission.isActive,
88
+ });
89
+ } else {
90
+ toast.error("Couldn't find this permission.");
91
+ }
92
+ })
93
+ .catch((err) => toast.error(errorMessage(err)))
94
+ .finally(() => setLoading(false));
95
+ });
76
96
  // `form` is stable across renders (react-hook-form memoizes it), so it's safe to omit here.
77
97
  // eslint-disable-next-line react-hooks/exhaustive-deps
78
98
  }, [canDefine, id]);
79
99
 
80
- const groupOptions = useMemo(() => deriveGroupOptions(permissions), [permissions]);
100
+ const groupOptions = useMemo(
101
+ () => deriveGroupOptions(permissions),
102
+ [permissions],
103
+ );
81
104
 
82
- if (!canDefine) return <PermissionRequired permission={PERMISSIONS.permissionsDefine} what="Editing a permission" />;
105
+ if (!canDefine)
106
+ return (
107
+ <PermissionRequired
108
+ permission={PERMISSIONS.permissionsDefine}
109
+ what="Editing a permission"
110
+ />
111
+ );
83
112
 
84
113
  function handleGroupSelect(option: ComboboxOptions) {
85
114
  form.setValue("group", option.value, { shouldValidate: true });
@@ -119,11 +148,16 @@ export default function EditPermissionPage() {
119
148
  ]}
120
149
  />
121
150
  <div className="flex items-start justify-between">
122
- <Heading title="Edit Permission" description="Update permission details" />
151
+ <Heading
152
+ title="Edit Permission"
153
+ description="Update permission details"
154
+ />
123
155
  </div>
124
156
  <Separator />
125
157
 
126
- {loading && !editing && <p className="text-sm text-muted-foreground">Loading…</p>}
158
+ {loading && !editing && (
159
+ <p className="text-sm text-muted-foreground">Loading…</p>
160
+ )}
127
161
 
128
162
  {editing && (
129
163
  <Card>
@@ -131,16 +165,26 @@ export default function EditPermissionPage() {
131
165
  <CardContent>
132
166
  <FormErrorAlert messages={formError} />
133
167
  <Form {...form}>
134
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full">
168
+ <form
169
+ onSubmit={form.handleSubmit(onSubmit)}
170
+ className="space-y-8 w-full"
171
+ >
135
172
  <Card className="w-full">
136
173
  <CardHeader className="border-b bg-muted/50">
137
- <CardTitle className="text-2xl">Permission Information</CardTitle>
138
- <CardDescription className="text-base">Editing is an upsert keyed on slug — the slug itself can&apos;t change here</CardDescription>
174
+ <CardTitle className="text-2xl">
175
+ Permission Information
176
+ </CardTitle>
177
+ <CardDescription className="text-base">
178
+ Editing is an upsert keyed on slug — the slug itself
179
+ can&apos;t change here
180
+ </CardDescription>
139
181
  </CardHeader>
140
182
  <CardContent className="pt-6">
141
183
  <div className="space-y-6">
142
184
  <div className="space-y-4">
143
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Basic Information</h3>
185
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
186
+ Basic Information
187
+ </h3>
144
188
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
145
189
  <FormField
146
190
  control={form.control}
@@ -203,7 +247,9 @@ export default function EditPermissionPage() {
203
247
  </div>
204
248
 
205
249
  <div className="space-y-4">
206
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Grouping</h3>
250
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
251
+ Grouping
252
+ </h3>
207
253
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
208
254
  <FormField
209
255
  control={form.control}
@@ -238,11 +284,20 @@ export default function EditPermissionPage() {
238
284
  type="number"
239
285
  disabled={saving}
240
286
  value={field.value ?? ""}
241
- onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
287
+ onChange={(e) =>
288
+ field.onChange(
289
+ e.target.value === ""
290
+ ? undefined
291
+ : Number(e.target.value),
292
+ )
293
+ }
242
294
  className="bg-background border-2 focus:border-purple-500 transition-colors"
243
295
  />
244
296
  </FormControl>
245
- <FormDescription>Derived from the group — override to reorder groups.</FormDescription>
297
+ <FormDescription>
298
+ Derived from the group — override to reorder
299
+ groups.
300
+ </FormDescription>
246
301
  <FormMessage />
247
302
  </FormItem>
248
303
  )}
@@ -258,11 +313,20 @@ export default function EditPermissionPage() {
258
313
  type="number"
259
314
  disabled={saving}
260
315
  value={field.value ?? ""}
261
- onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
316
+ onChange={(e) =>
317
+ field.onChange(
318
+ e.target.value === ""
319
+ ? undefined
320
+ : Number(e.target.value),
321
+ )
322
+ }
262
323
  className="bg-background border-2 focus:border-purple-500 transition-colors"
263
324
  />
264
325
  </FormControl>
265
- <FormDescription>Derived from the group&apos;s existing permissions.</FormDescription>
326
+ <FormDescription>
327
+ Derived from the group&apos;s existing
328
+ permissions.
329
+ </FormDescription>
266
330
  <FormMessage />
267
331
  </FormItem>
268
332
  )}
@@ -279,11 +343,21 @@ export default function EditPermissionPage() {
279
343
  render={({ field }) => (
280
344
  <FormItem className="flex flex-row items-center space-x-3 space-y-0 rounded-lg border border-purple-500 bg-purple-50 dark:bg-purple-950/20 p-4">
281
345
  <FormControl>
282
- <Checkbox checked={field.value ?? true} onCheckedChange={(checked) => field.onChange(checked === true)} />
346
+ <Checkbox
347
+ checked={field.value ?? true}
348
+ onCheckedChange={(checked) =>
349
+ field.onChange(checked === true)
350
+ }
351
+ />
283
352
  </FormControl>
284
353
  <div className="space-y-1 leading-none">
285
- <FormLabel className="text-base font-medium">Active Status</FormLabel>
286
- <FormDescription className="text-sm">This permission will be active and can be assigned to roles</FormDescription>
354
+ <FormLabel className="text-base font-medium">
355
+ Active Status
356
+ </FormLabel>
357
+ <FormDescription className="text-sm">
358
+ This permission will be active and can be assigned
359
+ to roles
360
+ </FormDescription>
287
361
  </div>
288
362
  </FormItem>
289
363
  )}
@@ -291,10 +365,19 @@ export default function EditPermissionPage() {
291
365
  </div>
292
366
 
293
367
  <CardFooter className="flex justify-center gap-4 mt-8 pb-8">
294
- <Button type="button" variant="outline" onClick={() => router.push("/permissions")} disabled={saving}>
368
+ <Button
369
+ type="button"
370
+ variant="outline"
371
+ onClick={() => router.push("/permissions")}
372
+ disabled={saving}
373
+ >
295
374
  Cancel
296
375
  </Button>
297
- <Button disabled={saving} className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-600 dark:hover:bg-purple-700 min-w-32" type="submit">
376
+ <Button
377
+ disabled={saving}
378
+ className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-600 dark:hover:bg-purple-700 min-w-32"
379
+ type="submit"
380
+ >
298
381
  {saving ? "Updating..." : "Update Permission"}
299
382
  </Button>
300
383
  </CardFooter>
@@ -3,15 +3,24 @@
3
3
  import { useAbility } from "@casl/react";
4
4
  import Link from "next/link";
5
5
  import { useParams } from "next/navigation";
6
- import { useCallback, useEffect, useState } from "react";
7
- import { AuthApiError, type PermissionSummary } from "@simple-auth-kit/auth-client";
6
+ import { startTransition, useCallback, useEffect, useState } from "react";
7
+ import {
8
+ AuthApiError,
9
+ type PermissionSummary,
10
+ } from "@simple-auth-kit/auth-client";
8
11
  import { PencilIcon } from "lucide-react";
9
12
  import { toast } from "sonner";
10
13
  import { Breadcrumb } from "@/components/breadcrumb";
11
14
  import { PermissionRequired } from "@/components/permission-required";
12
15
  import { Badge } from "@/components/ui/badge";
13
16
  import { Button, buttonVariants } from "@/components/ui/button";
14
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from "@/components/ui/card";
15
24
  import { PERMISSIONS, hasPermission, type AppAbility } from "@/lib/ability";
16
25
  import { authClient } from "@/lib/auth-client";
17
26
 
@@ -52,27 +61,57 @@ export default function PermissionDetailPage() {
52
61
 
53
62
  useEffect(() => {
54
63
  if (!canRead) return;
55
- void load();
64
+ startTransition(() => void load());
56
65
  }, [canRead, load]);
57
66
 
58
- if (!canRead) return <PermissionRequired permission={PERMISSIONS.permissionsRead} what="Permission details" />;
67
+ if (!canRead)
68
+ return (
69
+ <PermissionRequired
70
+ permission={PERMISSIONS.permissionsRead}
71
+ what="Permission details"
72
+ />
73
+ );
59
74
 
60
75
  async function toggleActive() {
61
76
  if (!permission) return;
62
77
  try {
63
- await authClient.definePermission({ slug: permission.slug, isActive: !permission.isActive });
64
- setPermission((prev) => (prev ? { ...prev, isActive: !prev.isActive } : prev));
65
- toast.success(permission.isActive ? "Permission deactivated." : "Permission activated.");
78
+ await authClient.definePermission({
79
+ slug: permission.slug,
80
+ isActive: !permission.isActive,
81
+ });
82
+ setPermission((prev) =>
83
+ prev ? { ...prev, isActive: !prev.isActive } : prev,
84
+ );
85
+ toast.success(
86
+ permission.isActive
87
+ ? "Permission deactivated."
88
+ : "Permission activated.",
89
+ );
66
90
  } catch (err) {
67
- toast.error(apiErrorMessage(err, "Couldn't change this permission's status. Try again."));
91
+ toast.error(
92
+ apiErrorMessage(
93
+ err,
94
+ "Couldn't change this permission's status. Try again.",
95
+ ),
96
+ );
68
97
  }
69
98
  }
70
99
 
71
100
  return (
72
101
  <div className="flex flex-col gap-4">
73
- <Breadcrumb items={[{ title: "Permissions", href: "/permissions" }, { title: permission?.displayName ?? "Details", href: `/permissions/${id}` }]} />
102
+ <Breadcrumb
103
+ items={[
104
+ { title: "Permissions", href: "/permissions" },
105
+ {
106
+ title: permission?.displayName ?? "Details",
107
+ href: `/permissions/${id}`,
108
+ },
109
+ ]}
110
+ />
74
111
 
75
- {loading && !permission && <p className="text-sm text-muted-foreground">Loading…</p>}
112
+ {loading && !permission && (
113
+ <p className="text-sm text-muted-foreground">Loading…</p>
114
+ )}
76
115
 
77
116
  {permission && (
78
117
  <>
@@ -85,8 +124,15 @@ export default function PermissionDetailPage() {
85
124
  </CardDescription>
86
125
  </div>
87
126
  <div className="flex items-center gap-1.5">
88
- <Badge variant={permission.isActive ? "success" : "destructive"}>{permission.isActive ? "Active" : "Inactive"}</Badge>
89
- <Link href={`/permissions/${id}/edit`} className={buttonVariants({ variant: "outline", size: "sm" })}>
127
+ <Badge
128
+ variant={permission.isActive ? "success" : "destructive"}
129
+ >
130
+ {permission.isActive ? "Active" : "Inactive"}
131
+ </Badge>
132
+ <Link
133
+ href={`/permissions/${id}/edit`}
134
+ className={buttonVariants({ variant: "outline", size: "sm" })}
135
+ >
90
136
  <PencilIcon />
91
137
  Edit
92
138
  </Link>
@@ -107,7 +153,11 @@ export default function PermissionDetailPage() {
107
153
  <CardTitle>Danger zone</CardTitle>
108
154
  </CardHeader>
109
155
  <CardContent className="flex flex-wrap gap-2">
110
- <Button variant={permission.isActive ? "destructive" : "outline"} disabled={!canDefine} onClick={() => void toggleActive()}>
156
+ <Button
157
+ variant={permission.isActive ? "destructive" : "outline"}
158
+ disabled={!canDefine}
159
+ onClick={() => void toggleActive()}
160
+ >
111
161
  {permission.isActive ? "Deactivate" : "Activate"}
112
162
  </Button>
113
163
  </CardContent>