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
@@ -126,6 +126,17 @@
126
126
 
127
127
  </div>
128
128
 
129
+ <!-- 資料已被更新提示(R-PRINCIPLE-00 原則 7): 開框後他人或外部同步改了群組表, 本視窗之草稿與基準維持開框時之版本、不重建, 存檔將被伺服端拒收; 以 v-for 渲染(無提示時不渲染任何節點) -->
130
+ <div
131
+ v-for="ks in (staleNotice ? 1 : 0)"
132
+ :key="`stale-${ks}`"
133
+ data-fmid="pemi-blng-grups-banner-stale"
134
+ style="padding:5px 15px; border-top:1px solid #ddd; background:#FFE0B2; color:#444; font-size:0.8rem; overflow-wrap:anywhere;"
135
+ >
136
+ <img style="vertical-align:sub; width:16px; height:16px; margin-right:4px;" :src="$ui.getIcon('warning')" />
137
+ <span>{{ $t('staleDraftNoticeDialog') }}</span>
138
+ </div>
139
+
129
140
  </div>
130
141
 
131
142
  <template
@@ -140,16 +151,32 @@
140
151
  :opt="opt"
141
152
  >
142
153
  <template v-slot:cell-render="props">
154
+ <!-- 警告圖示: 該列群組之 cpemis 有衝突時於「所屬權限名稱」儲存格最左顯示, 理由與版面同 VeGrupBlngUsers 該處註解(無警告者與修前同一節點) -->
155
+ <div
156
+ style="display:flex; align-items:center; height:100%;"
157
+ v-if="props.key==='pemisNames' && props.row.warnText"
158
+ >
159
+ <span style="flex:0 0 auto; display:flex; align-items:center;" :title="props.row.warnText"><img style="vertical-align:sub; width:16px; height:16px; margin-right:4px;" :src="$ui.getIcon('warning')" /></span>
160
+ <RelationChips
161
+ style="flex:1 1 auto;"
162
+ :items="props.row.pemis"
163
+ :editable="isEditable"
164
+ :popupTitle="$t('belongPemisNames')"
165
+ :tooltipAll="$t('chipsShowAll')"
166
+ :labelContent="'modeSelect'"
167
+ @input="(item)=>{showVePemiBlngGrupsToggleItemModeByName(props.row.name, item, props.row.id)}"
168
+ ></RelationChips>
169
+ </div>
143
170
  <RelationChips
144
- v-if="props.key==='pemisNames'"
171
+ v-else-if="props.key==='pemisNames'"
145
172
  :items="props.row.pemis"
146
173
  :editable="isEditable"
147
174
  :popupTitle="$t('belongPemisNames')"
148
175
  :tooltipAll="$t('chipsShowAll')"
149
176
  :labelContent="'modeSelect'"
150
- @input="(item)=>{showVePemiBlngGrupsToggleItemModeByName(props.row.name, item)}"
177
+ @input="(item)=>{showVePemiBlngGrupsToggleItemModeByName(props.row.name, item, props.row.id)}"
151
178
  ></RelationChips>
152
- <input v-else-if="props.key==='enable'" type="checkbox" :checked="props.value === 'y'" @click="showVePemiBlngGrupsToggleItemEnableByName(props.row.name)" :disabled="!isEditable" />
179
+ <input v-else-if="props.key==='enable'" type="checkbox" :checked="props.value === 'y'" @click="showVePemiBlngGrupsToggleItemEnableByName(props.row.name, props.row.id)" :disabled="!isEditable" />
153
180
  <span v-else>{{ props.value }}</span>
154
181
  </template>
155
182
  </WAggridVue>
@@ -181,7 +208,6 @@ import size from 'lodash-es/size.js'
181
208
  import filter from 'lodash-es/filter.js'
182
209
  import join from 'lodash-es/join.js'
183
210
  import sortBy from 'lodash-es/sortBy.js'
184
- import isEqual from 'lodash-es/isEqual.js'
185
211
  import cloneDeep from 'lodash-es/cloneDeep.js'
186
212
  import genPm from 'wsemi/src/genPm.mjs'
187
213
  import haskey from 'wsemi/src/haskey.mjs'
@@ -189,6 +215,16 @@ import delay from 'wsemi/src/delay.mjs'
189
215
  import iseobj from 'wsemi/src/iseobj.mjs'
190
216
  import isestr from 'wsemi/src/isestr.mjs'
191
217
  import eachOwn from '../js/eachOwn.mjs'
