@edc-motor/admin-kit 0.4.23 → 0.4.24
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 +2 -2
- package/scss/_admin-kit.scss +1 -0
- package/scss/components/_menu-manager.scss +133 -0
- package/src/content/MenuManager.vue +391 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/admin-kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.24",
|
|
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.4.
|
|
36
|
+
"@edc-motor/ui": "^0.4.24",
|
|
37
37
|
"axios": "^1.7.0",
|
|
38
38
|
"vue-draggable-plus": "^0.6.1"
|
|
39
39
|
},
|
package/scss/_admin-kit.scss
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
@use "tokens" as *;
|
|
2
|
+
|
|
3
|
+
// Configurador del menú público (doc 10 ampliado): filas tipo PageBlocks
|
|
4
|
+
// (icono + etiqueta + badges + acciones), sin drag — el orden se mueve con
|
|
5
|
+
// flechas, dentro de cada nivel (raíz o hijos de un grupo).
|
|
6
|
+
.menu-manager {
|
|
7
|
+
display: grid;
|
|
8
|
+
gap: $space-3;
|
|
9
|
+
|
|
10
|
+
&__bar {
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: flex-end;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&__empty { color: $text-3; margin: 0; }
|
|
16
|
+
|
|
17
|
+
&__list {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: $space-2;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&__item {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
flex-wrap: wrap;
|
|
26
|
+
gap: $space-2;
|
|
27
|
+
border: 1px solid $border;
|
|
28
|
+
border-radius: $radius-md;
|
|
29
|
+
background: $surface;
|
|
30
|
+
padding: $space-2 $space-3;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
transition: border-color 0.15s ease;
|
|
33
|
+
|
|
34
|
+
&:hover { border-color: $border-strong; }
|
|
35
|
+
&.is-hidden { opacity: 0.65; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Hijos de un grupo: sangrados justo debajo de él (mismo lenguaje que los
|
|
39
|
+
// bloques hijos de PageBlocks).
|
|
40
|
+
&__item.is-child { margin-left: $space-7; }
|
|
41
|
+
|
|
42
|
+
&__icon {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
color: $text-3;
|
|
45
|
+
flex: 0 0 auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__label {
|
|
49
|
+
font-weight: $k-fw-semibold;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
text-overflow: ellipsis;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
margin-right: auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&__badges { display: flex; gap: $space-1; }
|
|
58
|
+
|
|
59
|
+
&__moves {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 2px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&__move {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
background: none;
|
|
69
|
+
border: 1px solid $border;
|
|
70
|
+
border-radius: $radius-sm;
|
|
71
|
+
color: $text-2;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
padding: $space-1;
|
|
74
|
+
transition: background-color 0.15s ease;
|
|
75
|
+
|
|
76
|
+
&:hover:not(:disabled) { background: $surface-2; }
|
|
77
|
+
&:disabled { opacity: 0.4; cursor: default; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&__visibility {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
background: transparent;
|
|
85
|
+
border: 1px solid color-mix(in srgb, $success 45%, transparent);
|
|
86
|
+
border-radius: $radius-sm;
|
|
87
|
+
color: $success;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
padding: $space-1;
|
|
90
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
91
|
+
|
|
92
|
+
&.is-on {
|
|
93
|
+
background: $success;
|
|
94
|
+
border-color: $success;
|
|
95
|
+
@include contrast-text($success);
|
|
96
|
+
}
|
|
97
|
+
&:disabled { opacity: 0.6; cursor: default; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&__group-select {
|
|
101
|
+
min-width: 160px;
|
|
102
|
+
|
|
103
|
+
.form-field__select { min-height: 34px; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&__group-actions {
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: $space-1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&__edit,
|
|
112
|
+
&__delete {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
background: none;
|
|
117
|
+
border: none;
|
|
118
|
+
border-radius: $radius-sm;
|
|
119
|
+
padding: $space-1;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition: background-color 0.15s ease;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&__edit {
|
|
125
|
+
color: $accent-500;
|
|
126
|
+
&:hover { background: color-mix(in srgb, $accent-500 12%, transparent); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&__delete {
|
|
130
|
+
color: $danger;
|
|
131
|
+
&:hover { background: color-mix(in srgb, $danger 12%, transparent); }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import type { AxiosInstance } from 'axios'
|
|
4
|
+
import {
|
|
5
|
+
ArrowDown,
|
|
6
|
+
ArrowUp,
|
|
7
|
+
Compass,
|
|
8
|
+
Eye,
|
|
9
|
+
EyeOff,
|
|
10
|
+
File,
|
|
11
|
+
Folder,
|
|
12
|
+
Plus,
|
|
13
|
+
SquarePen,
|
|
14
|
+
Trash2,
|
|
15
|
+
} from '@lucide/vue'
|
|
16
|
+
import {
|
|
17
|
+
BaseButton,
|
|
18
|
+
BaseSelect,
|
|
19
|
+
EditModal,
|
|
20
|
+
TranslatableInput,
|
|
21
|
+
useConfirm,
|
|
22
|
+
useToast,
|
|
23
|
+
} from '@edc-motor/ui'
|
|
24
|
+
|
|
25
|
+
// Configurador del menú de la web pública (doc 10 ampliado): mezcla páginas
|
|
26
|
+
// del CRM y "rutas" propias del juego bajo un árbol de UN nivel (los grupos
|
|
27
|
+
// son carpetas del admin; una página o ruta puede colgar de un grupo, un
|
|
28
|
+
// grupo no puede colgar de otro). Filas tipo PageBlocks (no cards):
|
|
29
|
+
// subir/bajar dentro de su nivel, interruptor de visibilidad, select de
|
|
30
|
+
// grupo y, en grupos, editar label + borrar. Agnóstico de i18n (DC-29):
|
|
31
|
+
// todos los textos por prop; `routeLabels` los pone el juego.
|
|
32
|
+
|
|
33
|
+
export interface MenuManagerLabels {
|
|
34
|
+
newGroup: string
|
|
35
|
+
newGroupTitle: string
|
|
36
|
+
editGroupTitle: string
|
|
37
|
+
groupLabel: string
|
|
38
|
+
save: string
|
|
39
|
+
cancel: string
|
|
40
|
+
delete: string
|
|
41
|
+
confirmDelete: string
|
|
42
|
+
empty: string
|
|
43
|
+
root: string
|
|
44
|
+
hidden: string
|
|
45
|
+
draft: string
|
|
46
|
+
moveUp: string
|
|
47
|
+
moveDown: string
|
|
48
|
+
visible: string
|
|
49
|
+
group: string
|
|
50
|
+
error: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultLabels: MenuManagerLabels = {
|
|
54
|
+
newGroup: 'Nuevo grupo',
|
|
55
|
+
newGroupTitle: 'Nuevo grupo',
|
|
56
|
+
editGroupTitle: 'Editar grupo',
|
|
57
|
+
groupLabel: 'Nombre del grupo',
|
|
58
|
+
save: 'Guardar',
|
|
59
|
+
cancel: 'Cancelar',
|
|
60
|
+
delete: 'Borrar',
|
|
61
|
+
confirmDelete: '¿Borrar este grupo? Sus elementos pasarán a la raíz.',
|
|
62
|
+
empty: 'El menú aún no tiene elementos.',
|
|
63
|
+
root: '— Raíz —',
|
|
64
|
+
hidden: 'Oculto',
|
|
65
|
+
draft: 'Borrador',
|
|
66
|
+
moveUp: 'Subir',
|
|
67
|
+
moveDown: 'Bajar',
|
|
68
|
+
visible: 'Visible',
|
|
69
|
+
group: 'Grupo',
|
|
70
|
+
error: 'No se ha podido completar la acción.',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = withDefaults(
|
|
74
|
+
defineProps<{
|
|
75
|
+
api: AxiosInstance
|
|
76
|
+
locales: { code: string; name: string }[]
|
|
77
|
+
/** Etiqueta visible de cada route_key que el juego ofrece al menú. */
|
|
78
|
+
routeLabels?: Record<string, string>
|
|
79
|
+
labels?: Partial<MenuManagerLabels>
|
|
80
|
+
}>(),
|
|
81
|
+
{ routeLabels: () => ({}), labels: () => ({}) },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const L = reactive({ ...defaultLabels, ...props.labels }) as MenuManagerLabels
|
|
85
|
+
|
|
86
|
+
const toast = useToast()
|
|
87
|
+
const { confirm } = useConfirm()
|
|
88
|
+
|
|
89
|
+
interface MenuPageInfo {
|
|
90
|
+
id: number
|
|
91
|
+
title: Record<string, string>
|
|
92
|
+
is_published: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface MenuNode {
|
|
96
|
+
id: number
|
|
97
|
+
type: 'page' | 'route' | 'group'
|
|
98
|
+
is_visible: boolean
|
|
99
|
+
order: number
|
|
100
|
+
route_key: string | null
|
|
101
|
+
label: Record<string, string> | null
|
|
102
|
+
page: MenuPageInfo | null
|
|
103
|
+
children: MenuNode[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tree = ref<MenuNode[]>([])
|
|
107
|
+
const busy = ref(false)
|
|
108
|
+
|
|
109
|
+
async function load() {
|
|
110
|
+
try {
|
|
111
|
+
const { data } = await props.api.get('/admin/menu')
|
|
112
|
+
tree.value = data.data
|
|
113
|
+
} catch {
|
|
114
|
+
toast.danger(L.error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Filas aplanadas para pintar (profundidad máxima 1: hijos de un grupo,
|
|
119
|
+
// indentados justo debajo de él — un grupo no puede colgar de otro).
|
|
120
|
+
interface Row {
|
|
121
|
+
node: MenuNode
|
|
122
|
+
depth: 0 | 1
|
|
123
|
+
parentId: number | null
|
|
124
|
+
}
|
|
125
|
+
const rows = computed<Row[]>(() => {
|
|
126
|
+
const out: Row[] = []
|
|
127
|
+
for (const node of tree.value) {
|
|
128
|
+
out.push({ node, depth: 0, parentId: null })
|
|
129
|
+
if (node.type === 'group') {
|
|
130
|
+
for (const child of node.children) out.push({ node: child, depth: 1, parentId: node.id })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const groups = computed(() => tree.value.filter((n) => n.type === 'group'))
|
|
137
|
+
|
|
138
|
+
function siblingIds(parentId: number | null): number[] {
|
|
139
|
+
if (parentId === null) return tree.value.map((n) => n.id)
|
|
140
|
+
return groups.value.find((g) => g.id === parentId)?.children.map((c) => c.id) ?? []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function siblingIndex(row: Row): number {
|
|
144
|
+
return siblingIds(row.parentId).indexOf(row.node.id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function canMoveUp(row: Row): boolean {
|
|
148
|
+
return siblingIndex(row) > 0
|
|
149
|
+
}
|
|
150
|
+
function canMoveDown(row: Row): boolean {
|
|
151
|
+
const ids = siblingIds(row.parentId)
|
|
152
|
+
const idx = siblingIndex(row)
|
|
153
|
+
return idx >= 0 && idx < ids.length - 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Primer texto disponible (agnóstico de locale, patrón EntityRefSelect). */
|
|
157
|
+
function firstText(map: Record<string, string> | null | undefined): string {
|
|
158
|
+
if (!map) return ''
|
|
159
|
+
return Object.values(map).find(Boolean) ?? ''
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function labelOf(node: MenuNode): string {
|
|
163
|
+
if (node.type === 'page') return node.page ? firstText(node.page.title) : ''
|
|
164
|
+
if (node.type === 'route') return props.routeLabels[node.route_key ?? ''] ?? node.route_key ?? ''
|
|
165
|
+
return firstText(node.label)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function move(row: Row, direction: 'up' | 'down') {
|
|
169
|
+
const ids = siblingIds(row.parentId)
|
|
170
|
+
const idx = ids.indexOf(row.node.id)
|
|
171
|
+
const swapWith = direction === 'up' ? idx - 1 : idx + 1
|
|
172
|
+
if (idx < 0 || swapWith < 0 || swapWith >= ids.length) return
|
|
173
|
+
const next = [...ids]
|
|
174
|
+
;[next[idx], next[swapWith]] = [next[swapWith], next[idx]]
|
|
175
|
+
busy.value = true
|
|
176
|
+
try {
|
|
177
|
+
await props.api.post('/admin/menu/reorder', { ids: next })
|
|
178
|
+
await load()
|
|
179
|
+
} catch {
|
|
180
|
+
toast.danger(L.error)
|
|
181
|
+
} finally {
|
|
182
|
+
busy.value = false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function toggleVisible(row: Row) {
|
|
187
|
+
busy.value = true
|
|
188
|
+
try {
|
|
189
|
+
await props.api.patch(`/admin/menu/${row.node.id}`, { is_visible: !row.node.is_visible })
|
|
190
|
+
await load()
|
|
191
|
+
} catch {
|
|
192
|
+
toast.danger(L.error)
|
|
193
|
+
} finally {
|
|
194
|
+
busy.value = false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function setGroup(row: Row, value: string) {
|
|
199
|
+
busy.value = true
|
|
200
|
+
try {
|
|
201
|
+
await props.api.patch(`/admin/menu/${row.node.id}`, {
|
|
202
|
+
parent_id: value ? Number(value) : null,
|
|
203
|
+
})
|
|
204
|
+
await load()
|
|
205
|
+
} catch {
|
|
206
|
+
toast.danger(L.error)
|
|
207
|
+
} finally {
|
|
208
|
+
busy.value = false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Modal de grupo (crear / editar label), compartido.
|
|
213
|
+
const modalOpen = ref(false)
|
|
214
|
+
const editingGroup = ref<MenuNode | null>(null)
|
|
215
|
+
const groupLabelForm = ref<Record<string, string>>({})
|
|
216
|
+
const saving = ref(false)
|
|
217
|
+
|
|
218
|
+
function openCreateGroup() {
|
|
219
|
+
editingGroup.value = null
|
|
220
|
+
groupLabelForm.value = {}
|
|
221
|
+
modalOpen.value = true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function openEditGroup(node: MenuNode) {
|
|
225
|
+
editingGroup.value = node
|
|
226
|
+
groupLabelForm.value = { ...(node.label ?? {}) }
|
|
227
|
+
modalOpen.value = true
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function saveGroup() {
|
|
231
|
+
saving.value = true
|
|
232
|
+
try {
|
|
233
|
+
if (editingGroup.value) {
|
|
234
|
+
await props.api.patch(`/admin/menu/${editingGroup.value.id}`, { label: groupLabelForm.value })
|
|
235
|
+
} else {
|
|
236
|
+
await props.api.post('/admin/menu/groups', { label: groupLabelForm.value })
|
|
237
|
+
}
|
|
238
|
+
modalOpen.value = false
|
|
239
|
+
await load()
|
|
240
|
+
} catch {
|
|
241
|
+
toast.danger(L.error)
|
|
242
|
+
} finally {
|
|
243
|
+
saving.value = false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function removeGroup(node: MenuNode) {
|
|
248
|
+
const ok = await confirm({
|
|
249
|
+
message: L.confirmDelete,
|
|
250
|
+
confirmLabel: L.delete,
|
|
251
|
+
cancelLabel: L.cancel,
|
|
252
|
+
variant: 'danger',
|
|
253
|
+
})
|
|
254
|
+
if (!ok) return
|
|
255
|
+
busy.value = true
|
|
256
|
+
try {
|
|
257
|
+
await props.api.delete(`/admin/menu/${node.id}`)
|
|
258
|
+
await load()
|
|
259
|
+
} catch {
|
|
260
|
+
toast.danger(L.error)
|
|
261
|
+
} finally {
|
|
262
|
+
busy.value = false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
onMounted(load)
|
|
267
|
+
</script>
|
|
268
|
+
|
|
269
|
+
<template>
|
|
270
|
+
<div class="menu-manager">
|
|
271
|
+
<div class="menu-manager__bar">
|
|
272
|
+
<BaseButton :disabled="busy" @click="openCreateGroup">
|
|
273
|
+
<template #icon><Plus :size="16" /></template>
|
|
274
|
+
{{ L.newGroup }}
|
|
275
|
+
</BaseButton>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<p v-if="!rows.length" class="menu-manager__empty">{{ L.empty }}</p>
|
|
279
|
+
|
|
280
|
+
<div class="menu-manager__list">
|
|
281
|
+
<article
|
|
282
|
+
v-for="row in rows"
|
|
283
|
+
:key="row.node.id"
|
|
284
|
+
class="menu-manager__item"
|
|
285
|
+
:class="{ 'is-child': row.depth === 1, 'is-hidden': !row.node.is_visible }"
|
|
286
|
+
>
|
|
287
|
+
<span class="menu-manager__icon">
|
|
288
|
+
<File v-if="row.node.type === 'page'" :size="16" />
|
|
289
|
+
<Compass v-else-if="row.node.type === 'route'" :size="16" />
|
|
290
|
+
<Folder v-else :size="16" />
|
|
291
|
+
</span>
|
|
292
|
+
|
|
293
|
+
<span class="menu-manager__label">{{ labelOf(row.node) }}</span>
|
|
294
|
+
|
|
295
|
+
<span class="menu-manager__badges">
|
|
296
|
+
<span v-if="!row.node.is_visible" class="chip is-missing">{{ L.hidden }}</span>
|
|
297
|
+
<span
|
|
298
|
+
v-if="row.node.type === 'page' && row.node.page && !row.node.page.is_published"
|
|
299
|
+
class="chip is-missing"
|
|
300
|
+
>
|
|
301
|
+
{{ L.draft }}
|
|
302
|
+
</span>
|
|
303
|
+
</span>
|
|
304
|
+
|
|
305
|
+
<span class="menu-manager__moves">
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
class="menu-manager__move"
|
|
309
|
+
:title="L.moveUp"
|
|
310
|
+
:aria-label="L.moveUp"
|
|
311
|
+
:disabled="busy || !canMoveUp(row)"
|
|
312
|
+
@click="move(row, 'up')"
|
|
313
|
+
>
|
|
314
|
+
<ArrowUp :size="14" />
|
|
315
|
+
</button>
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
class="menu-manager__move"
|
|
319
|
+
:title="L.moveDown"
|
|
320
|
+
:aria-label="L.moveDown"
|
|
321
|
+
:disabled="busy || !canMoveDown(row)"
|
|
322
|
+
@click="move(row, 'down')"
|
|
323
|
+
>
|
|
324
|
+
<ArrowDown :size="14" />
|
|
325
|
+
</button>
|
|
326
|
+
</span>
|
|
327
|
+
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
class="menu-manager__visibility"
|
|
331
|
+
:class="row.node.is_visible ? 'is-on' : 'is-off'"
|
|
332
|
+
:aria-pressed="row.node.is_visible"
|
|
333
|
+
:title="L.visible"
|
|
334
|
+
:disabled="busy"
|
|
335
|
+
@click="toggleVisible(row)"
|
|
336
|
+
>
|
|
337
|
+
<Eye v-if="row.node.is_visible" :size="15" />
|
|
338
|
+
<EyeOff v-else :size="15" />
|
|
339
|
+
</button>
|
|
340
|
+
|
|
341
|
+
<BaseSelect
|
|
342
|
+
v-if="row.node.type !== 'group'"
|
|
343
|
+
class="menu-manager__group-select"
|
|
344
|
+
:model-value="row.parentId === null ? '' : String(row.parentId)"
|
|
345
|
+
:options="[
|
|
346
|
+
{ value: '', label: L.root },
|
|
347
|
+
...groups.map((g) => ({ value: String(g.id), label: labelOf(g) })),
|
|
348
|
+
]"
|
|
349
|
+
@update:model-value="(v: string) => setGroup(row, v)"
|
|
350
|
+
/>
|
|
351
|
+
|
|
352
|
+
<span v-if="row.node.type === 'group'" class="menu-manager__group-actions">
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
class="menu-manager__edit"
|
|
356
|
+
:title="L.editGroupTitle"
|
|
357
|
+
:aria-label="L.editGroupTitle"
|
|
358
|
+
@click="openEditGroup(row.node)"
|
|
359
|
+
>
|
|
360
|
+
<SquarePen :size="15" />
|
|
361
|
+
</button>
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
class="menu-manager__delete"
|
|
365
|
+
:title="L.delete"
|
|
366
|
+
:aria-label="L.delete"
|
|
367
|
+
@click="removeGroup(row.node)"
|
|
368
|
+
>
|
|
369
|
+
<Trash2 :size="15" />
|
|
370
|
+
</button>
|
|
371
|
+
</span>
|
|
372
|
+
</article>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<EditModal
|
|
376
|
+
v-model="modalOpen"
|
|
377
|
+
:title="editingGroup ? L.editGroupTitle : L.newGroupTitle"
|
|
378
|
+
:submit-label="L.save"
|
|
379
|
+
:cancel-label="L.cancel"
|
|
380
|
+
:loading="saving"
|
|
381
|
+
@submit="saveGroup"
|
|
382
|
+
>
|
|
383
|
+
<TranslatableInput
|
|
384
|
+
v-model="groupLabelForm"
|
|
385
|
+
:locales="locales"
|
|
386
|
+
:label="L.groupLabel"
|
|
387
|
+
required
|
|
388
|
+
/>
|
|
389
|
+
</EditModal>
|
|
390
|
+
</div>
|
|
391
|
+
</template>
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,9 @@ export { useResource, type ResourceMeta } from './crud/useResource'
|
|
|
24
24
|
// CRM de páginas y bloques (doc 03): editor dirigido por esquema.
|
|
25
25
|
export { default as SchemaFields, type FieldSchema } from './content/SchemaFields.vue'
|
|
26
26
|
export { default as PageBlocks, type PageBlocksLabels } from './content/PageBlocks.vue'
|
|
27
|
+
// Configurador del menú público (doc 10 ampliado): páginas del CRM + rutas
|
|
28
|
+
// del juego, reordenables y agrupables. Agnóstico de i18n (DC-29).
|
|
29
|
+
export { default as MenuManager, type MenuManagerLabels } from './content/MenuManager.vue'
|
|
27
30
|
// Subidas de imagen diferidas al guardar (uploads de contenido sin huérfanos):
|
|
28
31
|
// las usan PageBlocks y las vistas del cascarón (Ajustes, form de página).
|
|
29
32
|
export {
|