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