-drag menu continua
This commit is contained in:
@@ -12,11 +12,6 @@
|
||||
height: 0.5px;
|
||||
}
|
||||
|
||||
.router-link-active {
|
||||
color: #027be3;
|
||||
background-color: #dadada !important;
|
||||
border-right: 2px solid #027be3;
|
||||
}
|
||||
|
||||
.list-label:first-child {
|
||||
line-height: 20px;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import { tools } from 'app/src/store/Modules/tools';
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
|
||||
const norm = (path: string): string =>
|
||||
path
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
const toNormPath = (p: any): string => {
|
||||
if (!p) return '';
|
||||
if (typeof p === 'string') return norm(p);
|
||||
return norm(p.path || '');
|
||||
};
|
||||
const norm = (path: string): string => tools.norm(path);
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CMenuItem',
|
||||
@@ -21,52 +12,68 @@ export default defineComponent({
|
||||
getmymenuclass: { type: Function, required: true },
|
||||
getimgiconclass: { type: Function, required: true },
|
||||
clBase: { type: String, default: '' },
|
||||
level: { type: Number, default: 1 },
|
||||
mainMenu: { type: Boolean, default: false },
|
||||
level: { type: Number, default: 0 },
|
||||
},
|
||||
setup(props) {
|
||||
const getmenuByPath = (input: any, depth = 0): any => {
|
||||
if (depth > 5) return null;
|
||||
|
||||
const path = toNormPath(input);
|
||||
if (!path) return null;
|
||||
|
||||
let page = props.tools.getmenuByPath ? props.tools.getmenuByPath(path) : null;
|
||||
if (!page) return null;
|
||||
|
||||
// Evita loop
|
||||
const selfPath = toNormPath(props.item);
|
||||
if (selfPath && path === selfPath) return null;
|
||||
|
||||
return page;
|
||||
};
|
||||
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
// Funzione per ottenere i figli ordinati
|
||||
const children = computed(() => {
|
||||
const item: any = props.item;
|
||||
const r2 = Array.isArray(item.routes2) ? item.routes2 : [];
|
||||
const sm = Array.isArray(item.sottoMenu) ? item.sottoMenu : [];
|
||||
|
||||
return [...r2, ...sm]
|
||||
.map((ref) =>
|
||||
typeof ref === 'string' || !ref.path ? getmenuByPath(ref, props.level) : ref
|
||||
)
|
||||
.filter(Boolean)
|
||||
.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0));
|
||||
let ris = null;
|
||||
|
||||
if (r2.length > 0) {
|
||||
ris = [...r2]
|
||||
.map((rec) => {
|
||||
const norm = tools.norm(rec.path);
|
||||
return props.tools.getmenuByPath(norm);
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0));
|
||||
} else {
|
||||
ris = [...sm]
|
||||
.map((path) => {
|
||||
const norm = tools.norm(path);
|
||||
return props.tools.getmenuByPath(norm);
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
// console.log('RIS', ris);
|
||||
return ris;
|
||||
});
|
||||
|
||||
// Determina se ha figli
|
||||
const hasChildren = computed(() => children.value.length > 0);
|
||||
|
||||
// Ottiene l'icona appropriata
|
||||
const icon = computed(() => {
|
||||
const item: any = props.item;
|
||||
return item.materialIcon || item.icon || 'far fa-file-alt';
|
||||
})
|
||||
});
|
||||
|
||||
// Gestisce il click
|
||||
function makeClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const route = props.getroute(props.item);
|
||||
|
||||
if (route) {
|
||||
window.location.href = typeof route === 'string' ? route : route.path;
|
||||
}
|
||||
|
||||
emit('click', props.item);
|
||||
}
|
||||
|
||||
return {
|
||||
children,
|
||||
hasChildren,
|
||||
icon,
|
||||
makeClick: () => {
|
||||
// niente per ora
|
||||
},
|
||||
makeClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<div :style="{ paddingLeft: `${level * 4}px` }">
|
||||
<q-separator v-if="item.isseparator" />
|
||||
|
||||
<!-- Nodo con figli -->
|
||||
<q-expansion-item
|
||||
v-else-if="hasChildren"
|
||||
:header-class="getmymenuclass(item)"
|
||||
:icon="item.materialIcon"
|
||||
:icon="icon"
|
||||
:label="tools.getLabelByItem(item)"
|
||||
expand-icon="fas fa-chevron-down"
|
||||
active-class="my-menu-active"
|
||||
@@ -26,7 +25,7 @@
|
||||
|
||||
<!-- Foglia -->
|
||||
<router-link v-else :to="getroute(item)">
|
||||
<q-item clickable :to="getroute(item)" @click="makeClick" active-class="my-menu-active">
|
||||
<q-item clickable :to="getroute(item)" active-class="my-menu-active">
|
||||
<q-item-section thumbnail>
|
||||
<q-avatar
|
||||
:icon="item.materialIcon"
|
||||
|
||||
@@ -1,80 +1,205 @@
|
||||
import { defineComponent, ref, computed, watch } from 'vue'
|
||||
// IconPicker.ts
|
||||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
|
||||
// Tipo per il valore del modello
|
||||
interface IconValue {
|
||||
icon?: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IconPicker',
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
modelValue: {
|
||||
type: Object as () => IconValue,
|
||||
default: () => ({})
|
||||
},
|
||||
icons: {
|
||||
type: Array as () => string[],
|
||||
// SOLO Font Awesome 5 (free)
|
||||
default: () => [
|
||||
'fas fa-home',
|
||||
'fas fa-book',
|
||||
'fas fa-star',
|
||||
'fas fa-heart',
|
||||
'fas fa-user',
|
||||
'fas fa-cog',
|
||||
'fas fa-info-circle',
|
||||
'far fa-newspaper',
|
||||
'fas fa-list',
|
||||
'fas fa-tags',
|
||||
'fas fa-chart-line',
|
||||
'fas fa-chart-bar',
|
||||
'fas fa-briefcase',
|
||||
'fas fa-calendar',
|
||||
'fas fa-envelope',
|
||||
'fas fa-image',
|
||||
'fas fa-list',
|
||||
'fas fa-tasks',
|
||||
'fas fa-th-large',
|
||||
'fas fa-th-list',
|
||||
'fas fa-thumbs-up',
|
||||
'fas fa-thumbs-down',
|
||||
'fas fa-star',
|
||||
'fas fa-star-half-alt',
|
||||
'fas fa-heart',
|
||||
'fas fa-comment',
|
||||
'fas fa-bell',
|
||||
'fas fa-search',
|
||||
'fas fa-filter',
|
||||
'fas fa-sort',
|
||||
'fas fa-plus',
|
||||
'fas fa-minus',
|
||||
'fas fa-edit',
|
||||
'fas fa-trash',
|
||||
'fas fa-copy',
|
||||
'fas fa-download',
|
||||
'fas fa-upload',
|
||||
'fas fa-share',
|
||||
'fas fa-print',
|
||||
'fas fa-eye',
|
||||
'fas fa-lock',
|
||||
'fas fa-unlock',
|
||||
'fas fa-key',
|
||||
'fas fa-shield-alt',
|
||||
'fas fa-globe',
|
||||
'fas fa-language',
|
||||
'fas fa-flag',
|
||||
'fas fa-map-marker-alt',
|
||||
'fas fa-phone',
|
||||
'fas fa-globe-europe'
|
||||
'fas fa-mobile-alt',
|
||||
'fas fa-clock',
|
||||
'fas fa-history',
|
||||
'fas fa-undo',
|
||||
'fas fa-redo',
|
||||
'fas fa-exclamation-triangle',
|
||||
'fas fa-check',
|
||||
'fas fa-times',
|
||||
'fas fa-info-circle',
|
||||
'fas fa-question-circle',
|
||||
'fas fa-exclamation-circle',
|
||||
'fas fa-check-circle',
|
||||
'fas fa-times-circle',
|
||||
'fas fa-info',
|
||||
'fas fa-question',
|
||||
'fas fa-bug',
|
||||
'fas fa-wrench',
|
||||
'fas fa-cogs',
|
||||
'fas fa-database',
|
||||
'fas fa-server',
|
||||
'fas fa-code',
|
||||
'fas fa-terminal',
|
||||
'fas fa-file',
|
||||
'fas fa-file-image',
|
||||
'fas fa-file-audio',
|
||||
'fas fa-file-video',
|
||||
'fas fa-file-pdf',
|
||||
'fas fa-file-word',
|
||||
'fas fa-file-excel',
|
||||
'fas fa-file-powerpoint',
|
||||
'fas fa-file-archive',
|
||||
'fas fa-file-code',
|
||||
'fas fa-folder',
|
||||
'fas fa-folder-open',
|
||||
'fas fa-folder-plus',
|
||||
'fas fa-folder-minus',
|
||||
'fas fa-tags',
|
||||
'fas fa-tag',
|
||||
'fas fa-bookmark',
|
||||
'fas fa-book',
|
||||
'fas fa-newspaper',
|
||||
'fas fa-magazine',
|
||||
'fas fa-camera',
|
||||
'fas fa-video',
|
||||
'fas fa-music',
|
||||
'fas fa-paint-brush',
|
||||
'fas fa-palette',
|
||||
'fas fa-font',
|
||||
'fas fa-bold',
|
||||
'fas fa-italic',
|
||||
'fas fa-underline',
|
||||
'fas fa-align-left',
|
||||
'fas fa-align-center',
|
||||
'fas fa-align-right',
|
||||
'fas fa-list-ul',
|
||||
'fas fa-list-ol',
|
||||
'fas fa-outdent',
|
||||
'fas fa-indent',
|
||||
'fas fa-link',
|
||||
'fas fa-unlink',
|
||||
'fas fa-paperclip',
|
||||
'fas fa-paper-plane',
|
||||
'fas fa-reply',
|
||||
'fas fa-reply-all',
|
||||
'fas fa-forward',
|
||||
'fas fa-share-square',
|
||||
]
|
||||
},
|
||||
// Dimensioni disponibili
|
||||
sizeOptions: {
|
||||
type: Array as () => string[],
|
||||
default: () => ['16px', '20px', '24px', '32px', '48px']
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
const local = ref(props.modelValue) // testo inserito/valore corrente
|
||||
const dialog = ref(false) // mostra/nasconde il picker
|
||||
const keyword = ref('') // filtro dentro il dialog
|
||||
emits: {
|
||||
'update:modelValue': (val: IconValue) => true,
|
||||
'change': (val: IconValue) => true
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const localIcon = ref(props.modelValue?.icon || '');
|
||||
const localSize = ref(props.modelValue?.size || '20px');
|
||||
const dialog = ref(false);
|
||||
const keyword = ref('');
|
||||
|
||||
watch(() => props.modelValue, v => { local.value = v })
|
||||
// Sincronizza i valori locali quando cambia modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
localIcon.value = val.icon || '';
|
||||
localSize.value = val.size || '20px';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
const k = keyword.value.trim().toLowerCase()
|
||||
if (!k) return props.icons
|
||||
return props.icons.filter(i =>
|
||||
i.toLowerCase().includes(k) ||
|
||||
// match anche sul nome “breve” (es: 'home')
|
||||
i.toLowerCase().split(' ').some(cls => cls.startsWith('fa-') && cls.includes(k))
|
||||
)
|
||||
})
|
||||
const k = keyword.value.trim().toLowerCase();
|
||||
if (!k) return props.icons;
|
||||
return props.icons.filter(
|
||||
(i) =>
|
||||
i.toLowerCase().includes(k) ||
|
||||
i
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.some((cls) => cls.startsWith('fa-') && cls.includes(k))
|
||||
);
|
||||
});
|
||||
|
||||
function select (val: string) {
|
||||
// applica la stringa così com’è; nessun fallback
|
||||
emit('update:modelValue', val || '')
|
||||
emit('change', val || '')
|
||||
function updateValue() {
|
||||
const newVal: IconValue = {
|
||||
icon: localIcon.value || undefined,
|
||||
size: localSize.value || undefined
|
||||
};
|
||||
emit('update:modelValue', newVal);
|
||||
emit('change', newVal);
|
||||
}
|
||||
|
||||
function choose (ic: string) {
|
||||
local.value = ic || ''
|
||||
select(local.value)
|
||||
dialog.value = false
|
||||
function choose(ic: string) {
|
||||
localIcon.value = ic;
|
||||
updateValue();
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
function clear () {
|
||||
local.value = ''
|
||||
select('')
|
||||
function clear() {
|
||||
localIcon.value = '';
|
||||
updateValue();
|
||||
}
|
||||
|
||||
function openPicker () {
|
||||
keyword.value = ''
|
||||
dialog.value = true
|
||||
function openPicker() {
|
||||
keyword.value = '';
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
local,
|
||||
localIcon,
|
||||
localSize,
|
||||
dialog,
|
||||
keyword,
|
||||
filteredIcons,
|
||||
select,
|
||||
sizeOptions: props.sizeOptions,
|
||||
choose,
|
||||
clear,
|
||||
openPicker
|
||||
}
|
||||
openPicker,
|
||||
updateValue
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<!-- IconPicker.vue -->
|
||||
<template>
|
||||
<div class="q-gutter-sm">
|
||||
<!-- Campo principale -->
|
||||
<div class="row items-center q-col-gutter-sm">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="local"
|
||||
v-model="localIcon"
|
||||
dense
|
||||
clearable
|
||||
label="Icona"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon v-if="local" :name="local" />
|
||||
<q-icon
|
||||
v-if="localIcon"
|
||||
:name="localIcon"
|
||||
:size="localSize"
|
||||
/>
|
||||
</template>
|
||||
<template #hint> Dimensione: {{ localSize }} </template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
@@ -18,8 +25,8 @@
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
label="Usa testo"
|
||||
@click="select(local)"
|
||||
label="Applica"
|
||||
@click="updateValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -28,30 +35,52 @@
|
||||
dense
|
||||
color="primary"
|
||||
outline
|
||||
label="Scegli icona"
|
||||
label="Scegli"
|
||||
@click="openPicker"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- La griglia icone appare solo nel dialog -->
|
||||
<!-- Dialog di selezione -->
|
||||
<q-dialog v-model="dialog">
|
||||
<q-card style="min-width: 640px; max-width: 90vw;">
|
||||
<q-card style="min-width: 640px; max-width: 90vw">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Seleziona icona</q-toolbar-title>
|
||||
<q-btn flat round dense icon="fas fa-times" v-close-popup />
|
||||
<q-toolbar-title>Scegli icona e dimensione</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="fas fa-times"
|
||||
v-close-popup
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<div class="q-pa-md">
|
||||
<!-- Cerca icona -->
|
||||
<q-input
|
||||
v-model="keyword"
|
||||
dense
|
||||
clearable
|
||||
autofocus
|
||||
label="Cerca (es: home, user, info...)"
|
||||
label="Cerca icona..."
|
||||
class="q-mb-sm"
|
||||
/>
|
||||
|
||||
<div class="row q-col-gutter-sm q-mt-sm">
|
||||
<!-- Selettore dimensione -->
|
||||
<div class="q-mt-lg">
|
||||
<q-select
|
||||
v-model="localSize"
|
||||
:options="sizeOptions"
|
||||
label="Dimensione icona"
|
||||
dense
|
||||
options-dense
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Griglia icone -->
|
||||
<div class="row q-col-gutter-sm q-mb-sm">
|
||||
<div
|
||||
v-for="ic in filteredIcons"
|
||||
:key="ic"
|
||||
@@ -61,6 +90,7 @@
|
||||
outline
|
||||
class="full-width"
|
||||
:icon="ic"
|
||||
:size="localSize"
|
||||
@click="choose(ic)"
|
||||
>
|
||||
<q-tooltip>{{ ic }}</q-tooltip>
|
||||
@@ -68,10 +98,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pulsanti di azione -->
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn outline label="Rimuovi icona" color="negative" @click="choose('')" />
|
||||
<q-btn
|
||||
outline
|
||||
label="Nessuna icona"
|
||||
color="negative"
|
||||
@click="choose('')"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn color="primary" label="Chiudi" v-close-popup />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Chiudi"
|
||||
v-close-popup
|
||||
@click="updateValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
@@ -82,5 +123,5 @@
|
||||
<script lang="ts" src="./IconPicker.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* opzionale: spaziatura minima sulle celle */
|
||||
/* Puoi aggiungere stili personalizzati qui */
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
.indent-spacer {
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
.q-item {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background-color: $primary !important;
|
||||
color: white;
|
||||
|
||||
.q-item__section--side {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-2;
|
||||
|
||||
.drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
29
src/components/MenuPageItem/MenuPageItem.ts
Normal file
29
src/components/MenuPageItem/MenuPageItem.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuPageItem',
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
depth: { type: Number, default: 0 },
|
||||
variant: { type: String, required: true },
|
||||
},
|
||||
emits: ['select', 'edit', 'delete', 'open'],
|
||||
setup(props, { emit }) {
|
||||
const showGrip = computed(() => props.variant === 'menu');
|
||||
|
||||
const displayPath = (path?: string) => {
|
||||
if (!path) return '-';
|
||||
return path.startsWith('/') ? path : '/' + path;
|
||||
};
|
||||
|
||||
return {
|
||||
showGrip,
|
||||
displayPath,
|
||||
emitSelect: () => emit('select', props.item.__key),
|
||||
emitEdit: () => emit('edit', props.item.__key),
|
||||
emitDelete: () => emit('delete', props.item.__key),
|
||||
emitOpen: () => emit('open', props.item.__key),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -3,12 +3,12 @@
|
||||
clickable
|
||||
:active="selected"
|
||||
@click="emitSelect"
|
||||
:class="{ 'menu-item': variant === 'menu' }"
|
||||
:class="{
|
||||
'depth-0': depth === 0,
|
||||
'depth-1': depth === 1
|
||||
}"
|
||||
>
|
||||
<q-item-section
|
||||
v-if="showGrip"
|
||||
avatar
|
||||
>
|
||||
<q-item-section v-if="showGrip" avatar>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
@@ -19,11 +19,7 @@
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="depth > 0"
|
||||
avatar
|
||||
class="q-pr-none"
|
||||
>
|
||||
<q-item-section v-if="depth > 0" avatar class="q-pr-none">
|
||||
<div :style="{ paddingLeft: `${depth * 20}px` }" />
|
||||
</q-item-section>
|
||||
|
||||
@@ -36,33 +32,17 @@
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label :class="{ 'text-grey-7': !active }">{{
|
||||
<q-item-label :class="{ 'text-grey-7': !item.active }">{{
|
||||
item.title || '(senza titolo)'
|
||||
}}</q-item-label>
|
||||
<q-item-label caption>{{ displayPath(item.path) }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="active"
|
||||
side
|
||||
class="float-right"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-circle"
|
||||
color="green"
|
||||
size="xs"
|
||||
/>
|
||||
<q-item-section v-if="item.active" side class="float-right">
|
||||
<q-icon name="fas fa-circle" color="green" size="xs" />
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
v-if="item.only_admin"
|
||||
side
|
||||
class="float-right"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-circle"
|
||||
color="red"
|
||||
size="xs"
|
||||
/>
|
||||
<q-item-section v-if="item.only_admin" side class="float-right">
|
||||
<q-icon name="fas fa-circle" color="red" size="xs" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
@@ -76,31 +56,17 @@
|
||||
>
|
||||
<q-menu>
|
||||
<q-list style="min-width: 140px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitOpen"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="emitOpen">
|
||||
<q-item-section side><q-icon name="fas fa-edit" /></q-item-section>
|
||||
<q-item-section>Modifica</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitEdit"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="emitEdit">
|
||||
<q-item-section side><q-icon name="fas fa-cog" /></q-item-section>
|
||||
<q-item-section>Impostazioni</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitDelete"
|
||||
>
|
||||
<q-item clickable v-close-popup @click="emitDelete">
|
||||
<q-item-section side
|
||||
><q-icon
|
||||
name="fas fa-trash"
|
||||
color="red"
|
||||
><q-icon name="fas fa-trash" color="red"
|
||||
/></q-item-section>
|
||||
<q-item-section>Elimina</q-item-section>
|
||||
</q-item>
|
||||
@@ -111,39 +77,7 @@
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuPageItem',
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
active: { type: Boolean, default: true },
|
||||
depth: { type: Number, default: 0 },
|
||||
variant: { type: String, required: true },
|
||||
},
|
||||
emits: ['select', 'update:active', 'edit', 'delete', 'open'],
|
||||
setup(props, { emit }) {
|
||||
const showGrip = true // computed(() => props.variant === 'menu');
|
||||
|
||||
const displayPath = (path?: string) => {
|
||||
if (!path) return '-';
|
||||
return path.startsWith('/') ? path : '/' + path;
|
||||
};
|
||||
|
||||
return {
|
||||
showGrip,
|
||||
displayPath,
|
||||
emitSelect: () => emit('select', props.item.__key),
|
||||
emitEdit: () => emit('edit', props.item.__key),
|
||||
emitDelete: () => emit('delete', props.item.__key),
|
||||
emitOpen: () => emit('open', props.item.__key),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts" src="./MenuPageItem.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './MenuPageItem.scss';
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.q-select {
|
||||
.q-item {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.q-item__section--main {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import { defineComponent, ref, computed, watch, reactive, toRaw, nextTick } from 'vue';
|
||||
import { defineComponent, reactive, computed, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import IconPicker from '../IconPicker/IconPicker.vue';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
import { IconPicker } from '@src/components/IconPicker';
|
||||
import { useGlobalStore } from 'app/src/store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { costanti } from '@costanti';
|
||||
import { CMyFieldRec } from '@src/components/CMyFieldRec';
|
||||
|
||||
const norm = (s?: string) => (s || '').trim().replace(/^\//, '').toLowerCase();
|
||||
const withSlash = (s?: string) => {
|
||||
const p = (s || '').trim();
|
||||
if (!p) return '/';
|
||||
return p.startsWith('/') ? p : `/${p}`;
|
||||
return p ? (p.startsWith('/') ? p : `/${p}`) : '/';
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageEditor',
|
||||
components: { IconPicker, CMyFieldRec },
|
||||
components: { IconPicker },
|
||||
props: {
|
||||
modelValue: { type: Object as () => IMyPage, required: true },
|
||||
nuovaPagina: { type: Boolean, required: true },
|
||||
@@ -27,480 +23,153 @@ export default defineComponent({
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const globalStore = useGlobalStore();
|
||||
const { mypage } = storeToRefs(globalStore);
|
||||
|
||||
const draft = reactive<IMyPage>({ ...props.modelValue });
|
||||
|
||||
const ui = reactive({
|
||||
pathText: toUiPath(draft.path),
|
||||
pathText: withSlash(draft.path),
|
||||
isSubmenu: !!draft.submenu,
|
||||
parentId: null as string | null,
|
||||
childrenPaths: [] as string[],
|
||||
});
|
||||
|
||||
watch(
|
||||
() => ui.isSubmenu,
|
||||
(isSub) => {
|
||||
draft.submenu = !!isSub;
|
||||
if (isSub) {
|
||||
// una pagina figlia non gestisce figli propri
|
||||
ui.childrenPaths = [];
|
||||
// se non c'è un parent pre-selezionato, azzera
|
||||
if (!ui.parentId) ui.parentId = findParentIdForChild(draft.path);
|
||||
} else {
|
||||
// tornando top-level, nessun parent selezionato
|
||||
ui.parentId = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
const iconModel = computed({
|
||||
get() {
|
||||
return {
|
||||
icon: draft.icon,
|
||||
size: draft.iconsize || '20px',
|
||||
};
|
||||
},
|
||||
set(value) {
|
||||
draft.icon = value.icon || '';
|
||||
draft.iconsize = value.size || '20px';
|
||||
},
|
||||
});
|
||||
|
||||
// Draft indipendente
|
||||
const saving = ref(false);
|
||||
const syncingFromProps = ref(false);
|
||||
const previousPath = ref<string>(draft.path || '');
|
||||
|
||||
// ===== INIT =====
|
||||
// parent corrente (se questa pagina è sottomenu)
|
||||
ui.parentId = findParentIdForChild(draft.path);
|
||||
// inizializza lista figli (per TOP-LEVEL) con i path presenti nel draft
|
||||
// Inizializza i figli
|
||||
ui.childrenPaths = Array.isArray(draft.sottoMenu)
|
||||
? draft.sottoMenu.map((p) => withSlash(p))
|
||||
: [];
|
||||
|
||||
// --- Sync IN: quando cambia il modelValue (esterno)
|
||||
// Watch per sincronizzare con le modifiche esterne
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, v || {});
|
||||
ui.pathText = toUiPath(draft.path);
|
||||
(v) => {
|
||||
Object.assign(draft, v);
|
||||
ui.pathText = withSlash(draft.path);
|
||||
ui.isSubmenu = !!draft.submenu;
|
||||
ui.parentId = findParentIdForChild(draft.path);
|
||||
ui.childrenPaths = Array.isArray(draft.sottoMenu)
|
||||
? draft.sottoMenu.map((p) => withSlash(p))
|
||||
: [];
|
||||
previousPath.value = draft.path || '';
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
},
|
||||
{ deep: false }
|
||||
);
|
||||
|
||||
// --- Propagazione live (solo se NON nuovaPagina)
|
||||
watch(
|
||||
draft,
|
||||
(val) => {
|
||||
if (syncingFromProps.value) return;
|
||||
if (props.nuovaPagina) return;
|
||||
upsertIntoStore(val, mypage.value);
|
||||
emit('update:modelValue', { ...val });
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function onToggleSubmenu(val: boolean) {
|
||||
draft.submenu = !!val;
|
||||
if (val) {
|
||||
draft.inmenu = true;
|
||||
ui.childrenPaths = []; // sicurezza
|
||||
}
|
||||
}
|
||||
|
||||
// ======= OPTIONS =======
|
||||
const parentOptions = computed(() =>
|
||||
(mypage.value || [])
|
||||
.filter(
|
||||
(p) =>
|
||||
p &&
|
||||
p.inmenu &&
|
||||
!p.submenu &&
|
||||
norm(p.path) !== norm(draft.path) &&
|
||||
p._id !== draft._id // <-- escludi se stesso
|
||||
)
|
||||
.map((p) => ({
|
||||
value: (p._id || p.path || '') as string,
|
||||
label: `${p.title || withSlash(p.path)}`,
|
||||
}))
|
||||
);
|
||||
|
||||
// Mappa path (display) -> page
|
||||
const pageByDisplayPath = computed(() => {
|
||||
const m = new Map<string, IMyPage>();
|
||||
(mypage.value || []).forEach((p) => {
|
||||
m.set(withSlash(p.path).toLowerCase(), p);
|
||||
});
|
||||
return m;
|
||||
});
|
||||
|
||||
// Mappa childPathDisplay -> parentId
|
||||
const parentByChildPath = computed(() => {
|
||||
const map = new Map<string, string>();
|
||||
(mypage.value || []).forEach((p) => {
|
||||
if (p && p.inmenu && !p.submenu && Array.isArray(p.sottoMenu)) {
|
||||
p.sottoMenu.forEach((sp) => {
|
||||
map.set(withSlash(sp).toLowerCase(), (p._id || p.path) as string);
|
||||
});
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
// Candidati figli per il selettore (indent: 0 per "disponibili", 1 per "già figli di altri", 1 anche per già figli miei)
|
||||
// Opzioni per i figli
|
||||
const childCandidateOptions = computed(() => {
|
||||
const selfPathDisp = withSlash(draft.path).toLowerCase();
|
||||
const selfPath = withSlash(draft.path).toLowerCase();
|
||||
const mine = new Set(ui.childrenPaths.map((x) => x.toLowerCase()));
|
||||
const opts: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
level: number;
|
||||
disabled?: boolean;
|
||||
hint?: string;
|
||||
}> = [];
|
||||
|
||||
(mypage.value || [])
|
||||
.filter((p) => p._id !== draft._id && norm(p.path) !== norm(draft.path)) // escludi se stesso
|
||||
.forEach((p) => {
|
||||
return globalStore.mypage
|
||||
.filter((p) => p._id !== draft._id && norm(p.path) !== norm(draft.path))
|
||||
.map((p) => {
|
||||
const disp = withSlash(p.path);
|
||||
const dispKey = disp.toLowerCase();
|
||||
const parentId = parentByChildPath.value.get(dispKey);
|
||||
|
||||
if (mine.has(dispKey)) {
|
||||
// già selezionato come mio figlio
|
||||
opts.push({
|
||||
value: disp,
|
||||
label: labelForPage(p),
|
||||
level: 1,
|
||||
hint: 'figlio di questa pagina',
|
||||
});
|
||||
} else if (!parentId || parentId === (draft._id || draft.path)) {
|
||||
// orfano (o già mio → già gestito sopra)
|
||||
opts.push({
|
||||
value: disp,
|
||||
label: labelForPage(p),
|
||||
level: 0,
|
||||
});
|
||||
} else {
|
||||
// figlio di un altro parent → lo mostro ma lo disabilito
|
||||
const parent = (mypage.value || []).find(
|
||||
(pp) => (pp._id || pp.path) === parentId
|
||||
);
|
||||
opts.push({
|
||||
value: disp,
|
||||
label: labelForPage(p),
|
||||
level: 1,
|
||||
disabled: true,
|
||||
hint: `già sotto " ${parent?.title || withSlash(parent?.path)}"`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Ordina per livello e label
|
||||
return opts.sort((a, b) => a.level - b.level || a.label.localeCompare(b.label));
|
||||
return {
|
||||
value: disp,
|
||||
label: p.title || disp,
|
||||
level: mine.has(dispKey) ? 1 : 0,
|
||||
disabled: mine.has(dispKey)
|
||||
? false
|
||||
: globalStore.mypage.some(
|
||||
(parent) =>
|
||||
Array.isArray(parent.sottoMenu) &&
|
||||
parent.sottoMenu.some((sp) => norm(sp) === norm(p.path))
|
||||
),
|
||||
hint: mine.has(dispKey) ? 'figlio di questa pagina' : undefined,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.level - b.level || a.label.localeCompare(b.label));
|
||||
});
|
||||
|
||||
function toUiPath(storePath?: string) {
|
||||
const p = (storePath || '').trim();
|
||||
if (!p) return '/';
|
||||
return p.startsWith('/') ? p : `/${p}`;
|
||||
}
|
||||
|
||||
function toStorePath(uiPath?: string) {
|
||||
const p = (uiPath || '').trim();
|
||||
if (!p) return '';
|
||||
return p.startsWith('/') ? p.slice(1) : p;
|
||||
}
|
||||
|
||||
function normalizeAndApplyPath() {
|
||||
let p = (ui.pathText || '/').trim();
|
||||
p = p.replace(/\s+/g, '-');
|
||||
// Normalizza il percorso
|
||||
function normalizePath() {
|
||||
let p = ui.pathText.trim().replace(/\s+/g, '-');
|
||||
if (!p.startsWith('/')) p = '/' + p;
|
||||
ui.pathText = p;
|
||||
draft.path = toStorePath(p);
|
||||
draft.path = p.startsWith('/') ? p.slice(1) : p;
|
||||
}
|
||||
|
||||
function pathRule(v: string) {
|
||||
// Validazione percorso
|
||||
function validatePath(v: string) {
|
||||
if (!v) return 'Percorso richiesto';
|
||||
if (!v.startsWith('/')) return 'Deve iniziare con /';
|
||||
if (/\s/.test(v)) return 'Nessuno spazio nel path';
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======= STORE UTILS =======
|
||||
function upsertIntoStore(page: IMyPage, arr: IMyPage[]) {
|
||||
if (!page) return;
|
||||
const keyId = page._id;
|
||||
const keyPath = page.path || '';
|
||||
let idx = -1;
|
||||
if (keyId) idx = arr.findIndex((p) => p._id === keyId);
|
||||
if (idx < 0 && keyPath) idx = arr.findIndex((p) => (p.path || '') === keyPath);
|
||||
if (idx >= 0) arr[idx] = { ...arr[idx], ...toRaw(page) };
|
||||
else arr.push({ ...toRaw(page) });
|
||||
}
|
||||
// Salva la pagina
|
||||
async function save() {
|
||||
normalizePath();
|
||||
|
||||
function findParentIdForChild(childPath?: string | null): string | null {
|
||||
const target = withSlash(childPath || '');
|
||||
for (const p of mypage.value) {
|
||||
if (p && p.inmenu && !p.submenu && Array.isArray(p.sottoMenu)) {
|
||||
if (
|
||||
p.sottoMenu.some((sp) => withSlash(sp).toLowerCase() === target.toLowerCase())
|
||||
) {
|
||||
return (p._id || p.path) as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findParentsReferencing(childDisplayPath: string): IMyPage[] {
|
||||
const target = withSlash(childDisplayPath).toLowerCase();
|
||||
return (mypage.value || []).filter(
|
||||
(p) =>
|
||||
p &&
|
||||
p.inmenu &&
|
||||
!p.submenu &&
|
||||
Array.isArray(p.sottoMenu) &&
|
||||
p.sottoMenu.some((sp) => withSlash(sp).toLowerCase() === target)
|
||||
);
|
||||
}
|
||||
|
||||
function addChildToParent(parent: IMyPage, childDisplayPath: string) {
|
||||
if (!Array.isArray(parent.sottoMenu)) parent.sottoMenu = [];
|
||||
const target = withSlash(childDisplayPath);
|
||||
if (
|
||||
!parent.sottoMenu.some(
|
||||
(sp) => withSlash(sp).toLowerCase() === target.toLowerCase()
|
||||
)
|
||||
) {
|
||||
parent.sottoMenu.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
function removeChildFromParent(parent: IMyPage, childDisplayPath: string) {
|
||||
if (!Array.isArray(parent.sottoMenu)) return;
|
||||
const target = withSlash(childDisplayPath).toLowerCase();
|
||||
parent.sottoMenu = parent.sottoMenu.filter(
|
||||
(sp) => withSlash(sp).toLowerCase() !== target
|
||||
);
|
||||
}
|
||||
|
||||
// ======= SAVE =======
|
||||
async function checkAndSave(payloadDraft?: IMyPage) {
|
||||
const cur = payloadDraft || draft;
|
||||
|
||||
// validazioni base
|
||||
if (!cur.title?.trim()) {
|
||||
// Validazioni
|
||||
if (!draft.title?.trim()) {
|
||||
$q.notify({ message: 'Inserisci il titolo della pagina', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
const pathText = (ui.pathText || '').trim();
|
||||
if (!pathText) {
|
||||
$q.notify({ message: 'Inserisci il percorso della pagina', type: 'warning' });
|
||||
|
||||
if (!validatePath(ui.pathText)) {
|
||||
$q.notify({ message: 'Percorso non valido', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const candidatePath = toStorePath(pathText).toLowerCase();
|
||||
const existPath = globalStore.mypage.find(
|
||||
(r) => (r.path || '').toLowerCase() === candidatePath && r._id !== cur._id
|
||||
);
|
||||
if (existPath) {
|
||||
$q.notify({
|
||||
message: "Esiste già un'altra pagina con questo percorso",
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
// Aggiorna la struttura
|
||||
draft.submenu = ui.isSubmenu;
|
||||
if (!ui.isSubmenu) {
|
||||
draft.sottoMenu = ui.childrenPaths.map((p) =>
|
||||
p.startsWith('/') ? p.slice(1) : p
|
||||
);
|
||||
}
|
||||
|
||||
const candidateTitle = (cur.title || '').toLowerCase();
|
||||
const existName = globalStore.mypage.find(
|
||||
(r) => (r.title || '').toLowerCase() === candidateTitle && r._id !== cur._id
|
||||
);
|
||||
if (existName) {
|
||||
$q.notify({ message: 'Il nome della pagina esiste già', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (ui.isSubmenu && !ui.parentId) {
|
||||
$q.notify({
|
||||
message: 'Seleziona la pagina padre per questo sottomenu',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await save();
|
||||
emit('hide');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
saving.value = true;
|
||||
normalizeAndApplyPath();
|
||||
|
||||
// sync flag submenu
|
||||
draft.submenu = !!ui.isSubmenu;
|
||||
|
||||
// se top-level, sincronizza anche i figli dal selettore
|
||||
if (!ui.isSubmenu) {
|
||||
draft.sottoMenu = ui.childrenPaths.slice();
|
||||
const saved = await globalStore.savePage({ ...draft });
|
||||
if (saved) {
|
||||
emit('update:modelValue', { ...saved });
|
||||
emit('apply', { ...saved });
|
||||
$q.notify({ type: 'positive', message: 'Pagina salvata' });
|
||||
}
|
||||
|
||||
// --- salva/aggiorna pagina corrente
|
||||
const payload: IMyPage = { ...toRaw(draft), path: draft.path || '' };
|
||||
const saved = await globalStore.savePage(payload);
|
||||
if (saved && typeof saved === 'object') {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, saved);
|
||||
upsertIntoStore(draft, mypage.value);
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
}
|
||||
|
||||
// --- aggiorna legami parentali
|
||||
const prevDisplay = withSlash(previousPath.value);
|
||||
const newDisplay = withSlash(draft.path);
|
||||
|
||||
// 1) questa pagina è figlia? collega/sgancia dal parent
|
||||
const parentsPrev = findParentsReferencing(prevDisplay);
|
||||
for (const p of parentsPrev) {
|
||||
// se la pagina è ancora sottomenu con lo stesso parent e path invariato, mantieni
|
||||
const keep =
|
||||
ui.isSubmenu &&
|
||||
ui.parentId === (p._id || p.path) &&
|
||||
prevDisplay.toLowerCase() === newDisplay.toLowerCase();
|
||||
if (!keep) {
|
||||
removeChildFromParent(p, prevDisplay);
|
||||
await globalStore.savePage(p);
|
||||
}
|
||||
}
|
||||
if (ui.isSubmenu && ui.parentId) {
|
||||
const parent =
|
||||
mypage.value.find((pp) => (pp._id || pp.path) === ui.parentId) || null;
|
||||
if (parent) {
|
||||
parent.inmenu = true;
|
||||
parent.submenu = false;
|
||||
addChildToParent(parent, newDisplay);
|
||||
await globalStore.savePage(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) se questa pagina è TOP-LEVEL, salva i riferimenti dei figli
|
||||
if (!ui.isSubmenu) {
|
||||
// rimuovi da tutti i parent eventuali vecchi riferimenti ai miei figli (che non sono più nella lista)
|
||||
const still = new Set(ui.childrenPaths.map((x) => x.toLowerCase()));
|
||||
const parentsTouch = (mypage.value || []).filter(
|
||||
(p) => p.inmenu && !p.submenu && Array.isArray(p.sottoMenu)
|
||||
);
|
||||
for (const pr of parentsTouch) {
|
||||
const before = (pr.sottoMenu || []).slice();
|
||||
pr.sottoMenu = (pr.sottoMenu || []).filter((sp) => {
|
||||
const spKey = withSlash(sp).toLowerCase();
|
||||
// tieni solo i figli che non appartengono a me oppure appartengono a me e sono ancora in lista
|
||||
const belongsToMe = (pr._id || pr.path) === (draft._id || draft.path);
|
||||
return !belongsToMe || still.has(spKey);
|
||||
});
|
||||
if (JSON.stringify(before) !== JSON.stringify(pr.sottoMenu)) {
|
||||
await globalStore.savePage(pr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:modelValue', { ...draft });
|
||||
emit('apply', { ...draft });
|
||||
$q.notify({ type: 'positive', message: 'Pagina salvata' });
|
||||
|
||||
previousPath.value = draft.path || '';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$q.notify({ type: 'negative', message: 'Errore nel salvataggio' });
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadFromStore() {
|
||||
try {
|
||||
const absolute = ui.pathText || '/';
|
||||
const page = await globalStore.loadPage(absolute, '', true);
|
||||
if (page) {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, page);
|
||||
ui.pathText = toUiPath(draft.path);
|
||||
ui.isSubmenu = !!draft.submenu;
|
||||
ui.parentId = findParentIdForChild(draft.path);
|
||||
ui.childrenPaths = Array.isArray(draft.sottoMenu)
|
||||
? draft.sottoMenu.map((p) => withSlash(p))
|
||||
: [];
|
||||
upsertIntoStore(draft, mypage.value);
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft });
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
previousPath.value = draft.path || '';
|
||||
$q.notify({ type: 'info', message: 'Pagina ricaricata' });
|
||||
} else {
|
||||
$q.notify({ type: 'warning', message: 'Pagina non trovata' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$q.notify({ type: 'negative', message: 'Errore nel ricaricare la pagina' });
|
||||
}
|
||||
}
|
||||
|
||||
function resetDraft() {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, props.modelValue || {});
|
||||
ui.pathText = toUiPath(draft.path);
|
||||
ui.isSubmenu = !!draft.submenu;
|
||||
ui.parentId = findParentIdForChild(draft.path);
|
||||
ui.childrenPaths = Array.isArray(draft.sottoMenu)
|
||||
? draft.sottoMenu.map((p) => withSlash(p))
|
||||
: [];
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft });
|
||||
nextTick(() => {
|
||||
syncingFromProps.value = false;
|
||||
});
|
||||
previousPath.value = draft.path || '';
|
||||
}
|
||||
|
||||
// === LABEL UTILS ===
|
||||
// Funzioni per label
|
||||
function labelForPage(p: IMyPage) {
|
||||
return p.title || withSlash(p.path);
|
||||
}
|
||||
|
||||
function labelForPath(dispPath: string) {
|
||||
const p = pageByDisplayPath.value.get(dispPath.toLowerCase());
|
||||
const p = globalStore.mypage.find(
|
||||
(page) => withSlash(page.path).toLowerCase() === dispPath.toLowerCase()
|
||||
);
|
||||
return p ? labelForPage(p) : dispPath;
|
||||
}
|
||||
|
||||
function modifElem() {
|
||||
/* per compat compat con CMyFieldRec */
|
||||
}
|
||||
|
||||
const absolutePath = computed(() => toUiPath(draft.path));
|
||||
|
||||
return {
|
||||
draft,
|
||||
ui,
|
||||
saving,
|
||||
t,
|
||||
costanti,
|
||||
// helpers UI
|
||||
pathRule,
|
||||
normalizeAndApplyPath,
|
||||
validatePath,
|
||||
normalizePath,
|
||||
labelForPath,
|
||||
labelForPage,
|
||||
withSlash,
|
||||
// actions
|
||||
checkAndSave,
|
||||
save,
|
||||
reloadFromStore,
|
||||
resetDraft,
|
||||
modifElem,
|
||||
onToggleSubmenu,
|
||||
// options
|
||||
parentOptions,
|
||||
childCandidateOptions,
|
||||
// expose util
|
||||
absolutePath,
|
||||
iconModel,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
v-model="ui.pathText"
|
||||
label="Percorso (relativo, es: /about)"
|
||||
dense
|
||||
:rules="[pathRule]"
|
||||
@blur="normalizeAndApplyPath"
|
||||
:rules="[validatePath]"
|
||||
@blur="normalizePath"
|
||||
>
|
||||
<template #prepend><q-icon name="fas fa-link" /></template>
|
||||
</q-input>
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<!-- ICONA -->
|
||||
<div class="col-12 col-md-6">
|
||||
<icon-picker v-model="draft.icon" />
|
||||
<icon-picker v-model="iconModel"/>
|
||||
</div>
|
||||
|
||||
<!-- STATO & VISIBILITÀ -->
|
||||
@@ -81,12 +81,12 @@
|
||||
<!-- GESTIONE FIGLI (solo se questa pagina è TOP-LEVEL) -->
|
||||
<div
|
||||
class="col-12"
|
||||
v-if="!ui.isSubmenu"
|
||||
v-if="draft.inmenu && !ui.isSubmenu"
|
||||
>
|
||||
<q-separator spaced />
|
||||
<div class="text-subtitle2 q-mb-sm">SottoMenu</div>
|
||||
|
||||
<!-- Selettore multivalore dei figli (con label indentata nell'option slot) -->
|
||||
<!-- Selettore multivalore dei figli -->
|
||||
<q-select
|
||||
v-model="ui.childrenPaths"
|
||||
:options="childCandidateOptions"
|
||||
@@ -124,7 +124,7 @@
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- Preview gerarchica reale (indentata) -->
|
||||
<!-- Preview gerarchica -->
|
||||
<div class="q-mt-md">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Anteprima struttura</div>
|
||||
<q-list
|
||||
@@ -169,13 +169,7 @@
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Salva"
|
||||
:loading="saving"
|
||||
@click="checkAndSave(draft)"
|
||||
/>
|
||||
<q-btn
|
||||
outline
|
||||
label="Ricarica"
|
||||
@click="reloadFromStore"
|
||||
@click="save"
|
||||
/>
|
||||
<q-btn
|
||||
color="grey-7"
|
||||
@@ -187,7 +181,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./PageEditor.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './PageEditor.scss';
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
.menu-container {
|
||||
min-height: 100px;
|
||||
.menu-item {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
z-index: 100;
|
||||
|
||||
.q-item {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-handle {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
opacity: 0.6;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.depth-0 {
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.depth-1 {
|
||||
border-left: 3px solid $primary;
|
||||
margin-left: 15px;
|
||||
|
||||
.q-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
import type { IMyPage } from 'app/src/model';
|
||||
import PageEditor from '@src/components/PageEditor/PageEditor.vue';
|
||||
import MenuPageItem from '@src/components/MenuPageItem/MenuPageItem.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useGlobalStore } from 'app/src/store';
|
||||
import draggable from 'vuedraggable';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
|
||||
type Bucket = 'menu' | 'off';
|
||||
type PageWithKey = IMyPage & { __key?: string };
|
||||
type PageRow = PageWithKey & { __depth: number }; // 0 = voce di menu, 1 = sottomenu
|
||||
type MenuItem = PageWithKey & { depth: number };
|
||||
|
||||
const byOrder = (a: IMyPage, b: IMyPage) => (a.order ?? 0) - (b.order ?? 0);
|
||||
const norm = (p?: string) => (p || '').trim().replace(/^\//, '').toLowerCase();
|
||||
|
||||
let uidSeed = 1;
|
||||
@@ -27,93 +25,142 @@ export default defineComponent({
|
||||
props: {
|
||||
modelValue: { type: Array as () => IMyPage[], required: true },
|
||||
},
|
||||
emits: ['update:modelValue', 'save', 'change-order'],
|
||||
emits: ['update:modelValue', 'save'],
|
||||
setup(props, { emit }) {
|
||||
const $q = useQuasar();
|
||||
const $router = useRouter();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const $router = useRouter();
|
||||
|
||||
const visualizzaEditor = ref(false);
|
||||
const nuovaPagina = ref(false);
|
||||
const selectedKey = ref<string | null>(null);
|
||||
|
||||
// Configurazione ordinamento gerarchico
|
||||
const ORDER_GROUP_STEP = 1000; // Passo per i top-level (1000, 2000, 3000...)
|
||||
const ORDER_CHILD_STEP = 100; // Passo per i sottomenu (100, 200, 300...)
|
||||
// Stato principale
|
||||
const pages = ref<PageWithKey[]>([]);
|
||||
const menuItems = ref<MenuItem[]>([]);
|
||||
const offList = ref<PageWithKey[]>([]);
|
||||
const isApplying = ref(false);
|
||||
|
||||
function getPageByKey(key?: string) {
|
||||
return key ? pages.value.find((p) => p.__key === key) : undefined;
|
||||
// Inizializzazione
|
||||
function init() {
|
||||
pages.value = props.modelValue.map(p => ({ ...p }));
|
||||
ensureKeys(pages.value);
|
||||
rebuildMenuStructure();
|
||||
}
|
||||
|
||||
// ===== NUOVO SISTEMA DI ORDINAMENTO GERARCHICO =====
|
||||
// Ricostruisci la struttura del menu
|
||||
function rebuildMenuStructure() {
|
||||
if (isApplying.value) return;
|
||||
|
||||
/**
|
||||
* Calcola l'ordine gerarchico in base alla posizione nella struttura
|
||||
* Formato: [gruppo].[ordine] dove:
|
||||
* - gruppo: identifica il livello gerarchico (1000 per top-level, 1100 per sottomenu del primo top-level, ecc.)
|
||||
* - ordine: posizione all'interno del gruppo
|
||||
*/
|
||||
function calculateHierarchicalOrder(rows: PageRow[], index: number): number {
|
||||
const row = rows[index];
|
||||
// Pulisci le liste
|
||||
menuItems.value = [];
|
||||
offList.value = [];
|
||||
|
||||
// Se è un top-level (depth 0)
|
||||
if (row.__depth === 0) {
|
||||
// Conta quanti top-level ci sono prima di questo
|
||||
let topLevelCount = 0;
|
||||
for (let i = 0; i <= index; i++) {
|
||||
if (rows[i].__depth === 0) topLevelCount++;
|
||||
}
|
||||
// Ordina tutte le pagine per order
|
||||
const sortedPages = [...pages.value].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
// Assegna un ordine multiplo di 1000 (1000, 2000, 3000, ...)
|
||||
return topLevelCount * ORDER_GROUP_STEP;
|
||||
}
|
||||
// Se è un sottomenu (depth 1)
|
||||
else {
|
||||
// Trova il parent (l'ultimo top-level prima di questo elemento)
|
||||
let parentIndex = -1;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (rows[i].__depth === 0) {
|
||||
parentIndex = i;
|
||||
break;
|
||||
// Prima identifica i top-level (non sono sottomenu di nessuno)
|
||||
const topLevel = sortedPages.filter(p =>
|
||||
p.inmenu &&
|
||||
!p.submenu
|
||||
);
|
||||
|
||||
// Costruisci la struttura gerarchica
|
||||
for (const parent of topLevel) {
|
||||
// Aggiungi il parent come top-level
|
||||
menuItems.value.push({ ...parent, depth: 0 });
|
||||
|
||||
// Aggiungi i figli diretti
|
||||
if (Array.isArray(parent.sottoMenu)) {
|
||||
for (const childPath of parent.sottoMenu) {
|
||||
const child = sortedPages.find(p => norm(p.path) === norm(childPath));
|
||||
if (child && child.inmenu) {
|
||||
menuItems.value.push({ ...child, depth: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIndex === -1) {
|
||||
// Nessun parent trovato - fallback a top-level
|
||||
return calculateHierarchicalOrder(rows, index);
|
||||
}
|
||||
|
||||
// Calcola l'ordine del parent
|
||||
const parentOrder = calculateHierarchicalOrder(rows, parentIndex);
|
||||
|
||||
// Conta quanti sottomenu ci sono sotto lo stesso parent fino a questo punto
|
||||
let childCount = 0;
|
||||
for (let i = parentIndex + 1; i <= index; i++) {
|
||||
if (rows[i].__depth === 1) childCount++;
|
||||
}
|
||||
|
||||
// Assegna un ordine nel formato parentOrder + childCount * 100 (es. 1000 + 100 = 1100)
|
||||
return parentOrder + childCount * ORDER_CHILD_STEP;
|
||||
}
|
||||
|
||||
// Aggiungi pagine orfane (sottomenu ma non referenziate)
|
||||
const orphaned = sortedPages.filter(p =>
|
||||
p.inmenu &&
|
||||
p.submenu &&
|
||||
!menuItems.value.some(mi => mi._id === p._id)
|
||||
);
|
||||
|
||||
for (const orphan of orphaned) {
|
||||
menuItems.value.push({ ...orphan, depth: 0 });
|
||||
}
|
||||
|
||||
// Popola la lista "fuori menu"
|
||||
offList.value = sortedPages.filter(p => !p.inmenu);
|
||||
|
||||
// Aggiorna il menu globale
|
||||
globalStore.aggiornaMenu($router);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggiorna gli ordini di TUTTI gli elementi in base alla loro posizione nella struttura
|
||||
* Questo è il cuore del nuovo sistema di ordinamento
|
||||
*/
|
||||
function updateAllOrders(rows: PageRow[]): { id: string; order: number }[] {
|
||||
const deltas: { id: string; order: number }[] = [];
|
||||
// Aggiorna la struttura gerarchica dopo il drag
|
||||
function updateHierarchyFromDrag(newItems: MenuItem[]) {
|
||||
isApplying.value = true;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const page = getPageByKey(row.__key);
|
||||
// Pulisci tutte le relazioni esistenti
|
||||
for (const page of pages.value) {
|
||||
if (page.inmenu && !page.submenu) {
|
||||
page.sottoMenu = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Ricostruisci le relazioni
|
||||
let currentParent: PageWithKey | null = null;
|
||||
|
||||
for (const item of newItems) {
|
||||
const page = pages.value.find(p => p.__key === item.__key);
|
||||
if (!page) continue;
|
||||
|
||||
const newOrder = calculateHierarchicalOrder(rows, i);
|
||||
if (item.depth === 0) {
|
||||
// Top-level
|
||||
page.inmenu = true;
|
||||
page.submenu = false;
|
||||
currentParent = page;
|
||||
|
||||
if (page.order !== newOrder) {
|
||||
if (!Array.isArray(page.sottoMenu)) {
|
||||
page.sottoMenu = [];
|
||||
}
|
||||
} else {
|
||||
// Sottomenu
|
||||
page.inmenu = true;
|
||||
page.submenu = true;
|
||||
|
||||
if (currentParent) {
|
||||
if (!Array.isArray(currentParent.sottoMenu)) {
|
||||
currentParent.sottoMenu = [];
|
||||
}
|
||||
|
||||
const pathNorm = norm(page.path);
|
||||
if (!currentParent.sottoMenu.some(p => norm(p) === pathNorm)) {
|
||||
currentParent.sottoMenu.push(page.path || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isApplying.value = false;
|
||||
}
|
||||
|
||||
// Aggiorna gli ordini in modo semplice e sequenziale
|
||||
function updateOrders(items: MenuItem[]) {
|
||||
const deltas: { id: string; order: number }[] = [];
|
||||
|
||||
// Assegna ordini incrementali (10, 20, 30, ...)
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const page = pages.value.find(p => p.__key === item.__key);
|
||||
|
||||
if (page && page.order !== (i + 1) * 10) {
|
||||
const newOrder = (i + 1) * 10;
|
||||
page.order = newOrder;
|
||||
|
||||
if (page._id) {
|
||||
deltas.push({ id: page._id, order: newOrder });
|
||||
}
|
||||
@@ -123,437 +170,106 @@ export default defineComponent({
|
||||
return deltas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola la profondità (depth) di un elemento basandosi sulla posizione nel drag
|
||||
* @param newRows - l'intera lista di righe dopo il drag
|
||||
* @param targetIndex - l'indice di destinazione
|
||||
* @param sourceIndex - l'indice di origine
|
||||
* @returns la profondità calcolata (0 per top-level, 1 per sottomenu)
|
||||
*/
|
||||
function calculateDepth(
|
||||
newRows: PageRow[],
|
||||
targetIndex: number,
|
||||
sourceIndex: number
|
||||
): number {
|
||||
// Se è il primo elemento, non può essere un sottomenu
|
||||
if (targetIndex === 0) return 0;
|
||||
// Gestisci il drag nel menu
|
||||
function handleMenuDrag(evt: any) {
|
||||
if (!evt.moved) return;
|
||||
|
||||
// Se è stato spostato verso l'alto, controlla l'elemento prima di esso
|
||||
const prevRow = newRows[targetIndex - 1];
|
||||
const { newIndex, oldIndex } = evt.moved;
|
||||
const newItems = [...menuItems.value];
|
||||
|
||||
// Se l'elemento precedente è un top-level, potrebbe essere un sottomenu
|
||||
if (prevRow.__depth === 0) {
|
||||
// Calcola la distanza visiva tra l'elemento trascinato e il precedente
|
||||
const visualDistance = calculateVisualDistance(targetIndex, sourceIndex);
|
||||
|
||||
// Se la distanza è piccola (es. < 30px), consideralo un sottomenu
|
||||
if (visualDistance < 30) {
|
||||
return 1;
|
||||
}
|
||||
// Determina la nuova profondità
|
||||
if (newIndex > 0 && newItems[newIndex - 1].depth === 0) {
|
||||
newItems[newIndex].depth = 1;
|
||||
} else {
|
||||
newItems[newIndex].depth = 0;
|
||||
}
|
||||
|
||||
// Se è stato spostato tra due sottomenu dello stesso gruppo
|
||||
if (prevRow.__depth === 1 && targetIndex > 1) {
|
||||
const grandParentRow = newRows[targetIndex - 2];
|
||||
if (grandParentRow && grandParentRow.__depth === 0) {
|
||||
return 1;
|
||||
}
|
||||
// Aggiorna la struttura gerarchica
|
||||
updateHierarchyFromDrag(newItems);
|
||||
|
||||
// Aggiorna gli ordini
|
||||
const deltas = updateOrders(newItems);
|
||||
|
||||
// Aggiorna le viste
|
||||
rebuildMenuStructure();
|
||||
|
||||
// Notifica i cambiamenti
|
||||
emit('update:modelValue', pages.value);
|
||||
if (deltas.length) {
|
||||
emit('save', deltas);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola la distanza visiva tra l'elemento trascinato e l'elemento precedente
|
||||
* @param targetIndex - indice di destinazione
|
||||
* @param sourceIndex - indice di origine
|
||||
* @returns distanza in pixel
|
||||
*/
|
||||
function calculateVisualDistance(targetIndex: number, sourceIndex: number): number {
|
||||
try {
|
||||
const menuContainer = document.querySelector('.menu-container');
|
||||
if (!menuContainer) return Infinity;
|
||||
|
||||
const items = menuContainer.querySelectorAll('.menu-item');
|
||||
if (targetIndex >= items.length || targetIndex <= 0) return Infinity;
|
||||
|
||||
const prevItem = items[targetIndex - 1] as HTMLElement;
|
||||
const draggedItem = items[sourceIndex] as HTMLElement;
|
||||
|
||||
if (!prevItem || !draggedItem) return Infinity;
|
||||
|
||||
const prevRect = prevItem.getBoundingClientRect();
|
||||
const draggedRect = draggedItem.getBoundingClientRect();
|
||||
|
||||
// Calcola la distanza verticale tra il fondo del precedente e l'inizio del trascinato
|
||||
return draggedRect.top - prevRect.bottom;
|
||||
} catch (e) {
|
||||
console.error('Errore nel calcolo della distanza visiva:', e);
|
||||
return 30; // Valore di default che indica "non troppo vicino"
|
||||
// Salva le modifiche
|
||||
for (const page of pages.value) {
|
||||
if (page._id) {
|
||||
globalStore.savePage(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggiorna la struttura gerarchica e gli ordini
|
||||
*/
|
||||
function applyMenuRows(newRows: PageRow[]): { id: string; order: number }[] {
|
||||
// 1) svuota i sottoMenu dei parent (ricostruiremo i link)
|
||||
for (const p of pages.value) {
|
||||
if (p.inmenu && !p.submenu) p.sottoMenu = [];
|
||||
}
|
||||
// Gestisci il drag nella lista "fuori menu"
|
||||
function handleOffDrag(evt: any) {
|
||||
if (!evt.added) return;
|
||||
|
||||
// 2) mappa chiave->page
|
||||
const key2page = new Map<string, PageWithKey>();
|
||||
for (const p of pages.value) if (p.__key) key2page.set(p.__key, p);
|
||||
|
||||
// 3) ricostruisci SOLO la struttura (inmenu/submenu e sottoMenu dei parent)
|
||||
let currentParent: PageWithKey | null = null;
|
||||
for (const row of newRows) {
|
||||
const page = row.__key ? key2page.get(row.__key) : undefined;
|
||||
if (!page) continue;
|
||||
|
||||
if (row.__depth <= 0 || !currentParent) {
|
||||
// top-level
|
||||
page.inmenu = true;
|
||||
page.submenu = false;
|
||||
currentParent = page;
|
||||
if (!Array.isArray(page.sottoMenu)) page.sottoMenu = [];
|
||||
} else {
|
||||
// child
|
||||
page.inmenu = true;
|
||||
page.submenu = true;
|
||||
page.mainMenu = true;
|
||||
const pathStr = page.path || '';
|
||||
if (currentParent) {
|
||||
if (!Array.isArray(currentParent.sottoMenu)) currentParent.sottoMenu = [];
|
||||
const exists = currentParent.sottoMenu.some((p) => norm(p) === norm(pathStr));
|
||||
if (!exists) currentParent.sottoMenu.push(pathStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) aggiorna TUTTI gli ordini in base alla posizione nella struttura
|
||||
return updateAllOrders(newRows);
|
||||
}
|
||||
|
||||
function applyOffList(
|
||||
newOff: PageWithKey[],
|
||||
movedIndex?: number
|
||||
): { id: string; order: number }[] {
|
||||
// 1) togli riferimenti dai sottoMenu dei parent
|
||||
const offPaths = new Set(newOff.map((x) => norm(x.path)));
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter((p) => !offPaths.has(norm(p)));
|
||||
}
|
||||
}
|
||||
|
||||
// 2) marca inmenu/submenu=false per tutti gli "off" presenti
|
||||
const offKeys = new Set(newOff.map((x) => x.__key));
|
||||
for (const p of pages.value) {
|
||||
if (p.__key && offKeys.has(p.__key)) {
|
||||
p.inmenu = false;
|
||||
p.submenu = false;
|
||||
p.mainMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) assegna ordine "sparso" al solo elemento spostato
|
||||
const deltas: { id: string; order: number }[] = [];
|
||||
if (typeof movedIndex === 'number') {
|
||||
const prev = newOff[movedIndex - 1];
|
||||
const next = newOff[movedIndex + 1];
|
||||
const cur = newOff[movedIndex];
|
||||
if (cur?.__key) {
|
||||
const curP = getPageByKey(cur.__key);
|
||||
const prevO = prev ? getPageByKey(prev.__key!)?.order : undefined;
|
||||
const nextO = next ? getPageByKey(next.__key!)?.order : undefined;
|
||||
|
||||
const pushDelta = (p: PageWithKey, val: number) => {
|
||||
if (p.order !== val) {
|
||||
p.order = val;
|
||||
if (p._id) deltas.push({ id: p._id, order: val });
|
||||
}
|
||||
};
|
||||
|
||||
// Calcola un nuovo ordine per l'elemento spostato
|
||||
if (prevO !== undefined && nextO !== undefined) {
|
||||
pushDelta(curP!, prevO + Math.floor((nextO - prevO) / 2));
|
||||
} else if (prevO !== undefined) {
|
||||
pushDelta(curP!, prevO + 10);
|
||||
} else if (nextO !== undefined) {
|
||||
pushDelta(curP!, nextO - 10);
|
||||
} else {
|
||||
pushDelta(curP!, 10000); // Default per elementi in fondo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce il drag nel menu
|
||||
*/
|
||||
function onMenuDragChange(evt: any) {
|
||||
console.log('onMenuDragChange', evt);
|
||||
applyingRows.value = true;
|
||||
let deltas: { id: string; order: number }[] = [];
|
||||
|
||||
const removed = evt?.removed;
|
||||
const added = evt?.added;
|
||||
const moved = evt?.moved;
|
||||
|
||||
// Creiamo una copia della lista corrente
|
||||
const newRows = [...menuRows.value];
|
||||
|
||||
// Gestisci la rimozione (da menu a fuori menu)
|
||||
if (removed) {
|
||||
const { element, oldIndex } = removed;
|
||||
const page = getPageByKey(element.__key);
|
||||
if (page) {
|
||||
// Imposta come non nel menu
|
||||
page.inmenu = false;
|
||||
page.submenu = false;
|
||||
// Rimuovi dai sottoMenu di tutti i parent
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter(
|
||||
(p) => norm(p) !== norm(page.path)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestisci l'aggiunta (da off a menu)
|
||||
if (added) {
|
||||
const { element, newIndex } = added;
|
||||
|
||||
// Determina la profondità iniziale (0 = top-level)
|
||||
newRows[newIndex].__depth = 0;
|
||||
const { element, newIndex } = evt.added;
|
||||
const page = pages.value.find(p => p.__key === element.__key);
|
||||
|
||||
if (page) {
|
||||
// Imposta come nel menu
|
||||
const page = getPageByKey(element.__key);
|
||||
if (page) {
|
||||
page.inmenu = true;
|
||||
page.submenu = false; // inizialmente top-level
|
||||
page.inmenu = true;
|
||||
page.submenu = false;
|
||||
|
||||
// Aggiorna la struttura
|
||||
rebuildMenuStructure();
|
||||
|
||||
// Notifica i cambiamenti
|
||||
emit('update:modelValue', pages.value);
|
||||
|
||||
// Salva la pagina
|
||||
if (page._id) {
|
||||
globalStore.savePage(page);
|
||||
}
|
||||
}
|
||||
|
||||
// Gestisci lo spostamento interno
|
||||
if (moved) {
|
||||
const { newIndex, oldIndex } = moved;
|
||||
|
||||
// Calcola la nuova profondità basata sulla posizione
|
||||
const newDepth = calculateDepth(newRows, newIndex, oldIndex);
|
||||
newRows[newIndex].__depth = newDepth;
|
||||
}
|
||||
|
||||
// Aggiorna la struttura gerarchica e calcola i delta di ordine
|
||||
deltas = applyMenuRows(newRows);
|
||||
|
||||
// Aggiorna le liste
|
||||
menuRows.value = newRows;
|
||||
|
||||
applyingRows.value = false;
|
||||
rebuildAllViews();
|
||||
|
||||
// Comunica i cambiamenti
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
if (deltas.length) emit('change-order', deltas);
|
||||
try {
|
||||
globalStore.aggiornaMenu($router);
|
||||
} catch (e) {
|
||||
console.error("Errore nell'aggiornamento del menu:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce il drag nella lista "fuori menu"
|
||||
*/
|
||||
function onOffDragChange(evt: any) {
|
||||
console.log('onOffDragChange...', evt);
|
||||
applyingRows.value = true;
|
||||
// Gestisci il drag dal menu alla lista "fuori menu"
|
||||
function handleMenuToOffDrag(evt: any) {
|
||||
if (!evt.removed) return;
|
||||
|
||||
// Creiamo una copia della lista corrente
|
||||
const newOff = [...offList.value];
|
||||
let deltas: { id: string; order: number }[] = [];
|
||||
const { element } = evt.removed;
|
||||
const page = pages.value.find(p => p.__key === element.__key);
|
||||
|
||||
// Gestisci la rimozione (da off a menu)
|
||||
if (evt?.removed) {
|
||||
const { element, oldIndex } = evt.removed;
|
||||
const page = getPageByKey(element.__key);
|
||||
if (page) {
|
||||
// Imposta come nel menu
|
||||
page.inmenu = true;
|
||||
// Non impostiamo submenu qui, verrà gestito in onMenuDragChange
|
||||
if (page) {
|
||||
// Imposta come non nel menu
|
||||
page.inmenu = false;
|
||||
page.submenu = false;
|
||||
|
||||
// Aggiorna l'ordine per la nuova voce di menu
|
||||
page.order = 10000; // Sarà ricalcolato quando verrà spostato nel menu
|
||||
|
||||
// Salva immediatamente la pagina per riflettere il cambiamento
|
||||
if (page._id) {
|
||||
globalStore.savePage(page);
|
||||
// Rimuovi dai sottoMenu di tutti i parent
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter(p => norm(p) !== norm(page.path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestisci l'aggiunta (da menu a off)
|
||||
if (evt?.added) {
|
||||
const { element, newIndex } = evt.added;
|
||||
const page = getPageByKey(element.__key);
|
||||
if (page) {
|
||||
// Imposta come non nel menu
|
||||
page.inmenu = false;
|
||||
page.submenu = false;
|
||||
// Rimuovi dai sottoMenu di tutti i parent
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter(
|
||||
(p) => norm(p) !== norm(page.path)
|
||||
);
|
||||
}
|
||||
}
|
||||
// Aggiorna la struttura
|
||||
rebuildMenuStructure();
|
||||
|
||||
// Notifica i cambiamenti
|
||||
emit('update:modelValue', pages.value);
|
||||
|
||||
// Salva la pagina
|
||||
if (page._id) {
|
||||
globalStore.savePage(page);
|
||||
}
|
||||
}
|
||||
|
||||
// Gestiamo gli eventi di spostamento
|
||||
if (evt?.moved) {
|
||||
const { newIndex, oldIndex } = evt.moved;
|
||||
deltas = applyOffList(newOff, newIndex);
|
||||
}
|
||||
|
||||
// Aggiorna la lista
|
||||
offList.value = newOff;
|
||||
|
||||
applyingRows.value = false;
|
||||
rebuildAllViews();
|
||||
|
||||
// Comunica i cambiamenti
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
if (deltas.length) emit('change-order', deltas);
|
||||
try {
|
||||
globalStore.aggiornaMenu($router);
|
||||
} catch (e) {
|
||||
console.error("Errore nell'aggiornamento del menu:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- STATE BASE --------------------------------------------------------
|
||||
const pages = ref<PageWithKey[]>(
|
||||
props.modelValue ? props.modelValue.map((p) => ({ ...p })) : []
|
||||
);
|
||||
ensureKeys(pages.value);
|
||||
pages.value.sort(byOrder);
|
||||
|
||||
// Liste derivate per UI
|
||||
const menuRows = ref<PageRow[]>([]); // lista piatta (top + figli) con depth
|
||||
const offList = ref<PageWithKey[]>([]); // voci fuori menu (inmenu=false)
|
||||
const applyingRows = ref(false); // guard per evitare rientri
|
||||
|
||||
// ---- BUILDERS (no side-effects) ---------------------------------------
|
||||
function rebuildMenuRows() {
|
||||
const mapByPath = new Map<string, PageWithKey>();
|
||||
for (const p of pages.value) mapByPath.set(norm(p.path), p);
|
||||
|
||||
const tops = pages.value
|
||||
.filter((p) => p.inmenu && !p.submenu)
|
||||
.sort(byOrder) as PageWithKey[];
|
||||
|
||||
const rows: PageRow[] = [];
|
||||
const usedChildKeys = new Set<string>();
|
||||
|
||||
for (const parent of tops) {
|
||||
rows.push({ ...(parent as any), __depth: 0 });
|
||||
const arr = Array.isArray(parent.sottoMenu) ? parent.sottoMenu : [];
|
||||
for (const childPath of arr) {
|
||||
const child = mapByPath.get(norm(childPath));
|
||||
if (child && child.inmenu !== false && child.submenu === true) {
|
||||
rows.push({ ...(child as any), __depth: 1 });
|
||||
if (child.__key) usedChildKeys.add(child.__key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orfani: sottomenu==true ma non referenziati da alcun parent
|
||||
const orphans = (pages.value as PageWithKey[])
|
||||
.filter((p) => p.inmenu && p.submenu && p.__key && !usedChildKeys.has(p.__key))
|
||||
.sort(byOrder);
|
||||
for (const ch of orphans) {
|
||||
rows.push({ ...(ch as any), __depth: 0 }); // fallback: top-level
|
||||
}
|
||||
|
||||
// Ordina le righe in base all'ordine gerarchico
|
||||
rows.sort((a, b) => {
|
||||
// Prima per ordine
|
||||
const orderDiff = (a.order ?? 0) - (b.order ?? 0);
|
||||
if (orderDiff !== 0) return orderDiff;
|
||||
|
||||
// Poi per profondità (top-level prima dei sottomenu)
|
||||
return a.__depth - b.__depth;
|
||||
});
|
||||
|
||||
menuRows.value = rows;
|
||||
}
|
||||
|
||||
function rebuildOffList() {
|
||||
offList.value = pages.value.filter((p) => !p.inmenu).sort(byOrder);
|
||||
}
|
||||
|
||||
function rebuildAllViews() {
|
||||
rebuildMenuRows();
|
||||
rebuildOffList();
|
||||
globalStore.aggiornaMenu($router);
|
||||
}
|
||||
|
||||
// ---- SELEZIONE / UTILS -------------------------------------------------
|
||||
const currentIdx = computed(() =>
|
||||
pages.value.findIndex((p) => p.__key === selectedKey.value)
|
||||
);
|
||||
|
||||
function select(key?: string) {
|
||||
selectedKey.value = key || null;
|
||||
}
|
||||
|
||||
function displayPath(path?: string) {
|
||||
if (!path) return '-';
|
||||
return path.startsWith('/') ? path : '/' + path;
|
||||
}
|
||||
|
||||
// ---- AZIONI UI ---------------------------------------------------------
|
||||
|
||||
function addPage(bucket: Bucket) {
|
||||
// Azioni UI
|
||||
function addPage(bucket: 'menu' | 'off') {
|
||||
visualizzaEditor.value = true;
|
||||
nuovaPagina.value = true;
|
||||
|
||||
// Calcola l'ordine in base al bucket
|
||||
let order = 0;
|
||||
if (bucket === 'menu') {
|
||||
// Per il menu, usa il sistema gerarchico
|
||||
const newRows = [
|
||||
...menuRows.value,
|
||||
{
|
||||
__key: `tmp_${uidSeed}`,
|
||||
__depth: 0,
|
||||
inmenu: true,
|
||||
submenu: false,
|
||||
path: '/nuova-pagina',
|
||||
title: '',
|
||||
order: 0,
|
||||
} as any,
|
||||
];
|
||||
order = calculateHierarchicalOrder(newRows, newRows.length - 1);
|
||||
} else {
|
||||
// Per "off", usa l'ordine massimo + 10
|
||||
order = Math.max(...pages.value.map((p) => p.order || 0)) + 10;
|
||||
}
|
||||
const baseOrder = (pages.value.length + 1) * 10;
|
||||
|
||||
const np: PageWithKey = {
|
||||
title: '',
|
||||
@@ -564,124 +280,93 @@ export default defineComponent({
|
||||
inmenu: bucket === 'menu',
|
||||
submenu: false,
|
||||
onlyif_logged: false,
|
||||
order: order,
|
||||
order: baseOrder,
|
||||
__key: `tmp_${uidSeed++}`,
|
||||
};
|
||||
|
||||
pages.value.push(np);
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
rebuildAllViews();
|
||||
rebuildMenuStructure();
|
||||
selectedKey.value = np.__key!;
|
||||
}
|
||||
|
||||
function removeAt(bucket: Bucket, idx: number) {
|
||||
const target = bucket === 'menu' ? menuRows.value[idx] : offList.value[idx];
|
||||
if (!target) return;
|
||||
function removePage(key: string) {
|
||||
const page = pages.value.find(p => p.__key === key);
|
||||
if (!page) return;
|
||||
|
||||
$q.dialog({
|
||||
title: 'Conferma cancellazione',
|
||||
message: `Sei sicuro di voler cancellare la pagina "${target.title || target.path}"?`,
|
||||
message: `Sei sicuro di voler cancellare la pagina "${page.title || page.path}"?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
// rimuovi il record da pages
|
||||
const key = target.__key;
|
||||
const pathN = norm(target.path);
|
||||
const i = pages.value.findIndex((p) => p.__key === key);
|
||||
if (i >= 0) pages.value.splice(i, 1);
|
||||
// Rimuovi la pagina
|
||||
const idx = pages.value.findIndex(p => p.__key === key);
|
||||
if (idx >= 0) pages.value.splice(idx, 1);
|
||||
|
||||
// pulisci eventuali riferimenti nei sottoMenu dei parent
|
||||
// Pulisci riferimenti nei sottoMenu
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter((p) => norm(p) !== pathN);
|
||||
parent.sottoMenu = parent.sottoMenu.filter(p => norm(p) !== norm(page.path));
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
rebuildAllViews();
|
||||
if (selectedKey.value === key) selectedKey.value = null;
|
||||
rebuildMenuStructure();
|
||||
emit('update:modelValue', pages.value);
|
||||
selectedKey.value = null;
|
||||
|
||||
// opzionale: elimina anche lato server
|
||||
try {
|
||||
await globalStore.deletePage($q, target._id || '');
|
||||
} catch {}
|
||||
// Elimina dal server se esiste già
|
||||
if (page._id) {
|
||||
try {
|
||||
await globalStore.deletePage($q, page._id);
|
||||
} catch (err) {
|
||||
console.error('Errore eliminazione pagina:', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editAt(idx: number) {
|
||||
const key = (menuRows.value[idx] || offList.value[idx])?.__key;
|
||||
selectedKey.value = key || selectedKey.value;
|
||||
|
||||
function editPage(key: string) {
|
||||
selectedKey.value = key;
|
||||
visualizzaEditor.value = true;
|
||||
nuovaPagina.value = false;
|
||||
}
|
||||
|
||||
function openKey(key?: string) {
|
||||
const p = pages.value.find((x) => x.__key === key);
|
||||
if (!p) return;
|
||||
$router.push(`/${p.path}?edit=1`);
|
||||
function openPage(key: string) {
|
||||
const page = pages.value.find(p => p.__key === key);
|
||||
if (page) {
|
||||
window.open(`/${page.path}?edit=1`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
function onApply() {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
const cur = currentIdx.value >= 0 ? pages.value[currentIdx.value] : undefined;
|
||||
emit('save', cur);
|
||||
rebuildAllViews();
|
||||
visualizzaEditor.value = false;
|
||||
nuovaPagina.value = false;
|
||||
}
|
||||
// Watchers
|
||||
watch(() => props.modelValue, init, { deep: true, immediate: true });
|
||||
watch(pages, rebuildMenuStructure, { deep: true });
|
||||
|
||||
// ---- WATCHERS ----------------------------------------------------------
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
pages.value = (v || []).map((p) => ({ ...p }));
|
||||
ensureKeys(pages.value);
|
||||
pages.value.sort(byOrder);
|
||||
rebuildAllViews();
|
||||
if (!pages.value.find((p) => p.__key === selectedKey.value))
|
||||
selectedKey.value = null;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// ricostruisci la vista quando pages cambia (evita durante apply)
|
||||
watch(
|
||||
() => pages.value,
|
||||
() => {
|
||||
if (applyingRows.value) return;
|
||||
rebuildAllViews();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// ---- EXPOSE ------------------------------------------------------------
|
||||
// Esposizione
|
||||
return {
|
||||
pages,
|
||||
menuRows,
|
||||
menuItems,
|
||||
offList,
|
||||
selectedKey,
|
||||
currentIdx,
|
||||
// actions
|
||||
select,
|
||||
addPage,
|
||||
removeAt,
|
||||
editAt,
|
||||
onMenuDragChange,
|
||||
onOffDragChange,
|
||||
onApply,
|
||||
displayPath,
|
||||
openKey,
|
||||
visualizzaEditor,
|
||||
nuovaPagina,
|
||||
// Azioni
|
||||
addPage,
|
||||
removePage,
|
||||
editPage,
|
||||
openPage,
|
||||
// Gestori drag
|
||||
handleMenuDrag,
|
||||
handleOffDrag,
|
||||
handleMenuToOffDrag,
|
||||
// Metodi per PageEditor
|
||||
getCurrentPage: () => pages.value.find(p => p.__key === selectedKey.value),
|
||||
saveCurrentPage: () => {
|
||||
const page = pages.value.find(p => p.__key === selectedKey.value);
|
||||
if (page && page._id) {
|
||||
globalStore.savePage(page);
|
||||
rebuildMenuStructure();
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,73 +1,48 @@
|
||||
<template>
|
||||
<div class="row q-col-gutter-md">
|
||||
<!-- COLONNA: Nel menu -->
|
||||
<!-- COLONNA: Menu -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
class="menu-container"
|
||||
>
|
||||
<q-card flat bordered>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Menu</q-toolbar-title>
|
||||
<q-badge
|
||||
color="primary"
|
||||
:label="menuRows.length"
|
||||
/>
|
||||
<q-badge color="primary" :label="menuItems.length" />
|
||||
<q-space />
|
||||
<q-btn
|
||||
dense
|
||||
icon="fas fa-plus"
|
||||
label="Nuova Pagina"
|
||||
@click="addPage('menu')"
|
||||
/>
|
||||
<q-btn dense icon="fas fa-plus" label="Nuova Pagina" @click="addPage('menu')" />
|
||||
</q-toolbar>
|
||||
|
||||
<draggable
|
||||
v-model="menuRows"
|
||||
v-model="menuItems"
|
||||
item-key="__key"
|
||||
group="pages"
|
||||
handle=".drag-handle"
|
||||
:animation="180"
|
||||
ghost-class="bg-grey-2"
|
||||
@change="onMenuDragChange($event)"
|
||||
@change="handleMenuDrag"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<MenuPageItem
|
||||
:item="element"
|
||||
:selected="selectedKey === element.__key"
|
||||
v-model:active="element.active"
|
||||
:depth="element.__depth"
|
||||
:depth="element.depth"
|
||||
variant="menu"
|
||||
class="menu-item"
|
||||
@select="select(element.__key)"
|
||||
@edit="editAt(index)"
|
||||
@delete="removeAt('menu', index)"
|
||||
@open="openKey(element.__key)"
|
||||
@select="selectedKey = element.__key"
|
||||
@edit="editPage(element.__key)"
|
||||
@delete="removePage(element.__key)"
|
||||
@open="openPage(element.__key)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- COLONNA: Fuori menu -->
|
||||
<!-- COLONNA: Pagine -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-card flat bordered>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Pagine</q-toolbar-title>
|
||||
<q-badge
|
||||
color="grey-7"
|
||||
:label="offList.length"
|
||||
/>
|
||||
<q-badge color="grey-7" :label="offList.length" />
|
||||
<q-space />
|
||||
<q-btn
|
||||
dense
|
||||
icon="fas fa-plus"
|
||||
label="Aggiungi"
|
||||
@click="addPage('off')"
|
||||
/>
|
||||
<q-btn dense icon="fas fa-plus" label="Aggiungi" @click="addPage('off')" />
|
||||
</q-toolbar>
|
||||
|
||||
<draggable
|
||||
@@ -77,18 +52,17 @@
|
||||
handle=".drag-handle"
|
||||
:animation="180"
|
||||
ghost-class="bg-grey-2"
|
||||
@change="onOffDragChange($event)"
|
||||
@change="handleOffDrag"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<MenuPageItem
|
||||
:item="element"
|
||||
:selected="selectedKey === element.__key"
|
||||
v-model:active="element.active"
|
||||
variant="off"
|
||||
:depth="0"
|
||||
@select="select(element.__key)"
|
||||
@delete="removeAt('off', index)"
|
||||
@open="openKey(element.__key)"
|
||||
@select="selectedKey = element.__key"
|
||||
@delete="removePage(element.__key)"
|
||||
@open="openPage(element.__key)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
@@ -96,15 +70,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<q-dialog
|
||||
v-model="visualizzaEditor"
|
||||
>
|
||||
<q-dialog v-model="visualizzaEditor">
|
||||
<page-editor
|
||||
v-if="currentIdx !== -1"
|
||||
v-model="pages[currentIdx]"
|
||||
@apply="onApply"
|
||||
v-if="selectedKey"
|
||||
:model-value="getCurrentPage()"
|
||||
@update:model-value="saveCurrentPage"
|
||||
@apply="saveCurrentPage"
|
||||
@hide="visualizzaEditor = false"
|
||||
:nuovaPagina="nuovaPagina"
|
||||
:nuova-pagina="nuovaPagina"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
@@ -113,8 +86,4 @@
|
||||
<script lang="ts" src="./PagesConfigurator.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './PagesConfigurator.scss';
|
||||
|
||||
.menu-container {
|
||||
min-height: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11191,18 +11191,16 @@ export const tools = {
|
||||
getmenuByPath(path: string) {
|
||||
const myroutes = static_data.routes;
|
||||
|
||||
const normalized = this.norm(path);
|
||||
const norm = path ? (path.startsWith('/') ? path : `/${path}`).trim().toLowerCase() : undefined;
|
||||
|
||||
const mymenus = myroutes.find((menu: any) => menu.path === normalized);
|
||||
const mymenus = myroutes.find((menu: any) => menu.path === norm);
|
||||
|
||||
// console.log('mymenus', mymenus, 'path', path, 'norm', norm);
|
||||
|
||||
return mymenus;
|
||||
},
|
||||
|
||||
norm(path: string): string {
|
||||
return path
|
||||
.trim()
|
||||
.replace(/^\/+|\/+$/g, '')
|
||||
.toLowerCase();
|
||||
norm(path?: string): string | undefined {
|
||||
return typeof path === 'string' ? path.trim().replace(/^\/+|\/+$/g, '').toLowerCase() : undefined;
|
||||
},
|
||||
|
||||
// FINE !
|
||||
|
||||
@@ -1476,6 +1476,12 @@ export const useGlobalStore = defineStore('GlobalStore', {
|
||||
return Api.SendReq('/savepage', 'POST', { page })
|
||||
.then((res) => {
|
||||
if (res && res.data && res.data.mypage) {
|
||||
const index = this.mypage.findIndex((rec) => rec.path === res.data.mypage.path);
|
||||
if (index >= 0) {
|
||||
this.mypage[index] = res.data.mypage;
|
||||
} else {
|
||||
this.mypage.push(res.data.mypage);
|
||||
}
|
||||
return res.data.mypage;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user