- gestione dell'editor delle pagine (non funzionante!)
This commit is contained in:
@@ -1,225 +1,506 @@
|
||||
import {
|
||||
defineComponent, ref, computed, watch, reactive, toRaw, nextTick
|
||||
} from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import IconPicker from '../IconPicker/IconPicker.vue'
|
||||
import { IMyPage } from 'app/src/model'
|
||||
import { useGlobalStore } from 'app/src/store'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { costanti } from '@costanti'
|
||||
import { defineComponent, ref, computed, watch, reactive, toRaw, nextTick } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import IconPicker from '../IconPicker/IconPicker.vue';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
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';
|
||||
|
||||
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}`;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageEditor',
|
||||
components: { IconPicker, CMyFieldRec },
|
||||
props: {
|
||||
modelValue: { type: Object as () => IMyPage, required: true },
|
||||
nuovaPagina: { type: Boolean, required: true } // <-- modalità "bozza"
|
||||
nuovaPagina: { type: Boolean, required: true },
|
||||
},
|
||||
emits: ['update:modelValue', 'apply', 'hide'],
|
||||
setup (props, { emit }) {
|
||||
const $q = useQuasar()
|
||||
setup(props, { emit }) {
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const globalStore = useGlobalStore();
|
||||
const { mypage } = storeToRefs(globalStore);
|
||||
|
||||
const { t } = useI18n()
|
||||
const globalStore = useGlobalStore()
|
||||
const { mypage } = storeToRefs(globalStore)
|
||||
const draft = reactive<IMyPage>({ ...props.modelValue });
|
||||
|
||||
// Draft locale indipendente dal parent (specie in nuovaPagina)
|
||||
const draft = reactive<IMyPage>({ ...props.modelValue })
|
||||
const ui = reactive({
|
||||
pathText: toUiPath(draft.path),
|
||||
isSubmenu: !!draft.submenu,
|
||||
parentId: null as string | null,
|
||||
childrenPaths: [] as string[],
|
||||
});
|
||||
|
||||
// UI helper: path mostrato con "/" iniziale
|
||||
const ui = reactive({ pathText: toUiPath(draft.path) })
|
||||
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 saving = ref(false)
|
||||
const syncingFromProps = ref(false) // anti-loop
|
||||
// Draft indipendente
|
||||
const saving = ref(false);
|
||||
const syncingFromProps = ref(false);
|
||||
const previousPath = ref<string>(draft.path || '');
|
||||
|
||||
// --- Sync IN: quando cambia il valore del parent, aggiorna solo il draft
|
||||
// ===== INIT =====
|
||||
// parent corrente (se questa pagina è sottomenu)
|
||||
ui.parentId = findParentIdForChild(draft.path);
|
||||
// inizializza lista figli (per TOP-LEVEL) con i path presenti nel draft
|
||||
ui.childrenPaths = Array.isArray(draft.sottoMenu)
|
||||
? draft.sottoMenu.map((p) => withSlash(p))
|
||||
: [];
|
||||
|
||||
// --- Sync IN: quando cambia il modelValue (esterno)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async v => {
|
||||
syncingFromProps.value = true
|
||||
Object.assign(draft, v || {})
|
||||
ui.pathText = toUiPath(draft.path)
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
async (v) => {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, v || {});
|
||||
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))
|
||||
: [];
|
||||
previousPath.value = draft.path || '';
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
},
|
||||
{ deep: false }
|
||||
)
|
||||
);
|
||||
|
||||
// --- Modifiche live: SE NON è nuovaPagina, aggiorna store e v-model del parent
|
||||
// --- Propagazione live (solo se NON nuovaPagina)
|
||||
watch(
|
||||
draft,
|
||||
(val) => {
|
||||
if (syncingFromProps.value) return
|
||||
if (props.nuovaPagina) return // <-- blocca ogni propagazione durante "nuova pagina"
|
||||
upsertIntoStore(val, mypage.value)
|
||||
emit('update:modelValue', { ...val })
|
||||
if (syncingFromProps.value) return;
|
||||
if (props.nuovaPagina) return;
|
||||
upsertIntoStore(val, mypage.value);
|
||||
emit('update:modelValue', { ...val });
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
);
|
||||
|
||||
// --- Helpers path
|
||||
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 () {
|
||||
// normalizza: niente spazi → trattini
|
||||
let p = (ui.pathText || '/').trim()
|
||||
p = p.replace(/\s+/g, '-')
|
||||
if (!p.startsWith('/')) p = '/' + p
|
||||
ui.pathText = p
|
||||
draft.path = toStorePath(p) // NB: scrive sul draft (watch sopra gestisce la propagazione)
|
||||
function onToggleSubmenu(val: boolean) {
|
||||
draft.submenu = !!val;
|
||||
if (val) {
|
||||
draft.inmenu = true;
|
||||
ui.childrenPaths = []; // sicurezza
|
||||
}
|
||||
}
|
||||
|
||||
function pathRule (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
|
||||
// ======= 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)
|
||||
const childCandidateOptions = computed(() => {
|
||||
const selfPathDisp = 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) => {
|
||||
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));
|
||||
});
|
||||
|
||||
function toUiPath(storePath?: string) {
|
||||
const p = (storePath || '').trim();
|
||||
if (!p) return '/';
|
||||
return p.startsWith('/') ? p : `/${p}`;
|
||||
}
|
||||
|
||||
// --- Upsert nello store.mypage (usato solo quando NON è nuovaPagina o dopo il save)
|
||||
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) })
|
||||
function toStorePath(uiPath?: string) {
|
||||
const p = (uiPath || '').trim();
|
||||
if (!p) return '';
|
||||
return p.startsWith('/') ? p.slice(1) : p;
|
||||
}
|
||||
|
||||
// --- VALIDAZIONE + COMMIT per nuova pagina (o anche per edit espliciti)
|
||||
async function checkAndSave (payloadDraft?: IMyPage) {
|
||||
const cur = payloadDraft || draft
|
||||
function normalizeAndApplyPath() {
|
||||
let p = (ui.pathText || '/').trim();
|
||||
p = p.replace(/\s+/g, '-');
|
||||
if (!p.startsWith('/')) p = '/' + p;
|
||||
ui.pathText = p;
|
||||
draft.path = toStorePath(p);
|
||||
}
|
||||
|
||||
function pathRule(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) });
|
||||
}
|
||||
|
||||
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()) {
|
||||
$q.notify({ message: 'Inserisci il titolo della pagina', type: 'warning' })
|
||||
return
|
||||
$q.notify({ message: 'Inserisci il titolo della pagina', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const pathText = (ui.pathText || '').trim()
|
||||
const pathText = (ui.pathText || '').trim();
|
||||
if (!pathText) {
|
||||
$q.notify({ message: 'Inserisci il percorso della pagina', type: 'warning' })
|
||||
return
|
||||
$q.notify({ message: 'Inserisci il percorso della pagina', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const candidatePath = toStorePath(pathText).toLowerCase()
|
||||
|
||||
// unicità PATH (ignora se stesso quando editing)
|
||||
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
|
||||
$q.notify({
|
||||
message: "Esiste già un'altra pagina con questo percorso",
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// unicità TITOLO (ignora se stesso quando editing)
|
||||
const candidateTitle = (cur.title || '').toLowerCase()
|
||||
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
|
||||
$q.notify({ message: 'Il nome della pagina esiste già', type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
await save() // esegue commit vero
|
||||
emit('hide') // chiudi il dialog (se usi dialog)
|
||||
if (ui.isSubmenu && !ui.parentId) {
|
||||
$q.notify({
|
||||
message: 'Seleziona la pagina padre per questo sottomenu',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await save();
|
||||
emit('hide');
|
||||
}
|
||||
|
||||
// --- Salvataggio esplicito (commit). Qui propaghiamo SEMPRE al parent/store.
|
||||
async function save () {
|
||||
async function save() {
|
||||
try {
|
||||
saving.value = true
|
||||
normalizeAndApplyPath() // assicura path coerente
|
||||
saving.value = true;
|
||||
normalizeAndApplyPath();
|
||||
|
||||
const payload: IMyPage = { ...toRaw(draft), path: draft.path || '' }
|
||||
const saved = await globalStore.savePage(payload)
|
||||
// sync flag submenu
|
||||
draft.submenu = !!ui.isSubmenu;
|
||||
|
||||
// se top-level, sincronizza anche i figli dal selettore
|
||||
if (!ui.isSubmenu) {
|
||||
draft.sottoMenu = ui.childrenPaths.slice();
|
||||
}
|
||||
|
||||
// --- 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) // ora è lecito anche per nuovaPagina
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, saved);
|
||||
upsertIntoStore(draft, mypage.value);
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
}
|
||||
|
||||
// IMPORTANTISSIMO: in nuovaPagina non abbiamo mai emesso prima → emettiamo ora
|
||||
emit('update:modelValue', { ...draft })
|
||||
emit('apply', { ...draft })
|
||||
$q.notify({ type: 'positive', message: 'Pagina salvata' })
|
||||
// --- 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' })
|
||||
console.error(err);
|
||||
$q.notify({ type: 'negative', message: 'Errore nel salvataggio' });
|
||||
} finally {
|
||||
saving.value = false
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ricarica (per editing). In modalità nuovaPagina non propaghiamo al parent.
|
||||
async function reloadFromStore () {
|
||||
async function reloadFromStore() {
|
||||
try {
|
||||
const absolute = ui.pathText || '/'
|
||||
const page = await globalStore.loadPage(absolute, '', true)
|
||||
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)
|
||||
upsertIntoStore(draft, mypage.value)
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
$q.notify({ type: 'info', message: 'Pagina ricaricata' })
|
||||
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' })
|
||||
$q.notify({ type: 'warning', message: 'Pagina non trovata' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
$q.notify({ type: 'negative', message: 'Errore nel ricaricare la pagina' })
|
||||
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)
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
|
||||
nextTick(() => { syncingFromProps.value = false })
|
||||
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 ===
|
||||
function labelForPage(p: IMyPage) {
|
||||
return p.title || withSlash(p.path);
|
||||
}
|
||||
|
||||
function labelForPath(dispPath: string) {
|
||||
const p = pageByDisplayPath.value.get(dispPath.toLowerCase());
|
||||
return p ? labelForPage(p) : dispPath;
|
||||
}
|
||||
|
||||
function modifElem() {
|
||||
|
||||
/* per compat compat con CMyFieldRec */
|
||||
}
|
||||
|
||||
const absolutePath = computed(() => toUiPath(draft.path))
|
||||
const absolutePath = computed(() => toUiPath(draft.path));
|
||||
|
||||
return {
|
||||
draft,
|
||||
ui,
|
||||
saving,
|
||||
t,
|
||||
costanti,
|
||||
// helpers UI
|
||||
pathRule,
|
||||
normalizeAndApplyPath,
|
||||
labelForPath,
|
||||
labelForPage,
|
||||
withSlash,
|
||||
// actions
|
||||
checkAndSave,
|
||||
save,
|
||||
reloadFromStore,
|
||||
resetDraft,
|
||||
absolutePath,
|
||||
checkAndSave,
|
||||
t,
|
||||
costanti,
|
||||
modifElem,
|
||||
}
|
||||
}
|
||||
})
|
||||
onToggleSubmenu,
|
||||
// options
|
||||
parentOptions,
|
||||
childCandidateOptions,
|
||||
// expose util
|
||||
absolutePath,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
class="q-pa-md"
|
||||
>
|
||||
<div class="row q-col-gutter-md">
|
||||
<!-- PATH -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="ui.pathText"
|
||||
@@ -13,12 +14,11 @@
|
||||
:rules="[pathRule]"
|
||||
@blur="normalizeAndApplyPath"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="fas fa-link" />
|
||||
</template>
|
||||
<template #prepend><q-icon name="fas fa-link" /></template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<!-- TITOLO -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="draft.title"
|
||||
@@ -28,6 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ORDINE -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model.number="draft.order"
|
||||
@@ -41,25 +42,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
SottoMenu:
|
||||
<CMyFieldRec
|
||||
title="SottoMenu:"
|
||||
table="pages"
|
||||
:id="draft._id"
|
||||
:rec="draft"
|
||||
field="sottoMenu"
|
||||
@update:model-value="modifElem"
|
||||
:canEdit="true"
|
||||
:canModify="true"
|
||||
:nosaveToDb="true"
|
||||
:fieldtype="costanti.FieldType.multiselect"
|
||||
>
|
||||
</CMyFieldRec>
|
||||
|
||||
<!-- ICONA -->
|
||||
<div class="col-12 col-md-6">
|
||||
<icon-picker v-model="draft.icon" />
|
||||
</div>
|
||||
|
||||
<!-- STATO & VISIBILITÀ -->
|
||||
<div class="col-12">
|
||||
<q-separator spaced />
|
||||
<div class="row items-center q-col-gutter-md">
|
||||
@@ -89,8 +77,91 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GESTIONE FIGLI (solo se questa pagina è TOP-LEVEL) -->
|
||||
<div
|
||||
class="col-12"
|
||||
v-if="!ui.isSubmenu"
|
||||
>
|
||||
<q-separator spaced />
|
||||
<div class="text-subtitle2 q-mb-sm">SottoMenu</div>
|
||||
|
||||
<!-- Selettore multivalore dei figli (con label indentata nell'option slot) -->
|
||||
<q-select
|
||||
v-model="ui.childrenPaths"
|
||||
:options="childCandidateOptions"
|
||||
multiple
|
||||
use-chips
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
label="Pagine figlie"
|
||||
>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<div :style="{ paddingLeft: scope.opt.level * 14 + 'px' }">
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="scope.opt.hint"
|
||||
class="text-caption text-grey-7"
|
||||
>
|
||||
{{ scope.opt.hint }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
side
|
||||
v-if="scope.opt.disabled"
|
||||
>
|
||||
<q-icon
|
||||
name="lock"
|
||||
class="text-grey-6"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- Preview gerarchica reale (indentata) -->
|
||||
<div class="q-mt-md">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Anteprima struttura</div>
|
||||
<q-list
|
||||
bordered
|
||||
class="rounded-borders"
|
||||
>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<div class="text-weight-medium">
|
||||
{{ draft.title || withSlash(draft.path) }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<template v-if="ui.childrenPaths.length">
|
||||
<q-item
|
||||
v-for="(cp, i) in ui.childrenPaths"
|
||||
:key="i"
|
||||
class="q-pl-md"
|
||||
>
|
||||
<q-item-section>
|
||||
<div class="text-body2">— {{ labelForPath(cp) }}</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="q-pa-sm text-grey-7 text-caption"
|
||||
>
|
||||
Nessun sottomenu selezionato.
|
||||
</div>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AZIONI -->
|
||||
<q-card-actions
|
||||
align="center"
|
||||
class="q-pa-md q-gutter-md"
|
||||
@@ -111,7 +182,6 @@
|
||||
label="Chiudi"
|
||||
v-close-popup
|
||||
/>
|
||||
<!--<q-btn flat color="grey-7" label="Reset draft" @click="resetDraft" />-->
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user