swallowkit 1.0.0-beta.5 → 1.0.0-beta.7
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 +251 -242
- package/README.md +252 -243
- package/dist/__tests__/fixtures.d.ts +14 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +85 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/create-model.js +14 -14
- package/dist/cli/commands/dev.d.ts +8 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +238 -30
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2507 -1664
- 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 +281 -117
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +28 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +5 -0
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +649 -218
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +1 -1
- package/dist/core/scaffold/model-parser.js +99 -99
- package/dist/core/scaffold/nextjs-generator.js +181 -181
- 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.js +656 -656
- 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/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +2 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +14 -10
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +81 -74
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
- package/src/__tests__/config.test.ts +122 -0
- package/src/__tests__/dev.test.ts +42 -0
- package/src/__tests__/fixtures.ts +83 -0
- package/src/__tests__/functions-generator.test.ts +101 -0
- package/src/__tests__/init.test.ts +59 -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/cli/commands/create-model.ts +141 -0
- package/src/cli/commands/dev.ts +794 -0
- package/src/cli/commands/index.ts +8 -0
- package/src/cli/commands/init.ts +3363 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +786 -0
- package/src/cli/index.ts +73 -0
- package/src/core/config.ts +244 -0
- package/src/core/scaffold/functions-generator.ts +674 -0
- package/src/core/scaffold/model-parser.ts +627 -0
- package/src/core/scaffold/nextjs-generator.ts +217 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +945 -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/types/index.ts +45 -0
- package/src/utils/package-manager.ts +229 -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
|
@@ -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,172 +56,172 @@ 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
|
-
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
67
|
-
|
|
68
|
-
export default function ${modelName}ListPage() {
|
|
69
|
-
const [${modelCamel}s, set${modelName}s] = useState<${modelName}[]>([]);
|
|
70
|
-
const [loading, setLoading] = useState(true);
|
|
71
|
-
const [error, setError] = useState<string | null>(null);
|
|
72
|
-
${hasForeignKeys ? foreignKeyStates : ''}
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
fetch('/api/${modelCamel}')
|
|
76
|
-
.then((res) => {
|
|
77
|
-
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}s');
|
|
78
|
-
return res.json();
|
|
79
|
-
})
|
|
80
|
-
.then((data) => {
|
|
81
|
-
set${modelName}s(data);
|
|
82
|
-
setLoading(false);
|
|
83
|
-
})
|
|
84
|
-
.catch((err) => {
|
|
85
|
-
setError(err.message);
|
|
86
|
-
setLoading(false);
|
|
87
|
-
});
|
|
88
|
-
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
89
|
-
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
const handleDelete = async (id: string) => {
|
|
93
|
-
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(\`/api/${modelCamel}/\${id}\`, {
|
|
97
|
-
method: 'DELETE',
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
101
|
-
|
|
102
|
-
set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
|
|
103
|
-
} catch (err: any) {
|
|
104
|
-
alert(\`Error: \${err.message}\`);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
if (loading) {
|
|
109
|
-
return (
|
|
110
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
111
|
-
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (error) {
|
|
117
|
-
return (
|
|
118
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
119
|
-
<div className="text-red-600 dark:text-red-400">Error: {error}</div>
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div className="container mx-auto px-4 py-8">
|
|
126
|
-
<div className="mb-4">
|
|
127
|
-
<Link
|
|
128
|
-
href="/"
|
|
129
|
-
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
|
|
130
|
-
>
|
|
131
|
-
← Home
|
|
132
|
-
</Link>
|
|
133
|
-
</div>
|
|
134
|
-
<div className="flex justify-between items-center mb-6">
|
|
135
|
-
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
|
|
136
|
-
<Link
|
|
137
|
-
href="/${modelKebab}/new"
|
|
138
|
-
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
139
|
-
>
|
|
140
|
-
Create New
|
|
141
|
-
</Link>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{${modelCamel}s.length === 0 ? (
|
|
145
|
-
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
146
|
-
No ${modelCamel}s found. Create your first one!
|
|
147
|
-
</div>
|
|
148
|
-
) : (
|
|
149
|
-
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
|
150
|
-
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
151
|
-
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
152
|
-
<tr>
|
|
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
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
67
|
+
|
|
68
|
+
export default function ${modelName}ListPage() {
|
|
69
|
+
const [${modelCamel}s, set${modelName}s] = useState<${modelName}[]>([]);
|
|
70
|
+
const [loading, setLoading] = useState(true);
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
72
|
+
${hasForeignKeys ? foreignKeyStates : ''}
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
fetch('/api/${modelCamel}')
|
|
76
|
+
.then((res) => {
|
|
77
|
+
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}s');
|
|
78
|
+
return res.json();
|
|
79
|
+
})
|
|
80
|
+
.then((data) => {
|
|
81
|
+
set${modelName}s(data);
|
|
82
|
+
setLoading(false);
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
setError(err.message);
|
|
86
|
+
setLoading(false);
|
|
87
|
+
});
|
|
88
|
+
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
89
|
+
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleDelete = async (id: string) => {
|
|
93
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(\`/api/${modelCamel}/\${id}\`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
101
|
+
|
|
102
|
+
set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
alert(\`Error: \${err.message}\`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (loading) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
111
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (error) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
119
|
+
<div className="text-red-600 dark:text-red-400">Error: {error}</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="container mx-auto px-4 py-8">
|
|
126
|
+
<div className="mb-4">
|
|
127
|
+
<Link
|
|
128
|
+
href="/"
|
|
129
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
|
|
130
|
+
>
|
|
131
|
+
← Home
|
|
132
|
+
</Link>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex justify-between items-center mb-6">
|
|
135
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
|
|
136
|
+
<Link
|
|
137
|
+
href="/${modelKebab}/new"
|
|
138
|
+
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
139
|
+
>
|
|
140
|
+
Create New
|
|
141
|
+
</Link>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{${modelCamel}s.length === 0 ? (
|
|
145
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
146
|
+
No ${modelCamel}s found. Create your first one!
|
|
147
|
+
</div>
|
|
148
|
+
) : (
|
|
149
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
|
150
|
+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
151
|
+
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
152
|
+
<tr>
|
|
153
153
|
${displayFields.map(f => {
|
|
154
154
|
const headerLabel = f.isNestedSchema && f.nestedModelName
|
|
155
155
|
? f.nestedModelName
|
|
156
156
|
: (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}
|
|
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}
|
|
159
159
|
</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">
|
|
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">
|
|
169
169
|
${displayFields.map(f => {
|
|
170
170
|
if (f.isNestedSchema && f.nestedModelName) {
|
|
171
171
|
const displayField = f.nestedDisplayField || 'name';
|
|
172
172
|
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(', ') : '-'}
|
|
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(', ') : '-'}
|
|
175
175
|
</td>`;
|
|
176
176
|
}
|
|
177
177
|
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} || '-'}
|
|
178
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
179
|
+
{item.${f.name}?.${displayField} || '-'}
|
|
180
180
|
</td>`;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
else if (f.isForeignKey && f.referencedModel) {
|
|
184
184
|
const refModel = f.referencedModel;
|
|
185
185
|
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}}
|
|
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}}
|
|
188
188
|
</td>`;
|
|
189
189
|
}
|
|
190
190
|
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})}
|
|
191
|
+
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
192
|
+
{String(item.${f.name})}
|
|
193
193
|
</td>`;
|
|
194
194
|
}
|
|
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
|
-
<Link
|
|
204
|
-
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
205
|
-
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
206
|
-
>
|
|
207
|
-
Edit
|
|
208
|
-
</Link>
|
|
209
|
-
<button
|
|
210
|
-
onClick={() => handleDelete(item.id)}
|
|
211
|
-
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
212
|
-
>
|
|
213
|
-
Delete
|
|
214
|
-
</button>
|
|
215
|
-
</td>
|
|
216
|
-
</tr>
|
|
217
|
-
))}
|
|
218
|
-
</tbody>
|
|
219
|
-
</table>
|
|
220
|
-
</div>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
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
|
+
<Link
|
|
204
|
+
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
205
|
+
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
206
|
+
>
|
|
207
|
+
Edit
|
|
208
|
+
</Link>
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => handleDelete(item.id)}
|
|
211
|
+
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
212
|
+
>
|
|
213
|
+
Delete
|
|
214
|
+
</button>
|
|
215
|
+
</td>
|
|
216
|
+
</tr>
|
|
217
|
+
))}
|
|
218
|
+
</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
225
|
`;
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
@@ -244,15 +244,15 @@ function generateDetailPage(model, sharedPackageName) {
|
|
|
244
244
|
const foreignKeyFetches = foreignKeyFields.map(f => {
|
|
245
245
|
const refModel = f.referencedModel;
|
|
246
246
|
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
|
-
})
|
|
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
|
+
})
|
|
256
256
|
.catch(err => console.error('Failed to fetch ${refModel}s:', err));`;
|
|
257
257
|
}).join('\n');
|
|
258
258
|
const schemaName = model.schemaName;
|
|
@@ -262,147 +262,147 @@ function generateDetailPage(model, sharedPackageName) {
|
|
|
262
262
|
const schemaImportLine = needsAlias
|
|
263
263
|
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
264
264
|
: `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
|
-
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
274
|
-
|
|
275
|
-
export default function ${modelName}DetailPage() {
|
|
276
|
-
const params = useParams();
|
|
277
|
-
const router = useRouter();
|
|
278
|
-
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
279
|
-
const [loading, setLoading] = useState(true);
|
|
280
|
-
const [error, setError] = useState<string | null>(null);
|
|
281
|
-
${hasForeignKeys ? foreignKeyStates : ''}
|
|
282
|
-
|
|
283
|
-
useEffect(() => {
|
|
284
|
-
const id = params?.id as string;
|
|
285
|
-
if (!id) return;
|
|
286
|
-
|
|
287
|
-
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
288
|
-
.then((res) => {
|
|
289
|
-
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}');
|
|
290
|
-
return res.json();
|
|
291
|
-
})
|
|
292
|
-
.then((data) => {
|
|
293
|
-
set${modelName}(data);
|
|
294
|
-
setLoading(false);
|
|
295
|
-
})
|
|
296
|
-
.catch((err) => {
|
|
297
|
-
setError(err.message);
|
|
298
|
-
setLoading(false);
|
|
299
|
-
});
|
|
300
|
-
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
301
|
-
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
302
|
-
}, [params]);
|
|
303
|
-
|
|
304
|
-
const handleDelete = async () => {
|
|
305
|
-
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
const res = await fetch(\`/api/${modelCamel}/\${params?.id}\`, {
|
|
309
|
-
method: 'DELETE',
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
313
|
-
|
|
314
|
-
router.push('/${modelKebab}');
|
|
315
|
-
} catch (err: any) {
|
|
316
|
-
alert(\`Error: \${err.message}\`);
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
if (loading) {
|
|
321
|
-
return (
|
|
322
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
323
|
-
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (error || !${modelCamel}) {
|
|
329
|
-
return (
|
|
330
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
331
|
-
<div className="text-red-600 dark:text-red-400">Error: {error || '${modelName} not found'}</div>
|
|
332
|
-
</div>
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return (
|
|
337
|
-
<div className="container mx-auto px-4 py-8">
|
|
338
|
-
<div className="max-w-2xl mx-auto">
|
|
339
|
-
<div className="flex justify-between items-center mb-6">
|
|
340
|
-
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
|
|
341
|
-
<div className="space-x-2">
|
|
342
|
-
<Link
|
|
343
|
-
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
344
|
-
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"
|
|
345
|
-
>
|
|
346
|
-
Edit
|
|
347
|
-
</Link>
|
|
348
|
-
<button
|
|
349
|
-
onClick={handleDelete}
|
|
350
|
-
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"
|
|
351
|
-
>
|
|
352
|
-
Delete
|
|
353
|
-
</button>
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
358
|
-
<dl className="space-y-4">
|
|
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
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
274
|
+
|
|
275
|
+
export default function ${modelName}DetailPage() {
|
|
276
|
+
const params = useParams();
|
|
277
|
+
const router = useRouter();
|
|
278
|
+
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
279
|
+
const [loading, setLoading] = useState(true);
|
|
280
|
+
const [error, setError] = useState<string | null>(null);
|
|
281
|
+
${hasForeignKeys ? foreignKeyStates : ''}
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const id = params?.id as string;
|
|
285
|
+
if (!id) return;
|
|
286
|
+
|
|
287
|
+
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
288
|
+
.then((res) => {
|
|
289
|
+
if (!res.ok) throw new Error('Failed to fetch ${modelCamel}');
|
|
290
|
+
return res.json();
|
|
291
|
+
})
|
|
292
|
+
.then((data) => {
|
|
293
|
+
set${modelName}(data);
|
|
294
|
+
setLoading(false);
|
|
295
|
+
})
|
|
296
|
+
.catch((err) => {
|
|
297
|
+
setError(err.message);
|
|
298
|
+
setLoading(false);
|
|
299
|
+
});
|
|
300
|
+
${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
|
|
301
|
+
${hasForeignKeys ? foreignKeyFetches : ''}
|
|
302
|
+
}, [params]);
|
|
303
|
+
|
|
304
|
+
const handleDelete = async () => {
|
|
305
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const res = await fetch(\`/api/${modelCamel}/\${params?.id}\`, {
|
|
309
|
+
method: 'DELETE',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
313
|
+
|
|
314
|
+
router.push('/${modelKebab}');
|
|
315
|
+
} catch (err: any) {
|
|
316
|
+
alert(\`Error: \${err.message}\`);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (loading) {
|
|
321
|
+
return (
|
|
322
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
323
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (error || !${modelCamel}) {
|
|
329
|
+
return (
|
|
330
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
331
|
+
<div className="text-red-600 dark:text-red-400">Error: {error || '${modelName} not found'}</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div className="container mx-auto px-4 py-8">
|
|
338
|
+
<div className="max-w-2xl mx-auto">
|
|
339
|
+
<div className="flex justify-between items-center mb-6">
|
|
340
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
|
|
341
|
+
<div className="space-x-2">
|
|
342
|
+
<Link
|
|
343
|
+
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
344
|
+
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"
|
|
345
|
+
>
|
|
346
|
+
Edit
|
|
347
|
+
</Link>
|
|
348
|
+
<button
|
|
349
|
+
onClick={handleDelete}
|
|
350
|
+
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"
|
|
351
|
+
>
|
|
352
|
+
Delete
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
358
|
+
<dl className="space-y-4">
|
|
359
359
|
${model.fields.map(f => {
|
|
360
360
|
if (f.isNestedSchema && f.nestedModelName) {
|
|
361
361
|
const displayField = f.nestedDisplayField || 'name';
|
|
362
362
|
const label = f.nestedModelName;
|
|
363
363
|
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>
|
|
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>
|
|
367
367
|
</div>`;
|
|
368
368
|
}
|
|
369
369
|
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>
|
|
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>
|
|
373
373
|
</div>`;
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
else if (f.isForeignKey && f.referencedModel) {
|
|
377
377
|
const refModel = f.referencedModel;
|
|
378
378
|
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>
|
|
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>
|
|
382
382
|
</div>`;
|
|
383
383
|
}
|
|
384
384
|
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>
|
|
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>
|
|
388
388
|
</div>`;
|
|
389
389
|
}
|
|
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
|
-
}
|
|
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
|
+
}
|
|
406
406
|
`;
|
|
407
407
|
}
|
|
408
408
|
/**
|
|
@@ -441,38 +441,38 @@ function generateFormComponent(model, sharedPackageName) {
|
|
|
441
441
|
const refModelCamel = (0, model_parser_1.toCamelCase)(f.nestedModelName);
|
|
442
442
|
const refModelPascal = f.nestedModelName;
|
|
443
443
|
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 }))))
|
|
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 }))))
|
|
448
448
|
.catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
|
|
449
449
|
}).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({
|
|
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({
|
|
476
476
|
${formFields.map(f => {
|
|
477
477
|
// ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
|
|
478
478
|
if (f.isNestedSchema) {
|
|
@@ -506,106 +506,106 @@ ${formFields.map(f => {
|
|
|
506
506
|
return ` ${f.name}: initialData?.${f.name} !== undefined ? String(initialData.${f.name}) : ${defaultValue},`;
|
|
507
507
|
}
|
|
508
508
|
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からオブジェクトに変換
|
|
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からオブジェクトに変換
|
|
547
547
|
${nestedSchemaFields.map(f => {
|
|
548
548
|
const refModelCamel = (0, model_parser_1.toCamelCase)(f.nestedModelName);
|
|
549
549
|
const refModelPascal = f.nestedModelName;
|
|
550
550
|
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;
|
|
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;
|
|
557
557
|
}`;
|
|
558
558
|
}
|
|
559
559
|
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;
|
|
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;
|
|
568
568
|
}`;
|
|
569
569
|
}
|
|
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">
|
|
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">
|
|
609
609
|
${formFields.map(f => {
|
|
610
610
|
// ネストスキーマの場合はセレクトボックス(単一)またはマルチセレクト(配列)
|
|
611
611
|
if (f.isNestedSchema && f.nestedModelName) {
|
|
@@ -613,222 +613,222 @@ ${formFields.map(f => {
|
|
|
613
613
|
const label = f.nestedModelName;
|
|
614
614
|
if (f.isArray) {
|
|
615
615
|
// 配列参照: マルチセレクト
|
|
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
|
-
)}
|
|
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
|
+
)}
|
|
639
639
|
</div>`;
|
|
640
640
|
}
|
|
641
641
|
else {
|
|
642
642
|
// 単一オブジェクト参照: セレクトボックス
|
|
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
|
-
)}
|
|
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
|
+
)}
|
|
663
663
|
</div>`;
|
|
664
664
|
}
|
|
665
665
|
}
|
|
666
666
|
// 外部キーの場合は参照先モデルのドロップダウン
|
|
667
667
|
if (f.isForeignKey && f.referencedModel) {
|
|
668
668
|
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
|
-
)}
|
|
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
|
+
)}
|
|
689
689
|
</div>`;
|
|
690
690
|
}
|
|
691
691
|
// Enum の場合は select 要素
|
|
692
692
|
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
|
-
)}
|
|
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
|
+
)}
|
|
711
711
|
</div>`;
|
|
712
712
|
}
|
|
713
713
|
// Boolean の場合は checkbox
|
|
714
714
|
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
|
-
)}
|
|
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
|
+
)}
|
|
730
730
|
</div>`;
|
|
731
731
|
}
|
|
732
732
|
// Array の場合はカンマ区切りテキスト
|
|
733
733
|
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
|
-
)}
|
|
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
|
+
)}
|
|
751
751
|
</div>`;
|
|
752
752
|
}
|
|
753
753
|
// Number の場合
|
|
754
754
|
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
|
-
)}
|
|
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
|
+
)}
|
|
771
771
|
</div>`;
|
|
772
772
|
}
|
|
773
773
|
// Date の場合
|
|
774
774
|
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
|
-
)}
|
|
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
|
+
)}
|
|
791
791
|
</div>`;
|
|
792
792
|
}
|
|
793
793
|
// デフォルト: 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
|
-
)}
|
|
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
|
+
)}
|
|
810
810
|
</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
|
-
}
|
|
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
|
+
}
|
|
832
832
|
`;
|
|
833
833
|
}
|
|
834
834
|
/**
|
|
@@ -837,20 +837,20 @@ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n
|
|
|
837
837
|
function generateNewPage(model) {
|
|
838
838
|
const modelName = model.name;
|
|
839
839
|
const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
|
|
840
|
-
return `import ${modelName}Form from '../_components/${modelName}Form';
|
|
841
|
-
|
|
842
|
-
export default function New${modelName}Page() {
|
|
843
|
-
return (
|
|
844
|
-
<div className="container mx-auto px-4 py-8">
|
|
845
|
-
<div className="max-w-2xl mx-auto">
|
|
846
|
-
<h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
|
|
847
|
-
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
848
|
-
<${modelName}Form />
|
|
849
|
-
</div>
|
|
850
|
-
</div>
|
|
851
|
-
</div>
|
|
852
|
-
);
|
|
853
|
-
}
|
|
840
|
+
return `import ${modelName}Form from '../_components/${modelName}Form';
|
|
841
|
+
|
|
842
|
+
export default function New${modelName}Page() {
|
|
843
|
+
return (
|
|
844
|
+
<div className="container mx-auto px-4 py-8">
|
|
845
|
+
<div className="max-w-2xl mx-auto">
|
|
846
|
+
<h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
|
|
847
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
848
|
+
<${modelName}Form />
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
854
|
`;
|
|
855
855
|
}
|
|
856
856
|
/**
|
|
@@ -867,61 +867,61 @@ function generateEditPage(model, sharedPackageName) {
|
|
|
867
867
|
const schemaImportLine = needsAlias
|
|
868
868
|
? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
|
|
869
869
|
: `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
|
-
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
879
|
-
|
|
880
|
-
export default function Edit${modelName}Page() {
|
|
881
|
-
const params = useParams();
|
|
882
|
-
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
883
|
-
const [loading, setLoading] = useState(true);
|
|
884
|
-
|
|
885
|
-
useEffect(() => {
|
|
886
|
-
const id = params?.id as string;
|
|
887
|
-
if (!id) return;
|
|
888
|
-
|
|
889
|
-
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
890
|
-
.then((res) => res.json())
|
|
891
|
-
.then((data) => {
|
|
892
|
-
set${modelName}(data);
|
|
893
|
-
setLoading(false);
|
|
894
|
-
})
|
|
895
|
-
.catch(() => setLoading(false));
|
|
896
|
-
}, [params]);
|
|
897
|
-
|
|
898
|
-
if (loading) {
|
|
899
|
-
return (
|
|
900
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
901
|
-
<div className="text-lg">Loading...</div>
|
|
902
|
-
</div>
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (!${modelCamel}) {
|
|
907
|
-
return (
|
|
908
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
909
|
-
<div className="text-red-600">${modelName} not found</div>
|
|
910
|
-
</div>
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return (
|
|
915
|
-
<div className="container mx-auto px-4 py-8">
|
|
916
|
-
<div className="max-w-2xl mx-auto">
|
|
917
|
-
<h1 className="text-3xl font-bold mb-6">Edit ${modelName}</h1>
|
|
918
|
-
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
919
|
-
<${modelName}Form initialData={${modelCamel}} isEdit={true} />
|
|
920
|
-
</div>
|
|
921
|
-
</div>
|
|
922
|
-
</div>
|
|
923
|
-
);
|
|
924
|
-
}
|
|
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
|
+
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
879
|
+
|
|
880
|
+
export default function Edit${modelName}Page() {
|
|
881
|
+
const params = useParams();
|
|
882
|
+
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
883
|
+
const [loading, setLoading] = useState(true);
|
|
884
|
+
|
|
885
|
+
useEffect(() => {
|
|
886
|
+
const id = params?.id as string;
|
|
887
|
+
if (!id) return;
|
|
888
|
+
|
|
889
|
+
fetch(\`/api/${modelCamel}/\${id}\`)
|
|
890
|
+
.then((res) => res.json())
|
|
891
|
+
.then((data) => {
|
|
892
|
+
set${modelName}(data);
|
|
893
|
+
setLoading(false);
|
|
894
|
+
})
|
|
895
|
+
.catch(() => setLoading(false));
|
|
896
|
+
}, [params]);
|
|
897
|
+
|
|
898
|
+
if (loading) {
|
|
899
|
+
return (
|
|
900
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
901
|
+
<div className="text-lg">Loading...</div>
|
|
902
|
+
</div>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!${modelCamel}) {
|
|
907
|
+
return (
|
|
908
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
909
|
+
<div className="text-red-600">${modelName} not found</div>
|
|
910
|
+
</div>
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return (
|
|
915
|
+
<div className="container mx-auto px-4 py-8">
|
|
916
|
+
<div className="max-w-2xl mx-auto">
|
|
917
|
+
<h1 className="text-3xl font-bold mb-6">Edit ${modelName}</h1>
|
|
918
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
919
|
+
<${modelName}Form initialData={${modelCamel}} isEdit={true} />
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
925
|
`;
|
|
926
926
|
}
|
|
927
927
|
//# sourceMappingURL=ui-generator.js.map
|