218
+ import getOwn from '../js/getOwn.mjs'
219
+ import cvKpToObj from '../js/cvKpToObj.mjs'
220
+ import getKpLossy from '../js/getKpLossy.mjs'
221
+ import fmtLossyMsg from '../js/fmtLossyMsg.mjs'
222
+ import pairRowsToSnapshot from '../js/pairRowsToSnapshot.mjs'
223
+ import { makeBaseline } from '../js/rowDigest.mjs'
224
+ import { isDraftStale } from '../js/draftSync.mjs'
225
+ import { makeRowStates, getChangedIdx, buildRowsSend } from '../js/relDialogSend.mjs'
226
+ import { getRefState, getRawModeMap, putRefEntry } from '../js/editorRefRows.mjs'
227
+ import { getRefWarns, getRefWarnText } from '../js/getAuthWarn.mjs'
192
228
  import WDialog from 'w-component-vue/src/components/WDialog.vue'
193
229
  import WButtonCircle from 'w-component-vue/src/components/WButtonCircle.vue'
194
230
  import WAggridVue from 'w-aggrid-vue/src/components/WAggridVue.vue'
@@ -228,9 +264,16 @@ export default {
228
264
  firstSetting: true,
229
265
  isEditable: false,
230
266
  isModified: false,
267
+ ownSaving: false, //本視窗自己之存檔已送出(成功後視窗隨即關閉): 自己之寫入所致之推播(grups 表更新)不是「他人更新」, watch 不得據以提示; 每次 show() 歸零, 存檔失敗時歸零
231
268
 
232
269
  pemi: null,
233
270
 
271
+ //baseline, rowStates: 開框(show)時綁定之草稿基準(F-26, R-BASELINE-01), 之後 store 推播變新不更新, 每次 show() 重新綁定;
272
+ //baseline 為草稿所依據之那份群組列之逐列內容指紋, 存檔隨 updateGrups 第 2 參送伺服端; rowStates 為開框時各列之歸屬狀態, 存檔時判定使用者有無變更該列
273
+ baseline: null,
274
+ rowStates: null,
275
+ staleNotice: false, //有未存編輯時收到推播、且群組表已異於本視窗載入時之基準(R-PRINCIPLE-00 原則 7): true 時顯示「資料已被更新」提示; 每次 show() 歸零
276
+
234
277
  items: [],
235
278
  itemsCheck: [],
236
279
  opt: null,
@@ -260,6 +303,17 @@ export default {
260
303
  }
261
304
 
262
305
  },
