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