w-web-perm 1.1.4 → 1.1.6
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 +1 -1
- package/dist/css/app.6918ec45.css +1 -0
- package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.3c580e72.css} +1 -1
- package/dist/index.tmp +2 -2
- package/dist/js/app-legacy.5f15a58f.js +1 -0
- package/dist/js/app.5f15a58f.js +1 -0
- package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.38f515f9.js} +16 -16
- package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.38f515f9.js} +16 -16
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +7 -7
- package/docs/global.html +2068 -111
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +63 -31
- package/docs/server_lockSave.mjs.html +115 -0
- package/docs/server_maskLog.mjs.html +176 -0
- package/docs/server_procCore.mjs.html +31 -5
- package/docs/server_wrapInjected.mjs.html +163 -0
- package/docs/src_fetchJson.mjs.html +31 -5
- package/docs/src_js_legendFit.mjs.html +225 -0
- package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
- package/docs/src_plugins_submitGuard.mjs.html +163 -0
- package/package.json +13 -13
- package/public/index.html +2 -0
- package/server/WWebPerm.mjs +61 -29
- package/server/lockSave.mjs +43 -0
- package/server/maskLog.mjs +104 -0
- package/server/procCore.mjs +29 -3
- package/server/procLang.mjs +8 -0
- package/server/procStaInfor.mjs +32 -14
- package/server/wrapInjected.mjs +91 -0
- package/src/components/Common/CheckYes.vue +6 -0
- package/src/components/Common/CheckYesNo.vue +6 -0
- package/src/components/LayoutContentGrups.vue +30 -21
- package/src/components/LayoutContentPemis.vue +30 -21
- package/src/components/LayoutContentStaInfor.vue +74 -5
- package/src/components/LayoutContentTargets.vue +36 -22
- package/src/components/LayoutContentUsers.vue +31 -21
- package/src/components/RelationChips.vue +11 -31
- package/src/components/VeCgrups.vue +8 -1
- package/src/components/VeCpemis.vue +8 -1
- package/src/components/VeCrules.vue +8 -1
- package/src/components/VeGrupBlngUsers.vue +37 -31
- package/src/components/VePemiBlngGrups.vue +37 -31
- package/src/fetchJson.mjs +28 -2
- package/src/getPerm.mjs +9 -6
- package/src/getPermUserInfor.mjs +12 -9
- package/src/js/legendFit.mjs +153 -0
- package/src/plugins/mUI.mjs +20 -0
- package/src/plugins/reuseDeletedIds.mjs +64 -0
- package/src/plugins/submitGuard.mjs +91 -0
- package/srv.mjs +20 -1
- package/dist/css/app.506ce4d8.css +0 -1
- package/dist/js/app-legacy.8e615442.js +0 -1
- package/dist/js/app.8e615442.js +0 -1
- package/tableTags-web-perm.json +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
2
|
+
import isarr from 'wsemi/src/isarr.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//maskLog: 權杖與網址寫入 console / srLog 前之遮罩(M 契約, spec/設計要點與取捨.md ADR-023)
|
|
6
|
+
//四 repo 一致(w-web-sso server/srLog.mjs 之 maskToken 與 src/maskUrl.mjs、w-web-api / w-web-task 之 maskTok), 以同一組測資守護(test/unit-maskLog.test.mjs)
|
|
7
|
+
//why: 原 console 站點原樣印出 token / req.query / authorization(Hapi 對重複之 query 參數給陣列, 型別檢查失敗之分支正是印出處), verifyConn 之 srLog 原樣記 referer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
//safe: 露出字元之控制字元換成 '?', 避免請求端以 CR/LF 偽造 log 行
|
|
11
|
+
function safe(s) {
|
|
12
|
+
return Array.from(s, (c) => {
|
|
13
|
+
let x = c.charCodeAt(0)
|
|
14
|
+
return (x < 32 || x === 127) ? '?' : c
|
|
15
|
+
}).join('')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 權杖遮罩
|
|
21
|
+
* - 空值(undefined / null / '')回 ''
|
|
22
|
+
* - 陣列逐元素遮罩
|
|
23
|
+
* - 字串(長度 n)露出前後各 k=min(4, floor(n/8)) 字: k=0 只回 '(len=n)', 否則 '前k...後k(len=n)'; 36 字元之 session token 為前 4 後 4(與 w-web-sso 格式相同)
|
|
24
|
+
* - 其他型別只回 '(typeof)', 絕不輸出原值
|
|
25
|
+
*
|
|
26
|
+
* @param {*} token 輸入權杖
|
|
27
|
+
* @returns {String|Array} 回傳遮罩後字串, 輸入陣列時回傳陣列
|
|
28
|
+
*/
|
|
29
|
+
function maskTok(token) {
|
|
30
|
+
if (isarr(token)) {
|
|
31
|
+
return token.map(maskTok)
|
|
32
|
+
}
|
|
33
|
+
if (token === undefined || token === null || token === '') {
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
if (!isestr(token)) {
|
|
37
|
+
return `(${typeof token})`
|
|
38
|
+
}
|
|
39
|
+
let n = token.length
|
|
40
|
+
let k = Math.min(4, Math.floor(n / 8))
|
|
41
|
+
if (k === 0) {
|
|
42
|
+
return `(len=${n})`
|
|
43
|
+
}
|
|
44
|
+
return `${safe(token.slice(0, k))}...${safe(token.slice(-k))}(len=${n})`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 查詢參數遮罩: 淺拷貝, 鍵名小寫為 token 者以 maskTok 處理, 其餘原樣(供印出 req.query)
|
|
50
|
+
* 非物件輸入依 maskTok 處理
|
|
51
|
+
*
|
|
52
|
+
* @param {*} q 輸入查詢參數物件(Hapi 之 req.query 為 null-prototype 物件)
|
|
53
|
+
* @returns {Object|String|Array} 回傳遮罩後之淺拷貝
|
|
54
|
+
*/
|
|
55
|
+
function maskQuery(q) {
|
|
56
|
+
if (q === null || typeof q !== 'object' || isarr(q)) {
|
|
57
|
+
return maskTok(q)
|
|
58
|
+
}
|
|
59
|
+
let r = {}
|
|
60
|
+
for (let k of Object.keys(q)) {
|
|
61
|
+
r[k] = (k.toLowerCase() === 'token') ? maskTok(q[k]) : q[k]
|
|
62
|
+
}
|
|
63
|
+
return r
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 網址遮罩: http(s) 只留 origin + pathname, 捨棄 query / fragment / userinfo(其內常含權杖, 如 ?token=)
|
|
69
|
+
* - 陣列逐元素處理; 空值回 ''; 其他非字串只回 '(typeof)'
|
|
70
|
+
* - 非 http(s) 之網址(如 data:、javascript:)路徑即全部內容, 只回 protocol(同 w-web-sso src/maskUrl.mjs)
|
|
71
|
+
* - 無法解析回 '(invalid-url)'
|
|
72
|
+
*
|
|
73
|
+
* @param {*} u 輸入網址
|
|
74
|
+
* @returns {String|Array} 回傳可寫入 log 之字串, 輸入陣列時回傳陣列
|
|
75
|
+
*/
|
|
76
|
+
function maskUrl(u) {
|
|
77
|
+
if (isarr(u)) {
|
|
78
|
+
return u.map(maskUrl)
|
|
79
|
+
}
|
|
80
|
+
if (u === undefined || u === null || u === '') {
|
|
81
|
+
return ''
|
|
82
|
+
}
|
|
83
|
+
if (!isestr(u)) {
|
|
84
|
+
return `(${typeof u})`
|
|
85
|
+
}
|
|
86
|
+
let x = null
|
|
87
|
+
try {
|
|
88
|
+
x = new URL(u)
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return '(invalid-url)'
|
|
92
|
+
}
|
|
93
|
+
if (x.protocol !== 'http:' && x.protocol !== 'https:') {
|
|
94
|
+
return x.protocol
|
|
95
|
+
}
|
|
96
|
+
return `${x.origin}${x.pathname}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
maskTok,
|
|
102
|
+
maskQuery,
|
|
103
|
+
maskUrl
|
|
104
|
+
}
|
package/server/procCore.mjs
CHANGED
|
@@ -49,7 +49,7 @@ function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
function proc(woItems, procOrm, { srLog, kmx }) {
|
|
52
|
+
function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
//updateTabItems
|
|
@@ -59,8 +59,21 @@ function proc(woItems, procOrm, { srLog, kmx }) {
|
|
|
59
59
|
//pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
|
|
60
60
|
let pickKeysOnly = get(opt, 'pickKeysOnly', null)
|
|
61
61
|
|
|
62
|
+
//idsNew: 前端標為新增(transient 欄位 _isNew === true, 清單頁 addItem / copyItem 標記)之列 id, 供下方比對既有列 (ADR-025);
|
|
63
|
+
//_isNew 不在 schema keys 內, 下方 ltdtmapping 取欄時即剝除不入庫. pickKeysOnly 模式不採納新增列(見 mergePickKeysOnly), 故不收集
|
|
64
|
+
let idsNew = []
|
|
65
|
+
if (!isearr(pickKeysOnly)) {
|
|
66
|
+
each(rows, (r) => {
|
|
67
|
+
if (get(r, '_isNew') === true) {
|
|
68
|
+
idsNew.push(get(r, 'id', ''))
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//雙擊防護(後端): 同一操作者之同表寫入處理中再送出即拒絕 'saveInProgress'(不排隊, 見 lockSave.mjs, ADR-025);
|
|
74
|
+
//占位包在 kmx 之外層, 不同操作者之並行寫入仍由 kmx 序列化、各自成功(ADR-012)
|
|
62
75
|
//序列化同表之並行整表批次寫入: 防 lost update (同 keyTable 序列化, 不同表並行)
|
|
63
|
-
return await kmx('updateTabItems:' + keyTable, async () => {
|
|
76
|
+
return await lockSave(`updateTabItems:${keyTable}`, userId, () => kmx('updateTabItems:' + keyTable, async () => {
|
|
64
77
|
|
|
65
78
|
//pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
|
|
66
79
|
if (isearr(pickKeysOnly)) {
|
|
@@ -137,6 +150,19 @@ function proc(woItems, procOrm, { srLog, kmx }) {
|
|
|
137
150
|
|
|
138
151
|
//ltdtDiffByKey
|
|
139
152
|
let ltdtOld = await woItems[keyTable].select()
|
|
153
|
+
|
|
154
|
+
//新增列已存在即拒絕 (ADR-025): 同一包重送且第 2 次於第 1 次完成後才處理時(占位已釋放), 第 1 次已建立之列會被當「修改」(diff),
|
|
155
|
+
//以前端之佔位字覆寫伺服器於新增時填入之建立者 / 建立時間(procOrm save 只覆寫 userIdUpdate / timeUpdate); 故整包拒絕、不寫入
|
|
156
|
+
if (size(idsNew) > 0) {
|
|
157
|
+
let kpIdOld = {}
|
|
158
|
+
each(ltdtOld, (v) => {
|
|
159
|
+
kpIdOld[get(v, 'id', '')] = true
|
|
160
|
+
})
|
|
161
|
+
if (idsNew.some((id) => kpIdOld[id] === true)) {
|
|
162
|
+
return Promise.reject('saveNewRowExists')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
let ltdtNew = rows
|
|
141
167
|
let r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
|
|
142
168
|
// console.log('ltdtDiffByKey r', r)
|
|
@@ -167,7 +193,7 @@ function proc(woItems, procOrm, { srLog, kmx }) {
|
|
|
167
193
|
|
|
168
194
|
return ltdtNew
|
|
169
195
|
|
|
170
|
-
})
|
|
196
|
+
}))
|
|
171
197
|
}
|
|
172
198
|
|
|
173
199
|
|
package/server/procLang.mjs
CHANGED
|
@@ -146,6 +146,14 @@ let kpLang = {
|
|
|
146
146
|
eng: `Duplicate field value in saved data`,
|
|
147
147
|
cht: `儲存資料的欄位值重複`,
|
|
148
148
|
},
|
|
149
|
+
saveInProgress: { //同一操作者之同表儲存處理中再送出 (ADR-025)
|
|
150
|
+
eng: `The previous save is still in progress. Please do not submit again.`,
|
|
151
|
+
cht: `上一次儲存仍在處理中,請勿重複送出`,
|
|
152
|
+
},
|
|
153
|
+
saveNewRowExists: { //標為新增之列已存在(同一包資料重送) (ADR-025)
|
|
154
|
+
eng: `The newly added data has already been saved. Please reload the page before making changes.`,
|
|
155
|
+
cht: `新增之資料已儲存過,請重新整理頁面後再操作`,
|
|
156
|
+
},
|
|
149
157
|
saveTabItemsSuccess: {
|
|
150
158
|
eng: `Saved successfully`,
|
|
151
159
|
cht: `儲存成功`,
|
package/server/procStaInfor.mjs
CHANGED
|
@@ -6,13 +6,23 @@ import staEvent from './staLogs/staEvent.callWorker.mjs'
|
|
|
6
6
|
import staEventTable from './staLogs/staEventTable.callWorker.mjs'
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
//mock 之事件名單:
|
|
10
|
+
// true(預設 5 種):既有 e2e 標準圖所用;
|
|
11
|
+
// 'many'(13 種):本系統實際會記錄之事件名(取自本機 log),供圖例列數跨門檻之 e2e(1440 寬時一般式 2 列,勾全部加總並切中文為 3 列)
|
|
12
|
+
let MOCK_EVENTS = ['verifyConn', 'updateTargets-success', 'checkUser-error', 'api/getPerm-success', 'getWebInfor-success']
|
|
13
|
+
let MOCK_EVENTS_MANY = ['api/getPermUserInfor-success', 'api/getUserByToken-success', 'getStaEvent-success', 'getStaEventTable-success', 'getTokenUser-error', 'getWebInfor-success', 'updateGrups-success', 'updatePemis-success', 'updateTabItems-pickKeysOnly', 'updateTargets-success', 'updateUsers-success', 'verifyConn', 'verifyConn-error']
|
|
14
|
+
function getMockEvents(mock) {
|
|
15
|
+
return mock === 'many' ? MOCK_EVENTS_MANY : MOCK_EVENTS
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
9
19
|
//mock 確定性資料集(供 e2e 統計圖穩定用):固定起點時間 + 固定 sin 計數,不依 now / log → 每次完全相同。
|
|
10
|
-
//觸發:opt.mock
|
|
11
|
-
function genMockStaEvent(timeInterval = 'hr') {
|
|
20
|
+
//觸發:opt.mock 為 true 或 'many'(由 settings.json staEventMock 經 srv.mjs → WWebPerm 傳入)。非 mock 時走真實 staEvent。
|
|
21
|
+
function genMockStaEvent(timeInterval = 'hr', mock = true) {
|
|
12
22
|
let fmt = timeInterval === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
|
|
13
23
|
let unit = timeInterval === 'hr' ? 'hour' : 'day'
|
|
14
24
|
let nBuckets = timeInterval === 'hr' ? 48 : 7
|
|
15
|
-
let events =
|
|
25
|
+
let events = getMockEvents(mock)
|
|
16
26
|
let base = ot('2025-01-01T00:00:00') //固定起點,不依 now
|
|
17
27
|
let rs = []
|
|
18
28
|
for (let i = 0; i < nBuckets; i++) {
|
|
@@ -30,15 +40,23 @@ function genMockStaEvent(timeInterval = 'hr') {
|
|
|
30
40
|
|
|
31
41
|
|
|
32
42
|
//mock 確定性資料集(供 e2e 統計表穩定用):固定窗計數,不依 now / log → 每次完全相同。
|
|
33
|
-
|
|
34
|
-
function genMockStaEventTable() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
//事件名單與 genMockStaEvent 相同;各事件滿足 last1Day>last8Hour>last4Hour>last1Hour,且各事件 last1Day 互不相同(排序明確、上多下少)。
|
|
44
|
+
function genMockStaEventTable(mock = true) {
|
|
45
|
+
if (mock !== 'many') {
|
|
46
|
+
let rs = [
|
|
47
|
+
{ event: 'verifyConn', last1Day: 240, last8Hour: 90, last4Hour: 50, last1Hour: 15 },
|
|
48
|
+
{ event: 'updateTargets-success', last1Day: 180, last8Hour: 70, last4Hour: 38, last1Hour: 11 },
|
|
49
|
+
{ event: 'checkUser-error', last1Day: 120, last8Hour: 45, last4Hour: 24, last1Hour: 7 },
|
|
50
|
+
{ event: 'api/getPerm-success', last1Day: 90, last8Hour: 33, last4Hour: 18, last1Hour: 5 },
|
|
51
|
+
{ event: 'getWebInfor-success', last1Day: 60, last8Hour: 22, last4Hour: 12, last1Hour: 3 },
|
|
52
|
+
]
|
|
53
|
+
return rs
|
|
54
|
+
}
|
|
55
|
+
//'many': 依名單順序遞減, last1Day 自 400 起每事件少 20(互不相同)
|
|
56
|
+
let rs = MOCK_EVENTS_MANY.map((event, k) => {
|
|
57
|
+
let last1Day = 400 - k * 20
|
|
58
|
+
return { event, last1Day, last8Hour: Math.round(last1Day * 0.4), last4Hour: Math.round(last1Day * 0.2), last1Hour: Math.round(last1Day * 0.05) }
|
|
59
|
+
})
|
|
42
60
|
return rs
|
|
43
61
|
}
|
|
44
62
|
|
|
@@ -66,7 +84,7 @@ function proc(opt = {}) {
|
|
|
66
84
|
|
|
67
85
|
//mock 模式回固定確定性資料集
|
|
68
86
|
if (mock) {
|
|
69
|
-
return genMockStaEvent(timeInterval)
|
|
87
|
+
return genMockStaEvent(timeInterval, mock)
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
//staEvent
|
|
@@ -98,7 +116,7 @@ function proc(opt = {}) {
|
|
|
98
116
|
|
|
99
117
|
//mock 模式回固定確定性資料集
|
|
100
118
|
if (mock) {
|
|
101
|
-
return genMockStaEventTable()
|
|
119
|
+
return genMockStaEventTable(mock)
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
//staEventTable
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
3
|
+
import isarr from 'wsemi/src/isarr.mjs'
|
|
4
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//wrapInjected: 部署方注入函數(getUserByToken / verifyClientUser / verifyAppUser)之包裝(W 契約, spec/設計要點與取捨.md ADR-023)
|
|
8
|
+
//why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
|
|
9
|
+
//部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址(修正前探測實證)。
|
|
10
|
+
//包裝後失敗視同否定結果(由呼叫端給 failValue), 走各呼叫點既有之查無 / 無權限分支回自家 key; 上游原文不回前端、不入 log。
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
//keysUpstream: 上游失敗可照記之 key 白名單 = w-web-sso 對外 helper 之 K 契約 14 個 key(w-web-sso ADR-068)
|
|
14
|
+
//與 sso 之同步點: sso 之 K 契約增減 key 時須同步本表(見 CLAUDE_rulebook.md「權杖不外洩」)
|
|
15
|
+
let keysUpstream = [
|
|
16
|
+
'invalidUrl',
|
|
17
|
+
'invalidTokenSelf',
|
|
18
|
+
'invalidTokenTar',
|
|
19
|
+
'invalidUserIdTar',
|
|
20
|
+
'noTokenKeyValueInUrl',
|
|
21
|
+
'noTokenKeyUserIdInUrl',
|
|
22
|
+
'noTokenInUrl',
|
|
23
|
+
'cannotGetUserByUrl',
|
|
24
|
+
'cannotGetUsersByUrl',
|
|
25
|
+
'cannotGetUserDataByUrl',
|
|
26
|
+
'cannotGetUsersDataByUrl',
|
|
27
|
+
'noUserDataByUrl',
|
|
28
|
+
'noUsersDataByUrl',
|
|
29
|
+
'noUserDataAfterConvert',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
//reName: Error.name 之名稱形狀(同 w-web-sso helper 診斷之 errName), 不符者不記(name 亦可被設為任意字串)
|
|
34
|
+
let reName = /^[A-Za-z]{1,40}$/
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 描述上游失敗, 供寫入 log: 絕不含上游原文
|
|
39
|
+
* - reject 值(字串)或其 message 屬白名單 key → { upstream: key }
|
|
40
|
+
* - 其餘 → { upstreamType, upstreamName }: 型別(null / array / typeof)與名稱形狀之 Error.name(無則 '')
|
|
41
|
+
*
|
|
42
|
+
* @param {*} err 輸入注入函數拋出或 reject 之值
|
|
43
|
+
* @returns {Object} 回傳描述物件
|
|
44
|
+
*/
|
|
45
|
+
function describeUpstream(err) {
|
|
46
|
+
let key = isestr(err) ? err : get(err, 'message')
|
|
47
|
+
if (isestr(key) && keysUpstream.includes(key)) {
|
|
48
|
+
return { upstream: key }
|
|
49
|
+
}
|
|
50
|
+
let upstreamType = (err === null) ? 'null' : (isarr(err) ? 'array' : typeof err)
|
|
51
|
+
let name = get(err, 'name')
|
|
52
|
+
let upstreamName = (isestr(name) && reName.test(name)) ? name : ''
|
|
53
|
+
return { upstreamType, upstreamName }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 包裝注入函數: 回傳(同步或 Promise)原樣回傳; 同步拋錯或 reject 回 failValue, 並以 describeUpstream(err) 呼叫 onFail
|
|
59
|
+
* 包裝後一律回 Promise(呼叫端既有之 ispm 判斷照常)
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} fn 輸入注入函數
|
|
62
|
+
* @param {Object} [opt={}] 輸入設定物件
|
|
63
|
+
* @param {*} [opt.failValue=null] 輸入失敗時之回傳值(getUserByToken 為 null 等同查無, verify* 為 false 等同無權限)
|
|
64
|
+
* @param {Function} [opt.onFail=null] 輸入失敗時之紀錄函數, 只收到 describeUpstream 之結果(收不到上游原文); 其自身拋錯不影響回傳
|
|
65
|
+
* @returns {Function} 回傳包裝後之函數
|
|
66
|
+
*/
|
|
67
|
+
function wrapInjected(fn, opt = {}) {
|
|
68
|
+
let failValue = get(opt, 'failValue', null)
|
|
69
|
+
let onFail = get(opt, 'onFail', null)
|
|
70
|
+
return async (...args) => {
|
|
71
|
+
try {
|
|
72
|
+
return await fn(...args) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (isfun(onFail)) {
|
|
76
|
+
try {
|
|
77
|
+
onFail(describeUpstream(err))
|
|
78
|
+
}
|
|
79
|
+
catch (e) {} //紀錄失敗不改變「失敗=否定結果」之語意
|
|
80
|
+
}
|
|
81
|
+
return failValue
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
keysUpstream,
|
|
89
|
+
describeUpstream
|
|
90
|
+
}
|
|
91
|
+
export default wrapInjected
|
|
@@ -181,10 +181,12 @@
|
|
|
181
181
|
:icon="mdiCloudUploadOutline"
|
|
182
182
|
:backgroundColor="'rgba(255,0,50,0.7)'"
|
|
183
183
|
:backgroundColorHover="'rgba(255,0,50,0.8)'"
|
|
184
|
+
:backgroundColorFocus="'rgba(255,0,50,0.8)'"
|
|
184
185
|
:textColor="'#eee'"
|
|
185
186
|
:textColorHover="'#fff'"
|
|
186
187
|
:iconColor="'#eee'"
|
|
187
188
|
:iconColorHover="'#fff'"
|
|
189
|
+
:iconColorFocus="'#fff'"
|
|
188
190
|
:shadow="false"
|
|
189
191
|
:promiseUnlock="true"
|
|
190
192
|
@click="saveGrups"
|
|
@@ -865,6 +867,8 @@ export default {
|
|
|
865
867
|
r.timeCreate = `{${vo.$t('grupAddIdNew')}}`
|
|
866
868
|
r.userIdUpdate = `{${vo.$t('grupAddIdNew')}}`
|
|
867
869
|
r.timeUpdate = `{${vo.$t('grupAddIdNew')}}`
|
|
870
|
+
//transient 欄位 _isNew: 標記未儲存之新列, 隨列送後端供「新增列已存在即拒絕」(saveNewRowExists), 後端檢查後剝除不入庫 (ADR-025)
|
|
871
|
+
r._isNew = true
|
|
868
872
|
// console.log('r', r)
|
|
869
873
|
|
|
870
874
|
//belongUsers, us
|
|
@@ -940,6 +944,8 @@ export default {
|
|
|
940
944
|
r.timeCreate = `{${vo.$t('grupAddIdNew')}}`
|
|
941
945
|
r.userIdUpdate = `{${vo.$t('grupAddIdNew')}}`
|
|
942
946
|
r.timeUpdate = `{${vo.$t('grupAddIdNew')}}`
|
|
947
|
+
//transient 欄位 _isNew: 複製出之列亦為新列(見 addItem) (ADR-025)
|
|
948
|
+
r._isNew = true
|
|
943
949
|
// console.log('r', r)
|
|
944
950
|
|
|
945
951
|
//belongUsers, us
|
|
@@ -1182,26 +1188,23 @@ export default {
|
|
|
1182
1188
|
},
|
|
1183
1189
|
|
|
1184
1190
|
saveGrups: function(msg) {
|
|
1185
|
-
|
|
1191
|
+
//promiseUnlock 之鎖交由 doSaveGrups 之 runSubmit 於請求結束時釋放, 不於此解鎖: 請求期間儲存鈕之滑鼠與鍵盤 Enter 皆擋 (ADR-025;
|
|
1192
|
+
//原第一行即 msg.pm.resolve(), 鎖立即解除, 焦點留在儲存鈕時連按 Enter 可送出 2 次)
|
|
1186
1193
|
let vo = this
|
|
1187
|
-
|
|
1188
|
-
//第一行立刻釋放按鈕視覺鎖
|
|
1189
|
-
msg.pm.resolve()
|
|
1190
|
-
|
|
1191
|
-
//fire-and-forget, 不 await
|
|
1192
|
-
vo.doSaveGrups()
|
|
1193
|
-
|
|
1194
|
+
vo.doSaveGrups({ pm: msg.pm })
|
|
1194
1195
|
},
|
|
1195
1196
|
|
|
1196
|
-
doSaveGrups: function() {
|
|
1197
|
+
doSaveGrups: function(opt = {}) {
|
|
1197
1198
|
// console.log('method doSaveGrups')
|
|
1198
1199
|
|
|
1199
1200
|
let vo = this
|
|
1200
1201
|
|
|
1201
|
-
async function core() {
|
|
1202
|
+
async function core(unlockBtn) {
|
|
1202
1203
|
|
|
1203
|
-
//1) 同步檢測 (在開 loading 之前)
|
|
1204
|
+
//1) 同步檢測 (在開 loading 之前); 早退之訊息框前以 unlockBtn() 釋放儲存鈕之 promiseUnlock 鎖: 按鈕鎖只涵蓋請求期間,
|
|
1205
|
+
//結果訊息框出現時按鈕已恢復(與打 API 之路徑一致); 流程狀態仍由 runSubmit 保持至訊息框關閉 (ADR-025)
|
|
1204
1206
|
if (isestr(vo.isError)) {
|
|
1207
|
+
unlockBtn()
|
|
1205
1208
|
await vo.$dg.showCheckYes(`${vo.isError}`)
|
|
1206
1209
|
return
|
|
1207
1210
|
}
|
|
@@ -1211,10 +1214,14 @@ export default {
|
|
|
1211
1214
|
|
|
1212
1215
|
//check
|
|
1213
1216
|
if (size(rows) === 0) {
|
|
1217
|
+
unlockBtn()
|
|
1214
1218
|
await vo.$dg.showCheckYes(`${vo.$t('grupAddEmpty')}`)
|
|
1215
1219
|
return
|
|
1216
1220
|
}
|
|
1217
1221
|
|
|
1222
|
+
//新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
|
|
1223
|
+
//(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
|
|
1224
|
+
|
|
1218
1225
|
//2) 確定打 API 才開 loading
|
|
1219
1226
|
vo.$ui.updateLoading(true)
|
|
1220
1227
|
|
|
@@ -1237,18 +1244,20 @@ export default {
|
|
|
1237
1244
|
|
|
1238
1245
|
}
|
|
1239
1246
|
|
|
1240
|
-
//
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
+
//runSubmit: 儲存流程(至結果訊息框關閉)進行中再觸發即略過; opt.pm 為儲存鈕之 promiseUnlock 鎖, 於請求結束(updateLoading(false))時釋放 (ADR-025)
|
|
1248
|
+
return vo.$ui.runSubmit('saveGrups', (unlockBtn) => {
|
|
1249
|
+
return core(unlockBtn)
|
|
1250
|
+
.catch((err) => {
|
|
1251
|
+
console.log('catch', err)
|
|
1252
|
+
vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })
|
|
1253
|
+
})
|
|
1254
|
+
.finally(() => {
|
|
1247
1255
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1256
|
+
//hide loading
|
|
1257
|
+
vo.$ui.updateLoading(false)
|
|
1250
1258
|
|
|
1251
|
-
|
|
1259
|
+
})
|
|
1260
|
+
}, opt)
|
|
1252
1261
|
|
|
1253
1262
|
},
|
|
1254
1263
|
|
|
@@ -181,10 +181,12 @@
|
|
|
181
181
|
:icon="mdiCloudUploadOutline"
|
|
182
182
|
:backgroundColor="'rgba(255,0,50,0.7)'"
|
|
183
183
|
:backgroundColorHover="'rgba(255,0,50,0.8)'"
|
|
184
|
+
:backgroundColorFocus="'rgba(255,0,50,0.8)'"
|
|
184
185
|
:textColor="'#eee'"
|
|
185
186
|
:textColorHover="'#fff'"
|
|
186
187
|
:iconColor="'#eee'"
|
|
187
188
|
:iconColorHover="'#fff'"
|
|
189
|
+
:iconColorFocus="'#fff'"
|
|
188
190
|
:shadow="false"
|
|
189
191
|
:promiseUnlock="true"
|
|
190
192
|
@click="savePemis"
|
|
@@ -866,6 +868,8 @@ export default {
|
|
|
866
868
|
r.timeCreate = `{${vo.$t('pemiAddIdNew')}}`
|
|
867
869
|
r.userIdUpdate = `{${vo.$t('pemiAddIdNew')}}`
|
|
868
870
|
r.timeUpdate = `{${vo.$t('pemiAddIdNew')}}`
|
|
871
|
+
//transient 欄位 _isNew: 標記未儲存之新列, 隨列送後端供「新增列已存在即拒絕」(saveNewRowExists), 後端檢查後剝除不入庫 (ADR-025)
|
|
872
|
+
r._isNew = true
|
|
869
873
|
// console.log('r', r)
|
|
870
874
|
|
|
871
875
|
//belongGrups, gs
|
|
@@ -941,6 +945,8 @@ export default {
|
|
|
941
945
|
r.timeCreate = `{${vo.$t('pemiAddIdNew')}}`
|
|
942
946
|
r.userIdUpdate = `{${vo.$t('pemiAddIdNew')}}`
|
|
943
947
|
r.timeUpdate = `{${vo.$t('pemiAddIdNew')}}`
|
|
948
|
+
//transient 欄位 _isNew: 複製出之列亦為新列(見 addItem) (ADR-025)
|
|
949
|
+
r._isNew = true
|
|
944
950
|
// console.log('r', r)
|
|
945
951
|
|
|
946
952
|
//belongGrups, gs
|
|
@@ -1184,26 +1190,23 @@ export default {
|
|
|
1184
1190
|
},
|
|
1185
1191
|
|
|
1186
1192
|
savePemis: function(msg) {
|
|
1187
|
-
|
|
1193
|
+
//promiseUnlock 之鎖交由 doSavePemis 之 runSubmit 於請求結束時釋放, 不於此解鎖: 請求期間儲存鈕之滑鼠與鍵盤 Enter 皆擋 (ADR-025;
|
|
1194
|
+
//原第一行即 msg.pm.resolve(), 鎖立即解除, 焦點留在儲存鈕時連按 Enter 可送出 2 次)
|
|
1188
1195
|
let vo = this
|
|
1189
|
-
|
|
1190
|
-
//第一行立刻釋放按鈕視覺鎖
|
|
1191
|
-
msg.pm.resolve()
|
|
1192
|
-
|
|
1193
|
-
//fire-and-forget, 不 await
|
|
1194
|
-
vo.doSavePemis()
|
|
1195
|
-
|
|
1196
|
+
vo.doSavePemis({ pm: msg.pm })
|
|
1196
1197
|
},
|
|
1197
1198
|
|
|
1198
|
-
doSavePemis: function() {
|
|
1199
|
+
doSavePemis: function(opt = {}) {
|
|
1199
1200
|
// console.log('method doSavePemis')
|
|
1200
1201
|
|
|
1201
1202
|
let vo = this
|
|
1202
1203
|
|
|
1203
|
-
async function core() {
|
|
1204
|
+
async function core(unlockBtn) {
|
|
1204
1205
|
|
|
1205
|
-
//1) 同步檢測 (在開 loading 之前)
|
|
1206
|
+
//1) 同步檢測 (在開 loading 之前); 早退之訊息框前以 unlockBtn() 釋放儲存鈕之 promiseUnlock 鎖: 按鈕鎖只涵蓋請求期間,
|
|
1207
|
+
//結果訊息框出現時按鈕已恢復(與打 API 之路徑一致); 流程狀態仍由 runSubmit 保持至訊息框關閉 (ADR-025)
|
|
1206
1208
|
if (isestr(vo.isError)) {
|
|
1209
|
+
unlockBtn()
|
|
1207
1210
|
await vo.$dg.showCheckYes(`${vo.isError}`)
|
|
1208
1211
|
return
|
|
1209
1212
|
}
|
|
@@ -1213,10 +1216,14 @@ export default {
|
|
|
1213
1216
|
|
|
1214
1217
|
//check
|
|
1215
1218
|
if (size(rows) === 0) {
|
|
1219
|
+
unlockBtn()
|
|
1216
1220
|
await vo.$dg.showCheckYes(`${vo.$t('pemiAddEmpty')}`)
|
|
1217
1221
|
return
|
|
1218
1222
|
}
|
|
1219
1223
|
|
|
1224
|
+
//新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
|
|
1225
|
+
//(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
|
|
1226
|
+
|
|
1220
1227
|
//2) 確定打 API 才開 loading
|
|
1221
1228
|
vo.$ui.updateLoading(true)
|
|
1222
1229
|
|
|
@@ -1239,18 +1246,20 @@ export default {
|
|
|
1239
1246
|
|
|
1240
1247
|
}
|
|
1241
1248
|
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
+
//runSubmit: 儲存流程(至結果訊息框關閉)進行中再觸發即略過; opt.pm 為儲存鈕之 promiseUnlock 鎖, 於請求結束(updateLoading(false))時釋放 (ADR-025)
|
|
1250
|
+
return vo.$ui.runSubmit('savePemis', (unlockBtn) => {
|
|
1251
|
+
return core(unlockBtn)
|
|
1252
|
+
.catch((err) => {
|
|
1253
|
+
console.log('catch', err)
|
|
1254
|
+
vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })
|
|
1255
|
+
})
|
|
1256
|
+
.finally(() => {
|
|
1249
1257
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1258
|
+
//hide loading
|
|
1259
|
+
vo.$ui.updateLoading(false)
|
|
1252
1260
|
|
|
1253
|
-
|
|
1261
|
+
})
|
|
1262
|
+
}, opt)
|
|
1254
1263
|
|
|
1255
1264
|
},
|
|
1256
1265
|
|