306
+ watch: {
307
+ //store 之群組表被推播更新(整表以新陣列取代): 本視窗不重建(草稿與基準維持開框時之版本, 見 show()). 此刻有未存編輯者, 若表已異於載入時之基準(任一列內容不同或列增刪; 僅 order/伺服器管理欄不同不算)
308
+ //即顯示「資料已被更新」提示, 存檔時由伺服端以表雜湊拒收(saveStale); 無未存編輯者維持原行為(靜默, 存檔時才由伺服端拒收). 唯讀檢視無存檔, 不提示
309
+ grups: function(rows) {
310
+ let vo = this
311
+ if (!vo.bShow || !vo.isEditable || !vo.isModified || vo.ownSaving || !Array.isArray(vo.baseline)) {
312
+ return
313
+ }
314
+ vo.staleNotice = isDraftStale({ keys: vo.$ds.grups.keys, rows, baseline: vo.baseline })
315
+ },
316
+ },
263
317
  computed: {
264
318
 
265
319
  syncState: function() {
@@ -543,6 +597,23 @@ export default {
543
597
  return rows
544
598
  },
545
599
 
600
+ getDisplayRows: function() {
601
+ let vo = this
602
+
603
+ //cmp
604
+ let cmp = get(vo, '$refs.rftable')
605
+ // console.log('cmp', cmp)
606
+
607
+ //getDisplayData 只回傳顯示中之欄位(name/enable/pemisNames), 不含 id; 批次勾選須以 id 定位列(群組表可有歷史同名), 故另以 ag-grid api 取篩選排序後之完整列(含 id), 同 VeGrupBlngUsers
608
+ let api = cmp.getApi()
609
+ let rows = []
610
+ api.forEachNodeAfterFilterAndSort((node) => {
611
+ rows.push(node.data)
612
+ })
613
+
614
+ return rows
615
+ },
616
+
546
617
  fltKpPemi: function(kp) {
547
618
  // console.log('fltKpPemi', kp)
548
619
 
@@ -574,8 +645,12 @@ export default {
574
645
  //items
575
646
  let items = map(grups, (g) => {
576
647
 
577
- //kpPemi
578
- let kpPemi = JSON5.parse(g.cpemis)
648
+ //kpPemi, 空字串(新增列之 funNew 預設值)與壞 JSON 視為空容器, 同 VeCpemis/清單頁/伺服端(R-REL-PARSE-01: 任一列解碼失敗不得使對話框打不開)
649
+ let kpPemi = Object.create(null)
650
+ try {
651
+ kpPemi = cvKpToObj(JSON5.parse(g.cpemis), { hasMode: true })
652
+ }
653
+ catch (err) {}
579
654
  // console.log(g.name, 'kpPemi', kpPemi)
580
655
 
581
656
  //fltKpPemi
@@ -589,16 +664,12 @@ export default {
589
664
  eachOwn(kpPemi, (v, k) => {
590
665
  // console.log(k, 'v', v)
591
666
 
592
- //_isActive, 此為使用者cpemis內設定, 可為y或n, 僅提取y, 再判斷此使用者是否有使用此pemi
593
- let _isActive = get(v, 'isActive', '')
594
- if (_isActive !== 'y') {
667
+ //st, 此為使用者cpemis內設定, 可為y或n, 僅提取y, 再判斷此使用者是否有使用此pemi; 與伺服端同解讀(getRefState, 理由同 VeGrupBlngUsers.genItems)
668
+ let st = getRefState(v)
669
+ if (st.enable !== 'y') {
595
670
  return true //跳出換下一個
596
671
  }
597
- // console.log(k, '_isActive', _isActive)
598
-
599
- //_mode
600
- let _mode = get(v, 'mode', '')
601
- // console.log(k, '_mode', _mode)
672
+ // console.log(k, 'st', st)
602
673
 
603
674
  //b, 使用者是否擁有指定權限群組
604
675
  let b = k === pemi.name
@@ -607,14 +678,15 @@ export default {
607
678
  //mode, isActive
608
679
  if (b) {
609
680
  enable = 'y'
610
- mode = _mode //使用使用者給予cpemis內指定權限群組之合併權限模式
681
+ mode = st.mode //使用使用者給予cpemis內指定權限群組之合併權限模式
611
682
  // console.log('使用使用者給予cpemis內指定權限群組之合併權限模式 pemi', pemi)
612
683
  }
613
684
 
614
- //push
685
+ //push, 僅供 RelationChips 顯示(name/mode/enable); 存檔不讀本陣列(doSave 以 putRefEntry 自快照字串只改本權限一項)
615
686
  pemis.push({
616
687
  name: k,
617
- ...v,
688
+ mode: st.mode,
689
+ isActive: 'y',
618
690
  enable: b ? 'y' : 'n', //給render使用
619
691
  })
620
692
 
@@ -626,13 +698,19 @@ export default {
626
698
  pemisNames = join(pemisNames, ' ')
627
699
  // console.log(g.name, 'pemisNames', pemisNames)
628
700
 
701
+ //warnText: 該群組 cpemis 之衝突提示(理由同 VeGrupBlngUsers.genItems; revRows 不回寫本欄, 警告反映開啟時之已儲存資料)
702
+ let resWarn = getRefWarns(g.cpemis, vo.pemis)
703
+ let warnText = resWarn.hasWarn ? getRefWarnText(resWarn, vo.$t, 'warnKindPemi') : ''
704
+
629
705
  return {
706
+ id: g.id, //列識別: 存檔時以 id(並驗 name)配對存檔當下之群組快照, 不以列索引(R-REL-PAIR-01)
630
707
  name: g.name,
631
708
  pemisNames,
632
709
  mode,
633
710
  enable,
634
711
  // kpPemi,
635
712
  pemis,
713
+ warnText,
636
714
  }
637
715
  })
638
716
  // console.log(g.name, 'items', items)
@@ -657,11 +735,20 @@ export default {
657
735
  let rows = get(vo, 'opt.rows', [])
658
736
  // console.log('rows', rows)
659
737
 
738
+ //以列身分(id)配對: 對話框之列 ↔ 目前 store 中同一個群組(R-REL-PAIR-01; 不得以列索引配對——store 於對話框開啟期間可被更新〔他人增刪、改 order〕而錯位), 並驗 name 相同(改名視為過期)
739
+ //krs, grupsRev: 對話框有列且 store 仍存在之群組(grupsRev[k] 即第 krs[k] 列所配到者; 配對不到者不更新其顯示, 存檔時由 doSave 整次拒存)
740
+ let { krs, matched: grupsRev } = pairRowsToSnapshot(rows, grups, { verifyKeys: ['name'] })
741
+
660
742
  //grups
661
- grups = map(grups, (g, kg) => {
743
+ grups = map(grupsRev, (g, kg) => {
744
+ let row = get(rows, krs[kg], {})
662
745
 
663
- //kpPemi
664
- let kpPemi = JSON5.parse(g.cpemis)
746
+ //kpPemi, 空字串與壞 JSON 視為空容器(R-REL-PARSE-01)
747
+ let kpPemi = Object.create(null)
748
+ try {
749
+ kpPemi = cvKpToObj(JSON5.parse(g.cpemis), { hasMode: true })
750
+ }
751
+ catch (err) {}
665
752
  // console.log(g.name, 'kpPemi', kpPemi)
666
753
 
667
754
  //fltKpPemi
@@ -669,7 +756,7 @@ export default {
669
756
  // console.log(g.name, 'kpPemi(fltKpPemi)', kpPemi)
670
757
 
671
758
  //mode
672
- let mode = get(rows, `${kg}.mode`, '')
759
+ let mode = get(row, 'mode', '')
673
760
  if (mode !== 'OR' && mode !== 'AND') {
674
761
  console.log(`非預期: mode應該皆已指定不應觸發`, mode)
675
762
  mode = 'OR'
@@ -677,7 +764,7 @@ export default {
677
764
  // console.log(g.name, 'mode', mode)
678
765
 
679
766
  //enable
680
- let enable = get(rows, `${kg}.enable`, '')
767
+ let enable = get(row, 'enable', '')
681
768
  if (enable !== 'y' && enable !== 'n') {
682
769
  console.log(`非預期: enable應該皆已指定不應觸發`, enable)
683
770
  enable = 'n'
@@ -710,8 +797,8 @@ export default {
710
797
  let items = vo.genItems(pemi, grups)
711
798
  // console.log('items', items)
712
799
 
713
- //update pemisNames, pemis
714
- each(items, (r, kr) => {
800
+ //update pemisNames, pemis, items 與 krs 同序(item k 對應對話框第 krs[k] 列)
801
+ each(items, (r, k) => {
715
802
  // name: g.name,
716
803
  // pemisNames,
717
804
  // mode,
@@ -719,6 +806,7 @@ export default {
719
806
  // pemis,
720
807
 
721
808
  //set
809
+ let kr = krs[k]
722
810
  set(vo, `opt.rows[${kr}].pemisNames`, r.pemisNames)
723
811
  set(vo, `opt.rows[${kr}].pemis`, r.pemis)
724
812
  // console.log('vo.opt.rows[kr]', cloneDeep(vo.opt.rows[kr]))
@@ -730,8 +818,8 @@ export default {
730
818
 
731
819
  },
732
820
 
733
- showVePemiBlngGrupsToggleItemModeByName: function(name, mode) {
734
- // console.log('showVePemiBlngGrupsToggleItemModeByName', name, mode)
821
+ showVePemiBlngGrupsToggleItemModeByName: function(name, mode, id) {
822
+ // console.log('showVePemiBlngGrupsToggleItemModeByName', name, mode, id)
735
823
 
736
824
  let vo = this
737
825
 
@@ -744,11 +832,11 @@ export default {
744
832
  //rows
745
833
  let rows = get(vo, 'opt.rows', [])
746
834
 
747
- //find
835
+ //find, 有 id 以 id 找列(群組表可有歷史同名〔外部同步〕, 以 name 會改到第一個同名群組、存檔依 id 寫入錯列; 同 VeGrupBlngUsers); 未給 id 之既有 $dg 呼叫端維持以 name 找
748
836
  let r = null
749
837
  let kr = null
750
838
  each(rows, (v, k) => {
751
- if (get(v, 'name', '') === name) {
839
+ if (isestr(id) ? get(v, 'id', '') === id : get(v, 'name', '') === name) {
752
840
  r = v
753
841
  kr = k
754
842
  return false //跳出
@@ -777,8 +865,8 @@ export default {
777
865
 
778
866
  },
779
867
 
780
- showVePemiBlngGrupsToggleItemEnableByName: function(name) {
781
- // console.log('showVePemiBlngGrupsToggleItemEnableByName', name)
868
+ showVePemiBlngGrupsToggleItemEnableByName: function(name, id) {
869
+ // console.log('showVePemiBlngGrupsToggleItemEnableByName', name, id)
782
870
 
783
871
  let vo = this
784
872
 
@@ -791,11 +879,11 @@ export default {
791
879
  //rows
792
880
  let rows = get(vo, 'opt.rows', [])
793
881
 
794
- //find
882
+ //find, 理由同 showVePemiBlngGrupsToggleItemModeByName: 有 id 以 id 找列
795
883
  let r = null
796
884
  let kr = null
797
885
  each(rows, (v, k) => {
798
- if (get(v, 'name', '') === name) {
886
+ if (isestr(id) ? get(v, 'id', '') === id : get(v, 'name', '') === name) {
799
887
  r = v
800
888
  kr = k
801
889
  return false //跳出
@@ -834,21 +922,21 @@ export default {
834
922
  let rows = get(vo, 'opt.rows', [])
835
923
  // console.log('rows', rows)
836
924
 
837
- //kpRow
925
+ //kpRow, 以群組 id 為鍵(群組表可有歷史同名, 以 name 會使同名者互相錯位、存檔依 id 寫入錯列; 同 VeGrupBlngUsers)
838
926
  let kpRow = Object.create(null)
839
927
  each(rows, (r, kr) => {
840
- kpRow[r.name] = kr
928
+ kpRow[r.id] = kr
841
929
  })
842
930
 
843
- //rowsEff
844
- let rowsEff = vo.getDisplayData()
931
+ //rowsEff, 篩選排序後之完整列(含 id; getDisplayData 不含 id)
932
+ let rowsEff = vo.getDisplayRows()
845
933
  // console.log('rowsEff', rowsEff)
846
934
 
847
935
  //toggle
848
936
  each(rowsEff, (r) => {
849
937
 
850
938
  //kr
851
- let kr = kpRow[r.name]
939
+ let kr = kpRow[r.id]
852
940
 
853
941
  //set
854
942
  set(vo, `opt.rows[${kr}].enable`, 'y')
@@ -873,21 +961,21 @@ export default {
873
961
  let rows = get(vo, 'opt.rows', [])
874
962
  // console.log('rows', rows)
875
963
 
876
- //kpRow
964
+ //kpRow, 以群組 id 為鍵(群組表可有歷史同名, 以 name 會使同名者互相錯位、存檔依 id 寫入錯列; 同 VeGrupBlngUsers)
877
965
  let kpRow = Object.create(null)
878
966
  each(rows, (r, kr) => {
879
- kpRow[r.name] = kr
967
+ kpRow[r.id] = kr
880
968
  })
881
969
 
882
- //rowsEff
883
- let rowsEff = vo.getDisplayData()
970
+ //rowsEff, 篩選排序後之完整列(含 id; getDisplayData 不含 id)
971
+ let rowsEff = vo.getDisplayRows()
884
972
  // console.log('rowsEff', rowsEff)
885
973
 
886
974
  //toggle
887
975
  each(rowsEff, (r) => {
888
976
 
889
977
  //kr
890
- let kr = kpRow[r.name]
978
+ let kr = kpRow[r.id]
891
979
 
892
980
  //set
893
981
  set(vo, `opt.rows[${kr}].enable`, 'n')
@@ -912,21 +1000,21 @@ export default {
912
1000
  let rows = get(vo, 'opt.rows', [])
913
1001
  // console.log('rows', rows)
914
1002
 
915
- //kpRow
1003
+ //kpRow, 以群組 id 為鍵(群組表可有歷史同名, 以 name 會使同名者互相錯位、存檔依 id 寫入錯列; 同 VeGrupBlngUsers)
916
1004
  let kpRow = Object.create(null)
917
1005
  each(rows, (r, kr) => {
918
- kpRow[r.name] = kr
1006
+ kpRow[r.id] = kr
919
1007
  })
920
1008
 
921
- //rowsEff
922
- let rowsEff = vo.getDisplayData()
1009
+ //rowsEff, 篩選排序後之完整列(含 id; getDisplayData 不含 id)
1010
+ let rowsEff = vo.getDisplayRows()
923
1011
  // console.log('rowsEff', rowsEff)
924
1012
 
925
1013
  //toggle
926
1014
  each(rowsEff, (r) => {
927
1015
 
928
1016
  //kr
929
- let kr = kpRow[r.name]
1017
+ let kr = kpRow[r.id]
930
1018
 
931
1019
  //enable
932
1020
  let _enable = get(r, 'enable', 'n')
@@ -946,16 +1034,19 @@ export default {
946
1034
 
947
1035
  },
948
1036
 
949
- saveGrups: async function(rows) {
950
- // console.log('method saveGrups', rows)
1037
+ saveGrups: async function(rows, baseline) {
1038
+ // console.log('method saveGrups', rows, baseline)
951
1039
 
952
1040
  let vo = this
953
1041
 
954
1042
  //3) updateGrups, 各自 catch + okX 旗標短路 (canonical 五段結構; 空清單檢查已上移至 doSave core())
1043
+ //第 2 參 { baseline }: 開框時綁定之草稿基準(由 doSave 於流程開頭取得並傳入, 不於此讀 vo.baseline——存檔進行中若再次 show() 會換成新草稿之基準); 伺服端比對「本次將寫入之列」之庫內版本, 草稿之後被改過即整次拒收 saveStale(失敗路徑沿用下方 catch 顯示語系文字)
955
1044
  let okSave = false
956
- await vo.$fapi.updateGrups(rows)
1045
+ vo.ownSaving = true //自己之寫入所致之推播不提示(成功路徑維持至視窗關閉; show() 歸零)
1046
+ await vo.$fapi.updateGrups(rows, { baseline })
957
1047
  .then(() => { okSave = true })
958
1048
  .catch(async (err) => {
1049
+ vo.ownSaving = false //失敗(含 saveStale 被拒): 視窗仍開著, 恢復監看
959
1050
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
960
1051
  await vo.$dg.showCheckYes(`${vo.$t('grupSaveGrupsFail')}: ${vo.$tErr(err)}`)
961
1052
  })
@@ -997,6 +1088,10 @@ export default {
997
1088
 
998
1089
  let vo = this
999
1090
 
1091
+ //pmDlg: 本次存檔流程所屬之對話框 promise(外審 sol R2): 存檔進行中(如 await delay)若再次 show() 開啟另一權限, vo.pm 會被換成新視窗之 promise,
1092
+ //舊流程完成時只能 resolve 自己的 promise、且不得關閉新視窗(見下方 .then)
1093
+ let pmDlg = vo.pm
1094
+
1000
1095
  async function core(unlockBtn) {
1001
1096
 
1002
1097
  //1) 事先檢測 (所有同步檢查在開 loading 之前, 空清單不應先閃 loading); 訊息框前以 unlockBtn() 釋放儲存鈕之 promiseUnlock 鎖:
@@ -1009,56 +1104,112 @@ export default {
1009
1104
  return
1010
1105
  }
1011
1106
 
1107
+ //草稿基準與開框時各列之歸屬狀態(F-26, R-BASELINE-01): 於流程開頭(任何 await 之前)取得並以區域變數持有——存檔進行中若再次 show() 會把 vo.baseline/vo.rowStates 換成新草稿之值, 舊草稿之列不可配新草稿之基準
1108
+ let baseline = cloneDeep(vo.baseline)
1109
+ let rowStates = vo.rowStates
1110
+
1111
+ //pemiName: 被編輯之權限名稱, 同樣於流程開頭(任何 await 之前)取得——等待後才讀 vo.pemi.name 會讀到新開啟之實體, 把舊草稿之歸屬寫成指派新實體(外審 sol R2)
1112
+ let pemiName = get(vo, 'pemi.name', '')
1113
+
1114
+ //1b) 無法無損表示之授權資料(R-AUTH-LOSSY-01, 主控複審 R4): 該列群組之 cpemis 含物件形式無法無損表示之格(getKpLossy)者, 本對話框不得改寫其字串——
1115
+ //本對話框所管之歸屬/模式有變更 → 整次儲存拒絕並點名(原子, 其餘列亦不寫); 未變更 → 該列字串逐字元不動(見下方 kpLossyIdx)
1116
+ //群組列之快照只取這一次: 有損檢查之值即下方寫入之值(外審 sol#2: 兩次讀取之間 store 可能被伺服器推送更新, 檢查與寫入看到不同資料)
1117
+ let grups = cloneDeep(vo.grups)
1118
+
1119
+ //1a) 以列身分(id, 並驗 name)配對快照(R-REL-PAIR-01, 主控 run02 F-24): 修前以開框時的列索引取快照列, store 於對話框開啟期間被更新(他人增刪、改 order)時關聯寫到另一個群組;
1120
+ //對話框有列所對應的群組已被刪除或改名者 → 整次拒存(原子, 不寫任何列)並提示重新整理, 不得退而配到其他列
1121
+ let { pairs, missing } = pairRowsToSnapshot(rows, grups, { verifyKeys: ['name'] })
1122
+ if (size(missing) > 0) {
1123
+ unlockBtn()
1124
+ await vo.$dg.showCheckYes(fmtLossyMsg(vo.$t('relationRowsStale'), map(missing, 'name')))
1125
+ return false
1126
+ }
1127
+
1128
+ //1a2) 使用者於本次編輯相對「開框時」有變更之列(F-26, S4; 外審 B.1): 對話框之列是開框時之草稿值, 他人於開框後改過而使用者未碰之列, 不得以草稿值改寫(覆蓋他人之變更);
1129
+ //故只有 kpChanged 之列才對存檔當下快照之字串套用變更(下方 putRefEntry), 其餘列(含開框後新增而不在對話框內者)一律原樣帶回存檔當下 store 之版本(逐字不動), 亦不納入下方有損檢查
1130
+ let kpChanged = getChangedIdx(rows, rowStates)
1131
+
1132
+ let kpLossyIdx = Object.create(null)
1133
+ let kpRawModes = Object.create(null) //列索引 → 該列快照字串之限制項原始 mode(getRawModeMap), 供下方 putRefEntry 寫回未改之限制項
1134
+ let lossyNames = []
1135
+ each(rows, (r, kr) => {
1136
+ if (kpChanged[kr] !== true) {
1137
+ return true //使用者未變更: 原樣帶回, 不檢查有損亦不需 rawModeMap
1138
+ }
1139
+ let g = pairs[kr]
1140
+ let kpRaw = null
1141
+ try {
1142
+ kpRaw = JSON5.parse(get(g, 'cpemis', ''))
1143
+ }
1144
+ catch (err) {
1145
+ return true //無法解析者交由下方原流程處理
1146
+ }
1147
+ kpRawModes[kr] = getRawModeMap(kpRaw)
1148
+ if (size(getKpLossy(kpRaw)) === 0) {
1149
+ return true //跳出換下一個
1150
+ }
1151
+ kpLossyIdx[kr] = true
1152
+ let p = getOwn(cvKpToObj(kpRaw, { hasMode: true }), pemiName, null)
1153
+ let was = get(p, 'isActive', '') === 'y'
1154
+ let wasMode = was ? get(p, 'mode', '') : ''
1155
+ let now = get(r, 'enable', '') === 'y'
1156
+ let nowMode = now ? get(r, 'mode', '') : ''
1157
+ if (was !== now || wasMode !== nowMode) {
1158
+ lossyNames.push(get(g, 'name', ''))
1159
+ }
1160
+ })
1161
+ if (size(lossyNames) > 0) {
1162
+ unlockBtn()
1163
+ await vo.$dg.showCheckYes(fmtLossyMsg(vo.$t('authLossyBlocked'), lossyNames))
1164
+ return false
1165
+ }
1166
+
1012
1167
  //2) 確定打 API 才開 loading
1013
1168
  vo.$ui.updateLoading(true)
1014
1169
 
1015
1170
  //delay
1016
1171
  await delay(300)
1017
1172
 
1018
- //grups
1019
- let grups = cloneDeep(vo.grups)
1020
- // console.log('grups', grups)
1173
+ //grups: 沿用 1b 之快照(見上)
1021
1174
 
1022
- //grupsNew
1023
- let grupsNew = map(rows, (r, kr) => {
1175
+ //kpNewCpemis: 列索引 → 該列新 cpemis 字串; 只收「使用者有變更(kpChanged)、非有損、且 putRefEntry 判定本項確有變更」者, 其餘一律不收(由 buildRowsSend 原樣帶回)
1176
+ let kpNewCpemis = Object.create(null)
1177
+ each(rows, (r, kr) => {
1024
1178
 
1025
- //g
1026
- let g = get(grups, kr, {})
1179
+ //使用者未變更之列(kpChanged)與有損列(kpLossyIdx): 歸屬未變更, 字串逐字元不動
1180
+ if (kpChanged[kr] !== true || kpLossyIdx[kr] === true) {
1181
+ return true //跳出換下一個
1182
+ }
1183
+
1184
+ //g, 以列身分配對之快照列(見 1a)
1185
+ let g = pairs[kr]
1027
1186
 
1028
1187
  //cpemis
1029
1188
  let cpemis = get(g, 'cpemis', '')
1030
1189
 
1031
- //kpPemi
1032
- let kpPemi = JSON5.parse(cpemis)
1190
+ //kpPemi, 空字串與壞 JSON 視為空容器(R-REL-PARSE-01)
1191
+ let kpPemi = Object.create(null)
1192
+ try {
1193
+ kpPemi = cvKpToObj(JSON5.parse(cpemis), { hasMode: true })
1194
+ }
1195
+ catch (err) {}
1033
1196
  // console.log('kpGrup', kpGrup)
1034
1197
 
1035
- //pemis
1036
- let pemis = get(r, 'pemis', [])
1037
-
1038
- //kpPemiNew
1198
+ //kpPemiNew: 只改本權限對應之一項(依列之 enable/mode), 其餘引用項(含懸空、名稱 ''、mode 缺/非法、停用項、純量值)原樣保留, 授權效果不變(putRefEntry; 理由同 VeGrupBlngUsers.doSave)
1199
+ //本項未變更 → 回 false, 字串逐字元不動; 其餘項中之限制項寫回原始 mode(kpRawModes[kr]; 理由同 VeGrupBlngUsers.doSave)
1039
1200
  let kpPemiNew = Object.create(null)
1040
- if (true) {
1041
- each(pemis, (p) => {
1042
- if (p.isActive === 'y') {
1043
- kpPemiNew[p.name] = {
1044
- mode: p.mode,
1045
- isActive: p.isActive,
1046
- }
1047
- }
1048
- })
1049
- }
1201
+ if (putRefEntry(kpPemiNew, kpPemi, pemiName, get(r, 'enable', ''), get(r, 'mode', ''), kpRawModes[kr])) {
1050
1202
 
1051
- //check
1052
- if (!isEqual(kpPemi, kpPemiNew)) {
1053
-
1054
- //cpemis
1055
- g.cpemis = JSON.stringify(kpPemiNew)
1203
+ //cpemis(鍵序無關授權, R-AUTH-MERGE-00 第 4 條: 不需保留原鍵序, 亦無整數式名稱易位之拒存)
1204
+ kpNewCpemis[kr] = JSON.stringify(kpPemiNew)
1056
1205
 
1057
1206
  }
1058
1207
 
1059
- return g
1060
1208
  })
1061
- console.log('grupsNew', grupsNew)
1209
+
1210
+ //grupsNew: 送出之整表=存檔當下 store 快照(grups)之全部列、依其 order 序(F-26 c0, S4): 修前只送「對話框之列」, 開框後他人/外部同步新增之列被伺服端整表 diff 刪除;
1211
+ //只有 kpNewCpemis 之列換其 cpemis 一欄, 其餘列(使用者未變更者、有損列、開框後新增而不在對話框內者)與快照同一物件、逐字不動(帶回存檔當下 store 版本以免誤刪/誤改他人之列; 但載入基準仍是開框時之版本, 伺服端表級比對〔R-BASELINE-01, run11〕會因開框後他人之任何變更而整次拒收 saveStale)
1212
+ let grupsNew = buildRowsSend(grups, pairs, kpNewCpemis, 'cpemis')
1062
1213
 
1063
1214
  // //check
1064
1215
  // if (size(grupsNew) === 0) {
@@ -1066,7 +1217,7 @@ export default {
1066
1217
  // }
1067
1218
 
1068
1219
  //saveGrups, 失敗時回 false: 失敗提示已由 saveGrups 顯示, 此處不關閉對話框(保留未存變更供重按儲存)
1069
- let okSave = await vo.saveGrups(grupsNew)
1220
+ let okSave = await vo.saveGrups(grupsNew, baseline)
1070
1221
  if (!okSave) {
1071
1222
  return false
1072
1223
  }
@@ -1085,10 +1236,12 @@ export default {
1085
1236
  }
1086
1237
 
1087
1238
  //resolve
1088
- vo.pm.resolve(rows)
1239
+ pmDlg.resolve(rows)
1089
1240
 
1090
1241
  //hide
1091
- vo.bShow = false
1242
+ if (vo.pm === pmDlg) { //期間若已 show() 開啟另一視窗, 舊流程不得關閉新視窗
1243
+ vo.bShow = false
1244
+ }
1092
1245
 
1093
1246
  })
1094
1247
  .catch((err) => {
@@ -1134,6 +1287,8 @@ export default {
1134
1287
 
1135
1288
  //default
1136
1289
  vo.isModified = false
1290
+ vo.staleNotice = false
1291
+ vo.ownSaving = false
1137
1292
 
1138
1293
  //isEditable, pemi
1139
1294
  let isEditable = get(msg, 'isEditable', false)
@@ -1145,13 +1300,22 @@ export default {
1145
1300
  //grups
1146
1301
  let grups = cloneDeep(vo.grups)
1147
1302
 
1303
+ //baseline(F-26, R-BASELINE-01; 外審 B.1): 於生成 items 之同一處, 以 items 所依據之那份群組列(grups)計算並綁定為本次草稿之載入基準; 之後 store 推播變新不得更新它
1304
+ //(存檔當下才由最新 store 取基準, 證明的是「store 新」而非「待存之編輯內容新」), 每次 show() 重新綁定. 欄位口徑與伺服端同一(vo.$ds.grups.keys 經 rowDigest.makeBaseline)
1305
+ let baseline = makeBaseline(grups, vo.$ds.grups.keys)
1306
+
1148
1307
  //items
1149
1308
  let items = vo.genItems(pemi, grups)
1150
1309
 
1310
+ //rowStates: 開框時各列之歸屬狀態(須在任何切換操作前由 items 記下; opt.rows 與 items 為同一份陣列, 切換會就地改 enable/mode), 供存檔時判定使用者有無變更該列
1311
+ let rowStates = makeRowStates(items)
1312
+
1151
1313
  //save
1152
1314
  vo.isEditable = isEditable
1153
1315
  vo.pemi = pemi
1154
1316
  vo.items = items
1317
+ vo.baseline = baseline
1318
+ vo.rowStates = rowStates
1155
1319
 
1156
1320
  //genOpt
1157
1321
  vo.genOpt()