|
|
|
@ -1,4 +1,6 @@ |
|
|
|
import Component from '../dom_components/model/Component'; |
|
|
|
import DataSource from '../data_sources/model/DataSource'; |
|
|
|
import DataRecord from '../data_sources/model/DataRecord'; |
|
|
|
import Components from '../dom_components/model/Components'; |
|
|
|
import { ComponentsEvents } from '../dom_components/types'; |
|
|
|
import CssRule from '../css_composer/model/CssRule'; |
|
|
|
@ -9,6 +11,8 @@ import EditorModel from '../editor/model/Editor'; |
|
|
|
import { EditorEvents } from '../editor/types'; |
|
|
|
import { createId } from '../utils/mixins'; |
|
|
|
import { enablePatches, produceWithPatches } from 'immer'; |
|
|
|
import Asset from '../asset_manager/model/Asset'; |
|
|
|
import Page from '../pages/model/Page'; |
|
|
|
import type { |
|
|
|
JsonPatch, |
|
|
|
PatchAdapter, |
|
|
|
@ -39,6 +43,7 @@ export default class PatchManager extends ItemManagerModule { |
|
|
|
private adapterListeners: { adapter: string; target: any; event: string; handler: (...args: any[]) => void }[] = []; |
|
|
|
private trackingBound = false; |
|
|
|
private fractionalGen?: (a: string | null, b: string | null) => string; |
|
|
|
private dataRecordAdapter?: PatchAdapter<DataRecord>; |
|
|
|
|
|
|
|
private internalSetOptions = { |
|
|
|
fromUndo: true, |
|
|
|
@ -72,6 +77,11 @@ export default class PatchManager extends ItemManagerModule { |
|
|
|
private registerDefaultAdapters() { |
|
|
|
this.registerAdapter(this.createComponentAdapter()); |
|
|
|
this.registerAdapter(this.createCssRuleAdapter()); |
|
|
|
this.dataRecordAdapter = this.createDataRecordAdapter(); |
|
|
|
this.registerAdapter(this.dataRecordAdapter); |
|
|
|
this.registerAdapter(this.createDataSourceAdapter()); |
|
|
|
this.registerAdapter(this.createAssetAdapter()); |
|
|
|
this.registerAdapter(this.createPageAdapter()); |
|
|
|
} |
|
|
|
|
|
|
|
registerAdapter<T>(adapter: PatchAdapter<T>) { |
|
|
|
@ -309,6 +319,95 @@ export default class PatchManager extends ItemManagerModule { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private createDataRecordAdapter(): PatchAdapter<DataRecord> { |
|
|
|
return { |
|
|
|
type: 'dataRecord', |
|
|
|
getId: (record) => this.buildDataRecordId(record), |
|
|
|
resolve: (em, id) => { |
|
|
|
const [dsId, recId] = id.split('::'); |
|
|
|
const ds = em.DataSources?.get(dsId); |
|
|
|
return ds?.records?.get(recId) || null; |
|
|
|
}, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private createDataSourceAdapter(): PatchAdapter<DataSource> { |
|
|
|
return { |
|
|
|
type: 'dataSource', |
|
|
|
sourceKeys: ['dataSource'], |
|
|
|
getId: (ds) => `${ds.id || ds.cid}`, |
|
|
|
resolve: (em, id) => em.DataSources?.get(id), |
|
|
|
events: [ |
|
|
|
{ |
|
|
|
event: 'add', |
|
|
|
target: () => this.getDataSources(), |
|
|
|
handler: ({ args }) => this.handleDataSourceAdd(args[0] as DataSource), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'remove', |
|
|
|
target: () => this.getDataSources(), |
|
|
|
handler: ({ args }) => this.handleDataSourceRemove(args[0] as DataSource), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'change', |
|
|
|
target: () => this.getDataSources(), |
|
|
|
handler: ({ args }) => this.handleGenericModelChange(args[0] as DataSource, 'dataSource'), |
|
|
|
}, |
|
|
|
], |
|
|
|
onReady: () => this.bindAllDataSourceRecords(), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private createAssetAdapter(): PatchAdapter<Asset> { |
|
|
|
return { |
|
|
|
type: 'asset', |
|
|
|
getId: (asset) => (asset.get ? asset.get('src') : (asset as any).src), |
|
|
|
resolve: (em, id) => em.Assets?.get(id), |
|
|
|
events: [ |
|
|
|
{ |
|
|
|
event: 'add', |
|
|
|
target: () => this.getAssets(), |
|
|
|
handler: ({ args }) => this.buildAddRemovePatch('asset', args[0] as Asset, 'add'), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'remove', |
|
|
|
target: () => this.getAssets(), |
|
|
|
handler: ({ args }) => this.buildAddRemovePatch('asset', args[0] as Asset, 'remove'), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'change', |
|
|
|
target: () => this.getAssets(), |
|
|
|
handler: ({ args }) => this.handleGenericModelChange(args[0] as Asset, 'asset'), |
|
|
|
}, |
|
|
|
], |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private createPageAdapter(): PatchAdapter<Page> { |
|
|
|
return { |
|
|
|
type: 'page', |
|
|
|
getId: (page) => `${(page as any).id || page.get('id') || page.cid}`, |
|
|
|
resolve: (em, id) => em.Pages?.get(id), |
|
|
|
events: [ |
|
|
|
{ |
|
|
|
event: 'add', |
|
|
|
target: () => this.getPages(), |
|
|
|
handler: ({ args }) => this.buildAddRemovePatch('page', args[0] as Page, 'add'), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'remove', |
|
|
|
target: () => this.getPages(), |
|
|
|
handler: ({ args }) => this.buildAddRemovePatch('page', args[0] as Page, 'remove'), |
|
|
|
}, |
|
|
|
{ |
|
|
|
event: 'change', |
|
|
|
target: () => this.getPages(), |
|
|
|
handler: ({ args }) => this.handleGenericModelChange(args[0] as Page, 'page'), |
|
|
|
}, |
|
|
|
], |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private cloneValue(value: any) { |
|
|
|
if (typeof value === 'undefined') return value; |
|
|
|
try { |
|
|
|
@ -343,6 +442,37 @@ export default class PatchManager extends ItemManagerModule { |
|
|
|
.filter(Boolean) as JsonPatch[]; |
|
|
|
} |
|
|
|
|
|
|
|
private buildAddRemovePatch(type: string, model: any, op: 'add' | 'remove') { |
|
|
|
if (!model) return; |
|
|
|
const adapter = this.adapters.get(type); |
|
|
|
if (!adapter) return; |
|
|
|
const id = adapter.getId(model); |
|
|
|
if (!id) return; |
|
|
|
const path = this.buildPath(type, `${id}`); |
|
|
|
const value = this.cloneValue(model.toJSON?.() || model); |
|
|
|
const patch: JsonPatch = op === 'add' ? { op: 'add', path, value } : { op: 'remove', path }; |
|
|
|
const inverse: JsonPatch = |
|
|
|
op === 'add' ? { op: 'remove', path } : { op: 'add', path, value: this.cloneValue(model.toJSON?.() || model) }; |
|
|
|
return { patches: [patch], inverse: [inverse] }; |
|
|
|
} |
|
|
|
|
|
|
|
private handleGenericModelChange(model: any, adapterType: string) { |
|
|
|
const adapter = this.adapters.get(adapterType); |
|
|
|
if (!adapter) return; |
|
|
|
const changed = typeof model.changedAttributes === 'function' ? model.changedAttributes() : null; |
|
|
|
if (!changed || !Object.keys(changed).length) return; |
|
|
|
const patches: JsonPatch[] = []; |
|
|
|
const inverse: JsonPatch[] = []; |
|
|
|
this.handleAdapterChange(adapter, { target: model, changed }, patches, inverse); |
|
|
|
return { patches, inverse }; |
|
|
|
} |
|
|
|
|
|
|
|
private buildDataRecordId(record: DataRecord) { |
|
|
|
const dsId = (record as any).dataSource?.id || (record as any).dataSource?.cid || 'ds'; |
|
|
|
const recId = record.id || (record as any).cid; |
|
|
|
return `${dsId}::${recId}`; |
|
|
|
} |
|
|
|
|
|
|
|
private buildPath(type: string, id: string, segments: (string | number)[] = []) { |
|
|
|
const data = [type, id, ...segments.map((seg) => `${seg}`)]; |
|
|
|
return `/${data.map(encodePointer).join('/')}`; |
|
|
|
@ -408,6 +538,67 @@ export default class PatchManager extends ItemManagerModule { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private getDataSources() { |
|
|
|
return this.em.DataSources?.all || this.em.DataSources?.getAll?.(); |
|
|
|
} |
|
|
|
|
|
|
|
private getAssets() { |
|
|
|
return this.em.Assets?.getAll?.(); |
|
|
|
} |
|
|
|
|
|
|
|
private getPages() { |
|
|
|
return this.em.Pages?.getAll?.(); |
|
|
|
} |
|
|
|
|
|
|
|
private bindAllDataSourceRecords() { |
|
|
|
const dss = this.getDataSources(); |
|
|
|
if (!dss) return; |
|
|
|
dss.each((ds: DataSource) => this.bindDataSourceRecords(ds)); |
|
|
|
if (dss.on) { |
|
|
|
dss.on('add', this.bindDataSourceRecords); |
|
|
|
this.adapterListeners.push({ adapter: 'dataRecord', target: dss, event: 'add', handler: this.bindDataSourceRecords }); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private bindDataSourceRecords = (ds: DataSource) => { |
|
|
|
if (!ds?.records) return; |
|
|
|
const recs = ds.records; |
|
|
|
const bind = (event: string, handler: (...args: any[]) => void) => { |
|
|
|
recs.on(event, handler); |
|
|
|
this.adapterListeners.push({ adapter: 'dataRecord', target: recs, event, handler }); |
|
|
|
}; |
|
|
|
bind('add', (record: DataRecord) => { |
|
|
|
const patch = this.buildAddRemovePatch('dataRecord', record, 'add'); |
|
|
|
patch && this.collect(patch.patches, patch.inverse || []); |
|
|
|
}); |
|
|
|
bind('remove', (record: DataRecord) => { |
|
|
|
const patch = this.buildAddRemovePatch('dataRecord', record, 'remove'); |
|
|
|
patch && this.collect(patch.patches, patch.inverse || []); |
|
|
|
}); |
|
|
|
bind('change', (record: DataRecord) => { |
|
|
|
const res = this.handleGenericModelChange(record, 'dataRecord'); |
|
|
|
res && this.collect(res.patches, res.inverse || []); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
private unbindDataSourceRecords(ds: DataSource) { |
|
|
|
if (!ds?.records || !ds.records.off) return; |
|
|
|
const recs = ds.records; |
|
|
|
const listeners = this.adapterListeners.filter((item) => item.target === recs); |
|
|
|
listeners.forEach(({ event, handler }) => recs.off(event, handler)); |
|
|
|
this.adapterListeners = this.adapterListeners.filter((item) => item.target !== recs); |
|
|
|
} |
|
|
|
|
|
|
|
private handleDataSourceAdd = (ds: DataSource) => { |
|
|
|
this.bindDataSourceRecords(ds); |
|
|
|
return this.buildAddRemovePatch('dataSource', ds, 'add'); |
|
|
|
}; |
|
|
|
|
|
|
|
private handleDataSourceRemove = (ds: DataSource) => { |
|
|
|
this.unbindDataSourceRecords(ds); |
|
|
|
return this.buildAddRemovePatch('dataSource', ds, 'remove'); |
|
|
|
}; |
|
|
|
|
|
|
|
// Lazy-load fractional-indexing to work in CJS/Jest environments without extra transpilation.
|
|
|
|
private getGenerateKeyBetween() { |
|
|
|
if (this.fractionalGen) return this.fractionalGen; |
|
|
|
|