w-web-perm 1.1.12 → 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 (124) hide show
  1. package/README.md +158 -123
  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 +23023 -1125
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +232 -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 +2 -2
  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 +2 -2
  31. package/docs/src_js_cvKpToObj.mjs.html +44 -33
  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 +2 -2
  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 +47 -52
  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 +4 -4
  52. package/docs/src_js_joinNames.mjs.html +2 -2
  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 +230 -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 +270 -3
  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/wrapInjected.mjs +1 -1
  79. package/src/components/LayoutContentGrups.vue +396 -75
  80. package/src/components/LayoutContentPemis.vue +395 -75
  81. package/src/components/LayoutContentTargets.vue +261 -25
  82. package/src/components/LayoutContentUsers.vue +334 -97
  83. package/src/components/VeCgrups.vue +88 -129
  84. package/src/components/VeCpemis.vue +86 -129
  85. package/src/components/VeCrules.vue +15 -5
  86. package/src/components/VeGrupBlngUsers.vue +202 -89
  87. package/src/components/VePemiBlngGrups.vue +209 -99
  88. package/src/js/cvKpToObj.mjs +42 -31
  89. package/src/js/draftSync.mjs +70 -0
  90. package/src/js/eachOwn.mjs +1 -1
  91. package/src/js/editorRefRows.mjs +284 -0
  92. package/src/js/externalRows.mjs +266 -0
  93. package/src/js/externalUi.mjs +158 -0
  94. package/src/js/findRefRows.mjs +35 -0
  95. package/src/js/findTouchedDup.mjs +78 -0
  96. package/src/js/fromAudit.mjs +417 -0
  97. package/src/js/getAddImpact.mjs +197 -0
  98. package/src/js/getAuthWarn.mjs +391 -0
  99. package/src/js/getCascadePreview.mjs +186 -0
  100. package/src/js/getDelRefCount.mjs +138 -0
  101. package/src/js/getEffMode.mjs +20 -0
  102. package/src/js/getKpLossy.mjs +44 -49
  103. package/src/js/getNameErrs.mjs +194 -0
  104. package/src/js/getOwn.mjs +1 -1
  105. package/src/js/getPropKey.mjs +37 -0
  106. package/src/js/isAuthMode.mjs +2 -2
  107. package/src/js/pairRowsToSnapshot.mjs +84 -0
  108. package/src/js/refAudit.mjs +287 -0
  109. package/src/js/refCascade.mjs +500 -0
  110. package/src/js/relDialogSend.mjs +127 -0
  111. package/src/js/rowDigest.mjs +193 -0
  112. package/src/plugins/dataReloadGuard.mjs +37 -0
  113. package/src/plugins/mShare.mjs +144 -140
  114. package/src/plugins/mUI.mjs +3 -8
  115. package/src/schema/tables/users.mjs +152 -152
  116. package/dist/css/app.6918ec45.css +0 -1
  117. package/dist/js/app-legacy.7cb7e394.js +0 -1
  118. package/dist/js/app.7cb7e394.js +0 -1
  119. package/docs/src_js_getKeyOrderLost.mjs.html +0 -121
  120. package/docs/src_js_getKpWriteLost.mjs.html +0 -100
  121. package/docs/src_js_keepKeyOrder.mjs.html +0 -139
  122. package/src/js/getKeyOrderLost.mjs +0 -49
  123. package/src/js/getKpWriteLost.mjs +0 -28
  124. package/src/js/keepKeyOrder.mjs +0 -67
@@ -1,8 +1,9 @@
1
1
  import iseobj from 'wsemi/src/iseobj.mjs'
2
- import isestr from 'wsemi/src/isestr.mjs'
3
2
  import eachOwn from './eachOwn.mjs'
4
3
  import cvKpToArr from './cvKpToArr.mjs'
5
4
  import isAuthMode from './isAuthMode.mjs'
5
+ import getEffMode from './getEffMode.mjs'
6
+ import getPropKey from './getPropKey.mjs'
6
7
 
7
8
 
