- inizio di modifiche all'editor di Pagine Web
This commit is contained in:
@@ -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à un’altra 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,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user