- inizio di modifiche all'editor di Pagine Web

This commit is contained in:
Surya Paolo
2025-09-05 01:05:36 +02:00
parent 574f389200
commit 63d0f865fd
55 changed files with 5356 additions and 3600 deletions

View File

@@ -1,141 +1,167 @@
import {
defineComponent,
ref,
computed,
onMounted,
watch,
onBeforeUnmount,
toRaw,
nextTick,
} from 'vue';
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 { useUserStore } from '@store/UserStore';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { tools } from '@tools';
import { useRouter } from 'vue-router';
import { reactive } from 'vue';
import { IMyPage } from 'app/src/model';
import IconPicker from '../IconPicker/IconPicker.vue';
import { useGlobalStore } from 'app/src/store';
import { storeToRefs } from 'pinia';
import { CMyFieldRec } from '@src/components/CMyFieldRec'
export default defineComponent({
name: 'PageEditor',
components: { IconPicker },
components: { IconPicker, CMyFieldRec },
props: {
modelValue: {
type: Object as () => IMyPage,
required: true,
},
modelValue: { type: Object as () => IMyPage, required: true },
nuovaPagina: { type: Boolean, required: true } // <-- modalità "bozza"
},
emits: ['update:modelValue', 'apply'],
setup(props, { emit }) {
const $q = useQuasar();
const globalStore = useGlobalStore();
const { mypage } = storeToRefs(globalStore);
emits: ['update:modelValue', 'apply', 'hide'],
setup (props, { emit }) {
const $q = useQuasar()
// DRaft locale
const draft = reactive<IMyPage>({ ...props.modelValue });
const { t } = useI18n()
const globalStore = useGlobalStore()
const { mypage } = storeToRefs(globalStore)
// Draft locale indipendente dal parent (specie in nuovaPagina)
const draft = reactive<IMyPage>({ ...props.modelValue })
// UI helper: path mostrato con "/" iniziale
const ui = reactive({
pathText: toUiPath(draft.path),
});
const ui = reactive({ pathText: toUiPath(draft.path) })
const saving = ref(false)
const syncingFromProps = ref(false) // <-- FLAG anti-loop
const syncingFromProps = ref(false) // anti-loop
// --- Watch input esterno: ricarica draft e UI path
// --- Watch input esterno: ricarica draft e UI path (NO scritture nello store qui!)
// --- Sync IN: quando cambia il valore del parent, aggiorna solo il draft
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)
await nextTick()
syncingFromProps.value = false
},
{ deep: false }
);
)
// --- Ogni modifica del draft: aggiorna store.mypage e emetti update:modelValue (solo se modifica nasce da UI)
// --- Modifiche live: SE NON è nuovaPagina, aggiorna store e v-model del parent
watch(
draft,
(val) => {
if (syncingFromProps.value) return; // evita ricorsione
upsertIntoStore(val, mypage.value);
emit('update:modelValue', { ...val });
if (syncingFromProps.value) return
if (props.nuovaPagina) return // <-- blocca ogni propagazione durante "nuova pagina"
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 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 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 normalizeAndApplyPath() {
// normalizza: niente spazi, minuscole, trattini
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
}
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;
// --- 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) })
}
// --- Upsert nello store.mypage
function upsertIntoStore(page: IMyPage, arr: IMyPage[]) {
if (!page) return;
// chiave di matching: prima _id, altrimenti path
const keyId = page._id;
const keyPath = page.path || '';
let idx = -1;
if (keyId) {
idx = arr.findIndex((p) => p._id === keyId);
// --- VALIDAZIONE + COMMIT per nuova pagina (o anche per edit espliciti)
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
}
if (idx < 0 && keyPath) {
idx = arr.findIndex((p) => (p.path || '') === keyPath);
const pathText = (ui.pathText || '').trim()
if (!pathText) {
$q.notify({ message: 'Inserisci il percorso della pagina', type: 'warning' })
return
}
if (idx >= 0) {
// merge preservando reattività
arr[idx] = { ...arr[idx], ...toRaw(page) };
} else {
arr.push({ ...toRaw(page) });
const candidatePath = toStorePath(pathText).toLowerCase()
// unicità PATH (ignora se stesso quando editing)
const existPath = globalStore.mypage.find(
(r) => (r.path || '').toLowerCase() === candidatePath && r._id !== cur._id
)
if (existPath) {
$q.notify({ message: 'Esiste già unaltra pagina con questo percorso', type: 'warning' })
return
}
// unicità TITOLO (ignora se stesso quando editing)
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
}
await save() // esegue commit vero
emit('hide') // chiudi il dialog (se usi dialog)
}
async function save () {
// --- Salvataggio esplicito (commit). Qui propaghiamo SEMPRE al parent/store.
async function save () {
try {
saving.value = true
normalizeAndApplyPath() // assicura path coerente
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)
upsertIntoStore(draft, mypage.value) // ora è lecito anche per nuovaPagina
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' })
} catch (err: any) {
} catch (err) {
console.error(err)
$q.notify({ type: 'negative', message: 'Errore nel salvataggio' })
} finally {
@@ -143,7 +169,7 @@ async function save () {
}
}
// --- Ricarica da sorgente
// --- Ricarica (per editing). In modalità nuovaPagina non propaghiamo al parent.
async function reloadFromStore () {
try {
const absolute = ui.pathText || '/'
@@ -153,8 +179,7 @@ async function save () {
Object.assign(draft, page)
ui.pathText = toUiPath(draft.path)
upsertIntoStore(draft, mypage.value)
console.log('page', draft)
emit('update:modelValue', { ...draft })
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
await nextTick()
syncingFromProps.value = false
$q.notify({ type: 'info', message: 'Pagina ricaricata' })
@@ -167,16 +192,18 @@ async function save () {
}
}
function resetDraft() {
console.log('resetDraft')
function resetDraft () {
syncingFromProps.value = true
Object.assign(draft, props.modelValue || {})
ui.pathText = toUiPath(draft.path)
// aggiorna i componenti
emit('update:modelValue', { ...draft })
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
nextTick(() => { syncingFromProps.value = false })
}
function modifElem() {
}
const absolutePath = computed(() => toUiPath(draft.path))
return {
@@ -189,6 +216,10 @@ async function save () {
reloadFromStore,
resetDraft,
absolutePath,
};
},
});
checkAndSave,
t,
costanti,
modifElem,
}
}
})