8
9
  /**
@@ -12,19 +13,21 @@ import isAuthMode from './isAuthMode.mjs'
12
13
  * 編輯對話框若依名稱為鍵查值會查不到陣列元素 → 顯示停用 → 管理者存檔即撤銷有效授權. 本函數與伺服端共用 cvKpToArr 之解碼(單一擁有者),
13
14
  * 並使「編輯器顯示啟用 ⇔ 該元素被伺服端採納(納入合併; 元素層級, 非最終權限樹)」.
14
15
  *
15
- * 存檔後授權不變(複審 R3/R4, 規則帳本 R-AUTH-ORDER-01/R-AUTH-LOSSY-01): 解碼後之鍵序即伺服端序, 編輯器以 keepKeyOrder 保留原鍵序後寫出;
16
- * 物件形式無法無損表示之格(同名有效 OR/AND 並存; 有效 AND 名稱 ≥ 2 且第一個 AND 名稱因整數式鍵先排而易位)由 getKpLossy 判定,
17
- * 編輯器對之拒存並顯示固定訊息(authLossyBlocked), 不寫出. 其餘格(含兩個以上 AND 群組)在保序下存檔前後授權相等(unit-cvKpToObj PR-001/PR-001b/PR-003/PR-004 窮舉).
16
+ * 存檔後授權不變(複審 R3/R4, 規則帳本 R-AUTH-LOSSY-01): 合併規則與項目之儲存順序、物件鍵序、陣列元素序無關(R-AUTH-MERGE-00 第 4 條), 故編輯器不需保留原鍵序;
17
+ * 物件形式無法無損表示之格(同名有效 OR/AND 並存)由 getKpLossy 判定, 編輯器對之拒存並顯示固定訊息(authLossyBlocked), 不寫出.
18
+ * 其餘格(含兩個以上 AND 群組)存檔前後授權相等(unit-cvKpToObj PR-001/PR-001b/PR-003/PR-004 窮舉).
18
19
  *
19
- * - opt.hasMode 為 false(預設, crules): 伺服端 mergeRules 以 kpRule[vv.name] = vv.isActive 收斂, 鍵經 JS 屬性鍵轉型(true → 'true'、['T1'] → 'T1'、非物件元素之索引 → '0'),
20
- * 同名後者覆蓋前者; 本函數以同一個指派式 o[r.name] = … 取得相同鍵與相同覆蓋順序(crules 之合併與順序無關, 無有損格).
21
- * - opt.hasMode 為 true(cgrups/cpemis): 伺服端 arrFinder 以 === 比對名稱(非字串名稱不授權), 且 isActive 為 'y' 而 mode 非 OR/AND 者不納入合併(不授權).
22
- * 故僅名稱為非空字串者保留; isActive 為 'y' 而 mode 不合法者改為 'n'(顯示停用——否則存檔會補預設 OR 而把「不授權」變成「授權」); 同名重複者, 各「有效啟用元素」(isActive 'y' 且 mode 合法)
23
- * 模式一致時合為一個啟用項(與伺服端逐項合併等價: 同模式同規則之重複不改變結果); 無有效啟用元素時為 'n'; 同名 AND 與 OR 並存者(物件形式無法無損表示, 由 getKpLossy 判定並拒存)
24
- * 此處僅取 AND 支作為顯示值. 鍵序: 有效啟用元素首次出現之序在前(伺服端 AND 合併之第一個 AND 項依此序), 其後為無有效元素之名稱(首次出現之序);
25
- * 整數式名稱(如 '1')受 JS 屬性序影響恆排前面, 該格由 getKpLossy 判定.
20
+ * - opt.hasMode 為 false(預設, crules): 伺服端 mergeRules 以 kpRule[vv.name] 收斂, 鍵經 JS 屬性鍵轉型(true → 'true'、['T1'] → 'T1'、非物件元素之索引 → '0'),
21
+ * 同名重複列所有列皆 y 才 y(任一列非 y 即 n, 與列序無關; 舊規則為後者覆蓋前者); 本函數以同一個鍵轉型(getPropKey)取得相同鍵與相同收斂結果;
22
+ * 屬性鍵轉型必拋者(name 為 { toString: null } 等物件, 伺服端 mergeRules 同樣略過)略過該元素, 不拋 TypeError(R-KEY-PROP-01).
23
+ * - opt.hasMode 為 true(cgrups/cpemis): 伺服端 findRefRows 以 === 比對名稱, isActive 為 'y' 之引用一律參與合併(R-PRINCIPLE-00 c/e/f): 查無實體(懸空)者內容全 n、
24
+ * mode 缺或非法者視為 AND 限制項(只當交集項、不單獨當基底;該層無合法 mode 之啟用項時全 n; 編輯器存檔時對未被改動者原樣寫回原始 mode, 見 editorRefRows.getRawModeMap, 故不判有損)、名稱為空字串者照常(與名稱為 '' 之列相符, 否則為懸空). 故名稱為字串者(含 '')保留; isActive 為 'y' 而 mode 不合法者改為 mode 'AND'(與伺服端同解讀,
25
+ * 存檔後授權不變——改為停用會丟掉 AND 限制而放寬, 補預設 OR 則多給); 同名重複者, 各啟用元素之有效模式一致時合為一個啟用項(與伺服端逐項合併等價: 同模式同名之重複不改變結果);
26
+ * 無啟用元素時為 'n'; 同名 AND 與 OR 並存者(物件形式無法無損表示, 由 getKpLossy 判定並拒存)此處僅取 AND 支作為顯示值. 名稱非字串之元素(物件形式無法表示)略過, 由 getKpLossy 判定並拒存.
27
+ * 鍵序: 名稱首次出現之序(僅供顯示, 不影響授權).
26
28
  *
27
- * 物件形式: 值為物件者去掉值內 name(外層鍵為名, 規則帳本 R-AUTH-PATH-01: 否則關聯對話框存檔會把授權改指到值內 name 所指之項目); hasMode 時另把 isActive 'y' 而 mode 不合法者改為 'n'.
29
+ * 物件形式: 值為物件者去掉值內 name(外層鍵為名, 規則帳本 R-AUTH-PATH-01: 否則關聯對話框存檔會把授權改指到值內 name 所指之項目); hasMode 時另把 isActive 'y' 而 mode 不合法者改為 mode 'AND'
30
+ * (伺服端以之為限制項; 該層無任何合法 mode 之啟用項時結果全 n, 編輯器存檔對未被改動者原樣寫回原始 mode〔getRawModeMap, 方案 A〕, 不再判有損〔run06 放寬〕); hasMode 時非物件值(舊式簡寫 { G1: 'y' })改為 { isActive: 值 }(值為 'y' 者另加 mode 'AND').
28
31
  * 無需修改時原樣回傳(同一參考, 既有行為不變). 陣列形式一律回傳無原型物件(原型鍵名〔__proto__、constructor、toString〕可為一般鍵).
29
32
  *
30
33
  * @param {Object|Array} kp 輸入 JSON5.parse(cgrups/cpemis/crules) 之結果
@@ -37,7 +40,7 @@ import isAuthMode from './isAuthMode.mjs'
37
40
  * console.log(kp.A)
38
41
  * // => { mode: 'OR', isActive: 'y' }
39
42
  * console.log(cvKpToObj([{ name: 'A', isActive: 'y' }], { hasMode: true }).A)
40
- * // => { isActive: 'n' } (缺 mode: 伺服端不授權)
43
+ * // => { isActive: 'y', mode: 'AND' } (缺 mode: 伺服端視為 AND 限制項, R-PRINCIPLE-00 e)
41
44
  *
42
45
  */
