@edc-motor/admin-kit 0.3.1 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/admin-kit",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "EdC Motor — kit del panel de administración: layout, gestores CRUD, imágenes, PDF, páginas, configuración, usuarios y copias (paquete fuente: lo compila el consumidor con Vite)",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@edc-motor/ui": "^0.3.0",
36
+ "@edc-motor/ui": "^0.4.0",
37
37
  "axios": "^1.7.0",
38
38
  "vue-draggable-plus": "^0.6.1"
39
39
  },
@@ -120,7 +120,20 @@
120
120
 
121
121
  display: grid;
122
122
  gap: $space-3;
123
+
124
+ // display:grid no gobierna el contenido desplegado de un <details>: el
125
+ // aire de los checkboxes (bajo el selector de color) y del select del
126
+ // padre va a mano, y cada checkbox en su propia línea.
127
+ > .checkbox {
128
+ display: flex;
129
+ margin-top: $space-2;
130
+ }
131
+ > .checkbox:first-of-type { margin-top: $space-4; }
132
+ > .form-field { margin-top: $space-3; }
123
133
  }
134
+
135
+ // Bloques hijos (parent_id): tarjeta sangrada bajo su padre.
136
+ &__item.is-child { margin-left: $space-7; }
124
137
  }
125
138
 
126
139
  .schema-fields {
@@ -6,6 +6,7 @@ import { VueDraggable } from 'vue-draggable-plus'
6
6
  import {
7
7
  BaseButton,
8
8
  BaseCheckbox,
9
+ BaseSelect,
9
10
  EditModal,
10
11
  useConfirm,
11
12
  useToast,
@@ -35,6 +36,8 @@ export interface PageBlocksLabels {
35
36
  panelTitle: string
36
37
  panelEmpty: string
37
38
  panelContent: string
39
+ parent: string
40
+ parentNone: string
38
41
  }
39
42
 
40
43
  const defaultLabels: PageBlocksLabels = {
@@ -52,6 +55,8 @@ const defaultLabels: PageBlocksLabels = {
52
55
  panelTitle: 'Bloque',
53
56
  panelEmpty: 'Selecciona un bloque para ver sus acciones.',
54
57
  panelContent: 'Contenido',
58
+ parent: 'Bloque padre (índices indentados)',
59
+ parentNone: '— Ninguno —',
55
60
  }
56
61
 
57
62
  const props = withDefaults(
@@ -90,6 +95,7 @@ interface BlockRow {
90
95
  id: number
91
96
  type: string
92
97
  order: number
98
+ parent_id: number | null
93
99
  settings: Record<string, unknown>
94
100
  is_printable: boolean
95
101
  is_indexable: boolean
@@ -128,6 +134,16 @@ const modalType = ref<BlockTypeSchema | null>(null)
128
134
  const form = ref<Record<string, unknown>>({})
129
135
  const formPrintable = ref(true)
130
136
  const formIndexable = ref(true)
137
+ const formParent = ref<number | null>(null)
138
+
139
+ // Padres elegibles: bloques raíz de la página (un solo nivel de anidado),
140
+ // nunca uno mismo.
141
+ const parentOptions = computed(() => {
142
+ const self = editing.value?.id
143
+ return blocks.value
144
+ .filter((b) => !b.parent_id && b.id !== self)
145
+ .map((b) => ({ value: String(b.id), label: `${typeName(b.type)} — ${summary(b) || b.id}` }))
146
+ })
131
147
 
132
148
  function typeName(key: string): string {
133
149
  const type = types.value.find((t) => t.key === key)
@@ -205,6 +221,18 @@ const selectedFields = computed(() => {
205
221
  .filter((entry) => entry.value)
206
222
  })
207
223
 
224
+ /** Hijos justo debajo de su padre (en su orden relativo): anidado visible. */
225
+ function arrange(list: BlockRow[]): BlockRow[] {
226
+ const parents = list.filter((b) => !b.parent_id)
227
+ const out: BlockRow[] = []
228
+ for (const parent of parents) {
229
+ out.push(parent, ...list.filter((b) => b.parent_id === parent.id))
230
+ }
231
+ // Huérfanos (padre borrado en otra sesión): al final, sin perderse.
232
+ out.push(...list.filter((b) => !out.includes(b)))
233
+ return out
234
+ }
235
+
208
236
  async function load() {
209
237
  try {
210
238
  const [palette, list] = await Promise.all([
@@ -212,7 +240,7 @@ async function load() {
212
240
  props.api.get(`/admin/pages/${props.pageId}/blocks`),
213
241
  ])
214
242
  types.value = palette.data.data
215
- blocks.value = list.data.data
243
+ blocks.value = arrange(list.data.data)
216
244
  } catch {
217
245
  toast.danger(L.error)
218
246
  }
@@ -229,6 +257,7 @@ function openCreate(type: BlockTypeSchema) {
229
257
  form.value = defaults
230
258
  formPrintable.value = true
231
259
  formIndexable.value = true
260
+ formParent.value = null
232
261
  modalOpen.value = true
233
262
  }
234
263
 
@@ -238,6 +267,7 @@ function openEdit(block: BlockRow) {
238
267
  form.value = { ...(block.settings ?? {}) }
239
268
  formPrintable.value = block.is_printable
240
269
  formIndexable.value = block.is_indexable
270
+ formParent.value = block.parent_id
241
271
  modalOpen.value = true
242
272
  }
243
273
 
@@ -250,6 +280,7 @@ async function save() {
250
280
  settings: form.value,
251
281
  is_printable: formPrintable.value,
252
282
  is_indexable: formIndexable.value,
283
+ parent_id: formParent.value,
253
284
  }
254
285
  if (editing.value) {
255
286
  await props.api.put(`/admin/blocks/${editing.value.id}`, payload)
@@ -282,8 +313,10 @@ async function remove(block: BlockRow) {
282
313
  }
283
314
  }
284
315
 
285
- /** El drag reordena en cliente; se persiste la lista de ids. */
316
+ /** El drag reordena en cliente; los hijos se recolocan bajo su padre y se
317
+ * persiste la lista de ids resultante. */
286
318
  async function persistOrder() {
319
+ blocks.value = arrange(blocks.value)
287
320
  try {
288
321
  await props.api.post(`/admin/pages/${props.pageId}/blocks/reorder`, {
289
322
  ids: blocks.value.map((b) => b.id),
@@ -337,7 +370,7 @@ defineExpose({ reload: load })
337
370
  v-for="block in blocks"
338
371
  :key="block.id"
339
372
  class="page-blocks__item"
340
- :class="{ 'is-active': selectedId === block.id }"
373
+ :class="{ 'is-active': selectedId === block.id, 'is-child': block.parent_id }"
341
374
  @click="(e) => selectBlock(block, e)"
342
375
  >
343
376
  <span class="page-blocks__grip"><GripVertical :size="16" /></span>
@@ -381,6 +414,12 @@ defineExpose({ reload: load })
381
414
  />
382
415
  <BaseCheckbox v-model="formPrintable" :label="L.printable" />
383
416
  <BaseCheckbox v-model="formIndexable" :label="L.indexable" />
417
+ <BaseSelect
418
+ :model-value="formParent === null ? '' : String(formParent)"
419
+ :label="L.parent"
420
+ :options="[{ value: '', label: L.parentNone }, ...parentOptions]"
421
+ @update:model-value="(v: string) => (formParent = v ? Number(v) : null)"
422
+ />
384
423
  </details>
385
424
  </template>
386
425
  </EditModal>
@@ -388,7 +427,11 @@ defineExpose({ reload: load })
388
427
  <!-- Acciones del bloque seleccionado, en el panel derecho (patrón kontuan) -->
389
428
  <Teleport defer to="#right-sidebar-target">
390
429
  <div class="manager-panel">
391
- <p v-if="!selected" class="manager-panel__empty">{{ L.panelEmpty }}</p>
430
+ <!-- Sin bloque seleccionado, la vista puede poner su propio panel
431
+ (p. ej. las acciones de la página) -->
432
+ <slot v-if="!selected" name="panel-default">
433
+ <p class="manager-panel__empty">{{ L.panelEmpty }}</p>
434
+ </slot>
392
435
  <template v-else>
393
436
  <p class="manager-panel__kicker">{{ typeName(selected.type) }}</p>
394
437