vue2-excel-grid 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/vue2-excel-grid.esm.js +951 -411
- package/dist/vue2-excel-grid.umd.js +3 -3
- package/package.json +1 -1
- package/src/components/EgLocalSyncModal.vue +99 -48
- package/src/components/ExcelGrid.vue +235 -151
- package/src/utils/dataHash.js +139 -0
- package/src/utils/i18n.js +32 -28
- package/src/utils/idbStore.js +122 -20
|
@@ -820,13 +820,16 @@
|
|
|
820
820
|
:visible="localSyncModal.visible"
|
|
821
821
|
:grid-options="_selfComponentDef"
|
|
822
822
|
:columns="_localSyncPreviewColumns"
|
|
823
|
-
:
|
|
823
|
+
:versions="localSyncModal.versions"
|
|
824
|
+
:selected-id="localSyncModal.selectedId"
|
|
825
|
+
:selected-version="localSyncModal.selectedVersion"
|
|
824
826
|
:locale="locale"
|
|
825
827
|
:theme="internalTheme"
|
|
826
828
|
@close="closeLocalSyncModal"
|
|
829
|
+
@select="_selectLocalSyncModalVersion"
|
|
827
830
|
@save="_onLocalSyncModalSave"
|
|
828
|
-
@restore="
|
|
829
|
-
@clear="
|
|
831
|
+
@restore="restoreLocalVersion"
|
|
832
|
+
@clear="_onLocalSyncModalDelete"
|
|
830
833
|
/>
|
|
831
834
|
|
|
832
835
|
<!-- Lock/unlock state feedback — brief centered icon flash -->
|
|
@@ -944,7 +947,8 @@ import EgLocalSyncModal from './EgLocalSyncModal.vue'
|
|
|
944
947
|
import { parseDateValue, formatDate } from '../utils/dateUtils.js'
|
|
945
948
|
import { formatNumber as _formatNumber } from '../utils/numberFormat.js'
|
|
946
949
|
import { Mask, MaskInput } from '../utils/mask.js'
|
|
947
|
-
import {
|
|
950
|
+
import { listVersions, getVersion, deleteVersion, saveOrTouchVersion } from '../utils/idbStore.js'
|
|
951
|
+
import { hashRowsAsync } from '../utils/dataHash.js'
|
|
948
952
|
|
|
949
953
|
const SIZE_CONFIG = {
|
|
950
954
|
small: { row: 22, header: 24, footer: 24, filter: 26 },
|
|
@@ -1409,34 +1413,48 @@ const ExcelGridComponent = {
|
|
|
1409
1413
|
sort: { type: Object, default: null },
|
|
1410
1414
|
|
|
1411
1415
|
/**
|
|
1412
|
-
* Unique key that opts the grid into local
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1416
|
+
* Unique key that opts the grid into local version history: when the page
|
|
1417
|
+
* is being left (tab hidden/closed, or this component unmounted — NOT on
|
|
1418
|
+
* every edit), the current `v-model` is content-hashed (via a Web Worker
|
|
1419
|
+
* when available, so the main thread stays responsive right as the page
|
|
1420
|
+
* may be unloading) and saved to the browser's IndexedDB as a **version**,
|
|
1421
|
+
* keyed by this string. If the hash matches an already-saved version for
|
|
1422
|
+
* this key, that version's timestamp is refreshed instead of creating a
|
|
1423
|
+
* duplicate — only genuinely different content produces a new version.
|
|
1424
|
+
* Also adds a toolbar button ('localSync' — see the `toolbar` prop's
|
|
1425
|
+
* `list`/`disabled`) that opens a modal listing saved versions, each with
|
|
1426
|
+
* a read-only preview and **Restore** / **Delete** actions, plus a
|
|
1427
|
+
* **Save** button for an explicit checkpoint.
|
|
1422
1428
|
*
|
|
1423
1429
|
* Two different grid instances must use different keys — the same key
|
|
1424
|
-
* means "the same
|
|
1425
|
-
* entirely to opt out completely: no IndexedDB access happens
|
|
1426
|
-
* and the toolbar button never renders.
|
|
1430
|
+
* means "the same version history" (concurrent tabs interleave). Omit
|
|
1431
|
+
* this prop entirely to opt out completely: no IndexedDB access happens
|
|
1432
|
+
* at all, and the toolbar button never renders.
|
|
1427
1433
|
*
|
|
1428
|
-
* See also the ref-callable methods
|
|
1429
|
-
* `
|
|
1430
|
-
* `
|
|
1431
|
-
* and the
|
|
1432
|
-
* `local-sync-save`/`local-sync-restore`/`local-sync-clear`/`local-sync-error`
|
|
1434
|
+
* See also `localSyncMaxVersions`, the ref-callable methods
|
|
1435
|
+
* `listLocalVersions()`, `getLocalVersion(id)`, `restoreLocalVersion(id)`,
|
|
1436
|
+
* `deleteLocalVersion(id)`, `saveLocalVersionNow()`,
|
|
1437
|
+
* `openLocalSyncModal()`/`closeLocalSyncModal()`, and the
|
|
1438
|
+
* `local-sync-save`/`local-sync-restore`/`local-sync-clear`/`local-sync-error`/`local-sync-available`
|
|
1433
1439
|
* events — [details](/local-sync).
|
|
1434
1440
|
* @type {String}
|
|
1435
1441
|
* @default null
|
|
1436
1442
|
* @example
|
|
1437
1443
|
* <excel-grid :columns="columns" v-model="data" local-sync-key="invoice-42" toolbar />
|
|
1438
1444
|
*/
|
|
1439
|
-
localSyncKey: { type: String, default: null }
|
|
1445
|
+
localSyncKey: { type: String, default: null },
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Maximum number of local versions kept per `localSyncKey`. Once a new
|
|
1449
|
+
* version pushes the count past this, the oldest version(s) (by
|
|
1450
|
+
* `createdAt`) are deleted automatically (FIFO). Has no effect without
|
|
1451
|
+
* `localSyncKey`.
|
|
1452
|
+
* @type {Number}
|
|
1453
|
+
* @default 10
|
|
1454
|
+
* @example
|
|
1455
|
+
* <excel-grid :columns="columns" v-model="data" local-sync-key="invoice-42" :local-sync-max-versions="20" />
|
|
1456
|
+
*/
|
|
1457
|
+
localSyncMaxVersions: { type: Number, default: 10 }
|
|
1440
1458
|
},
|
|
1441
1459
|
|
|
1442
1460
|
data() {
|
|
@@ -1506,12 +1524,14 @@ const ExcelGridComponent = {
|
|
|
1506
1524
|
importState: { active: false, percent: 0, message: '' },
|
|
1507
1525
|
exportState: { active: false },
|
|
1508
1526
|
|
|
1509
|
-
// localSyncKey feature — modal state
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1512
|
-
//
|
|
1513
|
-
|
|
1514
|
-
//
|
|
1527
|
+
// localSyncKey feature — modal state: the version list (metadata only)
|
|
1528
|
+
// plus which one is selected/previewed. `selectedVersion` holds that
|
|
1529
|
+
// version's full { id, data, createdAt, updatedAt } for the nested-grid
|
|
1530
|
+
// preview (see EgLocalSyncModal.vue).
|
|
1531
|
+
localSyncModal: { visible: false, versions: [], selectedId: null, selectedVersion: null },
|
|
1532
|
+
// True when one or more versions already existed at mount and the user
|
|
1533
|
+
// hasn't opened the modal yet — drives the toolbar button's "unseen
|
|
1534
|
+
// local versions" dot. See _loadLocalVersionsOnMount() for the full lifecycle.
|
|
1515
1535
|
localSyncAvailable: false,
|
|
1516
1536
|
|
|
1517
1537
|
resizing: {
|
|
@@ -2270,13 +2290,6 @@ const ExcelGridComponent = {
|
|
|
2270
2290
|
this.$nextTick(() => this.updateVirtualScroll())
|
|
2271
2291
|
},
|
|
2272
2292
|
|
|
2273
|
-
// Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:
|
|
2274
|
-
// true` prop doesn't flash the icon before the grid has even appeared.
|
|
2275
|
-
internalLocked(v) {
|
|
2276
|
-
if (!this._lockFlashArmed) return
|
|
2277
|
-
this._flashLock(v)
|
|
2278
|
-
},
|
|
2279
|
-
|
|
2280
2293
|
autoWidth(v) {
|
|
2281
2294
|
if (v) this._applyAutoWidth()
|
|
2282
2295
|
},
|
|
@@ -2347,9 +2360,21 @@ const ExcelGridComponent = {
|
|
|
2347
2360
|
immediate: true,
|
|
2348
2361
|
handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }
|
|
2349
2362
|
},
|
|
2363
|
+
// `immediate: true` guarantees this fires once, synchronously, right at
|
|
2364
|
+
// setup regardless of `v` — that's the one reliable signal that THIS
|
|
2365
|
+
// particular call is the initial sync, not a real host-driven change
|
|
2366
|
+
// (e.g. via `:locked.sync`). Every call after the first is a genuine
|
|
2367
|
+
// change, so only those flash. (Watching internalLocked itself for this
|
|
2368
|
+
// instead — the previous approach — can't tell "initial value happened
|
|
2369
|
+
// to be true" from "the user's first-ever real toggle": both look like
|
|
2370
|
+
// "the first time internalLocked actually changed value".)
|
|
2350
2371
|
locked: {
|
|
2351
2372
|
immediate: true,
|
|
2352
|
-
handler(v) {
|
|
2373
|
+
handler(v) {
|
|
2374
|
+
this.internalLocked = !!v
|
|
2375
|
+
if (this._lockPropSynced) this._flashLock(this.internalLocked)
|
|
2376
|
+
this._lockPropSynced = true
|
|
2377
|
+
}
|
|
2353
2378
|
},
|
|
2354
2379
|
data: {
|
|
2355
2380
|
immediate: true,
|
|
@@ -2419,6 +2444,12 @@ const ExcelGridComponent = {
|
|
|
2419
2444
|
this._dragScrollRAF = null
|
|
2420
2445
|
this._dragCleanupTimer = null
|
|
2421
2446
|
|
|
2447
|
+
// localSyncKey — the last `data` reference a version was actually saved
|
|
2448
|
+
// for. Initialized to the current (mount-time) value so an untouched grid
|
|
2449
|
+
// that's merely tab-switched away from doesn't spam a "nothing happened"
|
|
2450
|
+
// version on every hide — only a REAL edit (which replaces this array
|
|
2451
|
+
// reference, see _emitDataChange) makes the next hide event flush.
|
|
2452
|
+
this._lastFlushedDataRef = this.data
|
|
2422
2453
|
},
|
|
2423
2454
|
|
|
2424
2455
|
mounted() {
|
|
@@ -2450,8 +2481,17 @@ const ExcelGridComponent = {
|
|
|
2450
2481
|
// in both modes) fresh when the host's layout changes.
|
|
2451
2482
|
if (this.$el.parentElement) this._scrollBodyResizeObserver.observe(this.$el.parentElement)
|
|
2452
2483
|
}
|
|
2453
|
-
this.
|
|
2454
|
-
|
|
2484
|
+
this._loadLocalVersionsOnMount()
|
|
2485
|
+
// localSyncKey — save on page-hide, not per-edit (see _flushLocalVersionOnHide).
|
|
2486
|
+
// 'visibilitychange'->hidden is the primary trigger: it fires for tab
|
|
2487
|
+
// switches, minimizing, AND reliably before actual tab close/navigation on
|
|
2488
|
+
// both desktop and mobile, while the page is still very likely alive —
|
|
2489
|
+
// giving the async worker-hash + IndexedDB write a real chance to finish.
|
|
2490
|
+
// 'pagehide' is a backstop for the cases visibilitychange misses; it's
|
|
2491
|
+
// idempotent with visibilitychange (same content -> same hash -> no-op
|
|
2492
|
+
// beyond a timestamp touch) so firing both is harmless.
|
|
2493
|
+
document.addEventListener('visibilitychange', this._onVisibilityChangeForLocalSync)
|
|
2494
|
+
window.addEventListener('pagehide', this._onPageHideForLocalSync)
|
|
2455
2495
|
},
|
|
2456
2496
|
|
|
2457
2497
|
updated() {
|
|
@@ -2486,7 +2526,6 @@ const ExcelGridComponent = {
|
|
|
2486
2526
|
clearTimeout(this._onInputTimer)
|
|
2487
2527
|
clearTimeout(this._dragCleanupTimer)
|
|
2488
2528
|
clearTimeout(this._widthSyncChainResetTimer)
|
|
2489
|
-
clearTimeout(this._localSyncTimer)
|
|
2490
2529
|
if (this._dragScrollRAF) cancelAnimationFrame(this._dragScrollRAF)
|
|
2491
2530
|
document.removeEventListener('mousemove', this.onGlobalMouseMove)
|
|
2492
2531
|
document.removeEventListener('mouseup', this.onGlobalMouseUp)
|
|
@@ -2494,6 +2533,11 @@ const ExcelGridComponent = {
|
|
|
2494
2533
|
document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)
|
|
2495
2534
|
window.removeEventListener('resize', this.syncHeaderViewportWidth)
|
|
2496
2535
|
window.removeEventListener('scroll', this._onWindowScrollForDropdown, true)
|
|
2536
|
+
document.removeEventListener('visibilitychange', this._onVisibilityChangeForLocalSync)
|
|
2537
|
+
window.removeEventListener('pagehide', this._onPageHideForLocalSync)
|
|
2538
|
+
// Best-effort — the component may be torn down (SPA route change) without
|
|
2539
|
+
// the page itself unloading, which visibilitychange/pagehide won't catch.
|
|
2540
|
+
this._flushLocalVersionOnHide()
|
|
2497
2541
|
if (this.isFullscreen) {
|
|
2498
2542
|
document.body.style.overflow = this._prevBodyOverflow || ''
|
|
2499
2543
|
document.removeEventListener('keydown', this._onFullscreenKeyDown)
|
|
@@ -6011,7 +6055,6 @@ const ExcelGridComponent = {
|
|
|
6011
6055
|
// All edit paths already use spread ({ ...row }) so frozen rows are safe.
|
|
6012
6056
|
const data = result.data.map(row => Object.freeze(row))
|
|
6013
6057
|
this.$emit('input', data)
|
|
6014
|
-
this._scheduleLocalAutosave()
|
|
6015
6058
|
this.$emit('update:columns', columns)
|
|
6016
6059
|
|
|
6017
6060
|
const hasMerges = merges && merges.length > 0
|
|
@@ -6127,7 +6170,6 @@ const ExcelGridComponent = {
|
|
|
6127
6170
|
}
|
|
6128
6171
|
}
|
|
6129
6172
|
this.$emit('input', newData)
|
|
6130
|
-
this._scheduleLocalAutosave()
|
|
6131
6173
|
},
|
|
6132
6174
|
|
|
6133
6175
|
// Emit 'update:merges' and record a merges diff entry.
|
|
@@ -6161,7 +6203,6 @@ const ExcelGridComponent = {
|
|
|
6161
6203
|
newData.length = entry.before.rowCount
|
|
6162
6204
|
}
|
|
6163
6205
|
this.$emit('input', newData)
|
|
6164
|
-
this._scheduleLocalAutosave()
|
|
6165
6206
|
} else if (entry.type === 'merges') {
|
|
6166
6207
|
this.internalMerges = entry.before.slice()
|
|
6167
6208
|
this.$emit('update:merges', entry.before.slice())
|
|
@@ -6184,7 +6225,6 @@ const ExcelGridComponent = {
|
|
|
6184
6225
|
newData[entry.after.indices[i]] = entry.after.rows[i]
|
|
6185
6226
|
}
|
|
6186
6227
|
this.$emit('input', newData)
|
|
6187
|
-
this._scheduleLocalAutosave()
|
|
6188
6228
|
} else if (entry.type === 'merges') {
|
|
6189
6229
|
this.internalMerges = entry.after.slice()
|
|
6190
6230
|
this.$emit('update:merges', entry.after.slice())
|
|
@@ -6218,147 +6258,190 @@ const ExcelGridComponent = {
|
|
|
6218
6258
|
},
|
|
6219
6259
|
|
|
6220
6260
|
// =========================================================
|
|
6221
|
-
// LOCAL SYNC —
|
|
6261
|
+
// LOCAL SYNC — save `data` as a local IndexedDB *version*, keyed by
|
|
6262
|
+
// localSyncKey, only when the page is being left (not per-edit).
|
|
6222
6263
|
// =========================================================
|
|
6223
6264
|
|
|
6224
|
-
// Internal
|
|
6225
|
-
//
|
|
6226
|
-
//
|
|
6227
|
-
|
|
6228
|
-
// (_emitDataChange, undo, redo, import) — NOT from a $watch on `data`.
|
|
6229
|
-
// A watcher can't tell "the user edited a cell" from "the host replaced
|
|
6230
|
-
// `data` wholesale", and the second one is the common case right after
|
|
6231
|
-
// mount: a host that loads rows from an API assigns them ~100-2000ms in,
|
|
6232
|
-
// which a watcher would treat as a real change and autosave over the
|
|
6233
|
-
// user's previously saved draft — destroying exactly the work this
|
|
6234
|
-
// feature exists to protect, before they can open the modal to restore
|
|
6235
|
-
// it. Autosave must mean "the user worked", not "the data object
|
|
6236
|
-
// changed". restoreLocalSnapshot() likewise does NOT call this: the
|
|
6237
|
-
// snapshot it just applied is already what's stored.
|
|
6238
|
-
_scheduleLocalAutosave() {
|
|
6239
|
-
if (!this.localSyncKey) return
|
|
6240
|
-
clearTimeout(this._localSyncTimer)
|
|
6241
|
-
this._localSyncTimer = setTimeout(() => {
|
|
6242
|
-
this.saveLocalSnapshot()
|
|
6243
|
-
}, 800)
|
|
6244
|
-
},
|
|
6245
|
-
|
|
6246
|
-
// Internal, called once from mounted(): a snapshot that already exists at
|
|
6247
|
-
// mount is a draft from an EARLIER session, which the user has no way of
|
|
6248
|
-
// knowing about unless they happen to click the toolbar button. Flags it
|
|
6249
|
-
// so the button shows a dot, and emits local-sync-available so a host can
|
|
6250
|
-
// surface its own prompt ("restore unsaved changes?") instead.
|
|
6251
|
-
async _checkLocalSnapshotOnMount() {
|
|
6265
|
+
// Internal, called once from mounted(): populates the toolbar "unseen
|
|
6266
|
+
// local versions" dot and emits local-sync-available if any versions
|
|
6267
|
+
// already exist for this key (e.g. saved in an earlier visit).
|
|
6268
|
+
async _loadLocalVersionsOnMount() {
|
|
6252
6269
|
if (!this.localSyncKey) return
|
|
6253
|
-
const snapshot = await this.getLocalSnapshot()
|
|
6254
|
-
if (!snapshot) return
|
|
6255
|
-
this.localSyncAvailable = true
|
|
6256
|
-
this.$emit('local-sync-available', {
|
|
6257
|
-
key: this.localSyncKey,
|
|
6258
|
-
rows: snapshot.data.length,
|
|
6259
|
-
updatedAt: snapshot.updatedAt
|
|
6260
|
-
})
|
|
6261
|
-
},
|
|
6262
|
-
|
|
6263
|
-
// Public, ref-callable: saves the current `data` to IndexedDB right now,
|
|
6264
|
-
// bypassing the debounce (e.g. a host wants to force a checkpoint before
|
|
6265
|
-
// navigating away). No-op (resolves to null) when localSyncKey isn't set.
|
|
6266
|
-
async saveLocalSnapshot() {
|
|
6267
|
-
if (!this.localSyncKey) return null
|
|
6268
|
-
clearTimeout(this._localSyncTimer)
|
|
6269
6270
|
try {
|
|
6270
|
-
const
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
this
|
|
6274
|
-
|
|
6275
|
-
|
|
6271
|
+
const versions = await listVersions(this.localSyncKey)
|
|
6272
|
+
if (!versions.length) return
|
|
6273
|
+
this.localSyncAvailable = true
|
|
6274
|
+
this.$emit('local-sync-available', {
|
|
6275
|
+
key: this.localSyncKey,
|
|
6276
|
+
versions: versions.length,
|
|
6277
|
+
mostRecentUpdatedAt: versions[0].updatedAt
|
|
6278
|
+
})
|
|
6276
6279
|
} catch (error) {
|
|
6277
|
-
this.$emit('local-sync-error', { action: '
|
|
6278
|
-
return null
|
|
6280
|
+
this.$emit('local-sync-error', { action: 'load', error })
|
|
6279
6281
|
}
|
|
6280
6282
|
},
|
|
6281
6283
|
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
const updatedAt = await this.saveLocalSnapshot()
|
|
6289
|
-
if (updatedAt != null) {
|
|
6290
|
-
this.localSyncModal.snapshot = { data: this.data, updatedAt }
|
|
6291
|
-
}
|
|
6284
|
+
_onVisibilityChangeForLocalSync() {
|
|
6285
|
+
if (document.visibilityState === 'hidden') this._flushLocalVersionOnHide()
|
|
6286
|
+
},
|
|
6287
|
+
|
|
6288
|
+
_onPageHideForLocalSync() {
|
|
6289
|
+
this._flushLocalVersionOnHide()
|
|
6292
6290
|
},
|
|
6293
6291
|
|
|
6294
|
-
//
|
|
6295
|
-
//
|
|
6296
|
-
//
|
|
6297
|
-
|
|
6292
|
+
// The core save-on-leave path — replaces a per-edit autosave (see the
|
|
6293
|
+
// localSyncKey prop's JSDoc for why: an autosave tied to "data changed"
|
|
6294
|
+
// can't distinguish a user's edit from a host loading rows from an API,
|
|
6295
|
+
// and would silently overwrite a user's saved work the moment a host
|
|
6296
|
+
// does the latter). Triggered from mounted()'s visibilitychange/pagehide
|
|
6297
|
+
// listeners and beforeDestroy() — see those for the three trigger points.
|
|
6298
|
+
//
|
|
6299
|
+
// Content-hashes the current `data` (via a Web Worker when available —
|
|
6300
|
+
// src/utils/dataHash.js — falling back to the same algorithm
|
|
6301
|
+
// synchronously otherwise, so this always resolves) and either touches
|
|
6302
|
+
// an existing local version with the same hash (just refreshes its
|
|
6303
|
+
// timestamp — "ustiga yoziladi, alohida versiya qilinmaydi") or creates
|
|
6304
|
+
// a brand-new one, evicting the oldest past localSyncMaxVersions (FIFO —
|
|
6305
|
+
// see idbStore.js's saveOrTouchVersion()). Never throws — IndexedDB
|
|
6306
|
+
// failures become local-sync-error.
|
|
6307
|
+
//
|
|
6308
|
+
// `force` skips the "nothing changed since the last flush" short-circuit
|
|
6309
|
+
// below. The passive triggers (visibilitychange/pagehide/beforeDestroy)
|
|
6310
|
+
// fire often on an idle tab and must NOT wake the worker/IndexedDB for
|
|
6311
|
+
// no reason, so they leave it in place. saveLocalVersionNow() — an
|
|
6312
|
+
// explicit, infrequent, host/modal-initiated "checkpoint now" action —
|
|
6313
|
+
// passes force so it never silently does nothing: even unchanged data
|
|
6314
|
+
// still gets its existing version's timestamp refreshed via the same
|
|
6315
|
+
// hash-dedup path, rather than the caller seeing no effect at all.
|
|
6316
|
+
async _flushLocalVersionOnHide(force = false) {
|
|
6298
6317
|
if (!this.localSyncKey) return
|
|
6318
|
+
if (!force && this.data === this._lastFlushedDataRef) return // nothing changed since the last flush
|
|
6319
|
+
const data = this.data
|
|
6299
6320
|
try {
|
|
6300
|
-
const
|
|
6301
|
-
if (
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
this
|
|
6306
|
-
this
|
|
6321
|
+
const hash = await hashRowsAsync(data)
|
|
6322
|
+
if (this.data !== data) return // data changed again mid-hash — a later flush will catch it
|
|
6323
|
+
const { version, overwritten } = await saveOrTouchVersion(
|
|
6324
|
+
this.localSyncKey, data, hash, this.localSyncMaxVersions
|
|
6325
|
+
)
|
|
6326
|
+
this._lastFlushedDataRef = data
|
|
6327
|
+
this.$emit('local-sync-save', {
|
|
6328
|
+
key: this.localSyncKey,
|
|
6329
|
+
id: version.id,
|
|
6330
|
+
rows: data.length,
|
|
6331
|
+
updatedAt: version.updatedAt,
|
|
6332
|
+
overwritten
|
|
6333
|
+
})
|
|
6307
6334
|
} catch (error) {
|
|
6308
|
-
this.$emit('local-sync-error', { action: '
|
|
6335
|
+
this.$emit('local-sync-error', { action: 'save', error })
|
|
6309
6336
|
}
|
|
6310
6337
|
},
|
|
6311
6338
|
|
|
6312
|
-
// Public, ref-callable:
|
|
6313
|
-
//
|
|
6314
|
-
|
|
6315
|
-
|
|
6339
|
+
// Public, ref-callable: runs the save-on-leave flow immediately, for a
|
|
6340
|
+
// host that wants its own explicit "save checkpoint" button instead of
|
|
6341
|
+
// waiting for a page-hide. Always goes through IndexedDB (force — see
|
|
6342
|
+
// _flushLocalVersionOnHide()), but still through the same hash-dedup
|
|
6343
|
+
// logic, so repeated calls with unchanged data don't spam duplicate
|
|
6344
|
+
// versions — they just refresh the existing matching version.
|
|
6345
|
+
async saveLocalVersionNow() {
|
|
6346
|
+
await this._flushLocalVersionOnHide(true)
|
|
6347
|
+
},
|
|
6348
|
+
|
|
6349
|
+
// Public, ref-callable: lightweight metadata for every saved version of
|
|
6350
|
+
// this localSyncKey, newest first — no row data (use getLocalVersion(id)
|
|
6351
|
+
// for that). Powers the built-in modal's list; also usable to build a
|
|
6352
|
+
// custom UI.
|
|
6353
|
+
async listLocalVersions() {
|
|
6354
|
+
if (!this.localSyncKey) return []
|
|
6316
6355
|
try {
|
|
6317
|
-
await
|
|
6318
|
-
|
|
6319
|
-
this.localSyncAvailable = false
|
|
6320
|
-
this.$emit('local-sync-clear', { key: this.localSyncKey })
|
|
6356
|
+
const versions = await listVersions(this.localSyncKey)
|
|
6357
|
+
return versions.map(v => ({ id: v.id, createdAt: v.createdAt, updatedAt: v.updatedAt, rowCount: v.data.length }))
|
|
6321
6358
|
} catch (error) {
|
|
6322
|
-
this.$emit('local-sync-error', { action: '
|
|
6359
|
+
this.$emit('local-sync-error', { action: 'load', error })
|
|
6360
|
+
return []
|
|
6323
6361
|
}
|
|
6324
6362
|
},
|
|
6325
6363
|
|
|
6326
|
-
// Public, ref-callable:
|
|
6327
|
-
//
|
|
6328
|
-
//
|
|
6329
|
-
//
|
|
6330
|
-
|
|
6331
|
-
// the result into v-model as one step.
|
|
6332
|
-
async getLocalSnapshot() {
|
|
6333
|
-
if (!this.localSyncKey) return null
|
|
6364
|
+
// Public, ref-callable: the full { id, data, createdAt, updatedAt } for
|
|
6365
|
+
// one saved version, WITHOUT applying it to v-model — the pure "Read"
|
|
6366
|
+
// for a custom UI (as opposed to restoreLocalVersion(), which reads AND
|
|
6367
|
+
// commits in one step).
|
|
6368
|
+
async getLocalVersion(id) {
|
|
6334
6369
|
try {
|
|
6335
|
-
return await
|
|
6370
|
+
return await getVersion(id)
|
|
6336
6371
|
} catch (error) {
|
|
6337
6372
|
this.$emit('local-sync-error', { action: 'load', error })
|
|
6338
6373
|
return null
|
|
6339
6374
|
}
|
|
6340
6375
|
},
|
|
6341
6376
|
|
|
6342
|
-
// Public, ref-callable:
|
|
6343
|
-
//
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6377
|
+
// Public, ref-callable: writes one saved version back into v-model
|
|
6378
|
+
// (input/update:data) — the modal's "Restore" button calls this too.
|
|
6379
|
+
async restoreLocalVersion(id) {
|
|
6380
|
+
try {
|
|
6381
|
+
const version = await this.getLocalVersion(id)
|
|
6382
|
+
if (!version) return
|
|
6383
|
+
this.$emit('input', version.data)
|
|
6384
|
+
this.$emit('update:data', version.data)
|
|
6385
|
+
this._lastFlushedDataRef = version.data // what's now on screen is already what's stored
|
|
6386
|
+
this.localSyncAvailable = false
|
|
6387
|
+
this.$emit('local-sync-restore', { key: this.localSyncKey, id, rows: version.data.length })
|
|
6388
|
+
this.closeLocalSyncModal()
|
|
6389
|
+
} catch (error) {
|
|
6390
|
+
this.$emit('local-sync-error', { action: 'restore', error })
|
|
6391
|
+
}
|
|
6392
|
+
},
|
|
6393
|
+
|
|
6394
|
+
// Public, ref-callable: deletes one saved version — the modal's "Delete"
|
|
6395
|
+
// button calls this too (after its own inline confirm step).
|
|
6396
|
+
async deleteLocalVersion(id) {
|
|
6397
|
+
try {
|
|
6398
|
+
await deleteVersion(id)
|
|
6399
|
+
if (this.localSyncModal.selectedId === id) {
|
|
6400
|
+
this.localSyncModal.selectedId = null
|
|
6401
|
+
this.localSyncModal.selectedVersion = null
|
|
6402
|
+
}
|
|
6403
|
+
this.localSyncModal.versions = this.localSyncModal.versions.filter(v => v.id !== id)
|
|
6404
|
+
this.$emit('local-sync-clear', { key: this.localSyncKey, id })
|
|
6405
|
+
} catch (error) {
|
|
6406
|
+
this.$emit('local-sync-error', { action: 'clear', error })
|
|
6407
|
+
}
|
|
6349
6408
|
},
|
|
6350
6409
|
|
|
6351
6410
|
// Public, ref-callable: opens the local-sync modal (also reachable via
|
|
6352
|
-
// the toolbar button), loading the
|
|
6411
|
+
// the toolbar button), loading the version list fresh each time.
|
|
6353
6412
|
async openLocalSyncModal() {
|
|
6354
6413
|
if (!this.localSyncKey) return
|
|
6355
6414
|
this.localSyncModal.visible = true
|
|
6356
|
-
this.localSyncAvailable = false // the
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6415
|
+
this.localSyncAvailable = false // the version(s) have now been seen
|
|
6416
|
+
const versions = await this.listLocalVersions()
|
|
6417
|
+
this.localSyncModal.versions = versions
|
|
6418
|
+
await this._selectLocalSyncModalVersion(versions.length ? versions[0].id : null)
|
|
6419
|
+
},
|
|
6420
|
+
|
|
6421
|
+
// Internal: modal's list-selection handler — loads the selected
|
|
6422
|
+
// version's full data for the nested-grid preview (EgLocalSyncModal.vue).
|
|
6423
|
+
async _selectLocalSyncModalVersion(id) {
|
|
6424
|
+
this.localSyncModal.selectedId = id
|
|
6425
|
+
this.localSyncModal.selectedVersion = id ? await this.getLocalVersion(id) : null
|
|
6426
|
+
},
|
|
6427
|
+
|
|
6428
|
+
// Modal's "Save" button — same as saveLocalVersionNow(), then refreshes
|
|
6429
|
+
// the modal's own list/selection in place so the user sees it happened.
|
|
6430
|
+
async _onLocalSyncModalSave() {
|
|
6431
|
+
await this.saveLocalVersionNow()
|
|
6432
|
+
const versions = await this.listLocalVersions()
|
|
6433
|
+
this.localSyncModal.versions = versions
|
|
6434
|
+
await this._selectLocalSyncModalVersion(versions.length ? versions[0].id : null)
|
|
6435
|
+
},
|
|
6436
|
+
|
|
6437
|
+
// Modal's "Delete" button (after its own inline confirm) — deletes the
|
|
6438
|
+
// given version, then selects the next-most-recent one if the deleted
|
|
6439
|
+
// one was selected.
|
|
6440
|
+
async _onLocalSyncModalDelete(id) {
|
|
6441
|
+
await this.deleteLocalVersion(id)
|
|
6442
|
+
if (this.localSyncModal.selectedId == null) {
|
|
6443
|
+
const remaining = this.localSyncModal.versions
|
|
6444
|
+
await this._selectLocalSyncModalVersion(remaining.length ? remaining[0].id : null)
|
|
6362
6445
|
}
|
|
6363
6446
|
},
|
|
6364
6447
|
|
|
@@ -6371,6 +6454,7 @@ const ExcelGridComponent = {
|
|
|
6371
6454
|
this.internalLocked = !this.internalLocked
|
|
6372
6455
|
this.$emit('update:locked', this.internalLocked)
|
|
6373
6456
|
this.$emit('lock-change', { locked: this.internalLocked })
|
|
6457
|
+
this._flashLock(this.internalLocked)
|
|
6374
6458
|
},
|
|
6375
6459
|
|
|
6376
6460
|
// Pops a big lock/unlock icon in the center of the grid and fades it
|