43
46
  function cvKpToObj(kp, opt = {}) {
@@ -56,9 +59,17 @@ function cvKpToObj(kp, opt = {}) {
56
59
  nv = { ...v }
57
60
  delete nv.name
58
61
  if (hasMode && nv.isActive === 'y' && !isAuthMode(nv.mode)) {
59
- nv.isActive = 'n'
62
+ nv.mode = getEffMode(nv.mode) //伺服端視 mode 缺或非法者為 AND(R-PRINCIPLE-00 e): 編輯器同樣顯示並存為 AND, 存檔後授權不變(改為停用或 OR 皆會改變授權)
60
63
  }
61
- changed = changed || ('name' in v) || (nv.isActive !== v.isActive)
64
+ changed = changed || ('name' in v) || (nv.isActive !== v.isActive) || (nv.mode !== v.mode)
65
+ }
66
+ else if (hasMode) {
67
+ //非物件值(舊式簡寫 { G1: 'y' }): 伺服端 cvKpToArr 解為 { name, isActive: 值 }(無 mode)——啟用者 mode 缺, 與 mode 非法同解讀; 編輯器以物件值統一表示
68
+ nv = { isActive: v }
69
+ if (v === 'y') {
70
+ nv.mode = 'AND'
71
+ }
72
+ changed = true
62
73
  }
63
74
  o[k] = nv
64
75
  })
