w-web-perm 1.1.13 → 1.1.15

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.
Files changed (114) hide show
  1. package/README.md +98 -7
  2. package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.307bef4f.js +1 -0
  5. package/dist/js/app.307bef4f.js +1 -0
  6. package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.6cc4d11e.js} +1 -1
  7. package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.6cc4d11e.js} +1 -1
  8. package/dist/w-web-perm.umd.js +2 -2
  9. package/dist/w-web-perm.umd.js.map +1 -1
  10. package/docs/WWebPerm.html +15 -7
  11. package/docs/global.html +11733 -5078
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +154 -345
  14. package/docs/server_adminCheck.mjs.html +92 -0
  15. package/docs/server_cascadeWrite.mjs.html +5 -26
  16. package/docs/server_dataChannel.mjs.html +4 -4
  17. package/docs/server_keyCheck.mjs.html +150 -23
  18. package/docs/server_lockSave.mjs.html +2 -2
  19. package/docs/server_maskLog.mjs.html +2 -2
  20. package/docs/server_procCore.mjs.html +261 -65
  21. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  22. package/docs/server_syncCore.mjs.html +533 -0
  23. package/docs/server_syncKeep.mjs.html +27 -20
  24. package/docs/server_tools_authRefsAudit.mjs.html +30 -10
  25. package/docs/server_tools_authRefsClean.mjs.html +2 -2
  26. package/docs/server_tools_fromTools.mjs.html +2 -2
  27. package/docs/server_tools_isMain.mjs.html +2 -2
  28. package/docs/server_tools_nameDupReport.mjs.html +40 -7
  29. package/docs/server_validateRows.mjs.html +2 -2
  30. package/docs/server_wrapInjected.mjs.html +2 -2
  31. package/docs/src_fetchJson.mjs.html +2 -2
  32. package/docs/src_getPermUserInfor.mjs.html +175 -0
  33. package/docs/src_js_applySaved.mjs.html +128 -0
  34. package/docs/src_js_cellEditing.mjs.html +163 -0
  35. package/docs/src_js_cvKpToArr.mjs.html +2 -2
  36. package/docs/src_js_cvKpToObj.mjs.html +2 -2
  37. package/docs/src_js_draftSync.mjs.html +83 -7
  38. package/docs/src_js_eachOwn.mjs.html +2 -2
  39. package/docs/src_js_editorRefRows.mjs.html +2 -2
  40. package/docs/src_js_externalRows.mjs.html +34 -10
  41. package/docs/src_js_externalUi.mjs.html +4 -8
  42. package/docs/src_js_findRefRows.mjs.html +2 -2
  43. package/docs/src_js_findTouchedDup.mjs.html +2 -2
  44. package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
  45. package/docs/src_js_fromAudit.mjs.html +2 -2
  46. package/docs/src_js_getAddImpact.mjs.html +2 -2
  47. package/docs/src_js_getAuthWarn.mjs.html +2 -2
  48. package/docs/src_js_getCascadePreview.mjs.html +2 -2
  49. package/docs/src_js_getDelRefCount.mjs.html +2 -2
  50. package/docs/src_js_getEffMode.mjs.html +2 -2
  51. package/docs/src_js_getKpLossy.mjs.html +2 -2
  52. package/docs/src_js_getNameErrs.mjs.html +2 -2
  53. package/docs/src_js_getOwn.mjs.html +2 -2
  54. package/docs/src_js_getPropKey.mjs.html +2 -2
  55. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  56. package/docs/src_js_isAuthMode.mjs.html +2 -2
  57. package/docs/src_js_joinNames.mjs.html +2 -2
  58. package/docs/src_js_legendFit.mjs.html +2 -2
  59. package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
  60. package/docs/src_js_refAudit.mjs.html +2 -2
  61. package/docs/src_js_refCascade.mjs.html +2 -2
  62. package/docs/src_js_refIdentity.mjs.html +500 -0
  63. package/docs/src_js_relDialogSend.mjs.html +2 -2
  64. package/docs/src_js_rowDigest.mjs.html +75 -145
  65. package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
  66. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  67. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  68. package/g_provideGrups.mjs +1 -1
  69. package/g_providePemis.mjs +1 -1
  70. package/g_provideTargets.mjs +1 -1
  71. package/g_provideUsers.mjs +1 -1
  72. package/package.json +2 -2
  73. package/server/WWebPerm.mjs +152 -343
  74. package/server/adminCheck.mjs +20 -0
  75. package/server/cascadeWrite.mjs +3 -24
  76. package/server/dataChannel.mjs +2 -2
  77. package/server/keyCheck.mjs +148 -21
  78. package/server/procCore.mjs +259 -63
  79. package/server/procLang.mjs +78 -4
  80. package/server/provideTabs.mjs +6 -4
  81. package/server/syncCore.mjs +461 -0
  82. package/server/syncKeep.mjs +25 -18
  83. package/server/tools/authRefsAudit.mjs +28 -8
  84. package/server/tools/nameDupReport.mjs +38 -5
  85. package/src/App.vue +6 -1
  86. package/src/components/LayoutContentGrups.vue +57 -8
  87. package/src/components/LayoutContentPemis.vue +57 -8
  88. package/src/components/LayoutContentTargets.vue +38 -7
  89. package/src/components/LayoutContentUsers.vue +57 -8
  90. package/src/components/LayoutState.vue +12 -0
  91. package/src/components/VeGrupBlngUsers.vue +128 -20
  92. package/src/components/VePemiBlngGrups.vue +128 -20
  93. package/src/getPermUserInfor.mjs +10 -0
  94. package/src/js/applySaved.mjs +56 -0
  95. package/src/js/cellEditing.mjs +91 -0
  96. package/src/js/draftSync.mjs +81 -5
  97. package/src/js/externalRows.mjs +32 -8
  98. package/src/js/externalUi.mjs +2 -6
  99. package/src/js/refIdentity.mjs +428 -0
  100. package/src/js/rowDigest.mjs +73 -143
  101. package/src/plugins/mUI.mjs +5 -0
  102. package/src/schema/contracts/grups.mjs +8 -0
  103. package/src/schema/contracts/index.mjs +17 -0
  104. package/src/schema/contracts/pemis.mjs +8 -0
  105. package/src/schema/contracts/targets.mjs +8 -0
  106. package/src/schema/contracts/users.mjs +13 -0
  107. package/src/schema/tables/grups.mjs +4 -0
  108. package/src/schema/tables/pemis.mjs +4 -0
  109. package/src/schema/tables/targets.mjs +4 -0
  110. package/src/schema/tables/users.mjs +4 -0
  111. package/src/store/mutations.mjs +1 -1
  112. package/srv.mjs +43 -0
  113. package/dist/js/app-legacy.8af87052.js +0 -1
  114. package/dist/js/app.8af87052.js +0 -1
