swallowkit 1.0.0-beta.2 → 1.0.0-beta.21
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/LICENSE +21 -21
- package/README.ja.md +312 -215
- package/README.md +369 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +444 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +19 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +476 -117
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2627 -1708
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +617 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +164 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +63 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +204 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -213
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +329 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +156 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +184 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +85 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +263 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/dev.test.ts +154 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +80 -0
- package/src/__tests__/machine.test.ts +212 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +189 -0
- package/src/__tests__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +500 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +962 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3371 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1211 -0
- package/src/cli/index.ts +193 -0
- package/src/core/config.ts +308 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +221 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js UI コンポーネント生成
|
|
3
|
+
* CRUD 画面(一覧、詳細、新規作成、編集)を生成する
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
|
|
7
|
+
import { ModelAuthPolicy } from "../../types";
|
|
8
|
+
|
|
9
|
+
/** auth が有効かつ authPolicy が存在する場合に渡されるオプション */
|
|
10
|
+
export interface UIAuthOptions {
|
|
11
|
+
/** モデル固有のロールポリシー */
|
|
12
|
+
authPolicy: ModelAuthPolicy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 一覧画面を生成
|
|
17
|
+
*/
|
|
18
|
+
export function generateListPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
19
|
+
const modelName = model.name;
|
|
20
|
+
const modelCamel = toCamelCase(modelName);
|
|
21
|
+
const modelKebab = toKebabCase(modelName);
|
|
22
|
+
|
|
23
|
+
const hasAuth = !!authOptions;
|
|
24
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
25
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
26
|
+
|
|
27
|
+
// フィールドから表示するカラムを抽出(id以外の最初の3つ)
|
|
28
|
+
const displayFields = model.fields
|
|
29
|
+
.filter(f => f.name !== 'id')
|
|
30
|
+
.slice(0, 3);
|
|
31
|
+
|
|
32
|
+
// 外部キーフィールドを検出
|
|
33
|
+
const foreignKeyFields = displayFields.filter(f => f.isForeignKey);
|
|
34
|
+
const hasForeignKeys = foreignKeyFields.length > 0;
|
|
35
|
+
|
|
36
|
+
// 外部キー用のstate定義を生成
|
|
37
|
+
const foreignKeyStates = foreignKeyFields.map(f => {
|
|
38
|
+
const refModel = f.referencedModel!;
|
|
39
|
+
const refModelCamel = toCamelCase(refModel);
|
|
40
|
+
return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
|
|
41
|
+
}).join('\n');
|
|
42
|
+
|
|
43
|
+
// 外部キーデータのフェッチロジックを生成
|
|
44
|
+
const foreignKeyFetches = foreignKeyFields.map(f => {
|
|
45
|
+
const refModel = f.referencedModel!;
|
|
46
|
+
const refModelCamel = toCamelCase(refModel);
|
|
47
|
+
return ` fetch('/api/${refModelCamel}')
|
|
48
|
+
.then(res => res.json())
|
|
49
|
+
.then((data: any[]) => {
|
|
50
|
+
const map: Record<string, string> = {};
|
|
51
|
+
data.forEach(item => {
|
|
52
|
+
// name または title フィールドを表示用文字列として使用
|
|
53
|
+
map[item.id] = item.name || item.title || item.id;
|
|
54
|
+
});
|
|
55
|
+
set${refModel}Map(map);
|
|
56
|
+
})
|
|
57
|
+
.catch(err => console.error('Failed to fetch ${refModel}s:', err));`;
|
|
58
|
+
}).join('\n');
|
|
59
|
+
|
|
60
|
+
const schemaName = model.schemaName;
|
|
61
|
+
// schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
|
|
62
|
+
const needsAlias = schemaName === modelName;
|
|
63
|
+
const localSchemaName = needsAlias ? `${toCamelCase(modelName)}Schema` : schemaName;
|
|
64
|
+
const schemaImportLine = needsAlias
|
|
65
|
+
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
66
|
+
: `import { ${schemaName} } from '${sharedPackageName}';`;
|
|
67
|
+
|
|
68
|
+
return `'use client';
|
|
69
|
+
|
|
70
|
+
import { useEffect, useState } from 'react';
|
|
71
|
+
import Link from 'next/link';
|
|
72
|
+
import { z } from 'zod/v4';
|
|
73
|
+
${schemaImportLine}
|
|
74
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
75
|
+
|
|
76
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
77
|
+
|
|
78
|
+
export default function ${modelName}ListPage() {
|
|
79
|
+
const [${modelCamel}s, set${modelName}s] = useState<${modelName}[]>([]);
|
|
80
|
+
const [loading, setLoading] = useState(true);
|
|
81
|
+
const [error, setError] = useState<string | null>(null);
|
|
82
|
+
${hasForeignKeys ? foreignKeyStates : ''}
|
|
83
|
+
${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
|
|
84
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
|
|
85
|
+
const canWrite = !!user;` : ''}
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
fetch('/api/${modelCamel}')
|
|
89
|
+
.then((res) => {
|
|
90
|
+
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}s');
|
|
91
|
+
return res.json();
|
|
92
|
+
})
|
|
93
|
+
.then((data) => {
|
|
94
|
+
set${modelName}s(data);
|
|
95
|
+
setLoading(false);
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
setError(err.message);
|
|
99
|
+
setLoading(false);
|
|
100
|
+
});
|
|
101
|
+
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
102
|
+
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const handleDelete = async (id: string) => {
|
|
106
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(\`/api/${modelCamel}/\${id}\`, {
|
|
110
|
+
method: 'DELETE',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
114
|
+
|
|
115
|
+
set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
alert(\`Error: \${err.message}\`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (loading) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
124
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (error) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
132
|
+
<div className="text-red-600 dark:text-red-400">Error: {error}</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="container mx-auto px-4 py-8">
|
|
139
|
+
<div className="mb-4">
|
|
140
|
+
<Link
|
|
141
|
+
href="/"
|
|
142
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
|
|
143
|
+
>
|
|
144
|
+
← Home
|
|
145
|
+
</Link>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex justify-between items-center mb-6">
|
|
148
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
|
|
149
|
+
${hasAuth ? ` {canWrite && (
|
|
150
|
+
<Link
|
|
151
|
+
href="/${modelKebab}/new"
|
|
152
|
+
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
153
|
+
>
|
|
154
|
+
Create New
|
|
155
|
+
</Link>
|
|
156
|
+
)}` : ` <Link
|
|
157
|
+
href="/${modelKebab}/new"
|
|
158
|
+
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
159
|
+
>
|
|
160
|
+
Create New
|
|
161
|
+
</Link>`}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{${modelCamel}s.length === 0 ? (
|
|
165
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
166
|
+
No ${modelCamel}s found. Create your first one!
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
|
170
|
+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
171
|
+
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
172
|
+
<tr>
|
|
173
|
+
${displayFields.map(f => {
|
|
174
|
+
const headerLabel = f.isNestedSchema && f.nestedModelName
|
|
175
|
+
? f.nestedModelName
|
|
176
|
+
: (f.isForeignKey && f.referencedModel ? f.referencedModel : f.name);
|
|
177
|
+
return ` <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
178
|
+
${headerLabel}
|
|
179
|
+
</th>`;
|
|
180
|
+
}).join('\n')}
|
|
181
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
182
|
+
Actions
|
|
183
|
+
</th>
|
|
184
|
+
</tr>
|
|
185
|
+
</thead>
|
|
186
|
+
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
187
|
+
{${modelCamel}s.map((item) => (
|
|
188
|
+
<tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
189
|
+
${displayFields.map(f => {
|
|
190
|
+
if (f.isNestedSchema && f.nestedModelName) {
|
|
191
|
+
const displayField = f.nestedDisplayField || 'name';
|
|
192
|
+
if (f.isArray) {
|
|
193
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
194
|
+
{Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}
|
|
195
|
+
</td>`;
|
|
196
|
+
} else {
|
|
197
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
198
|
+
{item.${f.name}?.${displayField} || '-'}
|
|
199
|
+
</td>`;
|
|
200
|
+
}
|
|
201
|
+
} else if (f.isForeignKey && f.referencedModel) {
|
|
202
|
+
const refModel = f.referencedModel;
|
|
203
|
+
const refModelCamel = toCamelCase(refModel);
|
|
204
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
205
|
+
{${refModelCamel}Map[item.${f.name}] || item.${f.name}}
|
|
206
|
+
</td>`;
|
|
207
|
+
} else {
|
|
208
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
209
|
+
{String(item.${f.name})}
|
|
210
|
+
</td>`;
|
|
211
|
+
}
|
|
212
|
+
}).join('\n')}
|
|
213
|
+
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
214
|
+
<Link
|
|
215
|
+
href={\`/${modelKebab}/\${item.id}\`}
|
|
216
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
|
|
217
|
+
>
|
|
218
|
+
View
|
|
219
|
+
</Link>
|
|
220
|
+
${hasAuth ? ` {canWrite && (
|
|
221
|
+
<>
|
|
222
|
+
<Link
|
|
223
|
+
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
224
|
+
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
225
|
+
>
|
|
226
|
+
Edit
|
|
227
|
+
</Link>
|
|
228
|
+
<button
|
|
229
|
+
onClick={() => handleDelete(item.id)}
|
|
230
|
+
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
231
|
+
>
|
|
232
|
+
Delete
|
|
233
|
+
</button>
|
|
234
|
+
</>
|
|
235
|
+
)}` : ` <Link
|
|
236
|
+
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
237
|
+
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
238
|
+
>
|
|
239
|
+
Edit
|
|
240
|
+
</Link>
|
|
241
|
+
<button
|
|
242
|
+
onClick={() => handleDelete(item.id)}
|
|
243
|
+
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
244
|
+
>
|
|
245
|
+
Delete
|
|
246
|
+
</button>`}
|
|
247
|
+
</td>
|
|
248
|
+
</tr>
|
|
249
|
+
))}
|
|
250
|
+
</tbody>
|
|
251
|
+
</table>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 詳細画面を生成
|
|
262
|
+
*/
|
|
263
|
+
export function generateDetailPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
264
|
+
const modelName = model.name;
|
|
265
|
+
const modelCamel = toCamelCase(modelName);
|
|
266
|
+
const modelKebab = toKebabCase(modelName);
|
|
267
|
+
|
|
268
|
+
const hasAuth = !!authOptions;
|
|
269
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
270
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
271
|
+
|
|
272
|
+
// 外部キーフィールドを検出
|
|
273
|
+
const foreignKeyFields = model.fields.filter(f => f.isForeignKey);
|
|
274
|
+
const hasForeignKeys = foreignKeyFields.length > 0;
|
|
275
|
+
|
|
276
|
+
// 外部キー用のstate定義を生成
|
|
277
|
+
const foreignKeyStates = foreignKeyFields.map(f => {
|
|
278
|
+
const refModel = f.referencedModel!;
|
|
279
|
+
const refModelCamel = toCamelCase(refModel);
|
|
280
|
+
return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
|
|
281
|
+
}).join('\n');
|
|
282
|
+
|
|
283
|
+
// 外部キーデータのフェッチロジックを生成
|
|
284
|
+
const foreignKeyFetches = foreignKeyFields.map(f => {
|
|
285
|
+
const refModel = f.referencedModel!;
|
|
286
|
+
const refModelCamel = toCamelCase(refModel);
|
|
287
|
+
return ` fetch('/api/${refModelCamel}')
|
|
288
|
+
.then(res => res.json())
|
|
289
|
+
.then((data: any[]) => {
|
|
290
|
+
const map: Record<string, string> = {};
|
|
291
|
+
data.forEach(item => {
|
|
292
|
+
map[item.id] = item.name || item.title || item.id;
|
|
293
|
+
});
|
|
294
|
+
set${refModel}Map(map);
|
|
295
|
+
})
|
|
296
|
+
.catch(err => console.error('Failed to fetch ${refModel}s:', err));`;
|
|
297
|
+
}).join('\n');
|
|
298
|
+
|
|
299
|
+
const schemaName = model.schemaName;
|
|
300
|
+
// Zod公式パターン対応: schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
|
|
301
|
+
const needsAlias = schemaName === modelName;
|
|
302
|
+
const localSchemaName = needsAlias ? `${toCamelCase(modelName)}Schema` : schemaName;
|
|
303
|
+
const schemaImportLine = needsAlias
|
|
304
|
+
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
305
|
+
: `import { ${schemaName} } from '${sharedPackageName}';`;
|
|
306
|
+
|
|
307
|
+
return `'use client';
|
|
308
|
+
|
|
309
|
+
import { useEffect, useState } from 'react';
|
|
310
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
311
|
+
import Link from 'next/link';
|
|
312
|
+
import { z } from 'zod/v4';
|
|
313
|
+
${schemaImportLine}
|
|
314
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
315
|
+
|
|
316
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
317
|
+
|
|
318
|
+
export default function ${modelName}DetailPage() {
|
|
319
|
+
const params = useParams();
|
|
320
|
+
const router = useRouter();
|
|
321
|
+
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
322
|
+
const [loading, setLoading] = useState(true);
|
|
323
|
+
const [error, setError] = useState<string | null>(null);
|
|
324
|
+
${hasForeignKeys ? foreignKeyStates : ''}
|
|
325
|
+
${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
|
|
326
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
|
|
327
|
+
const canWrite = !!user;` : ''}
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
const id = params?.id as string;
|
|
331
|
+
if (!id) return;
|
|
332
|
+
|
|
333
|
+
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
334
|
+
.then((res) => {
|
|
335
|
+
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}');
|
|
336
|
+
return res.json();
|
|
337
|
+
})
|
|
338
|
+
.then((data) => {
|
|
339
|
+
set${modelName}(data);
|
|
340
|
+
setLoading(false);
|
|
341
|
+
})
|
|
342
|
+
.catch((err) => {
|
|
343
|
+
setError(err.message);
|
|
344
|
+
setLoading(false);
|
|
345
|
+
});
|
|
346
|
+
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
347
|
+
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
348
|
+
}, [params]);
|
|
349
|
+
|
|
350
|
+
const handleDelete = async () => {
|
|
351
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(\`/api/${modelCamel}/\${params?.id}\`, {
|
|
355
|
+
method: 'DELETE',
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
359
|
+
|
|
360
|
+
router.push('/${modelKebab}');
|
|
361
|
+
} catch (err: any) {
|
|
362
|
+
alert(\`Error: \${err.message}\`);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (loading) {
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
369
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (error || !${modelCamel}) {
|
|
375
|
+
return (
|
|
376
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
377
|
+
<div className="text-red-600 dark:text-red-400">Error: {error || '${modelName} not found'}</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div className="container mx-auto px-4 py-8">
|
|
384
|
+
<div className="max-w-2xl mx-auto">
|
|
385
|
+
<div className="flex justify-between items-center mb-6">
|
|
386
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
|
|
387
|
+
${hasAuth ? ` {canWrite && (
|
|
388
|
+
<div className="space-x-2">
|
|
389
|
+
<Link
|
|
390
|
+
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
391
|
+
className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
392
|
+
>
|
|
393
|
+
Edit
|
|
394
|
+
</Link>
|
|
395
|
+
<button
|
|
396
|
+
onClick={handleDelete}
|
|
397
|
+
className="inline-flex items-center bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded"
|
|
398
|
+
>
|
|
399
|
+
Delete
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}` : ` <div className="space-x-2">
|
|
403
|
+
<Link
|
|
404
|
+
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
405
|
+
className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
406
|
+
>
|
|
407
|
+
Edit
|
|
408
|
+
</Link>
|
|
409
|
+
<button
|
|
410
|
+
onClick={handleDelete}
|
|
411
|
+
className="inline-flex items-center bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded"
|
|
412
|
+
>
|
|
413
|
+
Delete
|
|
414
|
+
</button>
|
|
415
|
+
</div>`}
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
419
|
+
<dl className="space-y-4">
|
|
420
|
+
${model.fields.map(f => {
|
|
421
|
+
if (f.isNestedSchema && f.nestedModelName) {
|
|
422
|
+
const displayField = f.nestedDisplayField || 'name';
|
|
423
|
+
const label = f.nestedModelName;
|
|
424
|
+
if (f.isArray) {
|
|
425
|
+
return ` <div>
|
|
426
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
|
|
427
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}</dd>
|
|
428
|
+
</div>`;
|
|
429
|
+
} else {
|
|
430
|
+
return ` <div>
|
|
431
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
|
|
432
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${modelCamel}.${f.name}?.${displayField} || '-'}</dd>
|
|
433
|
+
</div>`;
|
|
434
|
+
}
|
|
435
|
+
} else if (f.isForeignKey && f.referencedModel) {
|
|
436
|
+
const refModel = f.referencedModel;
|
|
437
|
+
const refModelCamel = toCamelCase(refModel);
|
|
438
|
+
return ` <div>
|
|
439
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
|
|
440
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
|
|
441
|
+
</div>`;
|
|
442
|
+
} else {
|
|
443
|
+
return ` <div>
|
|
444
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
|
|
445
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(${modelCamel}.${f.name})}</dd>
|
|
446
|
+
</div>`;
|
|
447
|
+
}
|
|
448
|
+
}).join('\n')}
|
|
449
|
+
</dl>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div className="mt-6">
|
|
453
|
+
<Link
|
|
454
|
+
href="/${modelKebab}"
|
|
455
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
|
|
456
|
+
>
|
|
457
|
+
← Back to list
|
|
458
|
+
</Link>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* フォームコンポーネントを生成
|
|
469
|
+
*/
|
|
470
|
+
export function generateFormComponent(model: ModelInfo, sharedPackageName: string): string {
|
|
471
|
+
const modelName = model.name;
|
|
472
|
+
const modelCamel = toCamelCase(modelName);
|
|
473
|
+
const schemaName = model.schemaName;
|
|
474
|
+
// Zod公式パターン対応: schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
|
|
475
|
+
const needsAlias = schemaName === modelName;
|
|
476
|
+
const localSchemaName = needsAlias ? `${toCamelCase(modelName)}Schema` : schemaName;
|
|
477
|
+
const schemaImportLine = needsAlias
|
|
478
|
+
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
479
|
+
: `import { ${schemaName} } from '${sharedPackageName}';`;
|
|
480
|
+
|
|
481
|
+
// id, createdAt, updatedAt 以外のフィールド
|
|
482
|
+
const formFields = model.fields.filter(f =>
|
|
483
|
+
f.name !== 'id' &&
|
|
484
|
+
f.name !== 'createdAt' &&
|
|
485
|
+
f.name !== 'updatedAt'
|
|
486
|
+
);
|
|
487
|
+
// 外部キーフィールドを抽出
|
|
488
|
+
const foreignKeyFields = formFields.filter(f => f.isForeignKey);
|
|
489
|
+
const hasForignKeys = foreignKeyFields.length > 0;
|
|
490
|
+
|
|
491
|
+
// ネストスキーマフィールドを抽出
|
|
492
|
+
const nestedSchemaFields = formFields.filter(f => f.isNestedSchema);
|
|
493
|
+
const hasNestedSchemas = nestedSchemaFields.length > 0;
|
|
494
|
+
|
|
495
|
+
// useEffect が必要かどうか
|
|
496
|
+
const needsUseEffect = hasForignKeys || hasNestedSchemas;
|
|
497
|
+
|
|
498
|
+
// ネストスキーマ用のstate定義を生成
|
|
499
|
+
const nestedSchemaStates = nestedSchemaFields.map(f => {
|
|
500
|
+
const refModelCamel = toCamelCase(f.nestedModelName!);
|
|
501
|
+
const refModelPascal = f.nestedModelName!;
|
|
502
|
+
return ` const [${refModelCamel}Options, set${refModelPascal}Options] = useState<Array<{ id: string; name: string }>>([]);`;
|
|
503
|
+
}).join('\n');
|
|
504
|
+
|
|
505
|
+
// ネストスキーマデータのフェッチロジック
|
|
506
|
+
const nestedSchemaFetches = nestedSchemaFields.map(f => {
|
|
507
|
+
const refModelCamel = toCamelCase(f.nestedModelName!);
|
|
508
|
+
const refModelPascal = f.nestedModelName!;
|
|
509
|
+
const displayField = f.nestedDisplayField || 'name';
|
|
510
|
+
return ` // ${refModelPascal} の一覧を取得
|
|
511
|
+
fetch('/api/${refModelCamel}')
|
|
512
|
+
.then(res => res.json())
|
|
513
|
+
.then(data => set${refModelPascal}Options(data.map((item: any) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
|
|
514
|
+
.catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
|
|
515
|
+
}).join('\n');
|
|
516
|
+
|
|
517
|
+
return `'use client';
|
|
518
|
+
|
|
519
|
+
import { useState, FormEvent${needsUseEffect ? ', useEffect' : ''} } from 'react';
|
|
520
|
+
import { useRouter } from 'next/navigation';
|
|
521
|
+
import { z } from 'zod/v4';
|
|
522
|
+
${schemaImportLine}
|
|
523
|
+
|
|
524
|
+
// Input schema: SwallowKit-managed fields (id, createdAt, updatedAt) are optional
|
|
525
|
+
// These fields are ignored by the backend and auto-managed
|
|
526
|
+
const ${modelName}InputSchema = ${localSchemaName}.partial({ id: true, createdAt: true, updatedAt: true });
|
|
527
|
+
|
|
528
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
529
|
+
|
|
530
|
+
interface ${modelName}FormProps {
|
|
531
|
+
initialData?: ${modelName};
|
|
532
|
+
isEdit?: boolean;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export default function ${modelName}Form({ initialData, isEdit = false }: ${modelName}FormProps) {
|
|
536
|
+
const router = useRouter();
|
|
537
|
+
const [loading, setLoading] = useState(false);
|
|
538
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
539
|
+
${hasForignKeys ? foreignKeyFields.map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
|
|
540
|
+
${hasNestedSchemas ? nestedSchemaStates : ''}
|
|
541
|
+
|
|
542
|
+
const [formData, setFormData] = useState({
|
|
543
|
+
${formFields.map(f => {
|
|
544
|
+
// ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
|
|
545
|
+
if (f.isNestedSchema) {
|
|
546
|
+
if (f.isArray) {
|
|
547
|
+
return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item: any) => item.id) : []) : [] as string[],`;
|
|
548
|
+
} else {
|
|
549
|
+
return ` ${f.name}Id: initialData?.${f.name}?.id ?? '',`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let defaultValue = "''";
|
|
554
|
+
if (f.isArray) {
|
|
555
|
+
// Array の場合は空文字列(カンマ区切りで入力するため)
|
|
556
|
+
defaultValue = "''";
|
|
557
|
+
} else if (f.type === 'number') {
|
|
558
|
+
// Number の場合も空文字列を許容(オプショナル対応)
|
|
559
|
+
defaultValue = "''";
|
|
560
|
+
} else if (f.type === 'boolean') {
|
|
561
|
+
defaultValue = 'false';
|
|
562
|
+
} else {
|
|
563
|
+
defaultValue = "''";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// initialData からの取得も型に応じて変換
|
|
567
|
+
if (f.isArray) {
|
|
568
|
+
return ` ${f.name}: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.join(', ') : '') : ${defaultValue},`;
|
|
569
|
+
} else if (f.type === 'number') {
|
|
570
|
+
return ` ${f.name}: initialData?.${f.name} !== undefined ? String(initialData.${f.name}) : ${defaultValue},`;
|
|
571
|
+
}
|
|
572
|
+
return ` ${f.name}: initialData?.${f.name} ?? ${defaultValue},`;
|
|
573
|
+
}).join('\n')}
|
|
574
|
+
});
|
|
575
|
+
${needsUseEffect ? `
|
|
576
|
+
// 参照データの選択肢を取得
|
|
577
|
+
useEffect(() => {
|
|
578
|
+
${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
|
|
579
|
+
fetch('/api/${toKebabCase(f.referencedModel!)}')
|
|
580
|
+
.then(res => res.json())
|
|
581
|
+
.then(data => set${f.referencedModel}Options(data.map((item: any) => ({ id: item.id, name: item.name || item.title || item.id }))))
|
|
582
|
+
.catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
|
|
583
|
+
${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
584
|
+
}, []);
|
|
585
|
+
` : ''}
|
|
586
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
setLoading(true);
|
|
589
|
+
setErrors({});
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
// Array フィールドをカンマ区切りから配列に変換
|
|
593
|
+
const submitData: any = { ...formData };
|
|
594
|
+
${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
|
|
595
|
+
submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
|
|
596
|
+
}`).join('\n')}
|
|
597
|
+
${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
|
|
598
|
+
// Enum フィールドの空文字列を undefined に変換(.default() を有効にする)
|
|
599
|
+
${formFields.filter(f => f.enumValues && f.enumValues.length > 0).map(f => ` if (submitData.${f.name} === '') {
|
|
600
|
+
submitData.${f.name} = undefined;
|
|
601
|
+
}`).join('\n')}` : ''}
|
|
602
|
+
${formFields.filter(f => f.type === 'number').length > 0 ? `
|
|
603
|
+
// Number フィールドを変換(空文字列 → undefined、文字列 → 数値)
|
|
604
|
+
${formFields.filter(f => f.type === 'number').map(f => ` if (submitData.${f.name} === '' || submitData.${f.name} === null) {
|
|
605
|
+
submitData.${f.name} = undefined;
|
|
606
|
+
} else if (typeof submitData.${f.name} === 'string') {
|
|
607
|
+
submitData.${f.name} = Number(submitData.${f.name});
|
|
608
|
+
}`).join('\n')}` : ''}
|
|
609
|
+
${hasNestedSchemas ? `
|
|
610
|
+
// ネストスキーマ参照をIDからオブジェクトに変換
|
|
611
|
+
${nestedSchemaFields.map(f => {
|
|
612
|
+
const refModelCamel = toCamelCase(f.nestedModelName!);
|
|
613
|
+
const refModelPascal = f.nestedModelName!;
|
|
614
|
+
if (f.isArray) {
|
|
615
|
+
return ` // ${refModelPascal} 配列参照の変換
|
|
616
|
+
if (submitData.${f.name}Ids) {
|
|
617
|
+
submitData.${f.name} = submitData.${f.name}Ids
|
|
618
|
+
.map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
|
|
619
|
+
.filter(Boolean);
|
|
620
|
+
delete submitData.${f.name}Ids;
|
|
621
|
+
}`;
|
|
622
|
+
} else {
|
|
623
|
+
return ` // ${refModelPascal} 単一参照の変換
|
|
624
|
+
if (submitData.${f.name}Id) {
|
|
625
|
+
const selected = ${refModelCamel}Options.find(opt => opt.id === submitData.${f.name}Id);
|
|
626
|
+
submitData.${f.name} = selected || undefined;
|
|
627
|
+
delete submitData.${f.name}Id;
|
|
628
|
+
} else {
|
|
629
|
+
submitData.${f.name} = undefined;
|
|
630
|
+
delete submitData.${f.name}Id;
|
|
631
|
+
}`;
|
|
632
|
+
}
|
|
633
|
+
}).join('\n')}
|
|
634
|
+
` : ''}
|
|
635
|
+
|
|
636
|
+
// Validate input (excluding SwallowKit-managed fields)
|
|
637
|
+
${modelName}InputSchema.parse(submitData);
|
|
638
|
+
|
|
639
|
+
const url = isEdit ? \`/api/${modelCamel}/\${initialData!.id}\` : '/api/${modelCamel}';
|
|
640
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
641
|
+
|
|
642
|
+
const res = await fetch(url, {
|
|
643
|
+
method,
|
|
644
|
+
headers: { 'Content-Type': 'application/json' },
|
|
645
|
+
body: JSON.stringify(submitData),
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (!res.ok) {
|
|
649
|
+
const errorData = await res.json();
|
|
650
|
+
throw new Error(errorData.error || 'Failed to save ${modelCamel}');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
router.push('/${toKebabCase(modelName)}');
|
|
654
|
+
} catch (err: any) {
|
|
655
|
+
if (err.issues) {
|
|
656
|
+
// Zod validation errors
|
|
657
|
+
const fieldErrors: Record<string, string> = {};
|
|
658
|
+
err.issues.forEach((error: any) => {
|
|
659
|
+
const field = error.path[0];
|
|
660
|
+
fieldErrors[field] = error.message;
|
|
661
|
+
});
|
|
662
|
+
setErrors(fieldErrors);
|
|
663
|
+
} else {
|
|
664
|
+
alert(\`Error: \${err.message}\`);
|
|
665
|
+
}
|
|
666
|
+
setLoading(false);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
return (
|
|
671
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
672
|
+
${formFields.map(f => {
|
|
673
|
+
// ネストスキーマの場合はセレクトボックス(単一)またはマルチセレクト(配列)
|
|
674
|
+
if (f.isNestedSchema && f.nestedModelName) {
|
|
675
|
+
const optionsVar = `${toCamelCase(f.nestedModelName)}Options`;
|
|
676
|
+
const label = f.nestedModelName;
|
|
677
|
+
|
|
678
|
+
if (f.isArray) {
|
|
679
|
+
// 配列参照: マルチセレクト
|
|
680
|
+
return ` <div>
|
|
681
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
682
|
+
${label}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(複数選択可)</span>
|
|
683
|
+
</label>
|
|
684
|
+
<select
|
|
685
|
+
id="${f.name}"
|
|
686
|
+
name="${f.name}"
|
|
687
|
+
multiple
|
|
688
|
+
value={formData.${f.name}Ids}
|
|
689
|
+
onChange={(e) => {
|
|
690
|
+
const selected = Array.from(e.target.selectedOptions, option => option.value);
|
|
691
|
+
setFormData({ ...formData, ${f.name}Ids: selected });
|
|
692
|
+
}}
|
|
693
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400 min-h-[120px]"
|
|
694
|
+
${!f.isOptional ? 'required' : ''}
|
|
695
|
+
>
|
|
696
|
+
{${optionsVar}.map((option) => (
|
|
697
|
+
<option key={option.id} value={option.id}>{option.name}</option>
|
|
698
|
+
))}
|
|
699
|
+
</select>
|
|
700
|
+
{errors.${f.name} && (
|
|
701
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
702
|
+
)}
|
|
703
|
+
</div>`;
|
|
704
|
+
} else {
|
|
705
|
+
// 単一オブジェクト参照: セレクトボックス
|
|
706
|
+
return ` <div>
|
|
707
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
708
|
+
${label}${!f.isOptional ? ' *' : ''}
|
|
709
|
+
</label>
|
|
710
|
+
<select
|
|
711
|
+
id="${f.name}"
|
|
712
|
+
name="${f.name}"
|
|
713
|
+
value={formData.${f.name}Id}
|
|
714
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}Id: e.target.value })}
|
|
715
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
716
|
+
${!f.isOptional ? 'required' : ''}
|
|
717
|
+
>
|
|
718
|
+
<option value="">選択してください</option>
|
|
719
|
+
{${optionsVar}.map((option) => (
|
|
720
|
+
<option key={option.id} value={option.id}>{option.name}</option>
|
|
721
|
+
))}
|
|
722
|
+
</select>
|
|
723
|
+
{errors.${f.name} && (
|
|
724
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
725
|
+
)}
|
|
726
|
+
</div>`;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// 外部キーの場合は参照先モデルのドロップダウン
|
|
731
|
+
if (f.isForeignKey && f.referencedModel) {
|
|
732
|
+
const optionsVar = `${toCamelCase(f.referencedModel)}Options`;
|
|
733
|
+
return ` <div>
|
|
734
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
735
|
+
${f.referencedModel}${!f.isOptional ? ' *' : ''}
|
|
736
|
+
</label>
|
|
737
|
+
<select
|
|
738
|
+
id="${f.name}"
|
|
739
|
+
name="${f.name}"
|
|
740
|
+
value={formData.${f.name}}
|
|
741
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
742
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
743
|
+
${!f.isOptional ? 'required' : ''}
|
|
744
|
+
>
|
|
745
|
+
<option value="">選択してください</option>
|
|
746
|
+
{${optionsVar}.map((option) => (
|
|
747
|
+
<option key={option.id} value={option.id}>{option.name}</option>
|
|
748
|
+
))}
|
|
749
|
+
</select>
|
|
750
|
+
{errors.${f.name} && (
|
|
751
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
752
|
+
)}
|
|
753
|
+
</div>`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Enum の場合は select 要素
|
|
757
|
+
if (f.enumValues && f.enumValues.length > 0) {
|
|
758
|
+
return ` <div>
|
|
759
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
760
|
+
${f.name}${!f.isOptional ? ' *' : ''}
|
|
761
|
+
</label>
|
|
762
|
+
<select
|
|
763
|
+
id="${f.name}"
|
|
764
|
+
name="${f.name}"
|
|
765
|
+
value={formData.${f.name}}
|
|
766
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
767
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
768
|
+
${!f.isOptional ? 'required' : ''}
|
|
769
|
+
>
|
|
770
|
+
<option value="">選択してください</option>
|
|
771
|
+
${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n')}
|
|
772
|
+
</select>
|
|
773
|
+
{errors.${f.name} && (
|
|
774
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
775
|
+
)}
|
|
776
|
+
</div>`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Boolean の場合は checkbox
|
|
780
|
+
if (f.type === 'boolean') {
|
|
781
|
+
return ` <div className="flex items-center">
|
|
782
|
+
<input
|
|
783
|
+
type="checkbox"
|
|
784
|
+
id="${f.name}"
|
|
785
|
+
name="${f.name}"
|
|
786
|
+
checked={formData.${f.name}}
|
|
787
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.checked })}
|
|
788
|
+
className="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border border-gray-300 dark:border-gray-600 rounded"
|
|
789
|
+
/>
|
|
790
|
+
<label htmlFor="${f.name}" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
|
|
791
|
+
${f.name}
|
|
792
|
+
</label>
|
|
793
|
+
{errors.${f.name} && (
|
|
794
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
795
|
+
)}
|
|
796
|
+
</div>`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Array の場合はカンマ区切りテキスト
|
|
800
|
+
if (f.isArray) {
|
|
801
|
+
return ` <div>
|
|
802
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
803
|
+
${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(カンマ区切りで入力)</span>
|
|
804
|
+
</label>
|
|
805
|
+
<input
|
|
806
|
+
type="text"
|
|
807
|
+
id="${f.name}"
|
|
808
|
+
name="${f.name}"
|
|
809
|
+
value={formData.${f.name}}
|
|
810
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
811
|
+
placeholder="例: item1, item2, item3"
|
|
812
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
813
|
+
${!f.isOptional ? 'required' : ''}
|
|
814
|
+
/>
|
|
815
|
+
{errors.${f.name} && (
|
|
816
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
817
|
+
)}
|
|
818
|
+
</div>`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Number の場合
|
|
822
|
+
if (f.type === 'number') {
|
|
823
|
+
return ` <div>
|
|
824
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
825
|
+
${f.name}${!f.isOptional ? ' *' : ''}
|
|
826
|
+
</label>
|
|
827
|
+
<input
|
|
828
|
+
type="number"
|
|
829
|
+
id="${f.name}"
|
|
830
|
+
name="${f.name}"
|
|
831
|
+
value={formData.${f.name}}
|
|
832
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
833
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
834
|
+
${!f.isOptional ? 'required' : ''}
|
|
835
|
+
/>
|
|
836
|
+
{errors.${f.name} && (
|
|
837
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
838
|
+
)}
|
|
839
|
+
</div>`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Date の場合
|
|
843
|
+
if (f.type === 'date') {
|
|
844
|
+
return ` <div>
|
|
845
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
846
|
+
${f.name}${!f.isOptional ? ' *' : ''}
|
|
847
|
+
</label>
|
|
848
|
+
<input
|
|
849
|
+
type="date"
|
|
850
|
+
id="${f.name}"
|
|
851
|
+
name="${f.name}"
|
|
852
|
+
value={formData.${f.name}}
|
|
853
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
854
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
855
|
+
${!f.isOptional ? 'required' : ''}
|
|
856
|
+
/>
|
|
857
|
+
{errors.${f.name} && (
|
|
858
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
859
|
+
)}
|
|
860
|
+
</div>`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// デフォルト: text input
|
|
864
|
+
return ` <div>
|
|
865
|
+
<label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
866
|
+
${f.name}${!f.isOptional ? ' *' : ''}
|
|
867
|
+
</label>
|
|
868
|
+
<input
|
|
869
|
+
type="text"
|
|
870
|
+
id="${f.name}"
|
|
871
|
+
name="${f.name}"
|
|
872
|
+
value={formData.${f.name}}
|
|
873
|
+
onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
|
|
874
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
875
|
+
${!f.isOptional ? 'required' : ''}
|
|
876
|
+
/>
|
|
877
|
+
{errors.${f.name} && (
|
|
878
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
|
|
879
|
+
)}
|
|
880
|
+
</div>`;
|
|
881
|
+
}).join('\n\n')}
|
|
882
|
+
|
|
883
|
+
<div className="flex gap-4">
|
|
884
|
+
<button
|
|
885
|
+
type="submit"
|
|
886
|
+
disabled={loading}
|
|
887
|
+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
|
888
|
+
>
|
|
889
|
+
{loading ? 'Saving...' : isEdit ? 'Update' : 'Create'}
|
|
890
|
+
</button>
|
|
891
|
+
<button
|
|
892
|
+
type="button"
|
|
893
|
+
onClick={() => router.push('/${toKebabCase(modelName)}')}
|
|
894
|
+
className="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded"
|
|
895
|
+
>
|
|
896
|
+
Cancel
|
|
897
|
+
</button>
|
|
898
|
+
</div>
|
|
899
|
+
</form>
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 新規作成画面を生成
|
|
907
|
+
*/
|
|
908
|
+
export function generateNewPage(model: ModelInfo, authOptions?: UIAuthOptions): string {
|
|
909
|
+
const modelName = model.name;
|
|
910
|
+
const modelKebab = toKebabCase(modelName);
|
|
911
|
+
|
|
912
|
+
const hasAuth = !!authOptions;
|
|
913
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
914
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
915
|
+
|
|
916
|
+
if (!hasAuth) {
|
|
917
|
+
return `import ${modelName}Form from '../_components/${modelName}Form';
|
|
918
|
+
|
|
919
|
+
export default function New${modelName}Page() {
|
|
920
|
+
return (
|
|
921
|
+
<div className="container mx-auto px-4 py-8">
|
|
922
|
+
<div className="max-w-2xl mx-auto">
|
|
923
|
+
<h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
|
|
924
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
925
|
+
<${modelName}Form />
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return `'use client';
|
|
935
|
+
|
|
936
|
+
import { useEffect } from 'react';
|
|
937
|
+
import { useRouter } from 'next/navigation';
|
|
938
|
+
import ${modelName}Form from '../_components/${modelName}Form';
|
|
939
|
+
import { useAuth } from '@/lib/auth/auth-context';
|
|
940
|
+
|
|
941
|
+
export default function New${modelName}Page() {
|
|
942
|
+
const router = useRouter();
|
|
943
|
+
${hasWriteRoles ? ` const { hasAnyRole, loading } = useAuth();
|
|
944
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : ` const { user, loading } = useAuth();
|
|
945
|
+
const canWrite = !!user;`}
|
|
946
|
+
|
|
947
|
+
useEffect(() => {
|
|
948
|
+
if (!loading && !canWrite) {
|
|
949
|
+
router.push('/${modelKebab}');
|
|
950
|
+
}
|
|
951
|
+
}, [loading, canWrite, router]);
|
|
952
|
+
|
|
953
|
+
if (loading) {
|
|
954
|
+
return <div className="flex items-center justify-center min-h-screen"><div className="text-lg">Loading...</div></div>;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!canWrite) return null;
|
|
958
|
+
|
|
959
|
+
return (
|
|
960
|
+
<div className="container mx-auto px-4 py-8">
|
|
961
|
+
<div className="max-w-2xl mx-auto">
|
|
962
|
+
<h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
|
|
963
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
964
|
+
<${modelName}Form />
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* 編集画面を生成
|
|
975
|
+
*/
|
|
976
|
+
export function generateEditPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
977
|
+
const modelName = model.name;
|
|
978
|
+
const modelCamel = toCamelCase(modelName);
|
|
979
|
+
const modelKebab = toKebabCase(modelName);
|
|
980
|
+
|
|
981
|
+
const hasAuth = !!authOptions;
|
|
982
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
983
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
984
|
+
|
|
985
|
+
const schemaName = model.schemaName;
|
|
986
|
+
// Zod公式パターン対応: schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
|
|
987
|
+
const needsAlias = schemaName === modelName;
|
|
988
|
+
const localSchemaName = needsAlias ? `${toCamelCase(modelName)}Schema` : schemaName;
|
|
989
|
+
const schemaImportLine = needsAlias
|
|
990
|
+
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
991
|
+
: `import { ${schemaName} } from '${sharedPackageName}';`;
|
|
992
|
+
|
|
993
|
+
return `'use client';
|
|
994
|
+
|
|
995
|
+
import { useEffect, useState } from 'react';
|
|
996
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
997
|
+
import ${modelName}Form from '../../_components/${modelName}Form';
|
|
998
|
+
import { z } from 'zod/v4';
|
|
999
|
+
${schemaImportLine}
|
|
1000
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
1001
|
+
|
|
1002
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
1003
|
+
|
|
1004
|
+
export default function Edit${modelName}Page() {
|
|
1005
|
+
const params = useParams();
|
|
1006
|
+
${hasAuth ? ' const router = useRouter();' : ''}
|
|
1007
|
+
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
1008
|
+
const [loading, setLoading] = useState(true);
|
|
1009
|
+
${hasWriteRoles ? ` const { hasAnyRole, loading: authLoading } = useAuth();
|
|
1010
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user, loading: authLoading } = useAuth();
|
|
1011
|
+
const canWrite = !!user;` : ''}
|
|
1012
|
+
${hasAuth ? `
|
|
1013
|
+
useEffect(() => {
|
|
1014
|
+
if (!authLoading && !canWrite) {
|
|
1015
|
+
router.push('/${modelKebab}');
|
|
1016
|
+
}
|
|
1017
|
+
}, [authLoading, canWrite, router]);
|
|
1018
|
+
` : ''}
|
|
1019
|
+
|
|
1020
|
+
useEffect(() => {
|
|
1021
|
+
const id = params?.id as string;
|
|
1022
|
+
if (!id) return;
|
|
1023
|
+
|
|
1024
|
+
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
1025
|
+
.then((res) => res.json())
|
|
1026
|
+
.then((data) => {
|
|
1027
|
+
set${modelName}(data);
|
|
1028
|
+
setLoading(false);
|
|
1029
|
+
})
|
|
1030
|
+
.catch(() => setLoading(false));
|
|
1031
|
+
}, [params]);
|
|
1032
|
+
|
|
1033
|
+
if (loading) {
|
|
1034
|
+
return (
|
|
1035
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
1036
|
+
<div className="text-lg">Loading...</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (!${modelCamel}) {
|
|
1042
|
+
return (
|
|
1043
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
1044
|
+
<div className="text-red-600">${modelName} not found</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return (
|
|
1050
|
+
<div className="container mx-auto px-4 py-8">
|
|
1051
|
+
<div className="max-w-2xl mx-auto">
|
|
1052
|
+
<h1 className="text-3xl font-bold mb-6">Edit ${modelName}</h1>
|
|
1053
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
1054
|
+
<${modelName}Form initialData={${modelCamel}} isEdit={true} />
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
`;
|
|
1061
|
+
}
|