@@ -70,39 +81,39 @@ function cvKpToObj(kp, opt = {}) {
70
81
  let o = Object.create(null)
71
82
  if (!hasMode) {
72
83
  eachOwn(arr, (r) => {
84
+ let k = getPropKey(r.name) //同伺服端 mergeRules: 屬性鍵轉型必拋者(如 { toString: null })不對應任何規則, 略過而非拋 TypeError(R-KEY-PROP-01)
85
+ if (k === null) {
86
+ return true //跳出換下一個
87
+ }
73
88
  let rest = { ...r }
74
89
  delete rest.name
75
- o[r.name] = rest //同伺服端 kpRule[vv.name] = vv.isActive: 鍵轉型與覆蓋順序相同
90
+ if (k in o) {
91
+ rest.isActive = (o[k].isActive === 'y' && rest.isActive === 'y') ? 'y' : 'n' //同伺服端 mergeRules: 同名重複列所有列皆 y 才 y(與列序無關)
92
+ }
93
+ o[k] = rest
76
94
  })
77
95
  return o
78
96
  }
79
97
  let seen = [] //名稱之首次出現序(kpG 為物件, 整數式鍵會被 JS 屬性序重排, 故另存陣列)
80
- let kpG = Object.create(null) //名稱 → { acts: 有效啟用元素, last: 最後一個元素, effAt: 首個有效啟用元素之位置(無則 -1) }
81
- eachOwn(arr, (r, i) => {
82
- if (!isestr(r.name)) { //伺服端 arrFinder 以 === 比對, 非字串(含元素非物件之索引名稱)不授權
98
+ let kpG = Object.create(null) //名稱 → { acts: 有效啟用元素, last: 最後一個元素 }
99
+ eachOwn(arr, (r) => {
100
+ if (typeof r.name !== 'string') { //非字串名稱(含元素非物件之索引名稱)物件形式無法表示: 以 === 比對不到任何群組/權限, 但啟用者仍以懸空引用參與合併, 由 getKpLossy 判定並拒存(R-PRINCIPLE-00 f)
83
101
  return true //跳出換下一個
84
102
  }
85
103
  let rest = { ...r }
86
104
  delete rest.name
87
105
  if (!(r.name in kpG)) {
88
- kpG[r.name] = { acts: [], last: null, effAt: -1 }
106
+ kpG[r.name] = { acts: [], last: null }
89
107
  seen.push(r.name)
90
108
  }
91
109
  kpG[r.name].last = rest
92
- if (rest.isActive === 'y' && isAuthMode(rest.mode)) {
93
- kpG[r.name].acts.push(rest)
94
- if (kpG[r.name].effAt < 0) {
95
- kpG[r.name].effAt = i
96
- }
110
+ if (rest.isActive === 'y') { //啟用元素一律參與合併: mode 缺或非法者視為 AND 限制項(R-PRINCIPLE-00 e; 有合法項時與 AND 等價), 空字串名稱亦保留(R-PRINCIPLE-00 f)
111
+ kpG[r.name].acts.push({ ...rest, mode: getEffMode(rest.mode) })
97
112
  }
98
113
  })
99
114
 
100
- //鍵序(R-AUTH-ORDER-01): 有效啟用元素首次出現之序在前(停用或缺 mode 之元素不算, 否則停用在前者會使伺服端之第一個 AND 項易位), 其後為無有效元素之名稱(首次出現之序)
101
- let names = [
102
- ...seen.filter((n) => kpG[n].effAt >= 0).sort((a, b) => kpG[a].effAt - kpG[b].effAt),
103
- ...seen.filter((n) => kpG[n].effAt < 0),
104
- ]
105
- eachOwn(names, (name) => {
115
+ //鍵序無關授權(R-AUTH-MERGE-00 第 4 條), 依名稱首次出現之序輸出
116
+ eachOwn(seen, (name) => {
106
117
  let g = kpG[name]
107
118
  let modes = []
108
119
  eachOwn(g.acts, (a) => {
@@ -0,0 +1,70 @@
1
+ //清單頁「草稿」對 store 推播之處置(R-PRINCIPLE-00 原則 7, 使用者 2026-10-22 裁示; 規則帳本 R-BASELINE-01、R-DRAFT-KEEP-01; ADR-037 Update(run11)): 純函數(不依賴 Vue/DOM), 四個清單頁共用.
2
+ //
3
+ //背景: 清單頁以 computed(changeParams)監看 store, 資料推播(伺服器約 2 秒輪詢, 他人或外部同步改了表)使 store 變新時, 修改前一律重建 items 與基準, 使管理員尚未儲存之編輯被靜默丟棄,
4
+ //再存回「儲存成功」(verify01 情境 5 實證). 原則 7 要求: 有未存之編輯時收到推播, 保留草稿與其載入時之基準、提示「資料已被更新」, 存檔即被伺服端依表雜湊拒收並提示重新載入; 無未存編輯時照舊重建.
5
+ //
6
+ //changeParams 之重算另有其他觸發(genOpt 內讀取之反應式來源, 如語系切換使表頭文字重算): 那些觸發維持修改前之行為(重建), 只有「store 變新且有未存編輯」才保留草稿.
7
+
8
+ import { isBaselineStale } from './rowDigest.mjs'
9
+
10
+
11
+ /**
12
+ * 判斷來源 store 資料(任一依賴表之列陣列)是否已更新: 逐一比對參照(Vuex 每次推播以新陣列取代整表, 同參照即未更新)
13
+ *
14
+ * @param {Array|null} prev 輸入上次建立草稿時之各表參照陣列(null 為尚未建立過)
15
+ * @param {Array} now 輸入目前各表參照陣列
16
+ * @returns {Boolean} 回傳是否有任一表參照改變(prev 為 null 或長度不同視為已更新)
17
+ */
18
+ function refsChanged(prev, now) {
19
+ if (!Array.isArray(prev) || !Array.isArray(now) || prev.length !== now.length) {
20
+ return true
21
+ }
22
+ return now.some((r, k) => r !== prev[k])
23
+ }
24
+
25
+
26
+ /**
27
+ * 決定清單頁對這次 changeParams 重算之處置
28
+ *
29
+ * - 'keep': 來源 store 變新(推播)而有未存編輯 → 保留草稿與其載入時之基準, 不重建(基準是否已過期由呼叫端以 isDraftStale 判定並提示)
30
+ * - 'keepQuiet': 同 'keep'(保留草稿與基準, 不重建), 但此推播是「自己之存檔請求進行中」所致(伺服端已寫入、回應尚未抵達; run12)——不是他人更新, 呼叫端不得據以顯示過期提示:
31
+ * 存檔成功即重建(草稿被捨棄、提示歸零), 存檔失敗由存檔流程依目前 store 重算提示
32
+ * - 'rebuild': 其餘一律重建 items 與基準(同修改前)——首次載入; 使用者切換編輯模式(使用者自己的動作, 草稿隨之放棄); store 變新且無未存編輯; 其他反應式依賴觸發之重算(如語系切換)
33
+ *
34
+ * hasDraft、saving 以函數傳入、惰性求值: hasDraft 僅於 store 變新時才呼叫(非首次、編輯模式未變), saving 僅於確定要保留草稿時才呼叫。
35
+ *
36
+ * @param {Object} p 輸入參數物件
37
+ * @param {Boolean} p.first 輸入是否尚未建立過草稿(首次)
38
+ * @param {Boolean} p.editChanged 輸入編輯模式是否被切換
39
+ * @param {Boolean} p.storeChanged 輸入來源 store 是否變新(見 refsChanged)
40
+ * @param {Function} p.hasDraft 輸入回傳「是否有未存編輯」之函數
41
+ * @param {Function} [p.saving] 輸入回傳「自己之存檔請求是否進行中」之函數(未給視為否)
42
+ * @returns {String} 回傳 'rebuild'|'keep'|'keepQuiet'
43
+ */
44
+ function planDraftSync({ first, editChanged, storeChanged, hasDraft, saving }) {
45
+ if (first === true || editChanged === true || storeChanged !== true) {
46
+ return 'rebuild'
47
+ }
48
+ if (!(typeof hasDraft === 'function' && hasDraft() === true)) {
49
+ return 'rebuild'
50
+ }
51
+ return (typeof saving === 'function' && saving() === true) ? 'keepQuiet' : 'keep'
52
+ }
53
+
54
+
55
+ /**
56
+ * 草稿所依據之基準相對於目前 store 整表是否已過期(供顯示「資料已被更新」提示; 判準同伺服端 checkBaseline, 單一擁有者 rowDigest.compareBaseline)
57
+ *
58
+ * @param {Object} p 輸入參數物件
59
+ * @param {Array} p.keys 輸入該表 schema 欄位名陣列
60
+ * @param {Array} p.rows 輸入目前 store 之該表列
61
+ * @param {Array} p.baseline 輸入草稿載入時之基準
62
+ * @returns {Boolean} 回傳是否過期(有任一列內容不同或列被增刪; 僅 order/伺服器管理欄不同不算)
63
+ */
64
+ function isDraftStale({ keys, rows, baseline }) {
65
+ return isBaselineStale({ keys, rows, baseline })
66
+ }
67
+
68
+
69
+ export { refsChanged, planDraftSync, isDraftStale }
70
+ export default planDraftSync
@@ -1,5 +1,5 @@
1
1
  //迭代使用端傳入之物件: 供伺服端寫入前驗證(server/validateRows.mjs)、權限評估(src/plugins/mShare.mjs)與前端清單元件解析 cgrups/cpemis/crules 後之物件共用
2
- //規則見 CLAUDE_rulebook.md「迭代使用端傳入之物件」(R-OBJ-ITER-01)
2
+ //規則見 sgud/rule/rulebook.md「迭代使用端傳入之物件」(R-OBJ-ITER-01)
3
3
  //本檔無外部相依
4
4
 
5
5
 
@@ -0,0 +1,284 @@
1
+ import eachOwn from './eachOwn.mjs'
2
+ import getOwn from './getOwn.mjs'
3
+ import cvKpToArr from './cvKpToArr.mjs'
4
+ import getEffMode from './getEffMode.mjs'
5
+ import isAuthMode from './isAuthMode.mjs'
6
+
7
+
8
+ //編輯對話框之「引用清單(cgrups/cpemis) ↔ 對話框之列」轉換, 單一擁有者(規則帳本 R-PRINCIPLE-00 c/e/f、R-AUTH-MERGE-00 第 5 條):
9
+ // 直接編輯器 VeCgrups/VeCpemis(列=被引用表之全部列+額外列)與反向關聯對話框 VeGrupBlngUsers/VePemiBlngGrups(每列改寫另一實體之字串)共用.
10
+ // 伺服端 getUserRules 對 isActive 為 'y' 之引用一律參與合併——查無列(懸空)者以其 mode 參與、內容全 n; mode 缺或非法者視為 AND 限制項(有合法項時與 AND 等價); 名稱 '' 照常比對——
11
+ // 故編輯器存檔不得丟棄任何啟用中之引用(丟棄 AND 項即放寬). 輸入為 cvKpToObj(…, { hasMode: true }) 之結果(以名稱為鍵);
12
+ // 純函數, 不依賴 Vue/DOM/lodash, 不解析字串(JSON5.parse 由呼叫端執行, 站點見 unit-cvKpToObj SITE-AD-001). 往返不變式見 unit-editorRows.
13
+
14
+
15
+ let isPlainObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
16
+
17
+
18
+ //restoreLim: 限制項(mode 缺或非法之啟用引用)寫回其原始 mode(見 getRawModeMap): 保留解碼值之其他欄位, mode 換回原值(原為缺則不寫 mode 鍵), isActive 為 'y'
19
+ let restoreLim = (v, lim) => {
20
+ let o = isPlainObj(v) ? { ...v } : {}
21
+ delete o.mode
22
+ let m = getOwn(lim, 'mode')
23
+ if (m !== undefined) {
24
+ o.mode = m
25
+ }
26
+ o.isActive = 'y'
27
+ return o
28
+ }
29
+
30
+
31
+ /**
32
+ * 取得「mode 缺或非法之啟用引用」(伺服端之限制項)之原始 mode, 供存檔時原樣寫回(R-PRINCIPLE-00 e; 未被管理者改動之項寫回其原始 mode)
33
+ *
34
+ * 伺服端 mergeRules 把 mode 缺或非 OR/AND 之啟用項當「限制項」: 只當 AND 交集項、不單獨當基底, 該層沒有任何 mode 合法之啟用項時全 n.
35
+ * 編輯器(cvKpToObj)把此類項顯示為「AND、啟用」; 存檔若寫出合法 AND, 限制項即變成可當基底之 AND——例: { G1: 缺 mode, G2: AND } 管理者停用唯一合法項 G2 後存檔,
36
+ * G1 成為唯一基底而授予其內容(預期為全 n, 多給). 故記下原始 mode, 存檔時對仍為「AND、啟用」(開啟時之狀態, 視為未改)者寫回原始 mode(putRowsToKp/putRefEntry);
37
+ * 管理者改過 mode 或停用者照其選擇寫出.
38
+ * 只記「該名稱所有啟用元素皆 mode 缺或非法」者: 同名另有 mode 合法之啟用元素時(如陣列 [AND, 缺 mode]), 伺服端結果同該合法項, 寫出合法 mode 即授權不變, 寫回限制項反而收回.
39
+ * 純量值({ "G1": "y" })伺服端解為 { isActive: 'y' }(無 mode), 視同缺 mode. 名稱非字串之元素不記(由 getKpLossy 拒存).
40
+ *
41
+ * @param {*} raw 輸入 JSON5.parse(cgrups/cpemis) 之結果(物件或陣列形式; 其他視為無引用)
42
+ * @returns {Object} 回傳無原型物件 名稱 → { mode: 原始 mode 值(可為 undefined) }
43
+ * @example
44
+ *
45
+ * let kp = getRawModeMap(JSON.parse('{"G1":{"isActive":"y"},"G2":{"mode":"or","isActive":"y"},"G3":{"mode":"AND","isActive":"y"}}'))
46
+ * console.log(Object.keys(kp), kp.G2.mode)
47
+ * // => ['G1', 'G2'] or
48
+ *
49
+ */
50
+ function getRawModeMap(raw) {
51
+ let kpLim = Object.create(null) //無原型: 鍵為使用者資料(R-KEY-01)
52
+ let kpValid = Object.create(null)
53
+ eachOwn(cvKpToArr(raw), (r) => {
54
+ if (r.isActive !== 'y' || typeof r.name !== 'string') {
55
+ return true //跳出換下一個
56
+ }
57
+ if (isAuthMode(r.mode)) {
58
+ kpValid[r.name] = true
59
+ return true //跳出換下一個
60
+ }
61
+ if (!(r.name in kpLim)) {
62
+ kpLim[r.name] = { mode: r.mode }
63
+ }
64
+ })
65
+ let out = Object.create(null)
66
+ eachOwn(kpLim, (v, name) => {
67
+ if (kpValid[name] !== true) {
68
+ out[name] = v
69
+ }
70
+ })
71
+ return out
72
+ }
73
+
74
+
75
+ /**
76
+ * 由解碼後之單一引用值取得對話框列之狀態, 與伺服端同解讀
77
+ *
78
+ * - 值為物件: isActive 為 'y' 者啟用; 啟用者之 mode 取有效值(getEffMode: 缺或非法 → 'AND'), 停用者 mode 合法則沿用、否則顯示預設 'OR'(停用項不參與合併, mode 無授權效果, 同修前顯示).
79
+ * - 值為純量(物件形式 { "G1": "y" }; cvKpToObj 物件形式對非物件值原樣保留): 伺服端 cvKpToArr 解為 { isActive: 值 }(無 mode → AND), 此處同解讀——
80
+ * 修前以 lodash get(值, 'isActive') 取不到而顯示停用、存檔寫出 { mode: 'OR', isActive: 'n' }, 丟掉該 AND 引用即放寬.
81
+ * - 無此引用(null/undefined): 停用、'OR'.
82
+ *
83
+ * @param {*} p 輸入 getOwn(cvKpToObj 之結果, 名稱, null)
84
+ * @returns {Object} 回傳 { mode: 'OR'|'AND', enable: 'y'|'n' }
85
+ * @example
86
+ *
87
+ * console.log(getRefState({ mode: 'or', isActive: 'y' }))
88
+ * // => { mode: 'AND', enable: 'y' }
89
+ * console.log(getRefState('y'))
90
+ * // => { mode: 'AND', enable: 'y' }
91
+ *
92
+ */
93
+ function getRefState(p) {
94
+ let o = isPlainObj(p) ? p : { isActive: p }
95
+ let m = getOwn(o, 'mode')
96
+ let enable = getOwn(o, 'isActive') === 'y' ? 'y' : 'n'
97
+ let mode = enable === 'y' ? getEffMode(m) : (isAuthMode(m) ? m : 'OR')
98
+ return { mode, enable }
99
+ }
100
+
101
+
102
+ /**
103
+ * 取得「啟用中而對話框之表列未顯示」之引用, 各一列(由呼叫端附於表列之後), 使存檔原樣寫回(R-PRINCIPLE-00 c/f)
104
+ *
105
+ * 對話框之表列以 getOwn(解碼結果, 表列.name) 顯示引用, 存檔只寫出名稱為字串之列; 故「顯示到」以表列 name 為字串且 === 引用名稱判定.
106
+ * 與伺服端 findRefRows 之差異僅在表列無 name(findRefRows 視為 '')或 name 非字串者——此類表列不會以字串名稱寫出, 其所對應之引用仍須以額外列保留.
107
+ * 一般情形即「引用之名稱查無列(懸空; 含名稱 '')」. 停用之引用無授權影響, 不產生額外列(存檔時照舊不寫出). 順序: 解碼結果之鍵序(僅供顯示, 不影響授權).
108
+ *
109
+ * @param {Object} kpDec 輸入 cvKpToObj(…, { hasMode: true }) 之結果(非物件視為無引用)
110
+ * @param {Array} tabRows 輸入被引用表之列(群組表/權限表)
111
+ * @returns {Array} 回傳額外列 [{ name, mode, enable: 'y', dangling: true }]
112
+ */
113
+ function getRefRowsExtra(kpDec, tabRows) {
114
+ let rs = []
115
+ if (!isPlainObj(kpDec)) {
116
+ return rs
117
+ }
118
+ let kpShown = Object.create(null) //無原型: 鍵為使用者資料(R-KEY-01)
119
+ eachOwn(tabRows, (v) => {
120
+ let n = isPlainObj(v) ? v.name : undefined
121
+ if (typeof n === 'string') {
122
+ kpShown[n] = true
123
+ }
124
+ })
125
+ eachOwn(kpDec, (p, name) => {
126
+ let st = getRefState(p)
127
+ if (st.enable !== 'y' || kpShown[name] === true) {
128
+ return true //跳出換下一個
129
+ }
130
+ rs.push({
131
+ name,
132
+ mode: st.mode,
133
+ enable: 'y',
134
+ dangling: true,
135
+ })
136
+ })
137
+ return rs
138
+ }
139
+
140
+
141
+ /**
142
+ * 取得同名多列(被引用表之歷史同名, R-PRINCIPLE-00 d)中 (mode, enable) 不全相同者之名稱
143
+ *
144
+ * 物件形式每個名稱只有一個值, 同名各列設定不同時無法寫出(寫出任一列即靜默捨棄其他列之設定), 編輯器拒存(warnDupRowsInconsistent); 全相同則寫出一次.
145
+ *
146
+ * @param {Array} rows 輸入對話框之列 [{ name, mode, enable }]
147
+ * @returns {Array} 回傳名稱陣列(不重複, 依首次出現序); 名稱非字串之列不計
148
+ */
149
+ function getDupRowsInconsistent(rows) {
150
+ let kpFirst = Object.create(null) //無原型: 鍵為使用者資料(R-KEY-01)
151
+ let out = []
152
+ eachOwn(rows, (r) => {
153
+ if (!isPlainObj(r) || typeof r.name !== 'string') {
154
+ return true //跳出換下一個
155
+ }
156
+ if (!(r.name in kpFirst)) {
157
+ kpFirst[r.name] = r
158
+ return true //跳出換下一個
159
+ }
160
+ let f = kpFirst[r.name]
161
+ if ((f.mode !== r.mode || f.enable !== r.enable) && !out.includes(r.name)) {
162
+ out.push(r.name)
163
+ }
164
+ })
165
+ return out
166
+ }
167
+
168
+
169
+ /**
170
+ * 由直接編輯器之列寫出引用清單物件 { 名稱: { mode, isActive } }, 寫入呼叫端提供之無原型容器(R-KEY-01: 名稱可為 __proto__ 等原型鍵)
171
+ *
172
+ * 名稱為字串者(含 '', R-PRINCIPLE-00 f: 原樣保留; 含懸空之額外列)寫出; 名稱非字串、mode 非 OR/AND、enable 非非空字串之列略過(對話框不會產生此類列).
173
+ * 同名多列後者覆蓋前者(呼叫端須先以 getDupRowsInconsistent 拒存不一致者; 一致者覆蓋結果相同).
174
+ * rawModeMap(getRawModeMap 之結果, 選填): 限制項之列仍為「啟用、mode 為其有效值(AND)」(開啟時之狀態, 視為未改)者寫回原始 mode(restoreLim), 不寫成合法 AND——
175
+ * 否則限制項變為可當基底之 AND 而多給(見 getRawModeMap); 管理者改過 mode 或停用者照列寫出. 不給=一律照列寫出.
176
+ *
177
+ * @param {Object} kpOut 輸入寫出用之無原型容器(呼叫端建立)
178
+ * @param {Array} rows 輸入對話框之列 [{ name, mode, enable }]
179
+ * @param {Object} [rawModeMap] 輸入 getRawModeMap(原始解析結果)
180
+ * @returns {Object} 回傳 kpOut
181
+ */
182
+ function putRowsToKp(kpOut, rows, rawModeMap) {
183
+ eachOwn(rows, (r) => {
184
+ if (!isPlainObj(r)) {
185
+ return true //跳出換下一個
186
+ }
187
+ let name = r.name
188
+ let mode = r.mode
189
+ let enable = r.enable
190
+ if (typeof name !== 'string' || !isAuthMode(mode) || typeof enable !== 'string' || enable === '') {
191
+ return true //跳出換下一個
192
+ }
193
+ let lim = getOwn(rawModeMap, name, null)
194
+ if (lim !== null && enable === 'y' && mode === getEffMode(getOwn(lim, 'mode'))) {
195
+ kpOut[name] = restoreLim(null, lim)
196
+ return true //跳出換下一個
197
+ }
198
+ kpOut[name] = {
199
+ mode,
200
+ isActive: enable,
201
+ }
202
+ })
203
+ return kpOut
204
+ }
205
+
206
+
207
+ /**
208
+ * 反向關聯對話框改寫單一實體(使用者/群組)之引用清單: 只改「名稱為 name」之一項, 其餘引用項(含懸空、名稱 ''、mode 缺/非法〔解碼已為 AND〕、停用項、純量值)原樣保留,
209
+ * 其授權效果不變(R-PRINCIPLE-00 c/e/f; 修前以列上經 fltKp* 篩過之 grups/pemis 重組, 查無列之啟用引用被丟棄即放寬)
210
+ *
211
+ * 目標狀態(enable 為 'y' 時 { mode, isActive: 'y' }; 否則移除該項)與現值(getRefState)相同 → 回 false(未變更: 呼叫端不改寫字串, 逐字元不動);
212
+ * 不同 → 把 kpDec 之自有項(name 除外)複製入 kpOut 並寫入目標狀態, 回 true. mode 取有效值(getEffMode: 非 OR 者為 AND).
213
+ * rawModeMap(getRawModeMap(該實體字串之原始解析結果), 選填): 複製其餘項時, 啟用中之限制項寫回其原始 mode(restoreLim; 解碼值為 AND, 寫成合法 AND 會多給, 見 getRawModeMap).
214
+ *
215
+ * @param {Object} kpOut 輸入寫出用之無原型容器(R-KEY-01; 呼叫端建立)
216
+ * @param {Object} kpDec 輸入 cvKpToObj(…, { hasMode: true }) 之結果(非物件視為無引用)
217
+ * @param {String} name 輸入本對話框所管之群組/權限名稱
218
+ * @param {String} enable 輸入 'y' 為歸屬, 其他為不歸屬
219
+ * @param {String} mode 輸入歸屬時之合併模式
220
+ * @param {Object} [rawModeMap] 輸入 getRawModeMap(原始解析結果)
221
+ * @returns {Boolean} 回傳是否變更(true 時 kpOut 已填入欲寫出之內容)
222
+ * @example
223
+ *
224
+ * let kp = Object.create(null)
225
+ * console.log(putRefEntry(kp, { GX: { mode: 'AND', isActive: 'y' } }, 'G1', 'y', 'OR'), JSON.stringify(kp))
226
+ * // => true {"GX":{"mode":"AND","isActive":"y"},"G1":{"mode":"OR","isActive":"y"}}
227
+ *
228
+ */
229
+ function putRefEntry(kpOut, kpDec, name, enable, mode, rawModeMap) {
230
+ let cur = getRefState(getOwn(kpDec, name, null))
231
+ let on = enable === 'y'
232
+ let m = getEffMode(mode)
233
+ if (on === (cur.enable === 'y') && (!on || m === cur.mode)) {
234
+ return false
235
+ }
236
+ if (isPlainObj(kpDec)) {
237
+ eachOwn(kpDec, (v, k) => {
238
+ if (k === name) {
239
+ return true //跳出換下一個
240
+ }
241
+ let lim = getOwn(rawModeMap, k, null)
242
+ kpOut[k] = (lim !== null && getRefState(v).enable === 'y') ? restoreLim(v, lim) : v
243
+ })
244
+ }
245
+ if (on) {
246
+ kpOut[name] = {
247
+ mode: m,
248
+ isActive: 'y',
249
+ }
250
+ }
251
+ return true
252
+ }
253
+
254
+
255
+ /**
256
+ * 對話框之列中 row[key] 精確 === val 之全部索引(單列切換與批次操作用)
257
+ *
258
+ * 直接編輯器之同名多列代表同一個引用(物件形式一個名稱只有一個值), 切換須對所有精確同名列一併套用, 同名列狀態恆一致(warnDupRowsInconsistent 僅作防呆);
259
+ * 修前以 lodash get(row, 'name', '') 找第一列(批次以 kpRow[name] 只剩最後一列): 同名第二列之操作改到第一列、全選後同名列不一致而被拒存,
260
+ * 且表列無 name 欄者(undefined)被當成 '' 搶先命中名稱 '' 之額外列. 反向關聯對話框以列身分 id 定位(群組/使用者之 name 可重複).
261
+ * 比對為精確 ===(undefined 不等於 '', 數字 5 不等於 '5'); 非物件之列略過.
262
+ *
263
+ * @param {Array} rows 輸入對話框之列
264
+ * @param {String} key 輸入欄位名('name' 或 'id')
265
+ * @param {*} val 輸入欲比對之值
266
+ * @returns {Array} 回傳索引陣列(由小到大)
267
+ * @example
268
+ *
269
+ * console.log(getRowIdxs([{ name: 'A' }, { id: 'x' }, { name: 'A' }, { name: '' }], 'name', 'A'))
270
+ * // => [0, 2]
271
+ *
272
+ */
273
+ function getRowIdxs(rows, key, val) {
274
+ let ks = []
275
+ eachOwn(rows, (r, k) => {
276
+ if (isPlainObj(r) && getOwn(r, key) === val) {
277
+ ks.push(k)
278
+ }
279
+ })
280
+ return ks
281
+ }
282
+
283
+
284
+ export { getRefState, getRefRowsExtra, getDupRowsInconsistent, getRawModeMap, putRowsToKp, putRefEntry, getRowIdxs }