@@ -311,7 +311,10 @@ import { getRefWarns, getRefWarnText, fmtWarn } from '../js/getAuthWarn.mjs'
311
311
  import { getNameErrs, mapNameKp } from '../js/getNameErrs.mjs'
312
312
  import { isExternalRow } from '../js/externalRows.mjs'
313
313
  import { makeBaseline } from '../js/rowDigest.mjs'
314
- import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
314
+ import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
315
+ import { applySavedRows } from '../js/applySaved.mjs'
316
+ import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
317
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
315
318
  import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
316
319
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
317
320
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
@@ -421,6 +424,8 @@ export default {
421
424
  this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
422
425
  //saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
423
426
  this.saveMark = { saving: false }
427
+ //refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(群組名稱) → 選取當時對應之群組 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
428
+ this.refPicks = new Map()
424
429
  },
425
430
  mounted: function() {
426
431
  // console.log('mounted')
@@ -539,14 +544,14 @@ export default {
539
544
  first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
540
545
  editChanged: vo.syncMark.editable !== isEditable,
541
546
  storeChanged: refsChanged(vo.syncMark.refs, [rows]),
542
- hasDraft: () => vo.syncMark.dirty === true,
547
+ hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
543
548
  saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
544
549
  })
545
550
  if (act === 'rebuild') {
546
551
  vo.rebuildDraft(rows, isEditable)
547
552
  }
