vue2-excel-grid 1.7.2 → 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 +1193 -587
- 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 +258 -122
- package/src/utils/dataHash.js +139 -0
- package/src/utils/i18n.js +32 -24
- package/src/utils/idbStore.js +122 -20
|
@@ -61,11 +61,12 @@
|
|
|
61
61
|
<button
|
|
62
62
|
v-if="localSyncKey && toolbarHas('localSync')"
|
|
63
63
|
type="button"
|
|
64
|
-
class="eg-toolbar-btn"
|
|
64
|
+
class="eg-toolbar-btn eg-toolbar-btn-badged"
|
|
65
65
|
:disabled="toolbarDisabled('localSync')"
|
|
66
|
-
:title="t.localSync"
|
|
66
|
+
:title="localSyncAvailable ? t.localSyncAvailable : t.localSync"
|
|
67
67
|
@click="openLocalSyncModal"
|
|
68
|
-
><span class="eg-toolbar-icon eg-icon-local-sync"></span
|
|
68
|
+
><span class="eg-toolbar-icon eg-icon-local-sync"></span
|
|
69
|
+
><span v-if="localSyncAvailable" class="eg-toolbar-badge" aria-hidden="true"></span></button>
|
|
69
70
|
</div>
|
|
70
71
|
|
|
71
72
|
<slot name="toolbar-extra" :locked="internalLocked" :fullscreen="isFullscreen"></slot>
|
|
@@ -819,13 +820,16 @@
|
|
|
819
820
|
:visible="localSyncModal.visible"
|
|
820
821
|
:grid-options="_selfComponentDef"
|
|
821
822
|
:columns="_localSyncPreviewColumns"
|
|
822
|
-
:
|
|
823
|
+
:versions="localSyncModal.versions"
|
|
824
|
+
:selected-id="localSyncModal.selectedId"
|
|
825
|
+
:selected-version="localSyncModal.selectedVersion"
|
|
823
826
|
:locale="locale"
|
|
824
827
|
:theme="internalTheme"
|
|
825
828
|
@close="closeLocalSyncModal"
|
|
829
|
+
@select="_selectLocalSyncModalVersion"
|
|
826
830
|
@save="_onLocalSyncModalSave"
|
|
827
|
-
@restore="
|
|
828
|
-
@clear="
|
|
831
|
+
@restore="restoreLocalVersion"
|
|
832
|
+
@clear="_onLocalSyncModalDelete"
|
|
829
833
|
/>
|
|
830
834
|
|
|
831
835
|
<!-- Lock/unlock state feedback — brief centered icon flash -->
|
|
@@ -943,7 +947,8 @@ import EgLocalSyncModal from './EgLocalSyncModal.vue'
|
|
|
943
947
|
import { parseDateValue, formatDate } from '../utils/dateUtils.js'
|
|
944
948
|
import { formatNumber as _formatNumber } from '../utils/numberFormat.js'
|
|
945
949
|
import { Mask, MaskInput } from '../utils/mask.js'
|
|
946
|
-
import {
|
|
950
|
+
import { listVersions, getVersion, deleteVersion, saveOrTouchVersion } from '../utils/idbStore.js'
|
|
951
|
+
import { hashRowsAsync } from '../utils/dataHash.js'
|
|
947
952
|
|
|
948
953
|
const SIZE_CONFIG = {
|
|
949
954
|
small: { row: 22, header: 24, footer: 24, filter: 26 },
|
|
@@ -1408,34 +1413,48 @@ const ExcelGridComponent = {
|
|
|
1408
1413
|
sort: { type: Object, default: null },
|
|
1409
1414
|
|
|
1410
1415
|
/**
|
|
1411
|
-
* Unique key that opts the grid into local
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
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.
|
|
1421
1428
|
*
|
|
1422
1429
|
* Two different grid instances must use different keys — the same key
|
|
1423
|
-
* means "the same
|
|
1424
|
-
* entirely to opt out completely: no IndexedDB access happens
|
|
1425
|
-
* 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.
|
|
1426
1433
|
*
|
|
1427
|
-
* See also the ref-callable methods
|
|
1428
|
-
* `
|
|
1429
|
-
* `
|
|
1430
|
-
* and the
|
|
1431
|
-
* `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`
|
|
1432
1439
|
* events — [details](/local-sync).
|
|
1433
1440
|
* @type {String}
|
|
1434
1441
|
* @default null
|
|
1435
1442
|
* @example
|
|
1436
1443
|
* <excel-grid :columns="columns" v-model="data" local-sync-key="invoice-42" toolbar />
|
|
1437
1444
|
*/
|
|
1438
|
-
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 }
|
|
1439
1458
|
},
|
|
1440
1459
|
|
|
1441
1460
|
data() {
|
|
@@ -1505,9 +1524,15 @@ const ExcelGridComponent = {
|
|
|
1505
1524
|
importState: { active: false, percent: 0, message: '' },
|
|
1506
1525
|
exportState: { active: false },
|
|
1507
1526
|
|
|
1508
|
-
// localSyncKey feature — modal state
|
|
1509
|
-
//
|
|
1510
|
-
|
|
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.
|
|
1535
|
+
localSyncAvailable: false,
|
|
1511
1536
|
|
|
1512
1537
|
resizing: {
|
|
1513
1538
|
active: false,
|
|
@@ -2265,13 +2290,6 @@ const ExcelGridComponent = {
|
|
|
2265
2290
|
this.$nextTick(() => this.updateVirtualScroll())
|
|
2266
2291
|
},
|
|
2267
2292
|
|
|
2268
|
-
// Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:
|
|
2269
|
-
// true` prop doesn't flash the icon before the grid has even appeared.
|
|
2270
|
-
internalLocked(v) {
|
|
2271
|
-
if (!this._lockFlashArmed) return
|
|
2272
|
-
this._flashLock(v)
|
|
2273
|
-
},
|
|
2274
|
-
|
|
2275
2293
|
autoWidth(v) {
|
|
2276
2294
|
if (v) this._applyAutoWidth()
|
|
2277
2295
|
},
|
|
@@ -2342,9 +2360,21 @@ const ExcelGridComponent = {
|
|
|
2342
2360
|
immediate: true,
|
|
2343
2361
|
handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }
|
|
2344
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".)
|
|
2345
2371
|
locked: {
|
|
2346
2372
|
immediate: true,
|
|
2347
|
-
handler(v) {
|
|
2373
|
+
handler(v) {
|
|
2374
|
+
this.internalLocked = !!v
|
|
2375
|
+
if (this._lockPropSynced) this._flashLock(this.internalLocked)
|
|
2376
|
+
this._lockPropSynced = true
|
|
2377
|
+
}
|
|
2348
2378
|
},
|
|
2349
2379
|
data: {
|
|
2350
2380
|
immediate: true,
|
|
@@ -2414,14 +2444,12 @@ const ExcelGridComponent = {
|
|
|
2414
2444
|
this._dragScrollRAF = null
|
|
2415
2445
|
this._dragCleanupTimer = null
|
|
2416
2446
|
|
|
2417
|
-
// localSyncKey
|
|
2418
|
-
//
|
|
2419
|
-
//
|
|
2420
|
-
//
|
|
2421
|
-
//
|
|
2422
|
-
|
|
2423
|
-
// modal. A plain $watch only fires on later, real changes.
|
|
2424
|
-
this.$watch('data', () => this._scheduleLocalAutosave())
|
|
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
|
|
2425
2453
|
},
|
|
2426
2454
|
|
|
2427
2455
|
mounted() {
|
|
@@ -2453,7 +2481,17 @@ const ExcelGridComponent = {
|
|
|
2453
2481
|
// in both modes) fresh when the host's layout changes.
|
|
2454
2482
|
if (this.$el.parentElement) this._scrollBodyResizeObserver.observe(this.$el.parentElement)
|
|
2455
2483
|
}
|
|
2456
|
-
this.
|
|
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)
|
|
2457
2495
|
},
|
|
2458
2496
|
|
|
2459
2497
|
updated() {
|
|
@@ -2488,7 +2526,6 @@ const ExcelGridComponent = {
|
|
|
2488
2526
|
clearTimeout(this._onInputTimer)
|
|
2489
2527
|
clearTimeout(this._dragCleanupTimer)
|
|
2490
2528
|
clearTimeout(this._widthSyncChainResetTimer)
|
|
2491
|
-
clearTimeout(this._localSyncTimer)
|
|
2492
2529
|
if (this._dragScrollRAF) cancelAnimationFrame(this._dragScrollRAF)
|
|
2493
2530
|
document.removeEventListener('mousemove', this.onGlobalMouseMove)
|
|
2494
2531
|
document.removeEventListener('mouseup', this.onGlobalMouseUp)
|
|
@@ -2496,6 +2533,11 @@ const ExcelGridComponent = {
|
|
|
2496
2533
|
document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)
|
|
2497
2534
|
window.removeEventListener('resize', this.syncHeaderViewportWidth)
|
|
2498
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()
|
|
2499
2541
|
if (this.isFullscreen) {
|
|
2500
2542
|
document.body.style.overflow = this._prevBodyOverflow || ''
|
|
2501
2543
|
document.removeEventListener('keydown', this._onFullscreenKeyDown)
|
|
@@ -6216,113 +6258,190 @@ const ExcelGridComponent = {
|
|
|
6216
6258
|
},
|
|
6217
6259
|
|
|
6218
6260
|
// =========================================================
|
|
6219
|
-
// 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).
|
|
6220
6263
|
// =========================================================
|
|
6221
6264
|
|
|
6222
|
-
// Internal
|
|
6223
|
-
//
|
|
6224
|
-
//
|
|
6225
|
-
|
|
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() {
|
|
6226
6269
|
if (!this.localSyncKey) return
|
|
6227
|
-
clearTimeout(this._localSyncTimer)
|
|
6228
|
-
this._localSyncTimer = setTimeout(() => {
|
|
6229
|
-
this.saveLocalSnapshot()
|
|
6230
|
-
}, 800)
|
|
6231
|
-
},
|
|
6232
|
-
|
|
6233
|
-
// Public, ref-callable: saves the current `data` to IndexedDB right now,
|
|
6234
|
-
// bypassing the debounce (e.g. a host wants to force a checkpoint before
|
|
6235
|
-
// navigating away). No-op (resolves to null) when localSyncKey isn't set.
|
|
6236
|
-
async saveLocalSnapshot() {
|
|
6237
|
-
if (!this.localSyncKey) return null
|
|
6238
|
-
clearTimeout(this._localSyncTimer)
|
|
6239
6270
|
try {
|
|
6240
|
-
const
|
|
6241
|
-
|
|
6242
|
-
|
|
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
|
+
})
|
|
6243
6279
|
} catch (error) {
|
|
6244
|
-
this.$emit('local-sync-error', { action: '
|
|
6245
|
-
return null
|
|
6246
|
-
}
|
|
6247
|
-
},
|
|
6248
|
-
|
|
6249
|
-
// Modal's "Save" button (push v-model -> IndexedDB — the other direction
|
|
6250
|
-
// from "Restore", which pulls IndexedDB -> v-model). Unlike
|
|
6251
|
-
// restoreLocalSnapshot(), this deliberately does NOT close the modal —
|
|
6252
|
-
// the freshly-saved snapshot is reflected in place (updated timestamp/
|
|
6253
|
-
// row count/preview) so the user sees the save actually happened.
|
|
6254
|
-
async _onLocalSyncModalSave() {
|
|
6255
|
-
const updatedAt = await this.saveLocalSnapshot()
|
|
6256
|
-
if (updatedAt != null) {
|
|
6257
|
-
this.localSyncModal.snapshot = { data: this.data, updatedAt }
|
|
6280
|
+
this.$emit('local-sync-error', { action: 'load', error })
|
|
6258
6281
|
}
|
|
6259
6282
|
},
|
|
6260
6283
|
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6284
|
+
_onVisibilityChangeForLocalSync() {
|
|
6285
|
+
if (document.visibilityState === 'hidden') this._flushLocalVersionOnHide()
|
|
6286
|
+
},
|
|
6287
|
+
|
|
6288
|
+
_onPageHideForLocalSync() {
|
|
6289
|
+
this._flushLocalVersionOnHide()
|
|
6290
|
+
},
|
|
6291
|
+
|
|
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) {
|
|
6265
6317
|
if (!this.localSyncKey) return
|
|
6318
|
+
if (!force && this.data === this._lastFlushedDataRef) return // nothing changed since the last flush
|
|
6319
|
+
const data = this.data
|
|
6266
6320
|
try {
|
|
6267
|
-
const
|
|
6268
|
-
if (
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
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
|
+
})
|
|
6273
6334
|
} catch (error) {
|
|
6274
|
-
this.$emit('local-sync-error', { action: '
|
|
6335
|
+
this.$emit('local-sync-error', { action: 'save', error })
|
|
6275
6336
|
}
|
|
6276
6337
|
},
|
|
6277
6338
|
|
|
6278
|
-
// Public, ref-callable:
|
|
6279
|
-
//
|
|
6280
|
-
|
|
6281
|
-
|
|
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 []
|
|
6282
6355
|
try {
|
|
6283
|
-
await
|
|
6284
|
-
|
|
6285
|
-
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 }))
|
|
6286
6358
|
} catch (error) {
|
|
6287
|
-
this.$emit('local-sync-error', { action: '
|
|
6359
|
+
this.$emit('local-sync-error', { action: 'load', error })
|
|
6360
|
+
return []
|
|
6288
6361
|
}
|
|
6289
6362
|
},
|
|
6290
6363
|
|
|
6291
|
-
// Public, ref-callable:
|
|
6292
|
-
//
|
|
6293
|
-
//
|
|
6294
|
-
//
|
|
6295
|
-
|
|
6296
|
-
// the result into v-model as one step.
|
|
6297
|
-
async getLocalSnapshot() {
|
|
6298
|
-
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) {
|
|
6299
6369
|
try {
|
|
6300
|
-
return await
|
|
6370
|
+
return await getVersion(id)
|
|
6301
6371
|
} catch (error) {
|
|
6302
6372
|
this.$emit('local-sync-error', { action: 'load', error })
|
|
6303
6373
|
return null
|
|
6304
6374
|
}
|
|
6305
6375
|
},
|
|
6306
6376
|
|
|
6307
|
-
// Public, ref-callable:
|
|
6308
|
-
//
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
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
|
+
}
|
|
6314
6408
|
},
|
|
6315
6409
|
|
|
6316
6410
|
// Public, ref-callable: opens the local-sync modal (also reachable via
|
|
6317
|
-
// the toolbar button), loading the
|
|
6411
|
+
// the toolbar button), loading the version list fresh each time.
|
|
6318
6412
|
async openLocalSyncModal() {
|
|
6319
6413
|
if (!this.localSyncKey) return
|
|
6320
6414
|
this.localSyncModal.visible = true
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
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)
|
|
6326
6445
|
}
|
|
6327
6446
|
},
|
|
6328
6447
|
|
|
@@ -6335,6 +6454,7 @@ const ExcelGridComponent = {
|
|
|
6335
6454
|
this.internalLocked = !this.internalLocked
|
|
6336
6455
|
this.$emit('update:locked', this.internalLocked)
|
|
6337
6456
|
this.$emit('lock-change', { locked: this.internalLocked })
|
|
6457
|
+
this._flashLock(this.internalLocked)
|
|
6338
6458
|
},
|
|
6339
6459
|
|
|
6340
6460
|
// Pops a big lock/unlock icon in the center of the grid and fades it
|
|
@@ -6789,6 +6909,22 @@ export default ExcelGridComponent
|
|
|
6789
6909
|
color: var(--eg-color-text-secondary, var(--eg-color-text-secondary-default));
|
|
6790
6910
|
}
|
|
6791
6911
|
|
|
6912
|
+
/* Anchors the "unseen local draft" dot (localSyncKey feature) — only the
|
|
6913
|
+
local-sync button carries it, but kept as its own class so any future
|
|
6914
|
+
badged tool gets the same positioning. */
|
|
6915
|
+
.eg-toolbar-btn-badged {
|
|
6916
|
+
position: relative;
|
|
6917
|
+
}
|
|
6918
|
+
.eg-toolbar-badge {
|
|
6919
|
+
position: absolute;
|
|
6920
|
+
top: 2px;
|
|
6921
|
+
right: 2px;
|
|
6922
|
+
width: 7px;
|
|
6923
|
+
height: 7px;
|
|
6924
|
+
border-radius: 50%;
|
|
6925
|
+
background: var(--eg-color-accent, var(--eg-color-accent-default));
|
|
6926
|
+
pointer-events: none;
|
|
6927
|
+
}
|
|
6792
6928
|
.eg-toolbar-btn:hover:not(:disabled) {
|
|
6793
6929
|
background: var(--eg-color-option-hover-bg, var(--eg-color-option-hover-bg-default));
|
|
6794
6930
|
border-color: var(--eg-color-border, var(--eg-color-border-default));
|