w-web-perm 1.1.11 → 1.1.13

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 (122) hide show
  1. package/README.md +158 -117
  2. package/dist/css/app.f09c7433.css +1 -0
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.8af87052.js +1 -0
  5. package/dist/js/app.8af87052.js +1 -0
  6. package/dist/js/{chunk-vendors.3e9e3ef1.js → chunk-vendors-legacy.ece5a361.js} +1 -1
  7. package/dist/js/{chunk-vendors-legacy.3e9e3ef1.js → chunk-vendors.ece5a361.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 +84 -4
  11. package/docs/global.html +24747 -832
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +245 -32
  14. package/docs/server_cascadeWrite.mjs.html +225 -0
  15. package/docs/server_dataChannel.mjs.html +2 -2
  16. package/docs/server_keyCheck.mjs.html +353 -0
  17. package/docs/server_lockSave.mjs.html +2 -2
  18. package/docs/server_maskLog.mjs.html +2 -2
  19. package/docs/server_procCore.mjs.html +180 -41
  20. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  21. package/docs/server_syncKeep.mjs.html +311 -0
  22. package/docs/server_tools_authRefsAudit.mjs.html +247 -0
  23. package/docs/server_tools_authRefsClean.mjs.html +335 -0
  24. package/docs/server_tools_fromTools.mjs.html +797 -0
  25. package/docs/server_tools_isMain.mjs.html +103 -0
  26. package/docs/server_tools_nameDupReport.mjs.html +280 -0
  27. package/docs/server_validateRows.mjs.html +15 -4
  28. package/docs/server_wrapInjected.mjs.html +3 -3
  29. package/docs/src_fetchJson.mjs.html +2 -2
  30. package/docs/src_js_cvKpToArr.mjs.html +125 -0
  31. package/docs/src_js_cvKpToObj.mjs.html +210 -0
  32. package/docs/src_js_draftSync.mjs.html +142 -0
  33. package/docs/src_js_eachOwn.mjs.html +3 -3
  34. package/docs/src_js_editorRefRows.mjs.html +356 -0
  35. package/docs/src_js_externalRows.mjs.html +338 -0
  36. package/docs/src_js_externalUi.mjs.html +230 -0
  37. package/docs/src_js_findRefRows.mjs.html +107 -0
  38. package/docs/src_js_findTouchedDup.mjs.html +150 -0
  39. package/docs/src_js_fmtLossyMsg.mjs.html +95 -0
  40. package/docs/src_js_fromAudit.mjs.html +489 -0
  41. package/docs/src_js_getAddImpact.mjs.html +269 -0
  42. package/docs/src_js_getAuthWarn.mjs.html +463 -0
  43. package/docs/src_js_getCascadePreview.mjs.html +258 -0
  44. package/docs/src_js_getDelRefCount.mjs.html +210 -0
  45. package/docs/src_js_getEffMode.mjs.html +92 -0
  46. package/docs/src_js_getKpLossy.mjs.html +155 -0
  47. package/docs/src_js_getNameErrs.mjs.html +266 -0
  48. package/docs/src_js_getOwn.mjs.html +3 -3
  49. package/docs/src_js_getPropKey.mjs.html +109 -0
  50. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  51. package/docs/src_js_isAuthMode.mjs.html +92 -0
  52. package/docs/src_js_joinNames.mjs.html +96 -0
  53. package/docs/src_js_legendFit.mjs.html +2 -2
  54. package/docs/src_js_pairRowsToSnapshot.mjs.html +156 -0
  55. package/docs/src_js_refAudit.mjs.html +359 -0
  56. package/docs/src_js_refCascade.mjs.html +572 -0
  57. package/docs/src_js_relDialogSend.mjs.html +199 -0
  58. package/docs/src_js_rowDigest.mjs.html +265 -0
  59. package/docs/src_plugins_dataReloadGuard.mjs.html +109 -0
  60. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  61. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  62. package/g_authRefsClean.mjs +79 -0
  63. package/g_fixFromType.mjs +92 -0
  64. package/g_initialData.mjs +3 -3
  65. package/g_purgeFrom.mjs +127 -0
  66. package/package.json +1 -1
  67. package/server/WWebPerm.mjs +243 -30
  68. package/server/cascadeWrite.mjs +153 -0
  69. package/server/keyCheck.mjs +281 -0
  70. package/server/procCore.mjs +178 -39
  71. package/server/procLang.mjs +271 -0
  72. package/server/syncKeep.mjs +239 -0
  73. package/server/tools/authRefsAudit.mjs +175 -0
  74. package/server/tools/authRefsClean.mjs +263 -0
  75. package/server/tools/fromTools.mjs +725 -0
  76. package/server/tools/isMain.mjs +31 -0
  77. package/server/tools/nameDupReport.mjs +208 -0
  78. package/server/validateRows.mjs +13 -2
  79. package/server/wrapInjected.mjs +1 -1
  80. package/src/components/LayoutContentGrups.vue +399 -77
  81. package/src/components/LayoutContentPemis.vue +400 -78
  82. package/src/components/LayoutContentTargets.vue +261 -25
  83. package/src/components/LayoutContentUsers.vue +336 -98
  84. package/src/components/VeCgrups.vue +119 -115
  85. package/src/components/VeCpemis.vue +117 -115
  86. package/src/components/VeCrules.vue +18 -2
  87. package/src/components/VeGrupBlngUsers.vue +240 -73
  88. package/src/components/VePemiBlngGrups.vue +247 -83
  89. package/src/js/cvKpToArr.mjs +53 -0
  90. package/src/js/cvKpToObj.mjs +138 -0
  91. package/src/js/draftSync.mjs +70 -0
  92. package/src/js/eachOwn.mjs +1 -1
  93. package/src/js/editorRefRows.mjs +284 -0
  94. package/src/js/externalRows.mjs +266 -0
  95. package/src/js/externalUi.mjs +158 -0
  96. package/src/js/findRefRows.mjs +35 -0
  97. package/src/js/findTouchedDup.mjs +78 -0
  98. package/src/js/fmtLossyMsg.mjs +23 -0
  99. package/src/js/fromAudit.mjs +417 -0
  100. package/src/js/getAddImpact.mjs +197 -0
  101. package/src/js/getAuthWarn.mjs +391 -0
  102. package/src/js/getCascadePreview.mjs +186 -0
  103. package/src/js/getDelRefCount.mjs +138 -0
  104. package/src/js/getEffMode.mjs +20 -0
  105. package/src/js/getKpLossy.mjs +83 -0
  106. package/src/js/getNameErrs.mjs +194 -0
  107. package/src/js/getOwn.mjs +1 -1
  108. package/src/js/getPropKey.mjs +37 -0
  109. package/src/js/isAuthMode.mjs +20 -0
  110. package/src/js/joinNames.mjs +24 -0
  111. package/src/js/pairRowsToSnapshot.mjs +84 -0
  112. package/src/js/refAudit.mjs +287 -0
  113. package/src/js/refCascade.mjs +500 -0
  114. package/src/js/relDialogSend.mjs +127 -0
  115. package/src/js/rowDigest.mjs +193 -0
  116. package/src/plugins/dataReloadGuard.mjs +37 -0
  117. package/src/plugins/mShare.mjs +147 -174
  118. package/src/plugins/mUI.mjs +3 -8
  119. package/src/schema/tables/users.mjs +152 -152
  120. package/dist/css/app.6918ec45.css +0 -1
  121. package/dist/js/app-legacy.963e800c.js +0 -1
  122. package/dist/js/app.963e800c.js +0 -1
@@ -198,6 +198,30 @@
198
198
 
199
199
  </div>
200
200
 
201
+ <!-- 外部列提示與外部同步警示橫幅(bannerLines 為空時 v-for 不渲染任何節點, 畫面與 DOM 同未啟用前) -->
202
+ <div
203
+ v-for="(b, kb) in bannerLines"
204
+ :key="kb"
205
+ :data-fmid="`targets-banner-${b.kind}`"
206
+ :style="getBannerStyle(b.kind)"
207
+ >
208
+ <svg v-if="b.kind==='ext'" style="vertical-align:sub; margin-right:4px;" width="16" height="16" viewBox="0 0 24 24"><path :d="mdiLock" fill="#666"></path></svg>
209
+ <img v-else style="vertical-align:sub; width:16px; height:16px; margin-right:4px;" :src="$ui.getIcon('warning')" />
210
+ <span>{{ b.text }}</span>
211
+ </div>
212
+
213
+ <!-- 資料已被更新提示(R-PRINCIPLE-00 原則 7): 有未存編輯而他人或外部同步改了本表時, 保留草稿並提示, 存檔將被伺服端拒收; 以 v-for 渲染(無提示時不渲染任何節點, 畫面與 DOM 同未啟用前) -->
214
+ <div
215
+ v-for="ks in (staleNotice ? 1 : 0)"
216
+ :key="`stale-${ks}`"
217
+ data-fmid="targets-banner-stale"
218
+ :style="getBannerStyle('warn')"
219
+ >
220
+ <img style="vertical-align:sub; width:16px; height:16px; margin-right:4px;" :src="$ui.getIcon('warning')" />
221
+ <span>{{ $t('staleDraftNotice') }}</span>
222
+ <button class="staleReloadBtn" data-fmid="targets-banner-stale-reload" @click="reloadDraft">{{ $t('staleDraftReload') }}</button>
223
+ </div>
224
+
201
225
  </div>
202
226
 
203
227
  <template
@@ -213,6 +237,7 @@
213
237
  >
214
238
  <template v-slot:cell-render="props">
215
239
  <template v-if="props.key==='id'">
240
+ <span v-for="loc in lockOfRow(props.row)" :key="loc.from" :title="getLockTitle(loc)" style="margin-right:4px;"><svg style="vertical-align:sub;" width="16" height="16" viewBox="0 0 24 24"><path :d="mdiLock" fill="#666"></path></svg></span>
216
241
  <span v-if="errItemsById[props.value]" :title="errItemsById[props.value]">
217
242
  <span style="color:#F57C00;">{{ props.value }}</span>
218
243
  <img style="vertical-align:sub; width:16px; height:16px;" :src="$ui.getIcon('warning')" />
@@ -237,7 +262,7 @@
237
262
  </template>
238
263
 
239
264
  <script>
240
- import { mdiGamepadCircle, mdiStackOverflow, mdiAccountGroupOutline, mdiBallotRecountOutline, mdiCloudUploadOutline, mdiTrashCanOutline, mdiTableHeadersEye, mdiPlus, mdiPencilOutline, mdiContentCopy } from '@mdi/js/mdi.js'
265
+ import { mdiGamepadCircle, mdiStackOverflow, mdiAccountGroupOutline, mdiBallotRecountOutline, mdiCloudUploadOutline, mdiTrashCanOutline, mdiTableHeadersEye, mdiPlus, mdiPencilOutline, mdiContentCopy, mdiLock } from '@mdi/js/mdi.js'
241
266
  import get from 'lodash-es/get.js'
242
267
  import each from 'lodash-es/each.js'
243
268
  import size from 'lodash-es/size.js'
@@ -251,6 +276,12 @@ import isnum from 'wsemi/src/isnum.mjs'
251
276
  import cdbl from 'wsemi/src/cdbl.mjs'
252
277
  import arrPull from 'wsemi/src/arrPull.mjs'
253
278
  import reuseDeletedIds from '../plugins/reuseDeletedIds.mjs'
279
+ import { getSavePreview } from '../js/getCascadePreview.mjs'
280
+ import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
281
+ import { isExternalRow } from '../js/externalRows.mjs'
282
+ import { makeBaseline } from '../js/rowDigest.mjs'
283
+ import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
284
+ import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
254
285
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
255
286
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
256
287
  import WButtonCircle from 'w-component-vue/src/components/WButtonCircle.vue'
@@ -286,6 +317,7 @@ export default {
286
317
  mdiPlus,
287
318
  mdiPencilOutline,
288
319
  mdiContentCopy,
320
+ mdiLock,
289
321
 
290
322
  panelHeight: 100,
291
323
  headHeight: 100,
@@ -329,11 +361,22 @@ export default {
329
361
  widthTargetDescription: null,
330
362
 
331
363
  items: [],
364
+ baseline: [], //載入基準([[id, 內容指紋]]): 與 items 於 changeParams 同一處由同一份來源列產生(草稿所依據之版本), 存檔時隨 RPC 送伺服端比對是否過期(F-26, R-BASELINE-01); 不得於存檔當下由最新 store 重算
365
+ baseRows: [], //草稿所依據之那份 store 列(與 baseline 同時產生): 存檔前之預覽、外部列守門以它為「舊」, 不用已變新之 store(有未存編輯時 store 可能已被推播更新, 見 draftSync)
366
+ staleNotice: false, //草稿所依據之整表已被他人/外部同步更新(R-PRINCIPLE-00 原則 7): true 時顯示「資料已被更新」橫幅與重新載入鈕
367
+ syncTick: 0, //重新載入觸發器(reloadDraft 遞增): changeParams 讀它而於重算時於 computed 內重建草稿, 使 genOpt 讀取之反應式來源(語系等)的依賴重新收集——保留草稿(keep)時 changeParams 不呼叫 genOpt, 依賴會收縮, 若在 computed 外直接重建則不會恢復(外審 sol 第 4 點)
332
368
  itemsCheck: [],
333
369
  opt: null,
334
370
 
335
371
  }
336
372
  },
373
+ created: function() {
374
+ //syncMark(非響應式; changeParams 讀它而不因它建立響應式依賴, 見 draftSync.planDraftSync): refs/editable 為上次建立草稿時之各表 store 參照與編輯模式, dirty 為未存編輯旗標(isModified)之鏡像(由 watch 維護),
375
+ //baseline 為草稿載入時之基準之副本(供保留草稿時判定是否已過期). 不讓 changeParams 依賴 isModified/baseline: 旗標切換或重新載入不得觸發它重算(其重算=重建草稿, 見 draftSync)
376
+ this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
377
+ //saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
378
+ this.saveMark = { saving: false }
379
+ },
337
380
  mounted: function() {
338
381
  // console.log('mounted')
339
382
 
@@ -360,6 +403,17 @@ export default {
360
403
  }
361
404
 
362
405
  },
406
+ watch: {
407
+ //未存編輯旗標: 維護 syncMark.dirty 鏡像(changeParams 以之判斷「有未存編輯」而不依賴旗標本身). 旗標歸零(已儲存或已放棄)而 store 於保留草稿期間已變新者, 立即依最新 store 重建
408
+ //(儲存成功之推播若先於儲存回應抵達, 其時有未存編輯而被保留; 回應回來旗標歸零時須補上這次重建, 否則畫面停在儲存前之草稿而基準已過期)
409
+ isModified: function(b) {
410
+ let vo = this
411
+ vo.syncMark.dirty = b === true
412
+ if (b !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
413
+ vo.reloadDraft()
414
+ }
415
+ },
416
+ },
363
417
  computed: {
364
418
 
365
419
  syncState: function() {
@@ -396,6 +450,22 @@ export default {
396
450
  return rs
397
451
  },
398
452
 
453
+ lockExternalRows: function() {
454
+ //後台是否不得改外部列(from 為非空字串者; 伺服端 opt.lockExternalRows 經 getWebInfor 帶來, 'y'/'n'; 舊伺服端無此鍵視為未啟用)
455
+ return get(this, 'webInfor.lockExternalRows', '')
456
+ },
457
+
458
+ isLockExternal: function() {
459
+ return this.lockExternalRows === 'y'
460
+ },
461
+
462
+ bannerLines: function() {
463
+ let vo = this
464
+
465
+ //清單上方橫幅各行: 外部列提示(啟用保護且有外部列; 標的無指派欄, 用 Target 專用文案)+本表之外部同步警示; 皆無為空陣列, 不渲染任何節點
466
+ return getBannerLines({ keyTable: 'targets', kind: 'Target', rows: vo.targets, lock: vo.isLockExternal, syncWarns: get(vo, 'webInfor.syncWarns', []), $t: (k) => vo.$t(k) })
467
+ },
468
+
399
469
  changeParams: function() {
400
470
  // console.log('computed changeParams')
401
471
 
@@ -403,18 +473,22 @@ export default {
403
473
 
404
474
  //trigger
405
475
  let isEditable = vo.isEditable
406
-
407
- //items
408
- let items = cloneDeep(vo.targets)
409
-
410
- //save
411
- vo.items = items
412
-
413
- //genOpt
414
- vo.genOpt({ isEditable })
415
-
416
- //firstLoading
417
- vo.firstLoading = false
476
+ let rows = vo.targets
477
+
478
+ //草稿同步(R-PRINCIPLE-00 原則 7, draftSync.planDraftSync): store 變新(推播)時, 無未存編輯 → 重建 items 與基準(同修改前); 有未存編輯 → 保留草稿與其載入時之基準, 不重建, 只判定是否提示「資料已被更新」
479
+ let act = planDraftSync({
480
+ first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
481
+ editChanged: vo.syncMark.editable !== isEditable,
482
+ storeChanged: refsChanged(vo.syncMark.refs, [rows]),
483
+ hasDraft: () => vo.syncMark.dirty === true,
484
+ saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
485
+ })
486
+ if (act === 'rebuild') {
487
+ vo.rebuildDraft(rows, isEditable)
488
+ }
489
+ else if (act === 'keep') {
490
+ vo.staleNotice = isDraftStale({ keys: vo.$ds.targets.keys, rows, baseline: vo.syncMark.baseline })
491
+ }
418
492
 
419
493
  return ''
420
494
  },
@@ -456,11 +530,17 @@ export default {
456
530
  //kpErr
457
531
  let kpErr = Object.create(null) //無原型: 鍵為使用者資料, 不得命中 constructor 等原型鍵(規則帳本 R-KEY-01)
458
532
  let kpId = Object.create(null)
533
+ let kpExt = Object.create(null) //id → 外部列之來源 from(共用該 id 之列中有外部列者; 重複之提示改為「外部來源 X 已使用此名稱」, 以 id 為名)
459
534
  each(rows, (v, k) => {
460
535
 
461
536
  //id
462
537
  let id = get(v, 'id', '')
463
538
 
539
+ //kpExt
540
+ if (isestr(id) && isExternalRow(v) && !haskey(kpExt, id)) {
541
+ kpExt[id] = v.from
542
+ }
543
+
464
544
  //check
465
545
  if (!isestr(id)) {
466
546
 
@@ -474,7 +554,7 @@ export default {
474
554
  if (haskey(kpId, id)) {
475
555
 
476
556
  //kpErr
477
- kpErr[id] = vo.$t(`targetIdDuplicate`)
557
+ kpErr[id] = haskey(kpExt, id) ? fmtVars(vo.$t('nameUsedByExternal'), { name: id, from: kpExt[id] }) : vo.$t(`targetIdDuplicate`)
478
558
 
479
559
  return true //跳出換下一個
480
560
  }
@@ -549,6 +629,68 @@ export default {
549
629
  return head
550
630
  },
551
631
 
632
+ lockOfRow: function(row) {
633
+ let vo = this
634
+
635
+ //啟用外部列保護且該列為外部列者回 [{ from }](首欄顯示鎖頭), 否則回空陣列(供 v-for 渲染 0 或 1 個節點: 無外部列時 DOM 同未啟用前, 不留 v-if 之註解節點)
636
+ if (vo.isLockExternal && isExternalRow(row)) {
637
+ return [{ from: row.from }]
638
+ }
639
+ return []
640
+ },
641
+
642
+ getLockTitle: function(loc) {
643
+ let vo = this
644
+
645
+ return fmtVars(vo.$t('externalRowLocked'), { from: loc.from })
646
+ },
647
+
648
+ getBannerStyle: function(kind) {
649
+
650
+ //橫幅樣式沿用既有色票與間距: 與功能區同為 border-top:1px solid #ddd; 字級 0.8rem(同副標題/waitingData)、水平內距 15px(同 waitingData)、垂直內距 5px(同功能區 padding:5px);
651
+ //外部列提示為中性灰底(#f2f2f2, 同按鈕 hover 底)、文字 #444(同工具鈕圖示色); 外部同步警示為淡橘底(#FFE0B2, 同編輯模式開關之橫條色)搭既有警告圖示; 長來源名稱以 overflow-wrap:anywhere 折行, 窄螢幕不溢出
652
+ let bg = kind === 'warn' ? '#FFE0B2' : '#f2f2f2'
653
+ return `padding:5px 15px; border-top:1px solid #ddd; background:${bg}; color:#444; font-size:0.8rem; overflow-wrap:anywhere;`
654
+ },
655
+
656
+ rebuildDraft: function(rows, isEditable) {
657
+ let vo = this
658
+
659
+ //items
660
+ let items = cloneDeep(rows)
661
+
662
+ //baseline, 與 items 同一處由同一份來源列(items)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
663
+ let baseline = makeBaseline(items, vo.$ds.targets.keys)
664
+
665
+ //save; 重建即放棄未存編輯(isModified 歸零)與過期提示, syncMark 記下本次所依據之 store 參照與編輯模式
666
+ vo.items = items
667
+ vo.baseline = baseline
668
+ vo.baseRows = rows
669
+ vo.isModified = false
670
+ vo.staleNotice = false
671
+ vo.syncMark = { refs: [rows], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
672
+
673
+ //genOpt
674
+ vo.genOpt({ isEditable })
675
+
676
+ //firstLoading
677
+ vo.firstLoading = false
678
+
679
+ },
680
+
681
+ curRefs: function() {
682
+ //目前各來源 store 之參照(與 changeParams 之 refsChanged 同一組)
683
+ let vo = this
684
+ return [vo.targets]
685
+ },
686
+
687
+ reloadDraft: function() {
688
+ //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
689
+ //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
690
+ let vo = this
691
+ vo.syncTick++
692
+ },
693
+
552
694
  genOpt: function() {
553
695
  // console.log('methods genOpt')
554
696
 
@@ -573,10 +715,13 @@ export default {
573
715
  let kpRowDrag = {}
574
716
  let kpHeadCheckBox = {}
575
717
  if (vo.isEditable) {
718
+
719
+ //外部列(from 非空)之身分/結構欄逐列唯讀(啟用保護時以函數判定: w-aggrid-vue 之 setobj 原樣取 kpCellEditable[key] 指給 ag-grid colDef.editable, 其接受 (params) => boolean);
720
+ //標的無指派欄; from 欄於所有列皆唯讀(外部同步所擁有, 後台新增列 from 恆為空)
721
+ let editable = vo.isLockExternal ? (params) => !isExternalRow(params.data) : true
576
722
  kpCellEditable = {
577
- 'id': true,
578
- 'description': true,
579
- 'from': true,
723
+ 'id': editable,
724
+ 'description': editable,
580
725
  }
581
726
  kpRowDrag = {
582
727
  'id': true,
@@ -770,6 +915,7 @@ export default {
770
915
  let _r = vo.$ds.targets.funNew()
771
916
  r.id = _r.id //使用新建方式預先產生id避免重複
772
917
  r.id = vo.$s.getNameNew(rows, 'id', nameOld, vo.$t('targetCopyNameNew'))
918
+ r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
773
919
  r.userId = `{${vo.$t('targetAddIdNew')}}`
774
920
  r.timeCreate = `{${vo.$t('targetAddIdNew')}}`
775
921
  r.userIdUpdate = `{${vo.$t('targetAddIdNew')}}`
@@ -803,6 +949,28 @@ export default {
803
949
  return
804
950
  }
805
951
 
952
+ //外部列保護(啟用時): 外部列(from 非空)不得刪除(伺服端 saveExternalRowChanged), 先過濾掉並以 showCheckYes 提示被略過之筆數(警告, 非 success), 使用者按確定後其餘照常;
953
+ //全為外部列則不刪除任何列
954
+ let sp = splitCheckedRows(vo.itemsCheck, vo.isLockExternal)
955
+ if (sp.nBlocked > 0) {
956
+ vo.$dg.showCheckYes(fmtVars(vo.$t('externalRowDelBlocked'), { n: sp.nBlocked }), { type: 'warning' })
957
+ .then(() => {
958
+ if (size(sp.rows) > 0) {
959
+ vo.deleteItems(sp.rows)
960
+ }
961
+ })
962
+ return
963
+ }
964
+
965
+ vo.deleteItems(sp.rows)
966
+
967
+ },
968
+
969
+ deleteItems: function(itemsDel) {
970
+ // console.log('method deleteItems')
971
+
972
+ let vo = this
973
+
806
974
  //cloneDeep
807
975
  let rows = get(vo, 'opt.rows', [])
808
976
 
@@ -810,7 +978,7 @@ export default {
810
978
  rows = cloneDeep(rows)
811
979
 
812
980
  //filter
813
- each(vo.itemsCheck, (v) => {
981
+ each(itemsDel, (v) => {
814
982
  // console.log('v', v)
815
983
  let id = get(v, 'data.id', '')
816
984
  if (!isestr(id)) {
@@ -856,8 +1024,9 @@ export default {
856
1024
  return
857
1025
  }
858
1026
 
859
- //rows
1027
+ //rows, baseline: 與 rows 同一刻(同步)擷取——兩者於 changeParams 同一處同時產生, 之後確認框期間若推播重建清單, 仍帶「草稿所依據之那一版」之基準(F-26), 不由最新 store 重算
860
1028
  let rows = get(vo, 'opt.rows', [])
1029
+ let baseline = cloneDeep(vo.baseline)
861
1030
 
862
1031
  //check
863
1032
  if (size(rows) === 0) {
@@ -866,20 +1035,70 @@ export default {
866
1035
  return
867
1036
  }
868
1037
 
1038
+ //外部列保護(啟用時): 外部列之身分/結構欄(含 id)以逐列唯讀與刪除過濾擋下, 但貼上(Ctrl+V)等路徑可繞過逐列唯讀, 送出前再比對一次(基準為 store 之 targets);
1039
+ //有違規即不送出並提示(伺服端同樣整次拒收 saveExternalRowChanged); 於開 loading 之前, 訊息框前釋放按鈕鎖
1040
+ if (vo.isLockExternal && getExternalEdits({ keyTable: 'targets', keys: vo.$ds.targets.keys, rowsNew: rows, rowsOld: vo.baseRows }) !== null) {
1041
+ unlockBtn()
1042
+ await vo.$dg.showCheckYes(vo.$t('saveExternalRowChanged'), { type: 'warning' })
1043
+ return
1044
+ }
1045
+
869
1046
  //新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
870
1047
  //(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025);
871
1048
  //本表 id 可就地改寫: 新增列之 id 改成本次刪除(或改掉 id)之既有標的者, 淨效果為修改該標的, 送出前改為一般列並沿用其建立者 / 建立時間,
872
1049
  //否則被後端當重送擋下(重新整理後重做仍被擋) (見 reuseDeletedIds)
873
- let rowsSend = reuseDeletedIds(rows, vo.targets)
1050
+ let rowsSend = reuseDeletedIds(rows, vo.baseRows)
1051
+
1052
+ //2) 引用連帶預覽(ADR-036、R-REF-CASCADE-01; 與伺服端同一純函數 planRefChanges, 基準為 store 之 targets/pemis, 以實際送出之列 rowsSend 規劃, 筆數以伺服端實際為準):
1053
+ //標的 id 即名稱, 可就地改寫, 改 id 對伺服端等同刪舊增新(不級聯), 引用該標的之 pemis.crules 項目於同一次寫入內被連帶清理(刪除亦同); 有權限之規則將被清理時先確認,
1054
+ //確認框文字之判定見 src/js/getCascadePreview.mjs(標的頁只確認、不改成功訊息)
1055
+ let pv = getSavePreview({ kind: 'Target', rowsOld: vo.baseRows, rowsNew: rowsSend, holdersOld: vo.pemis, $t: (k) => vo.$t(k) })
1056
+
1057
+ //新增撞懸空名(F-23/F-26 原則 7; src/js/getAddImpact.mjs): 本次新增之標的 id(含就地改寫成新 id 者)查無既有同名實體, 而仍有啟用中之權限 crules 規則指向該 id——新增後該規則即對新標的生效,
1058
+ //須明確同意才送出; 以實際送出之列 rowsSend 規劃(同上), 與預覽同於開 loading 之前同步計算(基準為 store 之 targets/pemis), 無懸空引用時不彈框; 與刪除/改 id 預覽依序(先預覽、再新增確認), 各自只在需要時彈出
1059
+ let addText = getAddConfirmText(getAddImpact({ keyTable: 'targets', rowsOld: vo.baseRows, rowsNew: rowsSend, holders: vo.pemis }), (k) => vo.$t(k), 'Target')
1060
+
1061
+ if (pv.confirmText !== '') {
1062
+
1063
+ //先開確認框之流程: 開框前 unlockBtn() 釋放儲存鈕之 promiseUnlock 鎖(同上, 按鈕鎖只涵蓋請求期間); 流程狀態仍由 runSubmit 保持至本流程結束,
1064
+ //確認框與後續請求期間之重入(焦點仍在儲存鈕時 Enter)皆被略過 (ADR-025)
1065
+ unlockBtn()
1066
+ let isGo = await vo.$dg.showCheckYesNo(pv.confirmText)
1067
+ .then(() => true)
1068
+ .catch(() => false)
1069
+
1070
+ //選否(或確認框被取代而視同選否): 中止儲存, 不送出; 尚未開 loading, 資料與 isModified 保留, 由 runSubmit 於回傳後結束流程
1071
+ if (!isGo) {
1072
+ return
1073
+ }
874
1074
 
875
- //2) 確定打 API 才開 loading
1075
+ }
1076
+
1077
+ //新增撞懸空名之確認(同上之鎖釋放與選否中止; 單一框、單一問句)
1078
+ if (addText !== '') {
1079
+ unlockBtn()
1080
+ let isGoAdd = await vo.$dg.showCheckYesNo(addText)
1081
+ .then(() => true)
1082
+ .catch(() => false)
1083
+ if (!isGoAdd) {
1084
+ return
1085
+ }
1086
+ }
1087
+
1088
+ //3) 確定打 API 才開 loading
876
1089
  vo.$ui.updateLoading(true)
877
1090
 
878
- //3) updateTargets, 各自 catch + 旗標短路
1091
+ //4) updateTargets, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
879
1092
  let okSave = false
880
- await vo.$fapi.updateTargets(rowsSend)
881
- .then(() => { okSave = true })
1093
+ vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1094
+ await vo.$fapi.updateTargets(rowsSend, { baseline })
1095
+ .then(() => {
1096
+ okSave = true
1097
+ vo.saveMark.saving = false
1098
+ })
882
1099
  .catch(async (err) => {
1100
+ vo.saveMark.saving = false
1101
+ vo.staleNotice = isDraftStale({ keys: vo.$ds.targets.keys, rows: vo.targets, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
883
1102
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
884
1103
  await vo.$dg.showCheckYes(`${vo.$t('targetSaveTargetsFail')}: ${vo.$tErr(err)}`)
885
1104
  })
@@ -890,7 +1109,7 @@ export default {
890
1109
 
891
1110
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
892
1111
  vo.$ui.updateLoading(false)
893
- await vo.$dg.showCheckYes(vo.$t('targetSaveTargetsSuccess'), { type: 'success' })
1112
+ await vo.$dg.showCheckYes(vo.$t('targetSaveTargetsSuccess'), { type: 'success' }) //標的頁之成功訊息不變(被清理之權限數已於確認框告知)
894
1113
 
895
1114
  }
896
1115
 
@@ -903,6 +1122,9 @@ export default {
903
1122
  })
904
1123
  .finally(() => {
905
1124
 
1125
+ //非預期例外時亦歸零, 避免之後之推播永遠被當成自己存檔所致
1126
+ vo.saveMark.saving = false
1127
+
906
1128
  //hide loading
907
1129
  vo.$ui.updateLoading(false)
908
1130
 
@@ -916,4 +1138,18 @@ export default {
916
1138
  </script>
917
1139
 
918
1140
  <style scoped>
1141
+ /* 「資料已被更新」橫幅之重新載入鈕: 底色 #fff(同工具鈕底)、hover #f2f2f2(同工具鈕 hover)、邊線 #ddd(同橫幅邊線)、文字 #444(同橫幅文字), 與橫幅同字級 */
1142
+ .staleReloadBtn {
1143
+ margin-left: 8px;
1144
+ padding: 1px 10px;
1145
+ font-size: 0.8rem;
1146
+ color: #444;
1147
+ background: #fff;
1148
+ border: 1px solid #ddd;
1149
+ border-radius: 4px;
1150
+ cursor: pointer;
1151
+ }
1152
+ .staleReloadBtn:hover {
1153
+ background: #f2f2f2;
1154
+ }
919
1155
  </style>