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.
- package/README.md +158 -117
- package/dist/css/app.f09c7433.css +1 -0
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.8af87052.js +1 -0
- package/dist/js/app.8af87052.js +1 -0
- package/dist/js/{chunk-vendors.3e9e3ef1.js → chunk-vendors-legacy.ece5a361.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.3e9e3ef1.js → chunk-vendors.ece5a361.js} +1 -1
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +84 -4
- package/docs/global.html +24747 -832
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +245 -32
- package/docs/server_cascadeWrite.mjs.html +225 -0
- package/docs/server_dataChannel.mjs.html +2 -2
- package/docs/server_keyCheck.mjs.html +353 -0
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +180 -41
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncKeep.mjs.html +311 -0
- package/docs/server_tools_authRefsAudit.mjs.html +247 -0
- package/docs/server_tools_authRefsClean.mjs.html +335 -0
- package/docs/server_tools_fromTools.mjs.html +797 -0
- package/docs/server_tools_isMain.mjs.html +103 -0
- package/docs/server_tools_nameDupReport.mjs.html +280 -0
- package/docs/server_validateRows.mjs.html +15 -4
- package/docs/server_wrapInjected.mjs.html +3 -3
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_js_cvKpToArr.mjs.html +125 -0
- package/docs/src_js_cvKpToObj.mjs.html +210 -0
- package/docs/src_js_draftSync.mjs.html +142 -0
- package/docs/src_js_eachOwn.mjs.html +3 -3
- package/docs/src_js_editorRefRows.mjs.html +356 -0
- package/docs/src_js_externalRows.mjs.html +338 -0
- package/docs/src_js_externalUi.mjs.html +230 -0
- package/docs/src_js_findRefRows.mjs.html +107 -0
- package/docs/src_js_findTouchedDup.mjs.html +150 -0
- package/docs/src_js_fmtLossyMsg.mjs.html +95 -0
- package/docs/src_js_fromAudit.mjs.html +489 -0
- package/docs/src_js_getAddImpact.mjs.html +269 -0
- package/docs/src_js_getAuthWarn.mjs.html +463 -0
- package/docs/src_js_getCascadePreview.mjs.html +258 -0
- package/docs/src_js_getDelRefCount.mjs.html +210 -0
- package/docs/src_js_getEffMode.mjs.html +92 -0
- package/docs/src_js_getKpLossy.mjs.html +155 -0
- package/docs/src_js_getNameErrs.mjs.html +266 -0
- package/docs/src_js_getOwn.mjs.html +3 -3
- package/docs/src_js_getPropKey.mjs.html +109 -0
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +92 -0
- package/docs/src_js_joinNames.mjs.html +96 -0
- package/docs/src_js_legendFit.mjs.html +2 -2
- package/docs/src_js_pairRowsToSnapshot.mjs.html +156 -0
- package/docs/src_js_refAudit.mjs.html +359 -0
- package/docs/src_js_refCascade.mjs.html +572 -0
- package/docs/src_js_relDialogSend.mjs.html +199 -0
- package/docs/src_js_rowDigest.mjs.html +265 -0
- package/docs/src_plugins_dataReloadGuard.mjs.html +109 -0
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_authRefsClean.mjs +79 -0
- package/g_fixFromType.mjs +92 -0
- package/g_initialData.mjs +3 -3
- package/g_purgeFrom.mjs +127 -0
- package/package.json +1 -1
- package/server/WWebPerm.mjs +243 -30
- package/server/cascadeWrite.mjs +153 -0
- package/server/keyCheck.mjs +281 -0
- package/server/procCore.mjs +178 -39
- package/server/procLang.mjs +271 -0
- package/server/syncKeep.mjs +239 -0
- package/server/tools/authRefsAudit.mjs +175 -0
- package/server/tools/authRefsClean.mjs +263 -0
- package/server/tools/fromTools.mjs +725 -0
- package/server/tools/isMain.mjs +31 -0
- package/server/tools/nameDupReport.mjs +208 -0
- package/server/validateRows.mjs +13 -2
- package/server/wrapInjected.mjs +1 -1
- package/src/components/LayoutContentGrups.vue +399 -77
- package/src/components/LayoutContentPemis.vue +400 -78
- package/src/components/LayoutContentTargets.vue +261 -25
- package/src/components/LayoutContentUsers.vue +336 -98
- package/src/components/VeCgrups.vue +119 -115
- package/src/components/VeCpemis.vue +117 -115
- package/src/components/VeCrules.vue +18 -2
- package/src/components/VeGrupBlngUsers.vue +240 -73
- package/src/components/VePemiBlngGrups.vue +247 -83
- package/src/js/cvKpToArr.mjs +53 -0
- package/src/js/cvKpToObj.mjs +138 -0
- package/src/js/draftSync.mjs +70 -0
- package/src/js/eachOwn.mjs +1 -1
- package/src/js/editorRefRows.mjs +284 -0
- package/src/js/externalRows.mjs +266 -0
- package/src/js/externalUi.mjs +158 -0
- package/src/js/findRefRows.mjs +35 -0
- package/src/js/findTouchedDup.mjs +78 -0
- package/src/js/fmtLossyMsg.mjs +23 -0
- package/src/js/fromAudit.mjs +417 -0
- package/src/js/getAddImpact.mjs +197 -0
- package/src/js/getAuthWarn.mjs +391 -0
- package/src/js/getCascadePreview.mjs +186 -0
- package/src/js/getDelRefCount.mjs +138 -0
- package/src/js/getEffMode.mjs +20 -0
- package/src/js/getKpLossy.mjs +83 -0
- package/src/js/getNameErrs.mjs +194 -0
- package/src/js/getOwn.mjs +1 -1
- package/src/js/getPropKey.mjs +37 -0
- package/src/js/isAuthMode.mjs +20 -0
- package/src/js/joinNames.mjs +24 -0
- package/src/js/pairRowsToSnapshot.mjs +84 -0
- package/src/js/refAudit.mjs +287 -0
- package/src/js/refCascade.mjs +500 -0
- package/src/js/relDialogSend.mjs +127 -0
- package/src/js/rowDigest.mjs +193 -0
- package/src/plugins/dataReloadGuard.mjs +37 -0
- package/src/plugins/mShare.mjs +147 -174
- package/src/plugins/mUI.mjs +3 -8
- package/src/schema/tables/users.mjs +152 -152
- package/dist/css/app.6918ec45.css +0 -1
- package/dist/js/app-legacy.963e800c.js +0 -1
- package/dist/js/app.963e800c.js +0 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import JSON5 from 'json5'
|
|
2
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
3
|
+
import getPropKey from './getPropKey.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
//引用連帶(規則帳本 R-PRINCIPLE-00 原則 6、適用表 m/n; R-REF-CASCADE-01; ADR-036): 群組/權限/標的「改名」或「被刪除」時, 引用它之授權字串須於同一次寫入內連帶更新:
|
|
7
|
+
// users.cgrups(引用 grups.name)、grups.cpemis(引用 pemis.name)、pemis.crules(引用 targets.id)
|
|
8
|
+
//本檔為純函數(不依賴 Vue/DOM/ORM), 伺服端寫入(server/procCore.mjs、server/WWebPerm.mjs 之外部同步)與前端預覽/刪除確認框(src/components 三個清單頁)共用同一份規劃(R-REF-CASCADE-01: 預覽與伺服端計畫同一純函數)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
//REF_OF: 被引用表 → 引用端表與欄位; kind 為引用名稱之比對方式('ref': cgrups/cpemis 以 === 比對字串名稱; 'rule': crules 以屬性鍵轉型後比對, 與伺服端 mergeRules 同)
|
|
12
|
+
let REF_OF = {
|
|
13
|
+
grups: { holder: 'users', field: 'cgrups', kind: 'ref' },
|
|
14
|
+
pemis: { holder: 'grups', field: 'cpemis', kind: 'ref' },
|
|
15
|
+
targets: { holder: 'pemis', field: 'crules', kind: 'rule' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let isStr = (v) => typeof v === 'string'
|
|
20
|
+
let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
//keyOf: 引用名稱 → 比對鍵; 'ref' 只認字串(非字串名稱以 === 比對不到任何實體, 不動), 'rule' 與伺服端 mergeRules 同取屬性鍵轉型(R-KEY-PROP-01)
|
|
24
|
+
let keyOf = (name, kind) => {
|
|
25
|
+
if (kind === 'rule') {
|
|
26
|
+
return getPropKey(name)
|
|
27
|
+
}
|
|
28
|
+
return isStr(name) ? name : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
//entName: 被引用表列之名稱(grups/pemis 為 name, 缺欄視為 ''(同 findRefRows); targets 為 id), 非字串回 null
|
|
33
|
+
let entName = (r, keyTable) => {
|
|
34
|
+
if (r === null || typeof r !== 'object') {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
let n = (keyTable === 'targets') ? r.id : (r.name === undefined ? '' : r.name)
|
|
38
|
+
return isStr(n) ? n : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
//decodeKp: 授權字串 → JSON5 解析結果(物件或陣列), 空字串/壞 JSON/純量回 null(伺服端 cvStrToArr 同為視為空容器, 不動)
|
|
43
|
+
let decodeKp = (str) => {
|
|
44
|
+
if (!isStr(str) || str.trim() === '') {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
let kp = null
|
|
48
|
+
try {
|
|
49
|
+
kp = JSON5.parse(str)
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return (kp !== null && typeof kp === 'object') ? kp : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
//listEntries: 容器內所有項目(含停用者)之 { key: 比對鍵, el: 原值, i: 物件形式之外層鍵或陣列索引, isObj, isActive }; 物件形式以外層鍵為名, 陣列形式以元素之 name 為名(非物件或無 name 之元素以索引為名, 同 cvKpToArr)
|
|
59
|
+
let listEntries = (kp, kind) => {
|
|
60
|
+
let rs = []
|
|
61
|
+
if (Array.isArray(kp)) {
|
|
62
|
+
kp.forEach((el, i) => {
|
|
63
|
+
let name = (iseobj(el) && hasOwn(el, 'name')) ? el.name : i
|
|
64
|
+
rs.push({ key: keyOf(name, kind), el, i, isObj: iseobj(el), isActive: iseobj(el) ? el.isActive : el })
|
|
65
|
+
})
|
|
66
|
+
return rs
|
|
67
|
+
}
|
|
68
|
+
Object.keys(kp).forEach((k) => {
|
|
69
|
+
let v = kp[k]
|
|
70
|
+
rs.push({ key: keyOf(k, kind), el: v, i: k, isObj: iseobj(v), isActive: iseobj(v) ? v.isActive : v })
|
|
71
|
+
})
|
|
72
|
+
return rs
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
//hasNonFinite: 解析結果內是否有非有限數值(Infinity、-Infinity、NaN; JSON5 可寫, JSON.stringify 會編成 null)
|
|
77
|
+
let hasNonFinite = (v) => {
|
|
78
|
+
if (typeof v === 'number') {
|
|
79
|
+
return !Number.isFinite(v)
|
|
80
|
+
}
|
|
81
|
+
if (v !== null && typeof v === 'object') {
|
|
82
|
+
return Object.keys(v).some((k) => hasNonFinite(v[k]))
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 改寫一個授權字串(cgrups/cpemis/crules)內對被改名/被刪除者之引用
|
|
90
|
+
*
|
|
91
|
+
* 同時套用所有改名與刪除(互換 A↔B、鏈式 A→B 且 B→C 皆正確, 不逐一套用); 先判刪除、再判改名. 物件形式(以名稱為鍵)仍輸出物件形式、陣列形式仍輸出陣列形式(不互轉, 免有損),
|
|
92
|
+
* 其餘項目(含值內之 mode/isActive/額外欄位)原樣; 未命中任何改名或刪除者「同一字串原樣回傳」(不重新編碼, 保留註解與排版); 命中者以 JSON.stringify 重新編碼(註解與單引號不保留).
|
|
93
|
+
* 壞 JSON/空字串/非容器視為空容器不動(與伺服端 cvStrToArr 容錯一致).
|
|
94
|
+
* unsafe: 容器內含無法原樣重新編碼之值(JSON5 Infinity/NaN 數值)而命中須改寫時, 不改寫(str 原樣、changed 為 false)並回報 true.
|
|
95
|
+
* conflict: 改名後新名與容器內另一個(未被一併改走或刪除之)同名項目相撞——自動改寫會與既有引用混成一項, 不改寫並回報(上層整次拒存).
|
|
96
|
+
*
|
|
97
|
+
* @param {String} str 輸入授權字串(JSON5)
|
|
98
|
+
* @param {Object} [opt={}] 輸入設定物件
|
|
99
|
+
* @param {String} [opt.kind='ref'] 輸入引用種類, 'ref'(cgrups/cpemis)或 'rule'(crules)
|
|
100
|
+
* @param {Map} [opt.rename] 輸入改名對映 Map(舊名 → 新名), 預設空
|
|
101
|
+
* @param {Set} [opt.del] 輸入被刪除之名稱 Set, 預設空
|
|
102
|
+
* @returns {Object} 回傳 { str, changed, nRename, nDel, conflict }: str 為改寫後之字串(未改為同一參考), nRename/nDel 為被改名/被刪除之項目數
|
|
103
|
+
* @example
|
|
104
|
+
*
|
|
105
|
+
* let r = rewriteRefs('{"A":{"mode":"OR","isActive":"y"}}', { rename: new Map([['A', 'B']]) })
|
|
106
|
+
* console.log(r.str)
|
|
107
|
+
* // => {"B":{"mode":"OR","isActive":"y"}}
|
|
108
|
+
*
|
|
109
|
+
*/
|
|
110
|
+
function rewriteRefs(str, opt = {}) {
|
|
111
|
+
let kind = opt.kind === 'rule' ? 'rule' : 'ref'
|
|
112
|
+
let rename = opt.rename instanceof Map ? opt.rename : new Map()
|
|
113
|
+
let del = opt.del instanceof Set ? opt.del : new Set()
|
|
114
|
+
let res = { str, changed: false, nRename: 0, nDel: 0, conflict: false, unsafe: false }
|
|
115
|
+
if (rename.size === 0 && del.size === 0) {
|
|
116
|
+
return res
|
|
117
|
+
}
|
|
118
|
+
let kp = decodeKp(str)
|
|
119
|
+
if (kp === null) {
|
|
120
|
+
return res
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//entries, present: 容器內既有之比對鍵(含停用項目)
|
|
124
|
+
let entries = listEntries(kp, kind)
|
|
125
|
+
let present = new Set()
|
|
126
|
+
entries.forEach((e) => {
|
|
127
|
+
if (e.key !== null) {
|
|
128
|
+
present.add(e.key)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
//conflict: 命中改名之項目, 其新名與容器內另一個「未被改走且未被刪除」之既有項目相撞
|
|
133
|
+
let isDel = (key) => key !== null && del.has(key)
|
|
134
|
+
let isRen = (key) => key !== null && !isDel(key) && rename.has(key)
|
|
135
|
+
for (let e of entries) {
|
|
136
|
+
if (isRen(e.key)) {
|
|
137
|
+
let to = rename.get(e.key)
|
|
138
|
+
if (present.has(to) && !isRen(to) && !isDel(to)) {
|
|
139
|
+
res.conflict = true
|
|
140
|
+
return res
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//改寫
|
|
146
|
+
let nRename = 0
|
|
147
|
+
let nDel = 0
|
|
148
|
+
let out = null
|
|
149
|
+
if (Array.isArray(kp)) {
|
|
150
|
+
out = []
|
|
151
|
+
|
|
152
|
+
//kFirstDel: 第一個被刪除項目之索引; 'rule'(crules)之索引名元素(非物件或無 name 者, 名稱即其索引)排在其後者, 刪除後索引前移會改指別的規則(伺服端 cvKpToArr 以索引為名),
|
|
153
|
+
//故具名化為其原索引(解碼結果不變: 非物件元素 { name: 索引, isActive: 值 }), 免靜默改變授權; 'ref' 之索引名為非字串, 不對應任何實體, 前移無影響
|
|
154
|
+
let kFirstDel = -1
|
|
155
|
+
if (kind === 'rule') {
|
|
156
|
+
entries.forEach((e) => {
|
|
157
|
+
if (kFirstDel < 0 && isDel(e.key)) {
|
|
158
|
+
kFirstDel = e.i
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
entries.forEach((e) => {
|
|
163
|
+
let hasName = e.isObj && hasOwn(e.el, 'name')
|
|
164
|
+
if (isDel(e.key)) {
|
|
165
|
+
nDel++
|
|
166
|
+
}
|
|
167
|
+
else if (isRen(e.key) && e.isObj) {
|
|
168
|
+
nRename++
|
|
169
|
+
out.push({ ...e.el, name: rename.get(e.key) })
|
|
170
|
+
}
|
|
171
|
+
else if (kFirstDel >= 0 && e.i > kFirstDel && !hasName) {
|
|
172
|
+
out.push(e.isObj ? { ...e.el, name: e.i } : { name: e.i, isActive: e.el })
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
out.push(e.el)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
out = Object.create(null) //無原型: 鍵為使用者資料, 含 __proto__ 者須為自有鍵(規則帳本 R-KEY-01)
|
|
181
|
+
entries.forEach((e) => {
|
|
182
|
+
if (isDel(e.key)) {
|
|
183
|
+
nDel++
|
|
184
|
+
}
|
|
185
|
+
else if (isRen(e.key)) {
|
|
186
|
+
nRename++
|
|
187
|
+
out[rename.get(e.key)] = e.el
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
out[e.i] = e.el
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (nRename === 0 && nDel === 0) {
|
|
195
|
+
return res
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//unsafe: 容器內含 JSON.stringify 無法原樣編碼之值(JSON5 之 Infinity/NaN 數值會被編成 null, 作為規則名時改指別的規則『null』, 作為值時亦改變語意), 命中而須重新編碼時不改寫並回報(上層整次拒存; 稽核工具列入 unsafe)
|
|
199
|
+
if (hasNonFinite(kp)) {
|
|
200
|
+
res.unsafe = true
|
|
201
|
+
return res
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { str: JSON.stringify(out), changed: true, nRename, nDel, conflict: false, unsafe: false }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 只依被引用表自身之新舊列, 判定「改名」與「須清理引用之被刪名稱」(與引用端無關; 後台整表寫入據以決定是否須再取引用端表之鎖, 僅被引用表有改名或刪除時才須)
|
|
210
|
+
*
|
|
211
|
+
* 身分以主鍵 id 判定: 同 id 且名稱皆為字串而不同=改名(僅 grups/pemis, 且 opt.cascadeRename 非 false; 互換與鏈式同時套用); 舊列 id 不在新表=刪除(targets 之 id 即名稱, 改 id 即刪舊增新, 標的 id 改名之級聯為下一階段);
|
|
212
|
+
* 刪除之名稱若仍有「同 id 同名之存續列」(歷史同名只刪其一)則不清理(引用仍解得出, 其授權變寬另由刪除前確認框提示); 刪除後以同名新增(新 id)者不繼承引用(原則 6: 不被同名新資料接管).
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} p 輸入參數物件, 同 planRefChanges(只用 keyTable、rowsOld、rowsNew、opt.cascadeRename)
|
|
215
|
+
* @returns {Object} 回傳 { renames, rename, delNames, del, oldById, newById }: renames 為 [{ id, from, to }], rename 為 Map(舊名 → 新名), delNames/del 為須清理之名稱陣列/Set, oldById/newById 為以字串主鍵建立之列 Map
|
|
216
|
+
*/
|
|
217
|
+
function diffRefTargets(p) {
|
|
218
|
+
let keyTable = p ? p.keyTable : ''
|
|
219
|
+
let rs = { renames: [], rename: new Map(), delNames: [], del: new Set(), oldById: new Map(), newById: new Map() }
|
|
220
|
+
if (!hasOwn(REF_OF, keyTable)) {
|
|
221
|
+
return rs
|
|
222
|
+
}
|
|
223
|
+
let cascadeRename = !(p.opt && p.opt.cascadeRename === false)
|
|
224
|
+
let rowsOld = Array.isArray(p.rowsOld) ? p.rowsOld : []
|
|
225
|
+
let rowsNew = Array.isArray(p.rowsNew) ? p.rowsNew : []
|
|
226
|
+
|
|
227
|
+
//oldById, newById: 只認字串主鍵(缺 id 之新列為新增, 不對應既有列)
|
|
228
|
+
let oldById = rs.oldById
|
|
229
|
+
rowsOld.forEach((r) => {
|
|
230
|
+
if (r !== null && typeof r === 'object' && isStr(r.id)) {
|
|
231
|
+
oldById.set(r.id, r)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
let newById = rs.newById
|
|
235
|
+
rowsNew.forEach((r) => {
|
|
236
|
+
if (r !== null && typeof r === 'object' && isStr(r.id)) {
|
|
237
|
+
newById.set(r.id, r)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
//renames: 同 id 且名稱皆為字串而不同; keptNames: 同 id 同名之存續列之名稱
|
|
242
|
+
let keptNames = new Set()
|
|
243
|
+
newById.forEach((rn, id) => {
|
|
244
|
+
let ro = oldById.get(id)
|
|
245
|
+
if (ro === undefined) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
let from = entName(ro, keyTable)
|
|
249
|
+
let to = entName(rn, keyTable)
|
|
250
|
+
if (from !== null && from === to) {
|
|
251
|
+
keptNames.add(from)
|
|
252
|
+
}
|
|
253
|
+
if (cascadeRename && keyTable !== 'targets' && from !== null && to !== null && from !== to) {
|
|
254
|
+
rs.renames.push({ id, from, to })
|
|
255
|
+
rs.rename.set(from, to)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
//delNames: 舊列 id 不在新表, 其名稱不再有同 id 同名之存續列者
|
|
260
|
+
oldById.forEach((ro, id) => {
|
|
261
|
+
if (newById.has(id)) {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
let name = entName(ro, keyTable)
|
|
265
|
+
if (name !== null && !keptNames.has(name)) {
|
|
266
|
+
rs.del.add(name)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
rs.delNames = [...rs.del]
|
|
270
|
+
|
|
271
|
+
return rs
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 計算「被引用表之寫入」對引用端授權字串之連帶更新計畫(改名級聯+刪除連帶清理)
|
|
277
|
+
*
|
|
278
|
+
* 改名與刪除之判定(身分以主鍵 id、同時套用、歷史同名只刪其一不清理、刪後同名新增不繼承)見 diffRefTargets.
|
|
279
|
+
* 拒絕(整次不寫入, 收嚴+提示, 原則 3、適用表 m):
|
|
280
|
+
* renameCascadeDupGroup 被改名者屬歷史同名組(庫內另有同名列)且該名稱仍被引用(級聯會把原綁各筆之交集的引用轉給被改名者, 外審 A.1)——須先清理再改名; 該名稱無人引用則不擋(歷史同名之清理路徑)
|
|
281
|
+
* renameCascadeNameInUse 新名為「仍被啟用中引用但無任何實體」之懸空名(級聯後舊引用與新實體混成一項, 授權被新名稱接管, Q4)
|
|
282
|
+
* renameCascadeConflict 某容器內新名與另一既有項目相撞, 無法自動改寫
|
|
283
|
+
* renameCascadeUnsupported 某容器內含無法安全重新編碼之內容(JSON5 之 Infinity/NaN 數值), 命中而須改寫時無法自動改寫(改刪除時亦同)
|
|
284
|
+
* opt.cascadeRename 為 false 時不做改名級聯(亦不做上述改名相關拒絕; 舊行為, 引用成為懸空); 刪除連帶清理不受此設定影響.
|
|
285
|
+
*
|
|
286
|
+
* @param {Object} p 輸入參數物件
|
|
287
|
+
* @param {String} p.keyTable 輸入被寫入之表, 'grups'、'pemis' 或 'targets'; 其他回傳空計畫
|
|
288
|
+
* @param {Array} p.rowsOld 輸入庫內該表全部列(寫入前)
|
|
289
|
+
* @param {Array} p.rowsNew 輸入寫入後該表全部列(後台為送入之整表; 外部同步為他 from 之既有列加本批列)
|
|
290
|
+
* @param {Array} p.holdersOld 輸入引用端表全部列(users/grups/pemis, 寫入前)
|
|
291
|
+
* @param {Object} [p.opt={}] 輸入設定物件
|
|
292
|
+
* @param {Boolean} [p.opt.cascadeRename=true] 輸入是否做改名級聯, 預設true
|
|
293
|
+
* @returns {Object} 回傳 { err, keyTable, holder, field, renames, delNames, changes, nHolders, nRenameHolders, nDelHolders, nExternal }:
|
|
294
|
+
* err 為 null 或拒絕之語系鍵; renames 為 [{ id, from, to }]; delNames 為須清理之名稱; changes 為須改寫之引用端列 [{ id, field, before, after, nRename, nDel, from }](before/after 為該欄位字串);
|
|
295
|
+
* nHolders 為須改寫之引用端列數, nRenameHolders/nDelHolders 為受改名/刪除影響之列數(一列可兩者皆計), nExternal 為其中來源 from 非空(外部應用)之列數
|
|
296
|
+
* @example
|
|
297
|
+
*
|
|
298
|
+
* let p = planRefChanges({
|
|
299
|
+
* keyTable: 'grups',
|
|
300
|
+
* rowsOld: [{ id: 'g1', name: 'A' }],
|
|
301
|
+
* rowsNew: [{ id: 'g1', name: 'B' }],
|
|
302
|
+
* holdersOld: [{ id: 'u1', cgrups: '{"A":{"mode":"OR","isActive":"y"}}' }],
|
|
303
|
+
* })
|
|
304
|
+
* console.log(p.changes[0].after)
|
|
305
|
+
* // => {"B":{"mode":"OR","isActive":"y"}}
|
|
306
|
+
*
|
|
307
|
+
*/
|
|
308
|
+
function planRefChanges(p) {
|
|
309
|
+
let keyTable = p ? p.keyTable : ''
|
|
310
|
+
let ref = hasOwn(REF_OF, keyTable) ? REF_OF[keyTable] : null
|
|
311
|
+
let plan = {
|
|
312
|
+
err: null,
|
|
313
|
+
keyTable,
|
|
314
|
+
holder: ref ? ref.holder : '',
|
|
315
|
+
field: ref ? ref.field : '',
|
|
316
|
+
renames: [],
|
|
317
|
+
delNames: [],
|
|
318
|
+
changes: [],
|
|
319
|
+
nHolders: 0,
|
|
320
|
+
nRenameHolders: 0,
|
|
321
|
+
nDelHolders: 0,
|
|
322
|
+
nExternal: 0,
|
|
323
|
+
}
|
|
324
|
+
if (ref === null) {
|
|
325
|
+
return plan
|
|
326
|
+
}
|
|
327
|
+
let holders = Array.isArray(p.holdersOld) ? p.holdersOld : []
|
|
328
|
+
|
|
329
|
+
//renames, delNames: 只依被引用表自身之新舊列判定(與引用端無關)
|
|
330
|
+
let d0 = diffRefTargets(p)
|
|
331
|
+
let { oldById, newById, rename, del } = d0
|
|
332
|
+
plan.renames = d0.renames
|
|
333
|
+
plan.delNames = d0.delNames
|
|
334
|
+
if (rename.size === 0 && del.size === 0) {
|
|
335
|
+
return plan
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
//防禦: 同批兩列改名為同一新名(或兩個舊名映射到同一新名)——引用改寫後兩項併為一項而改變授權; 上游名稱唯一檢查(findTouchedDup/findDupIndex/前端 getNameErrs)本會擋下, 此處不依賴它(外審 minimax R6)
|
|
339
|
+
let namesTo = new Set()
|
|
340
|
+
for (let rn of plan.renames) {
|
|
341
|
+
if (namesTo.has(rn.to)) {
|
|
342
|
+
plan.err = 'renameCascadeConflict'
|
|
343
|
+
return plan
|
|
344
|
+
}
|
|
345
|
+
namesTo.add(rn.to)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
//解碼各引用端列一次(歷史同名組檢查、接管檢查與改寫共用)
|
|
349
|
+
let decoded = holders.map((h) => {
|
|
350
|
+
let str = (h !== null && typeof h === 'object') ? h[ref.field] : undefined
|
|
351
|
+
return { h, str, kp: decodeKp(str) }
|
|
352
|
+
}).filter((d) => d.h !== null && typeof d.h === 'object' && isStr(d.h.id)) //缺字串 id 之引用端列無從寫回, 不納入
|
|
353
|
+
|
|
354
|
+
//歷史同名組之改名: 庫內另有同名列且該名稱仍被引用(含停用項)——引用原綁「各筆之交集」, 把引用轉給被改名者會改變授權(外審 A.1); 無人引用則無引用可轉, 不擋(清理歷史同名之改名路徑)
|
|
355
|
+
let dupRenames = plan.renames.filter((rn) => {
|
|
356
|
+
let nSame = 0
|
|
357
|
+
oldById.forEach((ro) => {
|
|
358
|
+
if (entName(ro, keyTable) === rn.from) {
|
|
359
|
+
nSame++
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
return nSame >= 2
|
|
363
|
+
})
|
|
364
|
+
if (dupRenames.length > 0) {
|
|
365
|
+
let namesDup = new Set(dupRenames.map((rn) => rn.from))
|
|
366
|
+
for (let d of decoded) {
|
|
367
|
+
if (d.kp !== null && listEntries(d.kp, ref.kind).some((e) => e.key !== null && namesDup.has(e.key))) {
|
|
368
|
+
plan.err = 'renameCascadeDupGroup'
|
|
369
|
+
return plan
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
//新名接管檢查: 新名無任何舊實體(或其舊實體不是在本次一併改走/刪除), 而仍被啟用中之引用指向(懸空名)
|
|
375
|
+
if (plan.renames.length > 0) {
|
|
376
|
+
let namesOld = new Set()
|
|
377
|
+
oldById.forEach((ro) => {
|
|
378
|
+
let n = entName(ro, keyTable)
|
|
379
|
+
if (n !== null) {
|
|
380
|
+
namesOld.add(n)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
let ghosts = new Set()
|
|
384
|
+
plan.renames.forEach((rn) => {
|
|
385
|
+
if (!namesOld.has(rn.to)) {
|
|
386
|
+
ghosts.add(rn.to)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
if (ghosts.size > 0) {
|
|
390
|
+
for (let d of decoded) {
|
|
391
|
+
if (d.kp === null) {
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
for (let e of listEntries(d.kp, ref.kind)) {
|
|
395
|
+
if (e.key !== null && ghosts.has(e.key) && e.isActive === 'y') {
|
|
396
|
+
plan.err = 'renameCascadeNameInUse'
|
|
397
|
+
return plan
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//改寫
|
|
405
|
+
for (let d of decoded) {
|
|
406
|
+
let r = rewriteRefs(d.str, { kind: ref.kind, rename, del })
|
|
407
|
+
if (r.conflict || r.unsafe) {
|
|
408
|
+
plan.err = r.conflict ? 'renameCascadeConflict' : 'renameCascadeUnsupported'
|
|
409
|
+
plan.changes = []
|
|
410
|
+
plan.nHolders = 0
|
|
411
|
+
plan.nRenameHolders = 0
|
|
412
|
+
plan.nDelHolders = 0
|
|
413
|
+
plan.nExternal = 0
|
|
414
|
+
return plan
|
|
415
|
+
}
|
|
416
|
+
if (!r.changed) {
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
let from = (isStr(d.h.from) && d.h.from !== '') ? d.h.from : ''
|
|
420
|
+
plan.changes.push({ id: d.h.id, field: ref.field, before: d.str, after: r.str, nRename: r.nRename, nDel: r.nDel, from })
|
|
421
|
+
if (r.nRename > 0) {
|
|
422
|
+
plan.nRenameHolders++
|
|
423
|
+
}
|
|
424
|
+
if (r.nDel > 0) {
|
|
425
|
+
plan.nDelHolders++
|
|
426
|
+
}
|
|
427
|
+
if (from !== '') {
|
|
428
|
+
plan.nExternal++
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
plan.nHolders = plan.changes.length
|
|
432
|
+
|
|
433
|
+
return plan
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 為「寫入後整表」中未帶主鍵 id 之列補規劃用之 id(僅供 diffRefTargets/planRefChanges, 不改寫入之列)
|
|
439
|
+
*
|
|
440
|
+
* 後台整表寫入(API 直打)與外部同步之列可不帶 id, 寫入時由 ORM 補隨機主鍵, 即每次皆為「舊列被刪、新列被新增」而身分不穩定; 若以 id 判定會把每次寫入都當成全部刪除再新增而清空引用.
|
|
441
|
+
* 故未帶 id 之列(grups/pemis)改以「名稱」對應 rowsPool 中 id 未被本批帶用之既有列(同名即同一實體, 不算刪除再新增; 同名多列依序對應); 名稱不同者視為新實體(給不與任何既有主鍵相同之規劃 id).
|
|
442
|
+
* 有限數字之 id 取其字串形式(同寫入前之主鍵正規化); targets 無此問題(id 即名稱, 必帶).
|
|
443
|
+
*
|
|
444
|
+
* @param {String} keyTable 輸入被寫入之表名
|
|
445
|
+
* @param {Array} rowsPool 輸入可供對應之既有列(後台為該表全部既有列; 外部同步為同一 from 之既有列)
|
|
446
|
+
* @param {Array} rows 輸入本批列
|
|
447
|
+
* @param {Array} [rowsAvoid=[]] 輸入規劃 id 須避開之其他既有列(外部同步之他 from 列), 預設空
|
|
448
|
+
* @returns {Array} 回傳補好規劃 id 之新列陣列(複本)
|
|
449
|
+
*/
|
|
450
|
+
function fillIdlessIds(keyTable, rowsPool, rows, rowsAvoid = []) {
|
|
451
|
+
let toId = (v) => {
|
|
452
|
+
let id = (v !== null && typeof v === 'object') ? v.id : undefined
|
|
453
|
+
if (isStr(id) && id !== '') {
|
|
454
|
+
return id
|
|
455
|
+
}
|
|
456
|
+
return (typeof id === 'number' && Number.isFinite(id)) ? String(id) : null
|
|
457
|
+
}
|
|
458
|
+
let idsGiven = new Set()
|
|
459
|
+
rows.forEach((v) => {
|
|
460
|
+
let id = toId(v)
|
|
461
|
+
if (id !== null) {
|
|
462
|
+
idsGiven.add(id)
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
let pool = new Map()
|
|
466
|
+
rowsPool.forEach((r) => {
|
|
467
|
+
if (r !== null && typeof r === 'object' && isStr(r.id) && !idsGiven.has(r.id) && isStr(r.name)) {
|
|
468
|
+
if (!pool.has(r.name)) {
|
|
469
|
+
pool.set(r.name, [])
|
|
470
|
+
}
|
|
471
|
+
pool.get(r.name).push(r.id)
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
//used: 已被占用之 id(pool 與 rowsAvoid 之既有列之全部主鍵與本批已帶之 id), 新實體之規劃 id 須避開; 外部同步之 pool 只含同 from 之列, 他 from 之列經 rowsAvoid 傳入(其主鍵仍屬寫入後整表, 撞號會使規劃把兩個實體併成一個)
|
|
475
|
+
let used = new Set(idsGiven)
|
|
476
|
+
;[...rowsPool, ...rowsAvoid].forEach((r) => {
|
|
477
|
+
if (r !== null && typeof r === 'object' && isStr(r.id)) {
|
|
478
|
+
used.add(r.id)
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
return rows.map((v, k) => {
|
|
482
|
+
let id = toId(v)
|
|
483
|
+
if (id === null && keyTable !== 'targets' && v !== null && typeof v === 'object' && isStr(v.name) && pool.has(v.name) && pool.get(v.name).length > 0) {
|
|
484
|
+
id = pool.get(v.name).shift()
|
|
485
|
+
}
|
|
486
|
+
if (id === null) {
|
|
487
|
+
let n = k
|
|
488
|
+
while (used.has(`__new__${n}`)) { //新實體: 不與任何既有主鍵或本批已帶 id 相同
|
|
489
|
+
n++
|
|
490
|
+
}
|
|
491
|
+
id = `__new__${n}`
|
|
492
|
+
used.add(id)
|
|
493
|
+
}
|
|
494
|
+
return { ...v, id }
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
export { REF_OF, rewriteRefs, diffRefTargets, planRefChanges, fillIdlessIds, keyOf, entName, decodeKp, listEntries } //keyOf/entName/decodeKp/listEntries: run07 新增匯出(行為不變), 供 getAddImpact 沿用同一套引用解碼與名稱比對口徑
|
|
500
|
+
export default planRefChanges
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
//反向關聯對話框(VeGrupBlngUsers/VePemiBlngGrups)存檔時「送出之整表」之組裝(F-26 對話框部分、S4; R-PRINCIPLE-00 原則 3; 規則帳本 R-BASELINE-01): 純函數(不依賴 Vue/DOM), 兩個對話框共用.
|
|
2
|
+
//
|
|
3
|
+
//背景: 對話框之列是開框時 store 快照之投影(只含「歸屬」一欄可改). 修前存檔以「對話框之列」映射成整表送出, 開框期間他人/外部同步新增之列不在對話框內, 被伺服端整表 diff 刪除(S4);
|
|
4
|
+
//且對話框之列為開框時之草稿值, 若某列之歸屬於開框後被他人改過而使用者並未碰它, 以草稿值改寫即以舊值覆蓋他人之變更.
|
|
5
|
+
//修後: 送出之整表=存檔當下 store 快照之全部列; 使用者於本次編輯有變更之列(相對開框時之歸屬狀態)才以 putRefEntry 只改本群組/權限之一項(套用於存檔當下快照之字串);
|
|
6
|
+
//其餘一律原樣帶回(同一個物件、逐字不動). 注意(run11 起): 伺服端載入基準比對為表級(整表, R-BASELINE-01), 與「帶回哪個版本之列」無關——開框後他人若改了任何一列或增刪列, 本次存檔仍整次拒收 saveStale(請重新開啟); 帶回存檔當下 store 版本之列只是避免誤刪/誤改他人之列.
|
|
7
|
+
//本檔不解析授權字串(解析與 putRefEntry 留在 .vue 之 doSave, 站點由 unit-cvKpToObj SITE-AD-001 盤點), 只處理狀態比對與整表組裝.
|
|
8
|
+
|
|
9
|
+
import getEffMode from './getEffMode.mjs'
|
|
10
|
+
import { toId } from './pairRowsToSnapshot.mjs'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let isObj = (v) => v !== null && typeof v === 'object'
|
|
14
|
+
let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 取得對話框列之歸屬有效狀態(與 putRefEntry 之相等判定同一口徑: 不歸屬 → 'n'; 歸屬 → 'y:' 加有效 mode)
|
|
19
|
+
*
|
|
20
|
+
* 不歸屬時 mode 無授權效果(putRefEntry 對停用目標不看 mode), 故不納入; 歸屬時 mode 取 getEffMode(非 'OR' 者為 'AND').
|
|
21
|
+
*
|
|
22
|
+
* @param {*} enable 輸入列之 enable('y' 為歸屬, 其他為不歸屬)
|
|
23
|
+
* @param {*} mode 輸入列之 mode
|
|
24
|
+
* @returns {String} 回傳 'n'、'y:OR' 或 'y:AND'
|
|
25
|
+
* @example
|
|
26
|
+
*
|
|
27
|
+
* console.log(getRowState('n', 'AND'), getRowState('y', 'OR'), getRowState('y', 'or'))
|
|
28
|
+
* // => n y:OR y:AND
|
|
29
|
+
*
|
|
30
|
+
*/
|
|
31
|
+
function getRowState(enable, mode) {
|
|
32
|
+
return enable === 'y' ? `y:${getEffMode(mode)}` : 'n'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 由開框時生成之列(items)記錄各列之歸屬有效狀態(以列身分為鍵), 供存檔時判定「使用者於本次編輯有無變更該列」
|
|
38
|
+
*
|
|
39
|
+
* 須於 show() 生成 items 之同一處、尚未有任何切換操作前呼叫(opt.rows 與 items 為同一份陣列, 切換會就地改 enable/mode).
|
|
40
|
+
*
|
|
41
|
+
* @param {Array} rows 輸入開框時生成之列(各列帶 id、enable、mode); 非陣列視為無列
|
|
42
|
+
* @returns {Object} 回傳無原型物件 id(字串形式; 數字身分以其字串形式) → 'n'|'y:OR'|'y:AND'; 缺身分之列不記, 同身分重複者取第一筆
|
|
43
|
+
*/
|
|
44
|
+
function makeRowStates(rows) {
|
|
45
|
+
let kp = Object.create(null) //無原型: 鍵為使用者資料(R-KEY-01)
|
|
46
|
+
;(Array.isArray(rows) ? rows : []).forEach((r) => {
|
|
47
|
+
let id = isObj(r) ? toId(r.id) : null
|
|
48
|
+
if (id !== null && !(id in kp)) {
|
|
49
|
+
kp[id] = getRowState(r.enable, r.mode)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
return kp
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 找出對話框之列中「使用者於本次編輯相對開框時有變更」者之列索引
|
|
58
|
+
*
|
|
59
|
+
* 比對列目前之歸屬有效狀態與開框時記錄者(makeRowStates); 相同(含切換後又切回)視為未變更.
|
|
60
|
+
* 無法證明未變更者一律視為有變更(fail toward 既有流程: 對存檔當下快照字串套用 putRefEntry, 再由伺服端基準比對把關): 列缺身分、開框時無該列之記錄、未給記錄(null/非物件).
|
|
61
|
+
*
|
|
62
|
+
* @param {Array} rows 輸入對話框目前之列(opt.rows); 非陣列回傳空物件
|
|
63
|
+
* @param {Object} states 輸入 makeRowStates 之結果
|
|
64
|
+
* @returns {Object} 回傳無原型物件 列索引 → true(只含有變更者)
|
|
65
|
+
* @example
|
|
66
|
+
*
|
|
67
|
+
* let s = makeRowStates([{ id: 'u1', enable: 'n', mode: 'OR' }, { id: 'u2', enable: 'y', mode: 'OR' }])
|
|
68
|
+
* let ch = getChangedIdx([{ id: 'u1', enable: 'y', mode: 'OR' }, { id: 'u2', enable: 'y', mode: 'OR' }], s)
|
|
69
|
+
* console.log(Object.keys(ch))
|
|
70
|
+
* // => ['0']
|
|
71
|
+
*
|
|
72
|
+
*/
|
|
73
|
+
function getChangedIdx(rows, states) {
|
|
74
|
+
let kp = Object.create(null)
|
|
75
|
+
if (!Array.isArray(rows)) {
|
|
76
|
+
return kp
|
|
77
|
+
}
|
|
78
|
+
for (let kr = 0; kr < rows.length; kr++) { //索引迴圈: 稀疏陣列之空洞視為無法證明未變更
|
|
79
|
+
let r = rows[kr]
|
|
80
|
+
let id = isObj(r) ? toId(r.id) : null
|
|
81
|
+
let st0 = (id !== null && isObj(states) && hasOwn(states, id)) ? states[id] : undefined
|
|
82
|
+
if (st0 === undefined || !isObj(r) || st0 !== getRowState(r.enable, r.mode)) {
|
|
83
|
+
kp[kr] = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return kp
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 組裝送出之整表: 存檔當下 store 快照之全部列(依快照列序), 僅「有新值」之列改其引用欄位, 其餘原樣帶回
|
|
92
|
+
*
|
|
93
|
+
* 不改輸入; 未被改之列即快照中同一個物件(逐字不動, 含開框後他人新增而不在對話框內之列、使用者未變更之列).
|
|
94
|
+
* 被改之列為快照列之淺拷貝加新值(只換 refKey 一欄, 其餘欄位逐字保留).
|
|
95
|
+
*
|
|
96
|
+
* @param {Array} snapshot 輸入存檔當下之 store 快照(依 order 排序之全部列); 非陣列回傳空陣列
|
|
97
|
+
* @param {Array} pairs 輸入 pairRowsToSnapshot 之 pairs(對話框第 kr 列配到之快照列, 同一個物件參考; 配不到者為 null)
|
|
98
|
+
* @param {Object} newVals 輸入列索引 → 該列新字串(只放使用者有變更且確需改寫者)
|
|
99
|
+
* @param {String} refKey 輸入引用欄位名('cgrups'|'cpemis')
|
|
100
|
+
* @returns {Array} 回傳送出之整表(與 snapshot 等長同序)
|
|
101
|
+
* @example
|
|
102
|
+
*
|
|
103
|
+
* let snapshot = [{ id: 'u1', cgrups: '{}' }, { id: 'u2', cgrups: '{"G":{}}' }, { id: 'u3', cgrups: '{}' }]
|
|
104
|
+
* let rows = buildRowsSend(snapshot, [snapshot[0], snapshot[1]], { 0: '{"G1":{"mode":"OR","isActive":"y"}}' }, 'cgrups')
|
|
105
|
+
* console.log(rows.map((r) => r.cgrups), rows[1] === snapshot[1], rows[2] === snapshot[2])
|
|
106
|
+
* // => [ '{"G1":{"mode":"OR","isActive":"y"}}', '{"G":{}}', '{}' ] true true
|
|
107
|
+
*
|
|
108
|
+
*/
|
|
109
|
+
function buildRowsSend(snapshot, pairs, newVals, refKey) {
|
|
110
|
+
if (!Array.isArray(snapshot)) {
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
let kpNew = new Map() //快照列(物件參考) → 新字串; 同一快照列被多個對話框列配到時取第一個(對話框每個身分只有一列, 防禦用)
|
|
114
|
+
if (Array.isArray(pairs) && isObj(newVals)) {
|
|
115
|
+
for (let kr = 0; kr < pairs.length; kr++) {
|
|
116
|
+
let u = pairs[kr]
|
|
117
|
+
if (isObj(u) && hasOwn(newVals, kr) && typeof newVals[kr] === 'string' && !kpNew.has(u)) {
|
|
118
|
+
kpNew.set(u, newVals[kr])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return snapshot.map((row) => (kpNew.has(row) ? { ...row, [refKey]: kpNew.get(row) } : row))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
export { getRowState, makeRowStates, getChangedIdx, buildRowsSend }
|
|
127
|
+
export default buildRowsSend
|