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
@@ -0,0 +1,391 @@
1
+ import JSON5 from 'json5'
2
+ import eachOwn from './eachOwn.mjs'
3
+ import cvKpToArr from './cvKpToArr.mjs'
4
+ import findRefRows from './findRefRows.mjs'
5
+ import getEffMode from './getEffMode.mjs'
6
+ import getPropKey from './getPropKey.mjs'
7
+ import isAuthMode from './isAuthMode.mjs'
8
+ import joinNames from './joinNames.mjs'
9
+
10
+
11
+ //授權資料「衝突提示」之偵測與文字, 單一擁有者(規則帳本 R-PRINCIPLE-00 原則 3「遇衝突:收嚴+提示」、R-AUTH-WARN-01):
12
+ // 伺服端 getUserRules 對衝突資料已收嚴(src/plugins/mShare.mjs); 本檔以相同判準(findRefRows/getEffMode/getPropKey)找出「哪裡有衝突」,
13
+ // 供清單頁(cgrups/cpemis/crules 儲存格)與五個編輯對話框於該處顯示警告圖示(三角形驚嘆號, $ui.getIcon('warning'))與 tooltip. 純函數, 不依賴 Vue/DOM.
14
+ //
15
+ // 警告碼(引用清單 cgrups/cpemis, 僅 isActive 為 'y' 之項目才有警告——停用項目不參與合併):
16
+ // dangling (c) 引用之名稱查無實體(懸空): 仍以其 mode 參與、內容全 n
17
+ // ambiguous (d) 引用之名稱對到多筆(歷史同名): 內容取各筆之交集
18
+ // modeInvalid(e) mode 缺或非 OR/AND: 視為 AND 限制項(只當交集項、不單獨授予)
19
+ // bothModes (b) 同名同時有 OR 與 AND 之啟用項目: 物件形式無法無損表示, 編輯器拒存(getKpLossy)
20
+ // emptyName (f) 名稱為空字串: 照原樣保留(與名稱為 '' 之列相符, 否則為懸空)
21
+ // noName (f) 名稱不是字串(陣列元素無 name 等): 物件形式無法表示, 編輯器拒存(getKpLossy)
22
+ // 規則清單 crules: ruleConflict (a) 同一管理對象被列兩次以上且有勾與沒勾並存: 所有列皆勾才勾(收嚴)
23
+
24
+
25
+ let CODES = ['dangling', 'ambiguous', 'modeInvalid', 'bothModes', 'emptyName', 'noName', 'unparsable']
26
+
27
+
28
+ let parseKp = (c) => {
29
+ if (typeof c === 'string') {
30
+ try {
31
+ return JSON5.parse(c)
32
+ }
33
+ catch (err) {
34
+ return null //損壞字串: 伺服端視為空(全部 n), 無從指出個別項目
35
+ }
36
+ }
37
+ return c
38
+ }
39
+
40
+
41
+ //isBadData: 非空字串而無法解析為物件/陣列(JSON5 解析失敗, 或解出 null/純量)——格式無法判讀, 伺服端視為空(全部 n, 收嚴), 頁面須提示(R-PRINCIPLE-00 原則 3; 外審 sol R8).
42
+ //空字串/空白(新增列之預設值)與非字串輸入不算損壞
43
+ let isBadData = (c) => {
44
+ if (typeof c !== 'string' || c.trim() === '') {
45
+ return false
46
+ }
47
+ try {
48
+ let kp = JSON5.parse(c)
49
+ return kp === null || typeof kp !== 'object'
50
+ }
51
+ catch (err) {
52
+ return true
53
+ }
54
+ }
55
+
56
+
57
+ /**
58
+ * 偵測引用清單(使用者 cgrups、群組 cpemis)中各項目之衝突
59
+ *
60
+ * @param {String|Object|Array} c 輸入 cgrups/cpemis 字串(JSON5)或已解析之結果
61
+ * @param {Array} rows 輸入被引用的表列(cgrups 對應 grups 表; cpemis 對應 pemis 表), 以 name 精確比對(findRefRows)
62
+ * @returns {Object} 回傳 { items, kpWarns, byCode, count, hasWarn }:
63
+ * items 每個解碼出之項目 [{ name, mode, effMode, isActive, active, nMatch, warns: [警告碼] }](nMatch 僅啟用且名稱為字串者有值, 其餘 -1)
64
+ * kpWarns 名稱 → 警告碼陣列(無原型物件; 僅名稱為字串之啟用項目)
65
+ * byCode 警告碼 → 受影響之名稱陣列(不重複; noName 以 #<序號>)
66
+ * count 有警告之項目數; hasWarn 是否有任何警告
67
+ * @example
68
+ *
69
+ * console.log(getRefWarns('{"G1":{"mode":"AND","isActive":"y"}}', []).byCode.dangling)
70
+ * // => ['G1']
71
+ *
72
+ */
73
+ function getRefWarns(c, rows) {
74
+ let arr = cvKpToArr(parseKp(c))
75
+
76
+ //items
77
+ let items = []
78
+ let kpModes = Object.create(null)
79
+ let k = 0
80
+ eachOwn(arr, (r) => {
81
+ k++
82
+ let active = r.isActive === 'y'
83
+ let it = {
84
+ name: r.name,
85
+ mode: r.mode,
86
+ effMode: getEffMode(r.mode),
87
+ isActive: r.isActive,
88
+ active,
89
+ nMatch: -1,
90
+ warns: [],
91
+ _k: k,
92
+ }
93
+ if (active) {
94
+ if (typeof r.name !== 'string') {
95
+ it.warns.push('noName')
96
+ }
97
+ else {
98
+ it.nMatch = findRefRows(rows, r.name).length
99
+ if (it.nMatch === 0) {
100
+ it.warns.push('dangling')
101
+ }
102
+ else if (it.nMatch >= 2) {
103
+ it.warns.push('ambiguous')
104
+ }
105
+ if (!isAuthMode(r.mode)) {
106
+ it.warns.push('modeInvalid')
107
+ }
108
+ if (r.name === '') {
109
+ it.warns.push('emptyName')
110
+ }
111
+ if (!(r.name in kpModes)) {
112
+ kpModes[r.name] = []
113
+ }
114
+ if (!kpModes[r.name].includes(it.effMode)) {
115
+ kpModes[r.name].push(it.effMode)
116
+ }
117
+ }
118
+ }
119
+ items.push(it)
120
+ })
121
+
122
+ //bothModes
123
+ eachOwn(items, (it) => {
124
+ if (it.active && typeof it.name === 'string' && kpModes[it.name].length > 1) {
125
+ it.warns.push('bothModes')
126
+ }
127
+ })
128
+
129
+ //kpWarns, byCode
130
+ let kpWarns = Object.create(null)
131
+ let byCode = {}
132
+ eachOwn(CODES, (code) => {
133
+ byCode[code] = []
134
+ })
135
+ let count = 0
136
+ eachOwn(items, (it) => {
137
+ if (it.warns.length === 0) {
138
+ return true //跳出換下一個
139
+ }
140
+ count++
141
+ let label = (typeof it.name === 'string') ? (it.name === '' ? '""' : it.name) : `#${it._k}` //空字串名稱顯示為 "", 否則 tooltip 中看不到
142
+ eachOwn(it.warns, (code) => {
143
+ if (!byCode[code].includes(label)) {
144
+ byCode[code].push(label)
145
+ }
146
+ if (typeof it.name === 'string') {
147
+ if (!(it.name in kpWarns)) {
148
+ kpWarns[it.name] = []
149
+ }
150
+ if (!kpWarns[it.name].includes(code)) {
151
+ kpWarns[it.name].push(code)
152
+ }
153
+ }
154
+ })
155
+ })
156
+ eachOwn(items, (it) => {
157
+ delete it._k
158
+ })
159
+
160
+ //unparsable: 整個字串無法判讀(無個別項目可掛), 以占位名稱 '-' 計入
161
+ if (isBadData(c)) {
162
+ byCode.unparsable.push('-')
163
+ count++
164
+ }
165
+
166
+ return {
167
+ items,
168
+ kpWarns,
169
+ byCode,
170
+ count,
171
+ hasWarn: count > 0,
172
+ }
173
+ }
174
+
175
+
176
+ /**
177
+ * 偵測規則清單(權限 crules)中「同一管理對象被列兩次以上且有勾與沒勾並存」(R-PRINCIPLE-00 a)
178
+ *
179
+ * 物件形式名稱即鍵、不會重複; 僅陣列形式 [{ name, isActive }] 會發生. 伺服端與編輯器解碼皆為「所有列皆 y 才 y」(收嚴).
180
+ * 值只分 y(isActive 為 'y')與 n(其他一律視為 n); 值全相同(全 y 或全 n)的重複不算衝突.
181
+ *
182
+ * @param {String|Object|Array} c 輸入 crules 字串(JSON5)或已解析之結果
183
+ * @returns {Object} 回傳 { conflicts, names, kpConflict, count, hasWarn }:
184
+ * conflicts [{ name(屬性鍵), n(列數), nY(勾選列數) }]; names 受影響之管理對象名稱陣列; kpConflict 名稱 → true(無原型物件)
185
+ * @example
186
+ *
187
+ * console.log(getRuleWarns('[{"name":"T1","isActive":"y"},{"name":"T1","isActive":"n"}]').names)
188
+ * // => ['T1']
189
+ *
190
+ */
191
+ function getRuleWarns(c) {
192
+ let arr = cvKpToArr(parseKp(c))
193
+ let kpCnt = Object.create(null)
194
+ eachOwn(arr, (r) => {
195
+ let key = getPropKey(r.name)
196
+ if (key === null) {
197
+ return true //跳出換下一個
198
+ }
199
+ if (!(key in kpCnt)) {
200
+ kpCnt[key] = { n: 0, nY: 0 }
201
+ }
202
+ kpCnt[key].n++
203
+ if (r.isActive === 'y') {
204
+ kpCnt[key].nY++
205
+ }
206
+ })
207
+ let conflicts = []
208
+ let kpConflict = Object.create(null)
209
+ eachOwn(kpCnt, (v, key) => {
210
+ if (v.n >= 2 && v.nY > 0 && v.nY < v.n) {
211
+ conflicts.push({ name: key, n: v.n, nY: v.nY })
212
+ kpConflict[key] = true
213
+ }
214
+ })
215
+ let names = conflicts.map((x) => x.name)
216
+ let unparsable = isBadData(c)
217
+ return {
218
+ conflicts,
219
+ names,
220
+ kpConflict,
221
+ unparsable,
222
+ count: conflicts.length + (unparsable ? 1 : 0),
223
+ hasWarn: conflicts.length > 0 || unparsable,
224
+ }
225
+ }
226
+
227
+
228
+ /**
229
+ * 計算「以 AND 引用被刪名稱且刪除後該名稱完全消失」之引用者數(R-PRINCIPLE-00 c: 刪除被 AND 引用之群組/權限前提示影響人數;
230
+ * run06 起刪除連帶清理會把該引用自引用端移除, 其 AND 限制隨之解除、授權可能變多, 見 ADR-036; run05 前為「AND 項變懸空、授權被收回」),
231
+ * 以及「刪除歷史同名之一列後其授權可能變多」之引用者數(R-PRINCIPLE-00 d: 同名多筆取各候選之交集, 候選變少則交集變寬)
232
+ *
233
+ * n/holders(既有): 引用者(使用者的 cgrups、群組的 cpemis)中, 有 isActive 為 'y'、有效模式為 AND(mode 缺或非法者亦視為 AND)、名稱屬於被刪名稱,
234
+ * 且刪除後被引用表已無該名稱之列者(刪除後該引用被連帶清理, 其 AND 限制解除).
235
+ * nWiden/holdersWiden(新增; 須給 rowsBefore): 引用者中有 isActive 為 'y' 之引用(任何 mode)、名稱屬於被刪名稱,
236
+ * 且刪除後該名稱仍有 ≥ 1 筆剩餘列但筆數比刪除前少者(歷史同名只刪其一), 該引用者之授權可能變多(少一個較嚴之候選).
237
+ * 未給 rowsBefore(非陣列)則不計, nWiden 為 0.
238
+ *
239
+ * @param {Array} holders 輸入引用者列(users 或 grups)
240
+ * @param {String} field 輸入引用欄位名('cgrups' 或 'cpemis')
241
+ * @param {Array} delNames 輸入被刪除之名稱陣列
242
+ * @param {Array} rowsRemain 輸入刪除後被引用表之剩餘列(grups 或 pemis)
243
+ * @param {Array} [rowsBefore] 輸入刪除前被引用表之列(基準, 已儲存者), 供計 nWiden
244
+ * @returns {Object} 回傳 { n, holders, nWiden, holdersWiden }: n 為以 AND 引用且名稱完全消失之引用者數(其 AND 限制將隨引用被清理而解除), holders 為其列; nWiden 為授權可能變多之引用者數, holdersWiden 為其列
245
+ */
246
+ function countAndRefs(holders, field, delNames, rowsRemain, rowsBefore) {
247
+ let setDel = new Set(delNames)
248
+ let canWiden = Array.isArray(rowsBefore)
249
+ let hit = []
250
+ let hitWiden = []
251
+ eachOwn(holders, (h) => {
252
+ let arr = cvKpToArr(parseKp(h ? h[field] : ''))
253
+ let b = false
254
+ let w = false
255
+ eachOwn(arr, (r) => {
256
+ if (r.isActive !== 'y' || typeof r.name !== 'string' || !setDel.has(r.name)) {
257
+ return true //跳出換下一個
258
+ }
259
+ let nAfter = findRefRows(rowsRemain, r.name).length
260
+ if (getEffMode(r.mode) === 'AND' && nAfter === 0) {
261
+ b = true
262
+ }
263
+ if (canWiden && nAfter >= 1 && nAfter < findRefRows(rowsBefore, r.name).length) {
264
+ w = true
265
+ }
266
+ if (b && (w || !canWiden)) {
267
+ return false //跳出
268
+ }
269
+ })
270
+ if (b) {
271
+ hit.push(h)
272
+ }
273
+ if (w) {
274
+ hitWiden.push(h)
275
+ }
276
+ })
277
+ return {
278
+ n: hit.length,
279
+ holders: hit,
280
+ nWiden: hitWiden.length,
281
+ holdersWiden: hitWiden,
282
+ }
283
+ }
284
+
285
+
286
+ /**
287
+ * 把訊息範本之 {names}、{kind}、{n}、{m}、{w}、{e}、{list} 代入(以 split/join 之等價單趟掃描, 名稱可含 $& 等字樣, 原樣顯示)
288
+ *
289
+ * names 以 joinNames 串接(至多 5 個, 超過加省略號); list 為呼叫端已組好之字串(如 getCascadePreview 之「舊→新」清單, 自訂截斷文字), 原樣代入;
290
+ * n、m、w、e 為筆數(刪除/改名確認框: n 為受影響之引用端列數, m 為其中以 AND 引用者, w 為歷史同名致授權可能變多者, e 為其中來源為外部應用者). 未給之佔位符原樣保留.
291
+ *
292
+ * @param {String} tpl 輸入範本
293
+ * @param {Object} vars 輸入 { names, kind, n, m, w, e, list }
294
+ * @returns {String} 回傳代入後之文字
295
+ */
296
+ function fmtWarn(tpl, vars = {}) {
297
+ //只掃描原始範本一次, 代入結果不再解析(名稱可含 {kind}、{n}、{names} 等字樣, 須原樣顯示; 外審 sol R11)
298
+ let kp = Object.create(null)
299
+ if ('names' in vars) {
300
+ kp.names = joinNames(vars.names)
301
+ }
302
+ if ('kind' in vars) {
303
+ kp.kind = String(vars.kind)
304
+ }
305
+ for (let k of ['n', 'm', 'w', 'e', 'list']) {
306
+ if (k in vars) {
307
+ kp[k] = String(vars[k])
308
+ }
309
+ }
310
+ return String(tpl).replace(/\{(names|kind|n|m|w|e|list)\}/g, (hit, k) => (k in kp ? kp[k] : hit))
311
+ }
312
+
313
+
314
+ let KEY_OF_CODE = {
315
+ dangling: 'warnRefDangling',
316
+ ambiguous: 'warnRefAmbiguous',
317
+ modeInvalid: 'warnRefModeInvalid',
318
+ bothModes: 'warnRefBothModes',
319
+ emptyName: 'warnRefEmptyName',
320
+ noName: 'warnRefNoName',
321
+ unparsable: 'warnDataUnparsable',
322
+ }
323
+
324
+
325
+ /**
326
+ * 引用清單警告之 tooltip 文字(整個儲存格; 每種警告一行, 換行以 \n)
327
+ *
328
+ * @param {Object} res 輸入 getRefWarns 之結果
329
+ * @param {Function} $t 輸入語系函數(key → 文字)
330
+ * @param {String} kindKey 輸入被引用者之種類語系鍵('warnKindGrup' 或 'warnKindPemi')
331
+ * @returns {String} 回傳 tooltip 文字, 無警告為空字串
332
+ */
333
+ function getRefWarnText(res, $t, kindKey) {
334
+ let kind = $t(kindKey)
335
+ let lines = []
336
+ eachOwn(CODES, (code) => {
337
+ let names = res.byCode[code]
338
+ if (names.length === 0) {
339
+ return true //跳出換下一個
340
+ }
341
+ lines.push(fmtWarn($t(KEY_OF_CODE[code]), { names, kind }))
342
+ })
343
+ return lines.join('\n')
344
+ }
345
+
346
+
347
+ /**
348
+ * 單一項目(編輯對話框之一列)警告之 tooltip 文字
349
+ *
350
+ * @param {Array} codes 輸入該項目之警告碼陣列(getRefWarns 的 kpWarns[name])
351
+ * @param {String} name 輸入該項目之名稱
352
+ * @param {Function} $t 輸入語系函數
353
+ * @param {String} kindKey 輸入被引用者之種類語系鍵
354
+ * @returns {String} 回傳 tooltip 文字, 無警告為空字串
355
+ */
356
+ function getItemWarnText(codes, name, $t, kindKey) {
357
+ let kind = $t(kindKey)
358
+ let lines = []
359
+ eachOwn(CODES, (code) => {
360
+ if (!Array.isArray(codes) || !codes.includes(code)) {
361
+ return true //跳出換下一個
362
+ }
363
+ lines.push(fmtWarn($t(KEY_OF_CODE[code]), { names: [name === '' ? '""' : name], kind }))
364
+ })
365
+ return lines.join('\n')
366
+ }
367
+
368
+
369
+ /**
370
+ * 規則清單警告之 tooltip 文字
371
+ *
372
+ * @param {Object} res 輸入 getRuleWarns 之結果
373
+ * @param {Function} $t 輸入語系函數
374
+ * @returns {String} 回傳 tooltip 文字, 無警告為空字串
375
+ */
376
+ function getRuleWarnText(res, $t) {
377
+ if (!res.hasWarn) {
378
+ return ''
379
+ }
380
+ let lines = []
381
+ if (res.names.length > 0) {
382
+ lines.push(fmtWarn($t('warnRuleConflict'), { names: res.names }))
383
+ }
384
+ if (res.unparsable) {
385
+ lines.push($t('warnDataUnparsable'))
386
+ }
387
+ return lines.join('\n')
388
+ }
389
+
390
+
391
+ export { CODES, getRefWarns, getRuleWarns, countAndRefs, fmtWarn, getRefWarnText, getItemWarnText, getRuleWarnText }
@@ -0,0 +1,186 @@
1
+ import getPropKey from './getPropKey.mjs'
2
+ import { fmtWarn, getRefWarns } from './getAuthWarn.mjs'
3
+ import { REF_OF, planRefChanges } from './refCascade.mjs'
4
+
5
+
6
+ //清單頁「儲存」前之引用連帶預覽(規則帳本 R-PRINCIPLE-00 原則 6、適用表 m/n; R-REF-CASCADE-01; ADR-036):
7
+ //群組頁/權限頁改名 → 引用端(使用者 cgrups/群組 cpemis)於同一次寫入內連帶改名; 標的頁刪除或改 id → 引用它之權限 crules 項目被連帶清理(標的 id 即名稱, 改 id 等同刪舊增新, 不級聯).
8
+ //預覽與伺服端寫入用同一純函數 planRefChanges(src/js/refCascade.mjs), 前端以 store 之基準列計, 伺服端以實際為準.
9
+ //本檔只判定「要不要彈確認框」與「改名之成功訊息是否附帶筆數」並產生文字(刪除與標的頁之成功訊息不變), 不依賴 Vue/DOM; 元件只做薄接線(確認框、鎖釋放與 loading 仍在元件之 core()).
10
+ //
11
+ //kind: 'Grup'(群組頁, 引用端 users)、'Pemi'(權限頁, 引用端 grups)、'Target'(標的頁, 引用端 pemis)
12
+
13
+
14
+ let KIND = {
15
+ Grup: {
16
+ keyTable: 'grups',
17
+ rename: true,
18
+ confirm: 'confirmSaveRenameRefsGrup',
19
+ confirmExt: 'confirmSaveRenameRefsExtGrup',
20
+ success: 'grupSaveGrupsSuccessCascade',
21
+ },
22
+ Pemi: {
23
+ keyTable: 'pemis',
24
+ rename: true,
25
+ confirm: 'confirmSaveRenameRefsPemi',
26
+ confirmExt: 'confirmSaveRenameRefsExtPemi',
27
+ success: 'pemiSavePemisSuccessCascade',
28
+ },
29
+ Target: {
30
+ keyTable: 'targets',
31
+ rename: false,
32
+ confirm: 'confirmSaveDelRefsTarget',
33
+ confirmExt: '',
34
+ success: '', //標的頁不改成功訊息(刪除之筆數已於確認框告知)
35
+ },
36
+ }
37
+
38
+
39
+ let MAX_NAMES = 5
40
+
41
+
42
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
43
+ let isStr = (v) => typeof v === 'string'
44
+
45
+
46
+ //showName: 名稱之顯示形式, 空字串顯示為 "" (否則清單中看不到, 同 getRefWarns 之 label)
47
+ let showName = (v) => (v === '' ? '""' : String(v))
48
+
49
+
50
+ /**
51
+ * 把項目陣列以「, 」串接, 超過上限者截斷並加「共 N 個」之語系文字(cascadeListMore, {n} 為項目總數)
52
+ *
53
+ * @param {Array} items 輸入項目字串陣列
54
+ * @param {Function} $t 輸入語系函數(key → 文字)
55
+ * @param {Number} [max=5] 輸入最多列出幾項, 預設5
56
+ * @returns {String} 回傳字串, 如 'a, b' 或 'a, b, c, d, e …等 12 個'
57
+ */
58
+ function joinList(items, $t, max = MAX_NAMES) {
59
+ let s = items.slice(0, max).join(', ')
60
+ if (items.length > max) {
61
+ s = `${s} ${fmtWarn($t('cascadeListMore'), { n: items.length })}`
62
+ }
63
+ return s
64
+ }
65
+
66
+
67
+ //getHitKeys: 計畫內「有被改寫之引用端列」之原授權字串中出現之名稱鍵集合(field 為 'nRename' 取有改名者、'nDel' 取有刪除者);
68
+ //解碼經 getRefWarns(getAuthWarn.mjs 之 parseKp+cvKpToArr, 與 rewriteRefs 及伺服端同一套; 不另開 JSON5.parse 讀取點, 見 test/unit-cvKpToObj SITE-AD-001), 被引用表列給空陣列(只取 items 之名稱, 不查實體);
69
+ //名稱依 kind 取比對鍵('ref' 只認字串, 'rule' 取屬性鍵轉型, 同 rewriteRefs), 供名單只列「真的有人引用」者, 不列無人引用之改名/刪除
70
+ let getHitKeys = (plan, field) => {
71
+ let set = new Set()
72
+ let kind = hasOwn(REF_OF, plan.keyTable) ? REF_OF[plan.keyTable].kind : 'ref'
73
+ plan.changes.forEach((c) => {
74
+ if (!(c[field] > 0) || !isStr(c.before)) {
75
+ return
76
+ }
77
+ getRefWarns(c.before, []).items.forEach((it) => {
78
+ if (kind === 'ref' && !isStr(it.name)) {
79
+ return
80
+ }
81
+ let key = getPropKey(it.name)
82
+ if (key !== null) {
83
+ set.add(key)
84
+ }
85
+ })
86
+ })
87
+ return set
88
+ }
89
+
90
+
91
+ /**
92
+ * 依引用連帶計畫產生儲存前確認框文字與儲存成功訊息(群組頁/權限頁/標的頁共用)
93
+ *
94
+ * 確認框(confirmText, 空字串不彈框):
95
+ * Grup/Pemi: plan.err 為 null、有改名(plan.renames)且有引用端列因改名被連帶更新(plan.nRenameHolders > 0)時, 列出受影響引用端數與「舊→新」(至多 5 個, 只列有人引用者, 其餘「…等 N 個」),
96
+ * 其中來源 from 非空(外部應用)之引用端另加一句警示(下次外部同步時可能被外部資料覆寫); 僅刪除所致之連帶清理已於「刪除」時確認(getDelImpact), 儲存時不再重複.
97
+ * Target: plan.err 為 null 且有權限之規則將被清理(plan.nDelHolders > 0)時, 列出受影響之權限數與被刪除或改 id 之管控對象(同上截斷).
98
+ * plan.err 非 null(伺服端將以固定語系鍵拒存)不彈框, 由呼叫端照常送出, 錯誤訊息由既有 showCheckYes 顯示.
99
+ * 成功訊息(successText, 空字串即使用原成功文字): 只有 Grup/Pemi 且 plan.err 為 null、有改名(plan.renames.length > 0)且有引用端因改名被連帶更新(plan.nRenameHolders > 0)時, 回傳附帶筆數(nRenameHolders)之「引用已更新為新名稱」訊息;
100
+ * 刪除所致之連帶清理(筆數已於刪除確認框告知, 且既有案例刪除被引用列後儲存之成功彈窗須維持原文字)與 Target 頁一律回空字串.
101
+ *
102
+ * @param {Object} plan 輸入 planRefChanges 之結果; null/非物件/keyTable 與 kind 不符視為無影響
103
+ * @param {Function} $t 輸入語系函數(key → 文字)
104
+ * @param {String} kind 輸入頁面種類: 'Grup'、'Pemi' 或 'Target'
105
+ * @returns {Object} 回傳 { confirmText, successText }
106
+ * @example
107
+ *
108
+ * let plan = planRefChanges({ keyTable: 'grups', rowsOld: [{ id: 'g1', name: 'A' }], rowsNew: [{ id: 'g1', name: 'B' }], holdersOld: [{ id: 'u1', cgrups: '{"A":{"mode":"OR","isActive":"y"}}' }] })
109
+ * console.log(getCascadePreview(plan, (k) => k, 'Grup').confirmText)
110
+ * // => confirmSaveRenameRefsGrup(語系函數為恆等時回傳鍵名)
111
+ *
112
+ */
113
+ function getCascadePreview(plan, $t, kind) {
114
+ let res = { confirmText: '', successText: '' }
115
+ if (!hasOwn(KIND, kind)) {
116
+ return res
117
+ }
118
+ let cfg = KIND[kind]
119
+ if (plan === null || typeof plan !== 'object' || plan.err !== null || plan.keyTable !== cfg.keyTable || !Array.isArray(plan.changes) || !Array.isArray(plan.renames)) {
120
+ return res
121
+ }
122
+
123
+ //confirmText, successText(successText 只描述改名, 條件同改名確認框, 筆數為 nRenameHolders 而非 nHolders)
124
+ if (cfg.rename) {
125
+ if (plan.renames.length > 0 && plan.nRenameHolders > 0) {
126
+ res.successText = fmtWarn($t(cfg.success), { n: plan.nRenameHolders })
127
+ let hit = getHitKeys(plan, 'nRename')
128
+ let rns = plan.renames.filter((rn) => hit.has(rn.from))
129
+ if (rns.length === 0) {
130
+ rns = plan.renames //防呆: 名單判不出時退回全部改名, 不讓確認框無名單
131
+ }
132
+ let list = joinList(rns.map((rn) => `${showName(rn.from)}→${showName(rn.to)}`), $t)
133
+ let nExt = plan.changes.filter((c) => c.nRename > 0 && isStr(c.from) && c.from !== '').length
134
+ res.confirmText = fmtWarn($t(nExt > 0 ? cfg.confirmExt : cfg.confirm), { n: plan.nRenameHolders, e: nExt, list })
135
+ }
136
+ }
137
+ else if (plan.nDelHolders > 0) {
138
+ let hit = getHitKeys(plan, 'nDel')
139
+ let names = plan.delNames.filter((v) => hit.has(v))
140
+ if (names.length === 0) {
141
+ names = plan.delNames
142
+ }
143
+ let list = joinList(names.map((v) => showName(v)), $t)
144
+ res.confirmText = fmtWarn($t(cfg.confirm), { n: plan.nDelHolders, list })
145
+ }
146
+
147
+ return res
148
+ }
149
+
150
+
151
+ /**
152
+ * 儲存前預覽: 以與伺服端同一純函數規劃(planRefChanges), 再產生確認框文字與成功訊息; 預覽為輔助資訊, 計畫過程拋例外不得擋住儲存(回傳全空, 伺服端以實際為準)
153
+ *
154
+ * @param {Object} p 輸入參數物件
155
+ * @param {String} p.kind 輸入頁面種類: 'Grup'、'Pemi' 或 'Target'
156
+ * @param {Array} p.rowsOld 輸入基準列(store 之 grups/pemis/targets)
157
+ * @param {Array} p.rowsNew 輸入欲送出之列(清單目前之列)
158
+ * @param {Array} p.holdersOld 輸入引用端列(store 之 users/grups/pemis)
159
+ * @param {Boolean} [p.cascadeRename=true] 輸入伺服端是否做改名級聯(webInfor.cascadeRename), 僅給 false 才關閉, 預設true
160
+ * @param {Function} p.$t 輸入語系函數(key → 文字)
161
+ * @returns {Object} 回傳 { plan, confirmText, successText }; 發生例外時 plan 為 null
162
+ */
163
+ function getSavePreview(p) {
164
+ let kind = p ? p.kind : ''
165
+ if (!hasOwn(KIND, kind)) {
166
+ return { plan: null, confirmText: '', successText: '' }
167
+ }
168
+ try {
169
+ let plan = planRefChanges({
170
+ keyTable: KIND[kind].keyTable,
171
+ rowsOld: p.rowsOld,
172
+ rowsNew: p.rowsNew,
173
+ holdersOld: p.holdersOld,
174
+ opt: { cascadeRename: p.cascadeRename !== false },
175
+ })
176
+ return { plan, ...getCascadePreview(plan, p.$t, kind) }
177
+ }
178
+ catch (err) {
179
+ console.log('getSavePreview', err)
180
+ return { plan: null, confirmText: '', successText: '' }
181
+ }
182
+ }
183
+
184
+
185
+ export { getCascadePreview, getSavePreview, joinList }
186
+ export default getCascadePreview