548
553
  else if (act === 'keep') {
549
- vo.staleNotice = isDraftStale({ keys: vo.$ds.users.keys, rows, baseline: vo.syncMark.baseline })
554
+ vo.staleNotice = isDraftStale({ keyTable: 'users', rows, baseline: vo.syncMark.baseline })
550
555
  }
551
556
 
552
557
  return ''
@@ -770,7 +775,7 @@ export default {
770
775
  let items = cloneDeep(rows)
771
776
 
772
777
  //baseline, 與 items 同一處由同一份來源列(items)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
773
- let baseline = makeBaseline(items, vo.$ds.users.keys)
778
+ let baseline = makeBaseline(items, 'users')
774
779
 
775
780
  //save; 重建即放棄未存編輯(isModified 歸零)與過期提示, syncMark 記下本次所依據之 store 參照與編輯模式
776
781
  vo.items = items
@@ -779,6 +784,7 @@ export default {
779
784
  vo.isModified = false
780
785
  vo.staleNotice = false
781
786
  vo.syncMark = { refs: [rows], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
787
+ vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
782
788
 
783
789
  //genOpt
784
790
  vo.genOpt({ isEditable })
@@ -786,6 +792,11 @@ export default {
786
792
  //firstLoading
787
793
  vo.firstLoading = false
788
794
 
795
+ //第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
796
+ vo.$nextTick(() => {
797
+ vo.bindEditing()
798
+ })
799
+
789
800
  },
790
801
 
791
802
  curRefs: function() {
@@ -794,6 +805,22 @@ export default {
794
805
  return [vo.users]
795
806
  },
796
807
 
808
+ bindEditing: function() {
809
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
810
+ let vo = this
811
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
812
+ vo.afterEditing()
813
+ })
814
+ },
815
+
816
+ afterEditing: function() {
817
+ //儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
818
+ let vo = this
819
+ if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
820
+ vo.reloadDraft()
821
+ }
822
+ },
823
+
797
824
  reloadDraft: function() {
798
825
  //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
799
826
  //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
@@ -1161,6 +1188,7 @@ export default {
1161
1188
  let _r = vo.$ds.users.funNew()
1162
1189
  r.id = _r.id //使用新建方式預先產生id避免重複
1163
1190
  r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
1191
+ r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
1164
1192
  r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('userCopyNameNew'))
1165
1193
  r.userId = `{${vo.$t('userAddIdNew')}}`
1166
1194
  r.timeCreate = `{${vo.$t('userAddIdNew')}}`
@@ -1334,6 +1362,8 @@ export default {
1334
1362
  //編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
1335
1363
  //父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
1336
1364
  let baseline0 = cloneDeep(vo.baseline)
1365
+ //refSnap: 開啟編輯器當下之「群組名稱 → 群組 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的群組即此刻 store 之群組, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍是同一實體(被刪除後同名重建〔id 不同〕即拒收)
1366
+ let refSnap = snapRefIds('users', vo.grups)
1337
1367
  //showVeCgrups
1338
1368
  vo.$dg.showVeCgrups({
1339
1369
  isEditable: vo.isEditable,
@@ -1356,11 +1386,14 @@ export default {
1356
1386
  }
1357
1387
 
1358
1388
  //草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
1359
- if (isDraftStale({ keys: vo.$ds.users.keys, rows: vo.baseRows, baseline: baseline0 })) {
1389
+ if (isDraftStale({ keyTable: 'users', rows: vo.baseRows, baseline: baseline0 })) {
1360
1390
  vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
1361
1391
  return
1362
1392
  }
1363
1393
 
1394
+ //選取記錄: 本次新增之啟用引用項(群組名稱)在編輯器開啟當下對應之群組 id(查無記空陣列)
1395
+ recordPicks({ keyTable: 'users', strOld: get(vo, `opt.rows[${krNow}].cgrups`, ''), strNew: cgrups, snap: refSnap, picks: vo.refPicks })
1396
+
1364
1397
  //set
1365
1398
  set(vo, `opt.rows[${krNow}].cgrups`, cgrups)
1366
1399
  // console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
@@ -1417,6 +1450,15 @@ export default {
1417
1450
  return
1418
1451
  }
1419
1452
 
1453
+ //正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(群組名稱)所指之群組,須仍是使用者選取當時的同一實體(同 id);
1454
+ //被刪除或刪除後以同名重建(id 不同)即不送出(零寫入)並提示重新載入,不得把授權寫到同名新群組. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於群組表鎖內以新鮮讀取再驗一次
1455
+ let refs = parseRefDecl(buildRefDecl({ keyTable: 'users', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.grups }))
1456
+ if (countRefMismatch({ keyTable: 'users', decl: refs, refRows: vo.grups }) > 0) {
1457
+ unlockBtn()
1458
+ await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
1459
+ return
1460
+ }
1461
+
1420
1462
  //新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
1421
1463
  //(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025);
1422
1464
  //modeEditUsers='for:grups' 時後端不採納新增列(mergePickKeysOnly), 亦不檢查
@@ -1426,15 +1468,17 @@ export default {
1426
1468
 
1427
1469
  //3) updateUsers, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
1428
1470
  let okSave = false
1471
+ let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
1429
1472
  vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1430
- await vo.$fapi.updateUsers(rows, { baseline })
1431
- .then(() => {
1473
+ await vo.$fapi.updateUsers(rows, { baseline, returnRows: true, refs })
1474
+ .then((res) => {
1432
1475
  okSave = true
1476
+ savedRes = res
1433
1477
  vo.saveMark.saving = false
1434
1478
  })
1435
1479
  .catch(async (err) => {
1436
1480
  vo.saveMark.saving = false
1437
- vo.staleNotice = isDraftStale({ keys: vo.$ds.users.keys, rows: vo.users, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1481
+ vo.staleNotice = isDraftStale({ keyTable: 'users', rows: vo.users, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1438
1482
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
1439
1483
  await vo.$dg.showCheckYes(`${vo.$t('userSaveUsersFail')}: ${vo.$tErr(err)}`)
1440
1484
  })
@@ -1442,6 +1486,11 @@ export default {
1442
1486
 
1443
1487
  //isModified
1444
1488
  vo.isModified = false
1489
+ vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
1490
+
1491
+ //存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
1492
+ //其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
1493
+ applySavedRows('users', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1445
1494
 
1446
1495
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
1447
1496
  vo.$ui.updateLoading(false)
@@ -54,6 +54,18 @@
54
54
 
55
55
  </template>
56
56
 
57
+ <template v-else-if="connState==='csErrNotAdmin'">
58
+
59
+ <img style="width:75px; height:75px;" :src="img_deny">
60
+
61
+ <div style="margin-left:10px;">
62
+ <div style="color:#aaa;">
63
+ {{$t('csErrNotAdmin')}}
64
+ </div>
65
+ </div>
66
+
67
+ </template>
68
+
57
69
  <template v-else>
58
70
 
59
71
  <img style="width:75px; height:75px;" :src="img_deny">
@@ -128,7 +128,7 @@
128
128
 
129
129
  <!-- 資料已被更新提示(R-PRINCIPLE-00 原則 7): 開框後他人或外部同步改了使用者表, 本視窗之草稿與基準維持開框時之版本、不重建, 存檔將被伺服端拒收; 以 v-for 渲染(無提示時不渲染任何節點) -->
130
130
  <div
131
- v-for="ks in (staleNotice ? 1 : 0)"
131
+ v-for="ks in ((staleNotice || subjectNotice) ? 1 : 0)"
132
132
  :key="`stale-${ks}`"
133
133
  data-fmid="grup-blng-users-banner-stale"
134
134
  style="padding:5px 15px; border-top:1px solid #ddd; background:#FFE0B2; color:#444; font-size:0.8rem; overflow-wrap:anywhere;"
@@ -222,7 +222,9 @@ import getKpLossy from '../js/getKpLossy.mjs'
222
222
  import fmtLossyMsg from '../js/fmtLossyMsg.mjs'
223
223
  import pairRowsToSnapshot from '../js/pairRowsToSnapshot.mjs'
224
224
  import { makeBaseline } from '../js/rowDigest.mjs'
225
- import { isDraftStale } from '../js/draftSync.mjs'
225
+ import { isDraftStale, planDialogSync, checkSubject } from '../js/draftSync.mjs'
226
+ import { applySavedRows } from '../js/applySaved.mjs'
227
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
226
228
  import { makeRowStates, getChangedIdx, buildRowsSend } from '../js/relDialogSend.mjs'
227
229
  import { getRefState, getRawModeMap, putRefEntry } from '../js/editorRefRows.mjs'
228
230
  import { getRefWarns, getRefWarnText } from '../js/getAuthWarn.mjs'
@@ -273,7 +275,10 @@ export default {
273
275
  //baseline 為草稿所依據之那份使用者列之逐列內容指紋, 存檔隨 updateUsers 第 2 參送伺服端; rowStates 為開框時各列之歸屬狀態, 存檔時判定使用者有無變更該列
274
276
  baseline: null,
275
277
  rowStates: null,
276
- staleNotice: false, //有未存編輯時收到推播、且使用者表已異於本視窗載入時之基準(R-PRINCIPLE-00 原則 7): true 時顯示「資料已被更新」提示; 每次 show() 歸零
278
+ staleNotice: false, //有未存編輯(含開啟中之儲存格編輯器)時收到推播、且使用者表已異於本視窗載入時之基準(R-PRINCIPLE-00 原則 7、10): true 時顯示「資料已被更新」提示; 每次 show() 歸零
279
+ subject: null, //開框時之主體摘要 { id, name }(第 114 項, R-SUBJECT-ID-01): 本對話框以**名稱**引用群組(使用者 cgrups 的鍵是群組名稱), 存檔時以 id 驗主體仍是同一個實體(未被刪除、改名、刪除後以同名重建)
280
+ subjectNotice: false, //主體(群組)於開框後已被刪除、改名或以同名重建(id 不同): true 時顯示「資料已被更新」提示, 存檔將被拒收; 每次 show() 歸零
281
+ systemProcing: false, //程式端重建對話框內容期間為 true, 用於排除非使用者操作之 rowsChange (見 $ui.markDataReload; 開框中因推播重建時須用)
277
282
 
278
283
  items: [],
279
284
  itemsCheck: [],
@@ -305,14 +310,12 @@ export default {
305
310
 
306
311
  },
307
312
  watch: {
308
- //store 之使用者表被推播更新(整表以新陣列取代): 本視窗不重建(草稿與基準維持開框時之版本, 見 show()). 此刻有未存編輯者, 若表已異於載入時之基準(任一列內容不同或列增刪; 僅 order/伺服器管理欄不同不算)
309
- //即顯示「資料已被更新」提示, 存檔時由伺服端以表雜湊拒收(saveStale); 無未存編輯者維持原行為(靜默, 存檔時才由伺服端拒收). 唯讀檢視無存檔, 不提示
310
- users: function(rows) {
311
- let vo = this
312
- if (!vo.bShow || !vo.isEditable || !vo.isModified || vo.ownSaving || !Array.isArray(vo.baseline)) {
313
- return
314
- }
315
- vo.staleNotice = isDraftStale({ keys: vo.$ds.users.keys, rows, baseline: vo.baseline })
313
+ //store 之使用者表或群組(主體)表被推播更新(整表以新陣列取代): 處置見 syncStore
314
+ users: function() {
315
+ this.syncStore()
316
+ },
317
+ grups: function() {
318
+ this.syncStore()
316
319
  },
317
320
  },
318
321
  computed: {
@@ -508,7 +511,7 @@ export default {
508
511
  // console.log('rowsChange cloneDeep(vo.opt.rows)', cloneDeep(vo.opt.rows))
509
512
 
510
513
  //check
511
- if (!vo.syncState || vo.firstLoading || vo.firstSetting) {
514
+ if (!vo.syncState || vo.firstLoading || vo.firstSetting || vo.systemProcing) {
512
515
  return
513
516
  }
514
517
 
@@ -1038,17 +1041,93 @@ export default {
1038
1041
 
1039
1042
  },
1040
1043
 
1041
- saveUsers: async function(rows, baseline) {
1044
+ syncStore: function() {
1045
+ //store 之使用者表或群組(主體)表被推播更新(整表以新陣列取代)時的處置(R-PRINCIPLE-00 原則 10; 規則帳本 R-DRAFT-KEEP-01 規則 11、12; draftSync.planDialogSync):
1046
+ // ① 無未存修改(含無開啟中之儲存格編輯器)而使用者表已異於載入基準 → 以新資料重建對話框內容並更新基準(對話框開而未編輯時不停在過期資料上; 修 (b));
1047
+ // ② 有未存修改或開啟中之編輯器 → 保留草稿、凍結基準, 顯示「資料已被更新」提示, 存檔時由伺服端拒收(saveStale);
1048
+ // ③ 自己存檔進行中所致之推播不是他人更新 → 不重建、不提示;
1049
+ // ④ 主體(群組)已被刪除、改名或以同名重建(第 114 項) → 不重建(無從續編), 顯示同一提示, 存檔時前端與之比對 id 拒收.
1050
+ let vo = this
1051
+ if (!vo.bShow || !Array.isArray(vo.baseline)) {
1052
+ return
1053
+ }
1054
+ let sc = checkSubject({ subject: vo.subject, rows: vo.grups })
1055
+ vo.subjectNotice = sc !== 'ok'
1056
+ let act = planDialogSync({
1057
+ show: true,
1058
+ storeChanged: true,
1059
+ stale: isDraftStale({ keyTable: 'users', rows: vo.users, baseline: vo.baseline }),
1060
+ dirty: vo.isModified === true,
1061
+ editing: isCellEditing(get(vo, '$refs.rftable', null)),
1062
+ saving: vo.ownSaving === true,
1063
+ })
1064
+ if (act === 'rebuild') {
1065
+ if (sc === 'ok') {
1066
+ vo.rebuildDialog()
1067
+ }
1068
+ }
1069
+ else if (act === 'keep') {
1070
+ vo.staleNotice = true
1071
+ }
1072
+ },
1073
+
1074
+ rebuildDialog: function() {
1075
+ //開框中以目前 store 重建對話框內容與基準(無未存修改時; 同 show() 之內容組裝, 不動 pm/bShow/isEditable/subject)
1076
+ let vo = this
1077
+ let g = vo.grups.find((r) => r.id === get(vo, 'subject.id', ''))
1078
+ let grup = { ...vo.grup, ...cloneDeep(g) }
1079
+ let users = cloneDeep(vo.users)
1080
+ let baseline = makeBaseline(users, 'users')
1081
+ let items = vo.genItems(grup, users)
1082
+ let rowStates = makeRowStates(items)
1083
+
1084
+ //markDataReload: 重建 opt 屬程式端寫入, 其後非同步觸發之 rowsChange 不得視為使用者變更
1085
+ vo.$ui.markDataReload(vo)
1086
+
1087
+ vo.grup = grup
1088
+ vo.items = items
1089
+ vo.baseline = baseline
1090
+ vo.rowStates = rowStates
1091
+ vo.isModified = false
1092
+ vo.staleNotice = false
1093
+ vo.genOpt()
1094
+ vo.$nextTick(() => {
1095
+ vo.bindEditing()
1096
+ })
1097
+ },
1098
+
1099
+ bindEditing: function() {
1100
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(syncStore 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
1101
+ let vo = this
1102
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
1103
+ vo.afterEditing()
1104
+ })
1105
+ },
1106
+
1107
+ afterEditing: function() {
1108
+ //儲存格編輯結束: 無未存修改而表於編輯期間已變新者, 補上一次同步處置(重建); 有未存修改者維持保留
1109
+ let vo = this
1110
+ if (vo.isModified !== true) {
1111
+ vo.syncStore()
1112
+ }
1113
+ },
1114
+
1115
+ saveUsers: async function(rows, baseline, subject) {
1042
1116
  // console.log('method saveUsers', rows, baseline)
1043
1117
 
1044
1118
  let vo = this
1045
1119
 
1046
1120
  //3) updateUsers, 各自 catch + okX 旗標短路 (canonical 五段結構; 空清單檢查已上移至 doSave core())
1047
- //第 2 參 { baseline }: 開框時綁定之草稿基準(由 doSave 於流程開頭取得並傳入, 不於此讀 vo.baseline——存檔進行中若再次 show() 會換成新草稿之基準); 伺服端比對「本次將寫入之列」之庫內版本, 草稿之後被改過即整次拒收 saveStale(失敗路徑沿用下方 catch 顯示語系文字)
1121
+ //第 2 參 { baseline, returnRows }: baseline 為開框時綁定之草稿基準(由 doSave 於流程開頭取得並傳入, 不於此讀 vo.baseline——存檔進行中若再次 show() 會換成新草稿之基準); 伺服端比對「本次將寫入之列」之庫內版本, 草稿之後被改過即整次拒收 saveStale(失敗路徑沿用下方 catch 顯示語系文字);
1122
+ //returnRows: 成功時回傳寫入後之列(鎖內新鮮讀取), 立即 commit 進 store(規格第 8 節; R-SAVED-ROWS-01), 其後再開此視窗時基準即為最新, 不會被自己上一次之存檔誤擋
1048
1123
  let okSave = false
1049
1124
  vo.ownSaving = true //自己之寫入所致之推播不提示(成功路徑維持至視窗關閉; show() 歸零)
1050
- await vo.$fapi.updateUsers(rows, { baseline })
1051
- .then(() => { okSave = true })
1125
+ //subject: 開框時之主體(群組)摘要 { id, name, saved }(由 doSave 於流程開頭取得並傳入): 伺服端於群組表鎖內新鮮讀取後以同一判準(draftSync.checkSubject)再驗一次, 被刪除、改名、刪除後同名重建一律拒收 saveSubjectChanged(R-SUBJECT-ID-01 規則 4; 前端自檢先擋, 此為推播未抵達時之伺服端強制)
1126
+ await vo.$fapi.updateUsers(rows, { baseline, returnRows: true, subject })
1127
+ .then((res) => {
1128
+ okSave = true
1129
+ applySavedRows('users', res, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1130
+ })
1052
1131
  .catch(async (err) => {
1053
1132
  vo.ownSaving = false //失敗(含 saveStale 被拒): 視窗仍開著, 恢復監看
1054
1133
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
@@ -1115,6 +1194,20 @@ export default {
1115
1194
  //grupName: 被編輯之群組名稱, 同樣於流程開頭(任何 await 之前)取得——等待後才讀 vo.grup.name 會讀到新開啟之實體, 把舊草稿之歸屬寫成指派新實體(外審 sol R2)
1116
1195
  let grupName = get(vo, 'grup.name', '')
1117
1196
 
1197
+ //subject: 開框時之主體摘要 { id, name }, 同樣於流程開頭取得並持有. isSubjectOk: 主體(群組)於存檔當下仍是開框時那一個實體(第 114 項, R-SUBJECT-ID-01; 原則 3、6):
1198
+ //本對話框以名稱引用群組(使用者 cgrups 的鍵是群組名稱)而不驗主體身分——開框後群組被刪除、改名, 或刪除後以同名重建(id 不同), 引用會靜默指向新實體(被同名新資料接管); 故以 id 比對存檔當下之群組表, 不符即整次拒存(零寫入)並提示重新載入
1199
+ let subject = vo.subject
1200
+ let isSubjectOk = () => checkSubject({ subject, rows: vo.grups }) === 'ok'
1201
+ let rejectSubject = async () => {
1202
+ vo.$ui.updateLoading(false)
1203
+ await vo.$dg.showCheckYes(vo.$t('relSubjectChangedGrup').split('{name}').join(get(subject, 'name', '')))
1204
+ return false
1205
+ }
1206
+ if (!isSubjectOk()) {
1207
+ unlockBtn()
1208
+ return await rejectSubject()
1209
+ }
1210
+
1118
1211
  //1b) 無法無損表示之授權資料(R-AUTH-LOSSY-01, 主控複審 R4): 該列使用者之 cgrups 含物件形式無法無損表示之格(getKpLossy)者, 本對話框不得改寫其字串——
1119
1212
  //本對話框所管之歸屬/模式有變更 → 整次儲存拒絕並點名(原子, 其餘列亦不寫); 未變更 → 該列字串逐字元不動(見下方 kpLossyIdx)
1120
1213
  //使用者列之快照只取這一次: 有損檢查之值即下方寫入之值(外審 sol#2: 兩次讀取之間 store 可能被伺服器推送更新, 檢查與寫入看到不同資料)
@@ -1174,6 +1267,11 @@ export default {
1174
1267
  //delay
1175
1268
  await delay(300)
1176
1269
 
1270
+ //主體身分複驗(第 114 項): 開 loading 與等待期間 store 可能再被推播更新, 送出前再比對一次
1271
+ if (!isSubjectOk()) {
1272
+ return await rejectSubject()
1273
+ }
1274
+
1177
1275
  //users: 沿用 1b 之快照(見上)
1178
1276
 
1179
1277
  //kpNewCgrups: 列索引 → 該列新 cgrups 字串; 只收「使用者有變更(kpChanged)、非有損、且 putRefEntry 判定本項確有變更」者, 其餘一律不收(由 buildRowsSend 原樣帶回)
@@ -1223,7 +1321,7 @@ export default {
1223
1321
  // }
1224
1322
 
1225
1323
  //saveUsers, 失敗時回 false: 失敗提示已由 saveUsers 顯示, 此處不關閉對話框(保留未存變更供重按儲存)
1226
- let okSave = await vo.saveUsers(usersNew, baseline)
1324
+ let okSave = await vo.saveUsers(usersNew, baseline, subject)
1227
1325
  if (!okSave) {
1228
1326
  return false
1229
1327
  }
@@ -1294,6 +1392,7 @@ export default {
1294
1392
  //default
1295
1393
  vo.isModified = false
1296
1394
  vo.staleNotice = false
1395
+ vo.subjectNotice = false
1297
1396
  vo.ownSaving = false
1298
1397
 
1299
1398
  //isEditable, grup
@@ -1303,12 +1402,15 @@ export default {
1303
1402
  //grup
1304
1403
  grup = cloneDeep(grup)
1305
1404
 
1405
+ //subject: 開框時之主體摘要(第 114 項): 存檔時以 id 驗主體仍是同一個實體
1406
+ let subject = { id: get(grup, 'id', ''), name: get(grup, 'name', ''), saved: vo.grups.some((r) => r.id === get(grup, 'id', '')) }
1407
+
1306
1408
  //users
1307
1409
  let users = cloneDeep(vo.users)
1308
1410
 
1309
- //baseline(F-26, R-BASELINE-01; 外審 B.1): 於生成 items 之同一處, 以 items 所依據之那份使用者列(users)計算並綁定為本次草稿之載入基準; 之後 store 推播變新不得更新它
1310
- //(存檔當下才由最新 store 取基準, 證明的是「store 新」而非「待存之編輯內容新」), 每次 show() 重新綁定. 欄位口徑與伺服端同一(vo.$ds.users.keys 經 rowDigest.makeBaseline)
1311
- let baseline = makeBaseline(users, vo.$ds.users.keys)
1411
+ //baseline(F-26, R-BASELINE-01; 外審 B.1): 於生成 items 之同一處, 以 items 所依據之那份使用者列(users)計算並綁定為本次草稿之載入基準; 之後 store 推播變新不得更新它,
1412
+ //除非無未存修改時 syncStore 整個重建(存檔當下才由最新 store 取基準, 證明的是「store 新」而非「待存之編輯內容新」), 每次 show() 重新綁定. 摘要口徑與伺服端同一(寫入契約 users, 經 rowDigest.makeBaseline/w-serv-orm buildBaseline)
1413
+ let baseline = makeBaseline(users, 'users')
1312
1414
 
1313
1415
  //items
1314
1416
  let items = vo.genItems(grup, users)
@@ -1319,6 +1421,7 @@ export default {
1319
1421
  //save
1320
1422
  vo.isEditable = isEditable
1321
1423
  vo.grup = grup
1424
+ vo.subject = subject
1322
1425
  vo.items = items
1323
1426
  vo.baseline = baseline
1324
1427
  vo.rowStates = rowStates
@@ -1329,6 +1432,11 @@ export default {
1329
1432
  //firstLoading
1330
1433
  vo.firstLoading = false
1331
1434
 
1435
+ //第 115 項: 對話框內之 ag-grid 綁「儲存格編輯結束」監聽
1436
+ vo.$nextTick(() => {
1437
+ vo.bindEditing()
1438
+ })
1439
+
1332
1440
  //bShow
1333
1441
  vo.bShow = true
1334
1442