View File

@@ -1,5 +1,9 @@
<template>
<q-card flat bordered class="q-pa-md">
<q-card
flat
bordered
class="q-pa-md"
>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-input
@@ -20,53 +24,99 @@
v-model="draft.title"
label="Titolo"
dense
:rules="[v => !!v || 'Titolo richiesto']"
:rules="[(v) => !!v || 'Titolo richiesto']"
/>
</div>
<div class="col-12 col-md-6">
<icon-picker v-model="draft.icon" />
<q-input
v-model.number="draft.order"
label="Ordine"
dense
type="number"
:rules="[
(v) => v !== null || 'Ordine richiesto',
(v) => v >= 0 || 'Ordine deve essere positivo',
]"
/>
</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>
<div class="col-12 col-md-6">
<q-input v-model="draft.iconsize" label="Dimensione icona (es: 24px)" dense />
<icon-picker v-model="draft.icon" />
</div>
<div class="col-12">
<q-separator spaced />
<div class="row items-center q-col-gutter-md">
<div class="col-auto">
<q-toggle v-model="draft.active" label="Attivo" />
<q-toggle
v-model="draft.active"
label="Attivo"
/>
</div>
<div class="col-auto">
<q-toggle v-model="draft.inmenu" label="Presente nel menu" />
<q-toggle
v-model="draft.inmenu"
label="Presente nel menu"
/>
</div>
<div class="col-auto">
<q-toggle v-model="draft.onlyif_logged" label="Solo se loggati" />
<q-toggle
v-model="draft.onlyif_logged"
:label="t('pages.onlyif_logged')"
/>
</div>
<div class="col-auto">
<q-toggle
v-model="draft.only_admin"
:label="t('pages.only_admin')"
/>
</div>
</div>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-auto">
<q-btn color="primary" label="Salva" :loading="saving" @click="save" />
</div>
<div class="col-auto">
<q-btn outline label="Ricarica" @click="reloadFromStore" />
</div>
<div class="col-auto">
<q-btn color="grey-7" label="Chiudi" @click="$emit('close', draft.path)" />
</div>
<div class="col-auto">
<q-btn flat color="grey-7" label="Reset draft" @click="resetDraft" />
</div>
</div>
<q-card-actions
align="center"
class="q-pa-md q-gutter-md"
>
<q-btn
color="primary"
label="Salva"
:loading="saving"
@click="checkAndSave(draft)"
/>
<q-btn
outline
label="Ricarica"
@click="reloadFromStore"
/>
<q-btn
color="grey-7"
label="Chiudi"
v-close-popup
/>
<!--<q-btn flat color="grey-7" label="Reset draft" @click="resetDraft" />-->
</q-card-actions>
</q-card>
</template>
<script lang="ts" src="./PageEditor.ts">
</script>
<script lang="ts" src="./PageEditor.ts"></script>
<style lang="scss" scoped>
@import './PageEditor.scss';