generator-pninja 1.0.3 → 1.0.4
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/generators/app/index.js +1 -1
- package/generators/app/templates/README.md.ejs +6 -0
- package/generators/app/templates/package.json.ejs +5 -1
- package/generators/client/index.js +1 -0
- package/generators/client/templates/react/src/components/entities/EntityForm.tsx.ejs +3 -1
- package/generators/client/templates/react/src/index.css.ejs +4 -1
- package/generators/client/templates/react/src/pages/entities/EntityList.tsx.ejs +7 -4
- package/generators/client/templates/react/src/pages/entities/EntityView.tsx.ejs +3 -1
- package/generators/entities/index.js +1 -0
- package/generators/entities/templates/DatabaseSeeder.php.ejs +101 -9
- package/generators/entities/templates/EntityController.php.ejs +35 -1
- package/package.json +1 -1
package/generators/app/index.js
CHANGED
|
@@ -116,7 +116,7 @@ export default class extends Generator {
|
|
|
116
116
|
type: "list",
|
|
117
117
|
name: "dbms",
|
|
118
118
|
message: "Your DBMS",
|
|
119
|
-
default:
|
|
119
|
+
default: this.config.get('dbms') || 'sqlite',
|
|
120
120
|
choices: [
|
|
121
121
|
{
|
|
122
122
|
name: `SQLite ${colors.dim('(Lightweight, serverless, file-based)')}`,
|
|
@@ -16,6 +16,12 @@ Start your **development client** (http://localhost:5173/) with:
|
|
|
16
16
|
npm run client
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Alternatively, you can start both the **development client and server** with:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
19
25
|
To **initialize** the database:
|
|
20
26
|
|
|
21
27
|
```sh
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"dev": "concurrently \"npm run server\" \"npm run client\"",
|
|
10
11
|
"client": "cd client && npm i --loglevel=error && npm run dev",
|
|
11
12
|
"dump-server": "cd server && php artisan dump-server",
|
|
12
13
|
"migrate": "cd server && php artisan migrate",
|
|
@@ -24,5 +25,8 @@
|
|
|
24
25
|
<% } -%>
|
|
25
26
|
"seed": "cd server && php artisan db:seed",
|
|
26
27
|
"server": "cd server && php artisan serve"
|
|
27
|
-
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"concurrently": "^9.2.1"
|
|
31
|
+
}
|
|
28
32
|
}
|
|
@@ -43,6 +43,7 @@ export default class AuthGenerator extends Generator {
|
|
|
43
43
|
type: 'checkbox',
|
|
44
44
|
name: 'languages',
|
|
45
45
|
message: 'Please choose additional languages to install',
|
|
46
|
+
default: this.config.get('languages') || [],
|
|
46
47
|
choices: (answers) => {
|
|
47
48
|
return getAvailableLanguages(answers.nativeLanguage);
|
|
48
49
|
},
|
|
@@ -181,6 +181,8 @@ interface <%= entity.name %>FormProps {
|
|
|
181
181
|
onCancel: () => void;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
const canDelete = () => true; // Placeholder for permission logic
|
|
185
|
+
|
|
184
186
|
export function <%= entity.name %>Form({ <%= to.camel(entity.name)%>, onSubmit, onCancel }: <%= entity.name%>FormProps) {
|
|
185
187
|
const { t } = useTranslation();
|
|
186
188
|
useAutoShortcuts();
|
|
@@ -392,7 +394,7 @@ export function <%= entity.name %>Form({ <%= to.camel(entity.name)%>, onSubmit,
|
|
|
392
394
|
<kbd className="kdb-shortcut">{isMac ? '⌘' : 'Ctrl'}</kbd>
|
|
393
395
|
<kbd className="kdb-shortcut">↵</kbd>
|
|
394
396
|
</button>
|
|
395
|
-
{<%= to.camel(entity.name)-%>?.id && <<%= entity.name %>DeleteButton id={<%= to.camel(entity.name)-%>.id} />}
|
|
397
|
+
{<%= to.camel(entity.name)-%>?.id && canDelete() && <<%= entity.name %>DeleteButton id={<%= to.camel(entity.name)-%>.id} />}
|
|
396
398
|
</div>
|
|
397
399
|
</form>
|
|
398
400
|
</FormProvider>
|
|
@@ -91,7 +91,10 @@
|
|
|
91
91
|
}
|
|
92
92
|
tbody {
|
|
93
93
|
td {
|
|
94
|
-
@apply px-6 py-4
|
|
94
|
+
@apply px-6 py-4;
|
|
95
|
+
&.truncate {
|
|
96
|
+
@apply truncate max-w-50;
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
tr {
|
|
97
100
|
@apply odd:bg-slate-100 even:bg-slate-200 text-slate-900 odd:dark:bg-slate-700 even:dark:bg-slate-800 dark:text-slate-100;
|
|
@@ -148,6 +148,9 @@ export default function <%= entity.name %>List() {
|
|
|
148
148
|
document.title = t('entities:<%-to.snake(entity.name)%>.<%- pluralize(entity.name)%>');
|
|
149
149
|
}, [i18n.language, t]);
|
|
150
150
|
|
|
151
|
+
const canEdit = () => true; // Placeholder for permission logic
|
|
152
|
+
const canView = () => true; // Placeholder for permission logic
|
|
153
|
+
|
|
151
154
|
return (
|
|
152
155
|
<>
|
|
153
156
|
<h1 className="text-2xl mb-4">{t('entities:<%-to.snake(entity.name)%>.<%- pluralize(entity.name)%>')}</h1>
|
|
@@ -190,7 +193,7 @@ export default function <%= entity.name %>List() {
|
|
|
190
193
|
<% for(column of columns) { -%>
|
|
191
194
|
<SortHeader field="<%-column%>" label={t('entities:<%-to.snake(entity.name)%>.<%-column%>')} />
|
|
192
195
|
<% } -%>
|
|
193
|
-
<%
|
|
196
|
+
<% for(r of relatedEntities) { -%>
|
|
194
197
|
<th scope="col">{t('entities:<%-to.snake(entity.name)%>.<%-r.field%>')}</th>
|
|
195
198
|
<% } -%>
|
|
196
199
|
<th scope="col">{t('actions.crud.actions')}</th>
|
|
@@ -220,7 +223,7 @@ export default function <%= entity.name %>List() {
|
|
|
220
223
|
</td>
|
|
221
224
|
<% } -%>
|
|
222
225
|
<% } -%>
|
|
223
|
-
<%
|
|
226
|
+
<% for(r of relatedEntities) { -%>
|
|
224
227
|
<td>
|
|
225
228
|
<% if(r.isArray) { -%>
|
|
226
229
|
{row?.<%-r.field%>?.map((row, index) => (<Fragment key={index}>{index !== 0 ? ', ': ''}<Link className="text-link" to={`/entities/<%=to.slug(pluralize(r.related))%>/view/${row?.id}`}>{row?.<%-to.snake(r.labelField)%>}</Link></Fragment>))}
|
|
@@ -231,8 +234,8 @@ export default function <%= entity.name %>List() {
|
|
|
231
234
|
<% } -%>
|
|
232
235
|
<td className="text-nowrap w-0 uppercase">
|
|
233
236
|
<div className="flex space-x-2">
|
|
234
|
-
<Link to={`/entities/<%=to.slug(pluralize(entity.name))%>/view/${row.id}`} className="text-link">{t('actions.crud.view')}</Link>
|
|
235
|
-
<Link to={`/entities/<%=to.slug(pluralize(entity.name))%>/edit/${row.id}`} className="text-link">{t('actions.crud.edit')}</Link>
|
|
237
|
+
{canView() && <Link to={`/entities/<%=to.slug(pluralize(entity.name))%>/view/${row.id}`} className="text-link">{t('actions.crud.view')}</Link>}
|
|
238
|
+
{canEdit() && <Link to={`/entities/<%=to.slug(pluralize(entity.name))%>/edit/${row.id}`} className="text-link">{t('actions.crud.edit')}</Link>}
|
|
236
239
|
</div>
|
|
237
240
|
</td>
|
|
238
241
|
</tr>
|
|
@@ -27,6 +27,8 @@ interface <%= entity.name %>ViewParams extends Record<string, string | undefined
|
|
|
27
27
|
id: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const canDelete = () => true; // Placeholder for permission logic
|
|
31
|
+
|
|
30
32
|
export function <%= entity.name %>DataView({ data }: <%= entity.name %>DataViewParams) {
|
|
31
33
|
const { t } = useTranslation();
|
|
32
34
|
if (!data) return '';
|
|
@@ -163,7 +165,7 @@ export default function <%= entity.name %>View() {
|
|
|
163
165
|
<Link to={`/entities/<%-to.slug(pluralize(entity.name))%>/edit/${id}`} className="primary-button">
|
|
164
166
|
{t('actions.crud.edit')}
|
|
165
167
|
</Link>
|
|
166
|
-
<<%= entity.name %>DeleteButton id={id} />
|
|
168
|
+
{canDelete() && <<%= entity.name %>DeleteButton id={id} />}
|
|
167
169
|
</div>
|
|
168
170
|
)}
|
|
169
171
|
</>
|
|
@@ -144,6 +144,7 @@ export default class EntityGenerator extends Generator {
|
|
|
144
144
|
|
|
145
145
|
this.fs.copyTpl(this.templatePath("blobs/dummy.pdf"), this.destinationPath(`server/database/factories/dummy.pdf`));
|
|
146
146
|
this.fs.copyTpl(this.templatePath("blobs/dummy.png"), this.destinationPath(`server/database/factories/dummy.png`));
|
|
147
|
+
this.fs.copyTpl(this.templatePath(".gitkeep.ejs"), this.destinationPath(`server/database/seeders/csv/.gitkeep`));
|
|
147
148
|
this.fs.copyTpl(this.templatePath(".gitkeep.ejs"), this.destinationPath(`server/storage/app/private/uploads/.gitkeep`));
|
|
148
149
|
this.fs.copyTpl(this.templatePath(".gitkeep.ejs"), this.destinationPath(`server/storage/app/public/uploads/.gitkeep`));
|
|
149
150
|
this.fs.copyTpl(this.templatePath("SessionAuth.php.ejs"), this.destinationPath(`server/app/Http/Middleware/SessionAuth.php`));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
namespace Database\Seeders;
|
|
4
4
|
|
|
5
5
|
use Illuminate\Database\Seeder;
|
|
6
|
+
use Illuminate\Support\Facades\File;
|
|
6
7
|
<% entities.forEach(function(entity) { %>
|
|
7
8
|
use App\Models\<%= entity.name %>;<% }); %>
|
|
8
9
|
class DatabaseSeeder extends Seeder
|
|
@@ -12,12 +13,103 @@ class DatabaseSeeder extends Seeder
|
|
|
12
13
|
*/
|
|
13
14
|
public function run(): void
|
|
14
15
|
{
|
|
15
|
-
<% entities.forEach(function(entity) {
|
|
16
|
-
|
|
17
|
-
<%
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<% });
|
|
23
|
-
}
|
|
16
|
+
<% entities.forEach(function(entity) { -%>
|
|
17
|
+
$this->seedModel(<%= entity.name %>::class, '<%= entity.name %>.csv', <%= n %>);
|
|
18
|
+
<% }); -%>
|
|
19
|
+
|
|
20
|
+
// Many-to-many relationships
|
|
21
|
+
<% manyToMany.forEach(function(rel) { -%>
|
|
22
|
+
$this->seedManyToMany(<%= rel.fromEntity %>::class, '<%= rel.relPropery %>', <%= rel.toEntity %>::class, '<%= rel.fromEntity %><%= rel.toEntity %>.csv', <%- Math.round(n/2) %>);
|
|
23
|
+
<% }); -%>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Seed from CSV or factory
|
|
28
|
+
*/
|
|
29
|
+
private function seedModel(string $modelClass, string $csvFilename, int $minCount): void
|
|
30
|
+
{
|
|
31
|
+
$csvPath = database_path("seeders/csv/{$csvFilename}");
|
|
32
|
+
|
|
33
|
+
if (File::exists($csvPath)) {
|
|
34
|
+
$this->seedFromCsv($modelClass, $csvPath);
|
|
35
|
+
} else {
|
|
36
|
+
// Use the factory
|
|
37
|
+
while ($modelClass::count() < $minCount) {
|
|
38
|
+
$modelClass::factory()->create();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load data from a CSV file
|
|
45
|
+
*/
|
|
46
|
+
private function seedFromCsv(string $modelClass, string $csvPath): void
|
|
47
|
+
{
|
|
48
|
+
$csv = array_map('str_getcsv', file($csvPath));
|
|
49
|
+
$headers = array_shift($csv);
|
|
50
|
+
|
|
51
|
+
foreach ($csv as $row) {
|
|
52
|
+
$data = array_combine($headers, $row);
|
|
53
|
+
|
|
54
|
+
// Rimuovi campi vuoti o null
|
|
55
|
+
$data = array_filter($data, function($value) {
|
|
56
|
+
return $value !== '' && $value !== null;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
$modelClass::create($data);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Seed many-to-many relationships
|
|
65
|
+
*/
|
|
66
|
+
private function seedManyToMany(
|
|
67
|
+
string $modelClass,
|
|
68
|
+
string $relationName,
|
|
69
|
+
string $relatedClass,
|
|
70
|
+
string $csvFilename,
|
|
71
|
+
int $randomCount
|
|
72
|
+
): void {
|
|
73
|
+
$csvPath = database_path("seeders/csv/{$csvFilename}");
|
|
74
|
+
|
|
75
|
+
if (File::exists($csvPath)) {
|
|
76
|
+
$this->seedManyToManyFromCsv($modelClass, $relationName, $csvPath);
|
|
77
|
+
} else {
|
|
78
|
+
// Use random associations
|
|
79
|
+
$modelClass::all()->each(function ($entity) use ($relationName, $relatedClass, $randomCount) {
|
|
80
|
+
$entity->$relationName()->attach(
|
|
81
|
+
$relatedClass::all()->random($randomCount)->pluck('id')->toArray()
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load many-to-many relationships from CSV
|
|
89
|
+
* The CSV must have columns: model_id, related_id
|
|
90
|
+
*/
|
|
91
|
+
private function seedManyToManyFromCsv(string $modelClass, string $relationName, string $csvPath): void
|
|
92
|
+
{
|
|
93
|
+
$csv = array_map('str_getcsv', file($csvPath));
|
|
94
|
+
$headers = array_shift($csv);
|
|
95
|
+
|
|
96
|
+
$grouped = [];
|
|
97
|
+
foreach ($csv as $row) {
|
|
98
|
+
$data = array_combine($headers, $row);
|
|
99
|
+
$modelId = $data['model_id'];
|
|
100
|
+
$relatedId = $data['related_id'];
|
|
101
|
+
|
|
102
|
+
if (!isset($grouped[$modelId])) {
|
|
103
|
+
$grouped[$modelId] = [];
|
|
104
|
+
}
|
|
105
|
+
$grouped[$modelId][] = $relatedId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
foreach ($grouped as $modelId => $relatedIds) {
|
|
109
|
+
$model = $modelClass::find($modelId);
|
|
110
|
+
if ($model) {
|
|
111
|
+
$model->$relationName()->attach($relatedIds);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -27,11 +27,12 @@ class <%- className %>Controller extends Controller
|
|
|
27
27
|
// use HandlesUserRoles;
|
|
28
28
|
|
|
29
29
|
// private $viewerRole = 'viewer';
|
|
30
|
+
// private $editorRole = 'editor';
|
|
30
31
|
|
|
31
32
|
public function index(Request $request)
|
|
32
33
|
{
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
+
if (!$this->canViewData()) return response()->json(['error' => 'Forbidden'], 403);
|
|
35
36
|
<% if (searchEngine !== 'null') {-%>
|
|
36
37
|
$search = $request->has('search') ? ScoutQuerySanitizer::sanitize($request->get('search')) : null;
|
|
37
38
|
<% } -%>
|
|
@@ -112,6 +113,7 @@ class <%- className %>Controller extends Controller
|
|
|
112
113
|
public function show($id)
|
|
113
114
|
{
|
|
114
115
|
try {
|
|
116
|
+
if (!$this->canViewData()) return response()->json(['error' => 'Forbidden'], 403);
|
|
115
117
|
<% if (withs) { -%>
|
|
116
118
|
$<%- entityName %> = <%- className %>::with(<%- withs %>)->findOrFail($id);
|
|
117
119
|
<% } else { -%>
|
|
@@ -126,6 +128,7 @@ class <%- className %>Controller extends Controller
|
|
|
126
128
|
public function store(Request $request)
|
|
127
129
|
{
|
|
128
130
|
try {
|
|
131
|
+
if (!$this->canCreateData()) return response()->json(['error' => 'Forbidden'], 403);
|
|
129
132
|
<% if(isEmpty(validationsStore)) {-%>
|
|
130
133
|
$data = $request->all();
|
|
131
134
|
<% } else {-%>
|
|
@@ -184,6 +187,8 @@ class <%- className %>Controller extends Controller
|
|
|
184
187
|
public function update($id, Request $request)
|
|
185
188
|
{
|
|
186
189
|
try {
|
|
190
|
+
if (!$this->canModifyData()) return response()->json(['error' => 'Forbidden'], 403);
|
|
191
|
+
|
|
187
192
|
<% if (withs) { -%>
|
|
188
193
|
$<%- entityName %> = <%- className %>::with(<%- withs %>)->findOrFail($id);
|
|
189
194
|
<% } else { -%>
|
|
@@ -257,6 +262,7 @@ class <%- className %>Controller extends Controller
|
|
|
257
262
|
public function destroy($id)
|
|
258
263
|
{
|
|
259
264
|
try {
|
|
265
|
+
if (!$this->canDeleteData()) return response()->json(['error' => 'Forbidden'], 403);
|
|
260
266
|
$<%- entityName %> = <%- className %>::findOrFail($id);
|
|
261
267
|
$<%- entityName %>->delete();
|
|
262
268
|
return response(null, 200);
|
|
@@ -292,4 +298,32 @@ class <%- className %>Controller extends Controller
|
|
|
292
298
|
}
|
|
293
299
|
}
|
|
294
300
|
<% }-%>
|
|
301
|
+
|
|
302
|
+
private function canViewData()
|
|
303
|
+
{
|
|
304
|
+
// Implement role checking logic here
|
|
305
|
+
// return $this->userHasRole($this->viewerRole);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private function canCreateData()
|
|
310
|
+
{
|
|
311
|
+
// Implement role checking logic here
|
|
312
|
+
// return $this->userHasRole($this->editorRole);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private function canModifyData()
|
|
317
|
+
{
|
|
318
|
+
// Implement role checking logic here
|
|
319
|
+
// return $this->userHasRole($this->editorRole);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private function canDeleteData()
|
|
324
|
+
{
|
|
325
|
+
// Implement role checking logic here
|
|
326
|
+
// return $this->userHasRole($this->editorRole);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
295
329
|
}
|