@simple-auth-kit/cli 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/copy.ts +67 -10
- package/package.json +1 -1
- package/registry/admin-apps/nextjs/shared/.husky/commit-msg +1 -0
- package/registry/admin-apps/nextjs/shared/.husky/pre-commit +1 -0
- package/registry/admin-apps/nextjs/shared/.lintstagedrc.json +4 -0
- package/registry/admin-apps/nextjs/shared/.prettierrc.json +3 -0
- package/registry/admin-apps/nextjs/shared/commitlint.config.js +5 -0
- package/registry/admin-apps/nextjs/shared/eslint.config.js +28 -0
- package/registry/admin-apps/nextjs/shared/gitignore +11 -0
- package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
- package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
- package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
- package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
- package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
- package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
- package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
- package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
- package/registry/admin-apps/react/shared/eslint.config.js +38 -0
- package/registry/admin-apps/react/shared/gitignore +17 -0
- package/registry/admin-apps/react/variants/base/package.json +15 -2
- package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
- package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
- package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
- package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
- package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
- package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
- package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
- package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
- package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
- package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
- package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
- package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
- package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
- package/registry/combos/express-drizzle/eslint.config.js +18 -0
- package/registry/combos/express-drizzle/package.json +5 -1
- package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
- package/registry/combos/express-prisma/eslint.config.js +18 -0
- package/registry/combos/express-prisma/package.json +5 -1
- package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
- package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
- package/registry/combos/nestjs-drizzle/package.json +5 -1
- package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
- package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
- package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
- package/registry/combos/nestjs-prisma/package.json +5 -1
- package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
- package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
- package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
- package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
- package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
- package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
- package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
- package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
- package/registry/mobile-apps/bare-rn/shared/gitignore +78 -0
- package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
- package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
- package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
- package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
- package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
- package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
- package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
- package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
- package/registry/mobile-apps/expo/shared/gitignore +44 -0
- package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
- package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
- package/registry/mobile-apps/expo/variants/base/package.json +12 -3
- package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
- package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
- package/registry.json +13 -36
- package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
- package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
package/lib/copy.ts
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
readdir,
|
|
5
|
+
readFile,
|
|
6
|
+
rm,
|
|
7
|
+
rmdir,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
3
10
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
11
|
|
|
5
12
|
// Never copied out of the registry: build output, the registry's own package/tsconfig wiring,
|
|
6
13
|
// and the per-variant test harness. A consumer gets source, not this repo's scaffolding.
|
|
7
|
-
export const NEVER_COPY = new Set([
|
|
14
|
+
export const NEVER_COPY = new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
"generated",
|
|
18
|
+
"test",
|
|
19
|
+
"scripts",
|
|
20
|
+
".variant",
|
|
21
|
+
"package.json",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
".env",
|
|
24
|
+
]);
|
|
8
25
|
|
|
9
26
|
// For "scaffold" installs (admin/mobile apps materialized as whole standalone projects, not
|
|
10
27
|
// merged into an existing one): package.json/tsconfig.json are real, consumer-facing content
|
|
11
28
|
// here, not the registry's own dev wiring, so they're copied like any other file.
|
|
12
|
-
export const SCAFFOLD_NEVER_COPY = new Set([
|
|
29
|
+
export const SCAFFOLD_NEVER_COPY = new Set([
|
|
30
|
+
"node_modules",
|
|
31
|
+
"dist",
|
|
32
|
+
"generated",
|
|
33
|
+
"test",
|
|
34
|
+
"scripts",
|
|
35
|
+
".variant",
|
|
36
|
+
".env",
|
|
37
|
+
]);
|
|
13
38
|
|
|
14
39
|
export interface CopyOptions {
|
|
15
40
|
/**
|
|
@@ -79,7 +104,8 @@ export interface CopyResult {
|
|
|
79
104
|
updated: string[];
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
export const sha256 = (content: string) =>
|
|
107
|
+
export const sha256 = (content: string) =>
|
|
108
|
+
createHash("sha256").update(content).digest("hex");
|
|
83
109
|
export const toPosix = (path: string) => path.split(sep).join("/");
|
|
84
110
|
|
|
85
111
|
async function readIfExists(path: string): Promise<string | null> {
|
|
@@ -90,7 +116,13 @@ async function readIfExists(path: string): Promise<string | null> {
|
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
export async function copyOneFile(
|
|
119
|
+
export async function copyOneFile(
|
|
120
|
+
srcPath: string,
|
|
121
|
+
destPath: string,
|
|
122
|
+
destRoot: string,
|
|
123
|
+
opts: CopyOptions,
|
|
124
|
+
result: CopyResult,
|
|
125
|
+
): Promise<void> {
|
|
94
126
|
const rel = toPosix(relative(destRoot, destPath));
|
|
95
127
|
|
|
96
128
|
if (opts.ignore?.includes(rel)) {
|
|
@@ -113,7 +145,12 @@ export async function copyOneFile(srcPath: string, destPath: string, destRoot: s
|
|
|
113
145
|
const existing = await readIfExists(destPath);
|
|
114
146
|
const recorded = opts.previous?.[rel];
|
|
115
147
|
const forced = opts.force || opts.forcePaths?.has(rel);
|
|
116
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
!forced &&
|
|
150
|
+
existing !== null &&
|
|
151
|
+
recorded !== undefined &&
|
|
152
|
+
sha256(existing) !== recorded
|
|
153
|
+
) {
|
|
117
154
|
result.skipped.push(rel);
|
|
118
155
|
result.manifest[rel] = sha256(existing);
|
|
119
156
|
return;
|
|
@@ -127,7 +164,11 @@ export async function copyOneFile(srcPath: string, destPath: string, destRoot: s
|
|
|
127
164
|
|
|
128
165
|
result.updated.push(rel);
|
|
129
166
|
result.manifest[rel] = newHash;
|
|
130
|
-
opts.collectDiffs?.push({
|
|
167
|
+
opts.collectDiffs?.push({
|
|
168
|
+
path: rel,
|
|
169
|
+
oldContent: existing,
|
|
170
|
+
newContent: content,
|
|
171
|
+
});
|
|
131
172
|
if (opts.dryRun) return;
|
|
132
173
|
|
|
133
174
|
await mkdir(dirname(destPath), { recursive: true });
|
|
@@ -135,15 +176,31 @@ export async function copyOneFile(srcPath: string, destPath: string, destRoot: s
|
|
|
135
176
|
}
|
|
136
177
|
|
|
137
178
|
/** Recursively copies srcDir into destDir, rewriting the placeholder core import alias in .ts files. */
|
|
138
|
-
export async function copyDir(
|
|
139
|
-
|
|
179
|
+
export async function copyDir(
|
|
180
|
+
srcDir: string,
|
|
181
|
+
destDir: string,
|
|
182
|
+
opts: CopyOptions,
|
|
183
|
+
destRoot: string = destDir,
|
|
184
|
+
result?: CopyResult,
|
|
185
|
+
): Promise<CopyResult> {
|
|
186
|
+
const acc: CopyResult = result ?? {
|
|
187
|
+
manifest: {},
|
|
188
|
+
skipped: [],
|
|
189
|
+
ignored: [],
|
|
190
|
+
updated: [],
|
|
191
|
+
};
|
|
140
192
|
const skipNames = opts.neverCopy ?? NEVER_COPY;
|
|
141
193
|
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
142
194
|
|
|
143
195
|
for (const entry of entries) {
|
|
144
196
|
if (skipNames.has(entry.name)) continue;
|
|
145
197
|
const srcPath = join(srcDir, entry.name);
|
|
146
|
-
|
|
198
|
+
// Registry sources name this "gitignore" (no leading dot) because `npm publish` silently
|
|
199
|
+
// drops every literal `.gitignore` file from the package — a well-known npm packing quirk,
|
|
200
|
+
// not specific to this file. Renamed back to `.gitignore` only on the way out, so a
|
|
201
|
+
// consumer's scaffolded project still gets a real one.
|
|
202
|
+
const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
203
|
+
const destPath = join(destDir, destName);
|
|
147
204
|
|
|
148
205
|
if (entry.isDirectory()) {
|
|
149
206
|
await copyDir(srcPath, destPath, opts, destRoot, acc);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-auth-kit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "simple-auth-kit add <combo> — copies registry source (backend combos, admin consoles, mobile apps) into a consumer repo, shadcn-CLI-style. Nothing is ever installed as a runtime dependency of the consumer.",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit "$1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
|
2
|
+
|
|
3
|
+
const config = [
|
|
4
|
+
{
|
|
5
|
+
// src/components/ui/** is shadcn-generated (components.json), not hand-authored — same
|
|
6
|
+
// "vendored, don't lint" treatment this repo already gives it for Biome (the surviving
|
|
7
|
+
// biome-ignore-all comment on multi-selector.tsx).
|
|
8
|
+
ignores: [
|
|
9
|
+
".next/**",
|
|
10
|
+
"node_modules/**",
|
|
11
|
+
"out/**",
|
|
12
|
+
"dist/**",
|
|
13
|
+
"src/components/ui/**",
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
...nextCoreWebVitals,
|
|
17
|
+
{
|
|
18
|
+
rules: {
|
|
19
|
+
// TanStack Table's useReactTable() is on React Compiler's own documented list of
|
|
20
|
+
// incompatible APIs (it returns functions that can't be safely memoized) — the compiler
|
|
21
|
+
// already handles this correctly by skipping memoization for it; there's no code change
|
|
22
|
+
// that "fixes" a structural library incompatibility.
|
|
23
|
+
"react-hooks/incompatible-library": "off",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export default config;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"dev": "next dev -p 3000",
|
|
9
9
|
"build": "next build",
|
|
10
10
|
"start": "next start -p 3000",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"prepare": "husky"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
16
|
"@casl/ability": "^7.0.1",
|
|
@@ -48,11 +50,18 @@
|
|
|
48
50
|
"zod": "^4.2.1"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
53
|
+
"@commitlint/cli": "^21.2.2",
|
|
54
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
51
55
|
"@tailwindcss/postcss": "^4.3.3",
|
|
52
56
|
"@types/node": "^26.2.0",
|
|
53
57
|
"@types/react": "^19.2.9",
|
|
54
58
|
"@types/react-dom": "^19.2.4",
|
|
59
|
+
"eslint": "^9.39.5",
|
|
60
|
+
"eslint-config-next": "^16.3.5",
|
|
61
|
+
"husky": "^9.1.7",
|
|
62
|
+
"lint-staged": "^17.5.1",
|
|
63
|
+
"prettier": "^3.9.6",
|
|
55
64
|
"tailwindcss": "^4.3.3",
|
|
56
|
-
"typescript": "^
|
|
65
|
+
"typescript": "^6.0.3"
|
|
57
66
|
}
|
|
58
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useAbility } from "@casl/react";
|
|
4
|
-
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { startTransition, useCallback, useEffect, useState } from "react";
|
|
5
5
|
import type { AuditLogEntry, PageMeta } from "@simple-auth-kit/auth-client";
|
|
6
6
|
import { Breadcrumb } from "@/components/breadcrumb";
|
|
7
7
|
import { PermissionRequired } from "@/components/permission-required";
|
|
@@ -11,7 +11,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
11
11
|
import { DateTimePicker } from "@/components/ui/datetime-picker";
|
|
12
12
|
import { Input } from "@/components/ui/input";
|
|
13
13
|
import { Label } from "@/components/ui/label";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableHead,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableRow,
|
|
21
|
+
} from "@/components/ui/table";
|
|
15
22
|
import { PERMISSIONS, hasPermission, type AppAbility } from "@/lib/ability";
|
|
16
23
|
import { authClient } from "@/lib/auth-client";
|
|
17
24
|
import { errorMessage } from "@/lib/error";
|
|
@@ -55,11 +62,17 @@ export default function AuditLogPage() {
|
|
|
55
62
|
|
|
56
63
|
useEffect(() => {
|
|
57
64
|
if (!canRead) return;
|
|
58
|
-
void load(1);
|
|
65
|
+
startTransition(() => void load(1));
|
|
59
66
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
67
|
}, [canRead]);
|
|
61
68
|
|
|
62
|
-
if (!canRead)
|
|
69
|
+
if (!canRead)
|
|
70
|
+
return (
|
|
71
|
+
<PermissionRequired
|
|
72
|
+
permission={PERMISSIONS.auditLogRead}
|
|
73
|
+
what="The audit log"
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
63
76
|
|
|
64
77
|
return (
|
|
65
78
|
<div className="flex flex-col">
|
|
@@ -80,19 +93,39 @@ export default function AuditLogPage() {
|
|
|
80
93
|
>
|
|
81
94
|
<div className="flex flex-col gap-1.5">
|
|
82
95
|
<Label htmlFor="filterUserId">User ID</Label>
|
|
83
|
-
<Input
|
|
96
|
+
<Input
|
|
97
|
+
id="filterUserId"
|
|
98
|
+
value={userId}
|
|
99
|
+
onChange={(e) => setUserId(e.target.value)}
|
|
100
|
+
placeholder="uuid"
|
|
101
|
+
/>
|
|
84
102
|
</div>
|
|
85
103
|
<div className="flex flex-col gap-1.5">
|
|
86
104
|
<Label htmlFor="filterAction">Action</Label>
|
|
87
|
-
<Input
|
|
105
|
+
<Input
|
|
106
|
+
id="filterAction"
|
|
107
|
+
value={action}
|
|
108
|
+
onChange={(e) => setAction(e.target.value)}
|
|
109
|
+
placeholder="role_assigned"
|
|
110
|
+
/>
|
|
88
111
|
</div>
|
|
89
112
|
<div className="flex flex-col gap-1.5">
|
|
90
113
|
<Label>Since</Label>
|
|
91
|
-
<DateTimePicker
|
|
114
|
+
<DateTimePicker
|
|
115
|
+
value={since}
|
|
116
|
+
onChange={setSince}
|
|
117
|
+
granularity="minute"
|
|
118
|
+
placeholder="Since"
|
|
119
|
+
/>
|
|
92
120
|
</div>
|
|
93
121
|
<div className="flex flex-col gap-1.5">
|
|
94
122
|
<Label>Until</Label>
|
|
95
|
-
<DateTimePicker
|
|
123
|
+
<DateTimePicker
|
|
124
|
+
value={until}
|
|
125
|
+
onChange={setUntil}
|
|
126
|
+
granularity="minute"
|
|
127
|
+
placeholder="Until"
|
|
128
|
+
/>
|
|
96
129
|
</div>
|
|
97
130
|
<div className="col-span-2 flex items-end gap-2 sm:col-span-4">
|
|
98
131
|
<Button type="submit" disabled={loading}>
|
|
@@ -136,18 +169,30 @@ export default function AuditLogPage() {
|
|
|
136
169
|
</TableCell>
|
|
137
170
|
<TableCell>
|
|
138
171
|
<div className="text-sm">{entry.name}</div>
|
|
139
|
-
<div className="font-mono text-xs text-muted-foreground">
|
|
172
|
+
<div className="font-mono text-xs text-muted-foreground">
|
|
173
|
+
{entry.action}
|
|
174
|
+
</div>
|
|
140
175
|
</TableCell>
|
|
141
|
-
<TableCell className="font-mono text-xs">
|
|
142
|
-
|
|
176
|
+
<TableCell className="font-mono text-xs">
|
|
177
|
+
{entry.userId ?? "—"}
|
|
178
|
+
</TableCell>
|
|
179
|
+
<TableCell
|
|
180
|
+
className="max-w-xs truncate font-mono text-xs text-muted-foreground"
|
|
181
|
+
title={JSON.stringify(entry.info)}
|
|
182
|
+
>
|
|
143
183
|
{JSON.stringify(entry.info)}
|
|
144
184
|
</TableCell>
|
|
145
|
-
<TableCell className="max-w-3xs truncate text-xs text-muted-foreground">
|
|
185
|
+
<TableCell className="max-w-3xs truncate text-xs text-muted-foreground">
|
|
186
|
+
{entry.remarks ?? "—"}
|
|
187
|
+
</TableCell>
|
|
146
188
|
</TableRow>
|
|
147
189
|
))}
|
|
148
190
|
{entries.length === 0 && !loading && (
|
|
149
191
|
<TableRow>
|
|
150
|
-
<TableCell
|
|
192
|
+
<TableCell
|
|
193
|
+
colSpan={5}
|
|
194
|
+
className="text-center text-sm text-muted-foreground"
|
|
195
|
+
>
|
|
151
196
|
No entries match these filters.
|
|
152
197
|
</TableCell>
|
|
153
198
|
</TableRow>
|
|
@@ -158,16 +203,27 @@ export default function AuditLogPage() {
|
|
|
158
203
|
{meta && meta.total > 0 && (
|
|
159
204
|
<div className="flex items-center justify-between">
|
|
160
205
|
<p className="text-sm text-muted-foreground">
|
|
161
|
-
Showing {(meta.page - 1) * meta.limit + 1}–
|
|
206
|
+
Showing {(meta.page - 1) * meta.limit + 1}–
|
|
207
|
+
{Math.min(meta.page * meta.limit, meta.total)} of {meta.total}
|
|
162
208
|
</p>
|
|
163
209
|
<div className="flex items-center gap-2">
|
|
164
|
-
<Button
|
|
210
|
+
<Button
|
|
211
|
+
variant="outline"
|
|
212
|
+
size="sm"
|
|
213
|
+
disabled={!meta.hasPreviousPage || loading}
|
|
214
|
+
onClick={() => load(meta.page - 1)}
|
|
215
|
+
>
|
|
165
216
|
Previous
|
|
166
217
|
</Button>
|
|
167
218
|
<span className="text-sm text-muted-foreground">
|
|
168
219
|
Page {meta.page} of {meta.pageCount}
|
|
169
220
|
</span>
|
|
170
|
-
<Button
|
|
221
|
+
<Button
|
|
222
|
+
variant="outline"
|
|
223
|
+
size="sm"
|
|
224
|
+
disabled={!meta.hasNextPage || loading}
|
|
225
|
+
onClick={() => load(meta.page + 1)}
|
|
226
|
+
>
|
|
171
227
|
Next
|
|
172
228
|
</Button>
|
|
173
229
|
</div>
|
package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
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 { useCallback, useEffect, useState } from "react";
|
|
6
|
+
import { startTransition, useCallback, useEffect, useState } from "react";
|
|
7
7
|
import { useForm } from "react-hook-form";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AuthApiError,
|
|
10
|
+
type CustomerSummary,
|
|
11
|
+
type UpdateCustomerInput,
|
|
12
|
+
} from "@simple-auth-kit/auth-client";
|
|
9
13
|
import { format } from "date-fns";
|
|
10
14
|
import { toast } from "sonner";
|
|
11
15
|
import { Breadcrumb } from "@/components/breadcrumb";
|
|
@@ -13,18 +17,37 @@ import { FormErrorAlert } from "@/components/form-error-alert";
|
|
|
13
17
|
import { PermissionRequired } from "@/components/permission-required";
|
|
14
18
|
import { PhotoUpload } from "@/components/photo-upload";
|
|
15
19
|
import { Button } from "@/components/ui/button";
|
|
16
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Card,
|
|
22
|
+
CardContent,
|
|
23
|
+
CardDescription,
|
|
24
|
+
CardFooter,
|
|
25
|
+
CardHeader,
|
|
26
|
+
CardTitle,
|
|
27
|
+
} from "@/components/ui/card";
|
|
17
28
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
18
29
|
import { Combobox } from "@/components/ui/combobox";
|
|
19
30
|
import { DatePicker } from "@/components/ui/date-picker";
|
|
20
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
Form,
|
|
33
|
+
FormControl,
|
|
34
|
+
FormDescription,
|
|
35
|
+
FormField,
|
|
36
|
+
FormItem,
|
|
37
|
+
FormLabel,
|
|
38
|
+
FormMessage,
|
|
39
|
+
} from "@/components/ui/form";
|
|
21
40
|
import { Heading } from "@/components/ui/heading";
|
|
22
41
|
import { Input } from "@/components/ui/input";
|
|
23
42
|
import { Separator } from "@/components/ui/separator";
|
|
24
43
|
import { PERMISSIONS, hasPermission, type AppAbility } from "@/lib/ability";
|
|
25
44
|
import { authClient } from "@/lib/auth-client";
|
|
26
45
|
import { errorMessage, errorMessages } from "@/lib/error";
|
|
27
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
GENDER_OPTIONS,
|
|
48
|
+
editCustomerSchema,
|
|
49
|
+
type EditCustomerFormValues,
|
|
50
|
+
} from "../../customer-schema";
|
|
28
51
|
|
|
29
52
|
function apiErrorMessage(err: unknown, fallback: string): string {
|
|
30
53
|
return err instanceof AuthApiError ? err.message : fallback;
|
|
@@ -88,10 +111,16 @@ export default function EditCustomerPage() {
|
|
|
88
111
|
|
|
89
112
|
useEffect(() => {
|
|
90
113
|
if (!canManage) return;
|
|
91
|
-
void load();
|
|
114
|
+
startTransition(() => void load());
|
|
92
115
|
}, [canManage, load]);
|
|
93
116
|
|
|
94
|
-
if (!canManage)
|
|
117
|
+
if (!canManage)
|
|
118
|
+
return (
|
|
119
|
+
<PermissionRequired
|
|
120
|
+
permission={PERMISSIONS.customersManage}
|
|
121
|
+
what="Editing a customer"
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
95
124
|
|
|
96
125
|
async function onSubmit(values: EditCustomerFormValues) {
|
|
97
126
|
if (!customer) return;
|
|
@@ -105,7 +134,9 @@ export default function EditCustomerPage() {
|
|
|
105
134
|
phone: values.phone || null,
|
|
106
135
|
dob: values.dob ? format(values.dob, "yyyy-MM-dd") : null,
|
|
107
136
|
gender: values.gender || null,
|
|
108
|
-
joinedDate: values.joinedDate
|
|
137
|
+
joinedDate: values.joinedDate
|
|
138
|
+
? format(values.joinedDate, "yyyy-MM-dd")
|
|
139
|
+
: undefined,
|
|
109
140
|
photo: values.photo || null,
|
|
110
141
|
isActive: values.isActive,
|
|
111
142
|
};
|
|
@@ -135,7 +166,9 @@ export default function EditCustomerPage() {
|
|
|
135
166
|
</div>
|
|
136
167
|
<Separator />
|
|
137
168
|
|
|
138
|
-
{loading && !customer &&
|
|
169
|
+
{loading && !customer && (
|
|
170
|
+
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
171
|
+
)}
|
|
139
172
|
|
|
140
173
|
{customer && (
|
|
141
174
|
<Card>
|
|
@@ -143,16 +176,25 @@ export default function EditCustomerPage() {
|
|
|
143
176
|
<CardContent>
|
|
144
177
|
<FormErrorAlert messages={formError} />
|
|
145
178
|
<Form {...form}>
|
|
146
|
-
<form
|
|
179
|
+
<form
|
|
180
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
181
|
+
className="space-y-8 w-full"
|
|
182
|
+
>
|
|
147
183
|
<Card className="w-full">
|
|
148
184
|
<CardHeader className="border-b bg-muted/50">
|
|
149
|
-
<CardTitle className="text-2xl">
|
|
150
|
-
|
|
185
|
+
<CardTitle className="text-2xl">
|
|
186
|
+
Customer Information
|
|
187
|
+
</CardTitle>
|
|
188
|
+
<CardDescription className="text-base">
|
|
189
|
+
Update customer's basic information
|
|
190
|
+
</CardDescription>
|
|
151
191
|
</CardHeader>
|
|
152
192
|
<CardContent className="pt-6">
|
|
153
193
|
<div className="space-y-6">
|
|
154
194
|
<div className="space-y-4">
|
|
155
|
-
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
195
|
+
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
196
|
+
Personal Information
|
|
197
|
+
</h3>
|
|
156
198
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
157
199
|
<FormField
|
|
158
200
|
control={form.control}
|
|
@@ -199,7 +241,9 @@ export default function EditCustomerPage() {
|
|
|
199
241
|
<FormItem>
|
|
200
242
|
<FormLabel>
|
|
201
243
|
Username
|
|
202
|
-
<span className="text-xs text-destructive dark:text-destructive-foreground">
|
|
244
|
+
<span className="text-xs text-destructive dark:text-destructive-foreground">
|
|
245
|
+
(Must be unique)
|
|
246
|
+
</span>
|
|
203
247
|
</FormLabel>
|
|
204
248
|
<FormControl>
|
|
205
249
|
<Input
|
|
@@ -217,7 +261,9 @@ export default function EditCustomerPage() {
|
|
|
217
261
|
</div>
|
|
218
262
|
|
|
219
263
|
<div className="space-y-4">
|
|
220
|
-
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
264
|
+
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
265
|
+
Contact Information
|
|
266
|
+
</h3>
|
|
221
267
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
222
268
|
<FormField
|
|
223
269
|
control={form.control}
|
|
@@ -226,7 +272,9 @@ export default function EditCustomerPage() {
|
|
|
226
272
|
<FormItem>
|
|
227
273
|
<FormLabel>
|
|
228
274
|
Email
|
|
229
|
-
<span className="text-xs text-destructive dark:text-destructive-foreground">
|
|
275
|
+
<span className="text-xs text-destructive dark:text-destructive-foreground">
|
|
276
|
+
(Must be unique)
|
|
277
|
+
</span>
|
|
230
278
|
</FormLabel>
|
|
231
279
|
<FormControl>
|
|
232
280
|
<Input
|
|
@@ -248,7 +296,9 @@ export default function EditCustomerPage() {
|
|
|
248
296
|
<FormItem>
|
|
249
297
|
<FormLabel>
|
|
250
298
|
Phone
|
|
251
|
-
<span className="text-xs text-muted-foreground">
|
|
299
|
+
<span className="text-xs text-muted-foreground">
|
|
300
|
+
(With country code)
|
|
301
|
+
</span>
|
|
252
302
|
</FormLabel>
|
|
253
303
|
<FormControl>
|
|
254
304
|
<Input
|
|
@@ -267,7 +317,9 @@ export default function EditCustomerPage() {
|
|
|
267
317
|
</div>
|
|
268
318
|
|
|
269
319
|
<div className="space-y-4">
|
|
270
|
-
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
320
|
+
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
321
|
+
Additional Details
|
|
322
|
+
</h3>
|
|
271
323
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
272
324
|
<FormField
|
|
273
325
|
control={form.control}
|
|
@@ -313,7 +365,9 @@ export default function EditCustomerPage() {
|
|
|
313
365
|
options={GENDER_OPTIONS}
|
|
314
366
|
selected={field.value ?? ""}
|
|
315
367
|
placeholder="Select Gender"
|
|
316
|
-
onChange={(option) =>
|
|
368
|
+
onChange={(option) =>
|
|
369
|
+
field.onChange(option.value)
|
|
370
|
+
}
|
|
317
371
|
showCreate={false}
|
|
318
372
|
popoverClassName="min-w-[200px]"
|
|
319
373
|
/>
|
|
@@ -330,7 +384,10 @@ export default function EditCustomerPage() {
|
|
|
330
384
|
|
|
331
385
|
<CardHeader className="border-b bg-muted/50 mt-6">
|
|
332
386
|
<CardTitle className="text-2xl">Profile Photo</CardTitle>
|
|
333
|
-
<CardDescription className="text-base">
|
|
387
|
+
<CardDescription className="text-base">
|
|
388
|
+
Upload customer's profile picture (Max size: 2MB,
|
|
389
|
+
Formats: JPG, PNG)
|
|
390
|
+
</CardDescription>
|
|
334
391
|
</CardHeader>
|
|
335
392
|
<CardContent className="pt-6">
|
|
336
393
|
<FormField
|
|
@@ -340,7 +397,12 @@ export default function EditCustomerPage() {
|
|
|
340
397
|
<FormItem>
|
|
341
398
|
<FormControl>
|
|
342
399
|
<div>
|
|
343
|
-
<PhotoUpload
|
|
400
|
+
<PhotoUpload
|
|
401
|
+
photo={field.value}
|
|
402
|
+
fallback="?"
|
|
403
|
+
onChange={field.onChange}
|
|
404
|
+
disabled={saving}
|
|
405
|
+
/>
|
|
344
406
|
</div>
|
|
345
407
|
</FormControl>
|
|
346
408
|
<FormMessage />
|
|
@@ -356,11 +418,21 @@ export default function EditCustomerPage() {
|
|
|
356
418
|
render={({ field }) => (
|
|
357
419
|
<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">
|
|
358
420
|
<FormControl>
|
|
359
|
-
<Checkbox
|
|
421
|
+
<Checkbox
|
|
422
|
+
checked={field.value}
|
|
423
|
+
onCheckedChange={(checked) =>
|
|
424
|
+
field.onChange(checked === true)
|
|
425
|
+
}
|
|
426
|
+
/>
|
|
360
427
|
</FormControl>
|
|
361
428
|
<div className="space-y-1 leading-none">
|
|
362
|
-
<FormLabel className="text-base font-medium">
|
|
363
|
-
|
|
429
|
+
<FormLabel className="text-base font-medium">
|
|
430
|
+
Active Status
|
|
431
|
+
</FormLabel>
|
|
432
|
+
<FormDescription className="text-sm">
|
|
433
|
+
Customer will be active and available for
|
|
434
|
+
selection
|
|
435
|
+
</FormDescription>
|
|
364
436
|
</div>
|
|
365
437
|
</FormItem>
|
|
366
438
|
)}
|
|
@@ -368,10 +440,19 @@ export default function EditCustomerPage() {
|
|
|
368
440
|
</div>
|
|
369
441
|
|
|
370
442
|
<CardFooter className="flex justify-center gap-4 mt-8 pb-8">
|
|
371
|
-
<Button
|
|
443
|
+
<Button
|
|
444
|
+
type="button"
|
|
445
|
+
variant="outline"
|
|
446
|
+
onClick={() => router.push(`/customers/${id}`)}
|
|
447
|
+
disabled={saving}
|
|
448
|
+
>
|
|
372
449
|
Cancel
|
|
373
450
|
</Button>
|
|
374
|
-
<Button
|
|
451
|
+
<Button
|
|
452
|
+
disabled={saving}
|
|
453
|
+
className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-600 dark:hover:bg-purple-700 min-w-32"
|
|
454
|
+
type="submit"
|
|
455
|
+
>
|
|
375
456
|
{saving ? "Updating..." : "Update Customer"}
|
|
376
457
|
</Button>
|
|
377
458
|
</CardFooter>
|