@cosider.construction/eapp 1.0.0
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/package.json +15 -0
- package/src/generators/config.js +71 -0
- package/src/generators/entity.js +256 -0
- package/src/generators/menu.js +230 -0
- package/src/generators/misc.js +231 -0
- package/src/generators/module.js +202 -0
- package/src/generators/view.js +466 -0
- package/src/index.js +110 -0
- package/src/tui/engine.js +330 -0
- package/src/utils/fs.js +125 -0
- package/src/utils/log.js +11 -0
- package/src/utils/naming.js +95 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { parseEntityPath, toUpper, toLower, toPascal } from '../utils/naming.js';
|
|
4
|
+
import { writeFile, exists, assertProjectRoot, loadEntityRegistry, upsertMenuItem } from '../utils/fs.js';
|
|
5
|
+
import { log } from '../utils/log.js';
|
|
6
|
+
import { radioLine, optionsLine, fieldsTable } from '../tui/engine.js';
|
|
7
|
+
|
|
8
|
+
// ─── Behaviour stubs ──────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function indexStub({ module, entity, behaviour }) {
|
|
11
|
+
const Pascal = toPascal(entity);
|
|
12
|
+
const lower = toLower(entity);
|
|
13
|
+
|
|
14
|
+
const stubs = {
|
|
15
|
+
page: `<script setup>
|
|
16
|
+
import ${Pascal}List from './${Pascal}List.vue';
|
|
17
|
+
import ${Pascal}Form from './${Pascal}Form.vue';
|
|
18
|
+
import { ref } from 'vue';
|
|
19
|
+
const selected = ref(null);
|
|
20
|
+
</script>
|
|
21
|
+
<template>
|
|
22
|
+
<div class="${lower}-page">
|
|
23
|
+
<${Pascal}List @select="selected = $event" />
|
|
24
|
+
<router-view />
|
|
25
|
+
</div>
|
|
26
|
+
</template>`,
|
|
27
|
+
|
|
28
|
+
popup: `<script setup>
|
|
29
|
+
import ${Pascal}List from './${Pascal}List.vue';
|
|
30
|
+
import ${Pascal}Form from './${Pascal}Form.vue';
|
|
31
|
+
import { ref } from 'vue';
|
|
32
|
+
const selected = ref(null);
|
|
33
|
+
const showForm = ref(false);
|
|
34
|
+
function open(id) { selected.value = id; showForm.value = true; }
|
|
35
|
+
function close() { showForm.value = false; selected.value = null; }
|
|
36
|
+
</script>
|
|
37
|
+
<template>
|
|
38
|
+
<div class="${lower}-container">
|
|
39
|
+
<${Pascal}List @select="open" />
|
|
40
|
+
<teleport to="body">
|
|
41
|
+
<div v-if="showForm" class="overlay" @click.self="close">
|
|
42
|
+
<div class="popup">
|
|
43
|
+
<${Pascal}Form :id="selected" @saved="close" @close="close" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</teleport>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
<style scoped>
|
|
50
|
+
.overlay { position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:100; }
|
|
51
|
+
.popup { background:#fff;border-radius:10px;padding:2rem;min-width:480px;max-height:90vh;overflow:auto; }
|
|
52
|
+
</style>`,
|
|
53
|
+
|
|
54
|
+
right: splitStub(Pascal, lower, 'right'),
|
|
55
|
+
left: splitStub(Pascal, lower, 'left'),
|
|
56
|
+
top: splitStub(Pascal, lower, 'top'),
|
|
57
|
+
bottom: splitStub(Pascal, lower, 'bottom'),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return stubs[behaviour] || stubs.page;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function splitStub(Pascal, lower, side) {
|
|
64
|
+
const isVertical = side === 'top' || side === 'bottom';
|
|
65
|
+
const isFormFirst = side === 'left' || side === 'top';
|
|
66
|
+
const flexDir = isVertical ? 'column' : 'row';
|
|
67
|
+
const [first, sec] = isFormFirst
|
|
68
|
+
? [`<${Pascal}Form :id="selected" @saved="selected=null" />`, `<${Pascal}List @select="selected=$event" />`]
|
|
69
|
+
: [`<${Pascal}List @select="selected=$event" />`, `<${Pascal}Form :id="selected" @saved="selected=null" />`];
|
|
70
|
+
|
|
71
|
+
return `<script setup>
|
|
72
|
+
import ${Pascal}List from './${Pascal}List.vue';
|
|
73
|
+
import ${Pascal}Form from './${Pascal}Form.vue';
|
|
74
|
+
import { ref } from 'vue';
|
|
75
|
+
const selected = ref(null);
|
|
76
|
+
</script>
|
|
77
|
+
<template>
|
|
78
|
+
<div class="${lower}-split">
|
|
79
|
+
${first}
|
|
80
|
+
${sec}
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
<style scoped>
|
|
84
|
+
.${lower}-split { display:flex;flex-direction:${flexDir};gap:1rem;height:100%; }
|
|
85
|
+
.${lower}-split > * { flex:1;overflow:auto; }
|
|
86
|
+
</style>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── List component ───────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function listStub({ module, entity, hasRow, hasCard }) {
|
|
92
|
+
const Pascal = toPascal(entity);
|
|
93
|
+
const lower = toLower(entity);
|
|
94
|
+
const apiPath = `@/api/modules/${lower}.${lower}.api.js`;
|
|
95
|
+
|
|
96
|
+
const rowImport = hasRow ? `import ${Pascal}ListRow from './${Pascal}ListRow.vue';` : '';
|
|
97
|
+
const cardImport = hasCard ? `import ${Pascal}ListCard from './${Pascal}ListCard.vue';` : '';
|
|
98
|
+
const toggleBtn = (hasRow && hasCard)
|
|
99
|
+
? `<div class="render-toggle">
|
|
100
|
+
<button :class="{ active: render==='table' }" @click="render='table'">Table</button>
|
|
101
|
+
<button :class="{ active: render==='card' }" @click="render='card'">Cards</button>
|
|
102
|
+
</div>` : '';
|
|
103
|
+
const renderBlock = hasRow && hasCard
|
|
104
|
+
? `<${Pascal}ListRow v-if="render==='table'" v-for="item in items" :key="item.id" :item="item" @select="$emit('select', item.id)" />
|
|
105
|
+
<${Pascal}ListCard v-if="render==='card'" v-for="item in items" :key="item.id" :item="item" @select="$emit('select', item.id)" />`
|
|
106
|
+
: hasRow
|
|
107
|
+
? `<${Pascal}ListRow v-for="item in items" :key="item.id" :item="item" @select="$emit('select', item.id)" />`
|
|
108
|
+
: hasCard
|
|
109
|
+
? `<${Pascal}ListCard v-for="item in items" :key="item.id" :item="item" @select="$emit('select', item.id)" />`
|
|
110
|
+
: `<div v-for="item in items" :key="item.id" @click="$emit('select', item.id)">{{ item }}</div>`;
|
|
111
|
+
|
|
112
|
+
return `<script setup>
|
|
113
|
+
import { ref, onMounted } from 'vue';
|
|
114
|
+
import api from '${apiPath}';
|
|
115
|
+
${rowImport}
|
|
116
|
+
${cardImport}
|
|
117
|
+
|
|
118
|
+
// Props: parentId filters list to a specific parent (slave/scoped mode)
|
|
119
|
+
const props = defineProps({ parentId: { type: Number, default: null } });
|
|
120
|
+
const emit = defineEmits(['select']);
|
|
121
|
+
|
|
122
|
+
const items = ref([]);
|
|
123
|
+
const loading = ref(false);
|
|
124
|
+
${hasRow && hasCard ? "const render = ref('table');" : ''}
|
|
125
|
+
|
|
126
|
+
// <!-- FILTER_SLOT: plug your filter package here -->
|
|
127
|
+
|
|
128
|
+
async function load() {
|
|
129
|
+
loading.value = true;
|
|
130
|
+
try {
|
|
131
|
+
items.value = props.parentId != null
|
|
132
|
+
? await api.getAll({ parentId: props.parentId })
|
|
133
|
+
: await api.getAll();
|
|
134
|
+
} finally {
|
|
135
|
+
loading.value = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onMounted(load);
|
|
140
|
+
defineExpose({ reload: load });
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<div class="${lower}-list">
|
|
145
|
+
${toggleBtn}
|
|
146
|
+
<div v-if="loading">Loading…</div>
|
|
147
|
+
<div v-else>
|
|
148
|
+
${renderBlock}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</template>
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Form component ───────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function formStub({ module, entity }) {
|
|
158
|
+
const Pascal = toPascal(entity);
|
|
159
|
+
const lower = toLower(entity);
|
|
160
|
+
const apiPath = `@/api/modules/${lower}.${lower}.api.js`;
|
|
161
|
+
|
|
162
|
+
return `<script setup>
|
|
163
|
+
import { ref, watch } from 'vue';
|
|
164
|
+
import api from '${apiPath}';
|
|
165
|
+
|
|
166
|
+
const props = defineProps({ id: { type: Number, default: null } });
|
|
167
|
+
const emit = defineEmits(['saved', 'close']);
|
|
168
|
+
|
|
169
|
+
const form = ref({});
|
|
170
|
+
|
|
171
|
+
watch(() => props.id, async (id) => {
|
|
172
|
+
if (id) form.value = await api.getById(id);
|
|
173
|
+
else form.value = {};
|
|
174
|
+
}, { immediate: true });
|
|
175
|
+
|
|
176
|
+
async function save() {
|
|
177
|
+
if (form.value.id) await api.update(form.value.id, form.value);
|
|
178
|
+
else await api.add(form.value);
|
|
179
|
+
emit('saved');
|
|
180
|
+
}
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<template>
|
|
184
|
+
<div class="${lower}-form">
|
|
185
|
+
<h3>{{ form.id ? 'Edit' : 'New' }} ${Pascal}</h3>
|
|
186
|
+
<!-- Add form fields here -->
|
|
187
|
+
<div class="form-actions">
|
|
188
|
+
<button @click="save">Save</button>
|
|
189
|
+
<button @click="$emit('close')">Cancel</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</template>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── ListRow / ListCard stubs ─────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function listRowStub({ entity }) {
|
|
199
|
+
const Pascal = toPascal(entity);
|
|
200
|
+
return `<script setup>
|
|
201
|
+
defineProps({ item: { type: Object, required: true } });
|
|
202
|
+
defineEmits(['select']);
|
|
203
|
+
</script>
|
|
204
|
+
<template>
|
|
205
|
+
<tr class="${toLower(entity)}-row" @click="$emit('select', item.id)" style="cursor:pointer">
|
|
206
|
+
<!-- render row cells here -->
|
|
207
|
+
<td>{{ item.id }}</td>
|
|
208
|
+
</tr>
|
|
209
|
+
</template>
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function listCardStub({ entity }) {
|
|
214
|
+
const Pascal = toPascal(entity);
|
|
215
|
+
return `<script setup>
|
|
216
|
+
defineProps({ item: { type: Object, required: true } });
|
|
217
|
+
defineEmits(['select']);
|
|
218
|
+
</script>
|
|
219
|
+
<template>
|
|
220
|
+
<div class="${toLower(entity)}-card" @click="$emit('select', item.id)" style="cursor:pointer;border:1px solid #ccc;border-radius:8px;padding:1rem;">
|
|
221
|
+
<!-- render card content here -->
|
|
222
|
+
<strong>{{ item.id }}</strong>
|
|
223
|
+
</div>
|
|
224
|
+
</template>
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Slave part stub ──────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function slavePartStub({ masterEntity, slaveModule, slaveEntity, behaviour }) {
|
|
231
|
+
const MasterPascal = toPascal(masterEntity);
|
|
232
|
+
const SlavePascal = toPascal(slaveEntity);
|
|
233
|
+
const SlaveList = `${SlavePascal}List`;
|
|
234
|
+
const SlaveForm = `${SlavePascal}Form`;
|
|
235
|
+
const fkField = `${toLower(masterEntity)}_id`;
|
|
236
|
+
|
|
237
|
+
const formSection = behaviour === 'popup'
|
|
238
|
+
? `<teleport to="body">
|
|
239
|
+
<div v-if="showForm" class="overlay" @click.self="showForm=false">
|
|
240
|
+
<div class="popup"><${SlaveForm} :id="selectedId" @saved="showForm=false;listRef.reload()" @close="showForm=false" /></div>
|
|
241
|
+
</div>
|
|
242
|
+
</teleport>`
|
|
243
|
+
: behaviour === 'tab'
|
|
244
|
+
? `<!-- rendered as tab, see parent tabs -->`
|
|
245
|
+
: `<${SlaveForm} v-if="selectedId" :id="selectedId" @saved="selectedId=null;listRef.reload()" />`;
|
|
246
|
+
|
|
247
|
+
return `<script setup>
|
|
248
|
+
import { ref } from 'vue';
|
|
249
|
+
import ${SlaveList} from '@/views/modules/${toLower(slaveModule)}/${toLower(slaveEntity)}/${SlavePascal}List.vue';
|
|
250
|
+
${behaviour !== 'tab' ? `import ${SlaveForm} from '@/views/modules/${toLower(slaveModule)}/${toLower(slaveEntity)}/${SlavePascal}Form.vue';` : ''}
|
|
251
|
+
|
|
252
|
+
// Slave part: ${SlavePascal} scoped to ${MasterPascal}
|
|
253
|
+
const props = defineProps({ parentId: { type: Number, required: true } });
|
|
254
|
+
|
|
255
|
+
const listRef = ref(null);
|
|
256
|
+
const selectedId = ref(null);
|
|
257
|
+
${behaviour === 'popup' ? "const showForm = ref(false);" : ''}
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<template>
|
|
261
|
+
<div class="${toLower(masterEntity)}-${toLower(slaveEntity)}">
|
|
262
|
+
<${SlaveList} ref="listRef" :parentId="props.parentId" @select="selectedId = $event${behaviour === 'popup' ? '; showForm = true' : ''}" />
|
|
263
|
+
${formSection}
|
|
264
|
+
</div>
|
|
265
|
+
</template>
|
|
266
|
+
${behaviour === 'popup' ? `<style scoped>
|
|
267
|
+
.overlay { position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:100; }
|
|
268
|
+
.popup { background:#fff;border-radius:10px;padding:2rem;min-width:480px; }
|
|
269
|
+
</style>` : ''}
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Components table ─────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
const COMP_COLS = ['comp', 'route', 'menu', 'render'];
|
|
276
|
+
const COMP_WIDTHS = { comp: 16, route: 22, menu: 6, render: 16 };
|
|
277
|
+
|
|
278
|
+
// ─── Main generator ───────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
export async function generateView(args, rawArgs) {
|
|
281
|
+
assertProjectRoot();
|
|
282
|
+
|
|
283
|
+
const dryRun = rawArgs.includes('--dry-run');
|
|
284
|
+
const useDefault = rawArgs.includes('--default');
|
|
285
|
+
|
|
286
|
+
let parsed;
|
|
287
|
+
if (args[0]) {
|
|
288
|
+
try { parsed = parseEntityPath(args[0]); }
|
|
289
|
+
catch (e) { log.error(e.message); process.exit(1); }
|
|
290
|
+
} else {
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(chalk.bold.cyan(' eapp view') + chalk.dim(' <module/entity>'));
|
|
293
|
+
console.log('');
|
|
294
|
+
const { textInput } = await import('../tui/engine.js');
|
|
295
|
+
const input = await textInput('module/entity:');
|
|
296
|
+
if (!input) { log.warn('Cancelled.'); return; }
|
|
297
|
+
try { parsed = parseEntityPath(input); }
|
|
298
|
+
catch (e) { log.error(e.message); process.exit(1); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const { module, entity } = parsed;
|
|
302
|
+
const Pascal = toPascal(entity);
|
|
303
|
+
const cwd = process.cwd();
|
|
304
|
+
|
|
305
|
+
// ── Load entity registry to detect slaves ─────────────────────────────────
|
|
306
|
+
const registry = await loadEntityRegistry(cwd);
|
|
307
|
+
const entityMeta = registry.get(`${module}.${entity}`);
|
|
308
|
+
|
|
309
|
+
// Detect slaves — entities that have an FK pointing to this entity
|
|
310
|
+
const slaves = [];
|
|
311
|
+
for (const [key, meta] of registry.entries()) {
|
|
312
|
+
if (key === `${module}.${entity}`) continue;
|
|
313
|
+
const hasFk = meta.fields?.some(f => f.fk && f.field === `${entity}_id`);
|
|
314
|
+
if (hasFk) slaves.push(meta);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (useDefault) {
|
|
318
|
+
await _writeView({ module, entity, behaviour: 'page', hasRow: true, hasCard: false,
|
|
319
|
+
comps: [
|
|
320
|
+
{ comp: 'List', route: `/${module}/${entity}`, menu: true, render: 'row' },
|
|
321
|
+
{ comp: 'Form', route: '', menu: false, render: '' },
|
|
322
|
+
],
|
|
323
|
+
slaves: slaves.map(s => ({ ...s, behaviour: 'popup', menu: false })),
|
|
324
|
+
dryRun, cwd,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Step 2: behaviour ─────────────────────────────────────────────────────
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(chalk.bold.white(' Behaviour'));
|
|
332
|
+
const behaviour = await radioLine('', [
|
|
333
|
+
{ label: 'Page', value: 'page' },
|
|
334
|
+
{ label: 'Popup', value: 'popup' },
|
|
335
|
+
{ label: 'Right', value: 'right' },
|
|
336
|
+
{ label: 'Left', value: 'left' },
|
|
337
|
+
{ label: 'Top', value: 'top' },
|
|
338
|
+
{ label: 'Bottom', value: 'bottom' },
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
// ── Step 3: components table ──────────────────────────────────────────────
|
|
342
|
+
const defaultRoute = `/${toLower(module)}/${toLower(entity)}`;
|
|
343
|
+
const defaultComps = [
|
|
344
|
+
{ comp: 'List', route: defaultRoute, menu: 'on', render: 'row' },
|
|
345
|
+
{ comp: 'Form', route: '', menu: 'off', render: '' },
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const compRows = await fieldsTable(
|
|
349
|
+
defaultComps,
|
|
350
|
+
COMP_COLS,
|
|
351
|
+
COMP_WIDTHS,
|
|
352
|
+
'Components (+ add - remove ← → cols space menu toggle enter done)'
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const hasRow = compRows.some(r => r.render === 'row' || r.render === 'table');
|
|
356
|
+
const hasCard = compRows.some(r => r.render === 'card');
|
|
357
|
+
|
|
358
|
+
// ── Step 4: slaves ────────────────────────────────────────────────────────
|
|
359
|
+
let slaveConfigs = [];
|
|
360
|
+
if (slaves.length > 0) {
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(chalk.bold.white(` Slaves detected (${slaves.length})`));
|
|
363
|
+
|
|
364
|
+
const SLAVE_COLS = ['slave', 'behaviour', 'menu'];
|
|
365
|
+
const SLAVE_WIDTHS = { slave: 18, behaviour: 30, menu: 6 };
|
|
366
|
+
const slaveDefaults = slaves.map(s => ({
|
|
367
|
+
slave: `${s.module}/${s.entity}`,
|
|
368
|
+
behaviour: 'popup',
|
|
369
|
+
menu: 'off',
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
const slaveRows = await fieldsTable(slaveDefaults, SLAVE_COLS, SLAVE_WIDTHS,
|
|
373
|
+
'Slave components (behaviour: popup/tab/page/part)');
|
|
374
|
+
|
|
375
|
+
slaveConfigs = slaveRows.map((r, i) => ({
|
|
376
|
+
...slaves[i],
|
|
377
|
+
behaviour: r.behaviour || 'popup',
|
|
378
|
+
menu: r.menu === 'on',
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await _writeView({ module, entity, behaviour, hasRow, hasCard, comps: compRows, slaves: slaveConfigs, dryRun, cwd });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function _writeView({ module, entity, behaviour, hasRow, hasCard, comps, slaves, dryRun, cwd }) {
|
|
386
|
+
const Pascal = toPascal(entity);
|
|
387
|
+
const viewDir = module === 'shared'
|
|
388
|
+
? path.join(cwd, 'src', 'frontend', 'views', 'shared', toLower(entity))
|
|
389
|
+
: path.join(cwd, 'src', 'frontend', 'views', 'modules', toLower(module), toLower(entity));
|
|
390
|
+
|
|
391
|
+
// List
|
|
392
|
+
const listFile = path.join(viewDir, `${Pascal}List.vue`);
|
|
393
|
+
if (await exists(listFile)) {
|
|
394
|
+
log.info(`${Pascal}List.vue exists — skipping (behaviour change only affects Index.vue)`);
|
|
395
|
+
} else {
|
|
396
|
+
await writeFile(listFile, listStub({ module, entity, hasRow, hasCard }), { dryRun });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Form
|
|
400
|
+
const formFile = path.join(viewDir, `${Pascal}Form.vue`);
|
|
401
|
+
if (await exists(formFile)) {
|
|
402
|
+
log.info(`${Pascal}Form.vue exists — skipping`);
|
|
403
|
+
} else {
|
|
404
|
+
await writeFile(formFile, formStub({ module, entity }), { dryRun });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ListRow / ListCard
|
|
408
|
+
if (hasRow) await writeFile(path.join(viewDir, `${Pascal}ListRow.vue`), listRowStub({ entity }), { dryRun });
|
|
409
|
+
if (hasCard) await writeFile(path.join(viewDir, `${Pascal}ListCard.vue`), listCardStub({ entity }), { dryRun });
|
|
410
|
+
|
|
411
|
+
// Index (always regenerate — safe, only behaviour wrapper)
|
|
412
|
+
await writeFile(
|
|
413
|
+
path.join(viewDir, `${Pascal}Index.vue`),
|
|
414
|
+
indexStub({ module, entity, behaviour }),
|
|
415
|
+
{ dryRun }
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Slave parts
|
|
419
|
+
for (const slave of slaves) {
|
|
420
|
+
const slaveFile = path.join(viewDir, `${Pascal}${toPascal(slave.entity)}.vue`);
|
|
421
|
+
await writeFile(slaveFile,
|
|
422
|
+
slavePartStub({ masterEntity: entity, slaveModule: slave.module, slaveEntity: slave.entity, behaviour: slave.behaviour }),
|
|
423
|
+
{ dryRun }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// View-level menu skeleton (rh.employee.menu.json)
|
|
428
|
+
// loader.menu.js picks this up automatically — no router update needed
|
|
429
|
+
const viewMenuFile = path.join(cwd, 'src', 'frontend', 'menus',
|
|
430
|
+
module === 'shared' ? `shared.${toLower(entity)}.menu.json` : `${toLower(module)}.${toLower(entity)}.menu.json`
|
|
431
|
+
);
|
|
432
|
+
if (!(await exists(viewMenuFile))) {
|
|
433
|
+
const route = `/${toLower(module)}/${toLower(entity)}`;
|
|
434
|
+
const skeleton = {
|
|
435
|
+
route,
|
|
436
|
+
hMenu: {
|
|
437
|
+
style: 'classic',
|
|
438
|
+
items: [
|
|
439
|
+
{ label: 'New', action: 'emit:new', icon: '+' },
|
|
440
|
+
{ label: 'Export', action: 'emit:export', icon: '↓' },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
await writeFile(viewMenuFile, JSON.stringify(skeleton, null, 2), { dryRun });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// vMenu entry in module menu
|
|
448
|
+
const menuComp = comps.find(c => c.menu === 'on' || c.menu === true);
|
|
449
|
+
if (menuComp) {
|
|
450
|
+
const moduleMenuFile = module === 'shared'
|
|
451
|
+
? path.join(cwd, 'src', 'frontend', 'menus', 'main.menu.json')
|
|
452
|
+
: path.join(cwd, 'src', 'frontend', 'menus', `${toLower(module)}.menu.json`);
|
|
453
|
+
if (!dryRun) {
|
|
454
|
+
await upsertMenuItem(moduleMenuFile, {
|
|
455
|
+
module, entity,
|
|
456
|
+
route: menuComp.route || `/${toLower(module)}/${toLower(entity)}`,
|
|
457
|
+
hMenu: false,
|
|
458
|
+
vMenu: true,
|
|
459
|
+
});
|
|
460
|
+
} else {
|
|
461
|
+
log.info(`[dry-run] would add vMenu entry to ${path.basename(moduleMenuFile)}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
log.success(`View ${chalk.cyan(module + '/' + entity)} generated with behaviour: ${chalk.yellow(behaviour)}`);
|
|
466
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import minimist from 'minimist';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { generateEntity } from './generators/entity.js';
|
|
5
|
+
import { generateView } from './generators/view.js';
|
|
6
|
+
import { generateModule } from './generators/module.js';
|
|
7
|
+
import { generateMenu } from './generators/menu.js';
|
|
8
|
+
import { generateConfig } from './generators/config.js';
|
|
9
|
+
import { generatePivot, generateLayout, listModules, listEntities } from './generators/misc.js';
|
|
10
|
+
import { radioLine, showCursor } from './tui/engine.js';
|
|
11
|
+
|
|
12
|
+
const HELP = `
|
|
13
|
+
${chalk.bold.cyan('eapp')} — Electron + Vue ERP project CLI
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
eapp interactive launcher
|
|
17
|
+
eapp <command> [path] [options]
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
entity [module/entity.schema] Generate backend entity + api
|
|
21
|
+
view [module/entity] Generate Vue views
|
|
22
|
+
module [name] Add / manage domain modules
|
|
23
|
+
menu [module[/entity]] Manage navigation menus
|
|
24
|
+
config Edit db.config.json
|
|
25
|
+
pivot <a> <b> Generate pivot/join entity
|
|
26
|
+
layout [name] Generate layout component
|
|
27
|
+
modules List all modules
|
|
28
|
+
entities [module] List all entities
|
|
29
|
+
|
|
30
|
+
Path format:
|
|
31
|
+
/employee shared module, dbo schema
|
|
32
|
+
rh/employee rh module, rh schema
|
|
33
|
+
rh/employee.dbo rh module, dbo schema
|
|
34
|
+
employee shorthand for shared/employee
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--default Skip all prompts, use sensible defaults
|
|
38
|
+
--dry-run Print files without writing
|
|
39
|
+
--help, -h Show this help
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
async function launchInteractive() {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
|
|
45
|
+
console.log('');
|
|
46
|
+
const chosen = await radioLine('', [
|
|
47
|
+
{ label: 'Entity', value: 'entity' },
|
|
48
|
+
{ label: 'View', value: 'view' },
|
|
49
|
+
{ label: 'Module', value: 'module' },
|
|
50
|
+
{ label: 'Menu', value: 'menu' },
|
|
51
|
+
{ label: 'Config', value: 'config' },
|
|
52
|
+
{ label: 'Pivot', value: 'pivot' },
|
|
53
|
+
{ label: 'Layout', value: 'layout' },
|
|
54
|
+
{ label: 'List', value: 'list' },
|
|
55
|
+
]);
|
|
56
|
+
console.log('');
|
|
57
|
+
if (chosen === 'list') {
|
|
58
|
+
await listModules();
|
|
59
|
+
console.log('');
|
|
60
|
+
await listEntities([]);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await run(chosen, [], process.argv.slice(2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function run(command, args, rawArgs) {
|
|
67
|
+
switch (command) {
|
|
68
|
+
case 'entity': await generateEntity(args, rawArgs); break;
|
|
69
|
+
case 'view': await generateView(args, rawArgs); break;
|
|
70
|
+
case 'module': await generateModule(args, rawArgs); break;
|
|
71
|
+
case 'menu': await generateMenu(args, rawArgs); break;
|
|
72
|
+
case 'config': await generateConfig(args, rawArgs); break;
|
|
73
|
+
case 'pivot': await generatePivot(args, rawArgs); break;
|
|
74
|
+
case 'layout': await generateLayout(args, rawArgs); break;
|
|
75
|
+
case 'modules': await listModules(); break;
|
|
76
|
+
case 'entities': await listEntities(args); break;
|
|
77
|
+
case 'add:entity': await generateEntity(args, rawArgs); break;
|
|
78
|
+
case 'add:view': await generateView(args, rawArgs); break;
|
|
79
|
+
case 'add:module': await generateModule(args, rawArgs); break;
|
|
80
|
+
case 'add:menu': await generateMenu(args, rawArgs); break;
|
|
81
|
+
case 'add:pivot': await generatePivot(args, rawArgs); break;
|
|
82
|
+
case 'add:layout': await generateLayout(args, rawArgs); break;
|
|
83
|
+
case 'list:modules': await listModules(); break;
|
|
84
|
+
case 'list:entities': await listEntities(args); break;
|
|
85
|
+
default:
|
|
86
|
+
console.error(chalk.red(`Unknown command: "${command}"`));
|
|
87
|
+
console.log(`Run eapp --help for available commands.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const argv = minimist(process.argv.slice(2), { boolean: ['help','h','dry-run','default'], string: ['schema'] });
|
|
94
|
+
const [cmd, ...args] = argv._;
|
|
95
|
+
const rawArgs = process.argv.slice(2);
|
|
96
|
+
|
|
97
|
+
if (argv.help || argv.h) { console.log(HELP); process.exit(0); }
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (!cmd) { await launchInteractive(); return; }
|
|
101
|
+
await run(cmd, args, rawArgs);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.name === 'ExitPromptError') { showCursor(); console.log(chalk.yellow('\nCancelled.')); process.exit(0); }
|
|
104
|
+
console.error(chalk.red('✖'), err.message);
|
|
105
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|