w-web-perm 1.1.5 → 1.1.7

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 (42) hide show
  1. package/dist/css/{chunk-vendors.80aa3ae8.css → chunk-vendors.3c580e72.css} +1 -1
  2. package/dist/index.tmp +1 -1
  3. package/dist/js/app-legacy.6f82d8bd.js +1 -0
  4. package/dist/js/app.6f82d8bd.js +1 -0
  5. package/dist/js/{chunk-vendors.fbf771bd.js → chunk-vendors-legacy.00f159e8.js} +5 -5
  6. package/dist/js/{chunk-vendors-legacy.fbf771bd.js → chunk-vendors.00f159e8.js} +5 -5
  7. package/dist/w-web-perm.umd.js +2 -2
  8. package/dist/w-web-perm.umd.js.map +1 -1
  9. package/docs/WWebPerm.html +3 -3
  10. package/docs/global.html +474 -2
  11. package/docs/index.html +2 -2
  12. package/docs/server_WWebPerm.mjs.html +12 -3
  13. package/docs/server_lockSave.mjs.html +115 -0
  14. package/docs/server_maskLog.mjs.html +2 -2
  15. package/docs/server_procCore.mjs.html +31 -5
  16. package/docs/server_wrapInjected.mjs.html +2 -2
  17. package/docs/src_fetchJson.mjs.html +2 -2
  18. package/docs/src_js_legendFit.mjs.html +2 -2
  19. package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
  20. package/docs/src_plugins_submitGuard.mjs.html +163 -0
  21. package/package.json +13 -13
  22. package/server/WWebPerm.mjs +10 -1
  23. package/server/lockSave.mjs +43 -0
  24. package/server/procCore.mjs +29 -3
  25. package/server/procLang.mjs +8 -0
  26. package/src/components/Common/CheckYes.vue +6 -0
  27. package/src/components/Common/CheckYesNo.vue +6 -0
  28. package/src/components/LayoutContentGrups.vue +30 -21
  29. package/src/components/LayoutContentPemis.vue +30 -21
  30. package/src/components/LayoutContentTargets.vue +36 -22
  31. package/src/components/LayoutContentUsers.vue +31 -21
  32. package/src/components/VeCgrups.vue +8 -1
  33. package/src/components/VeCpemis.vue +8 -1
  34. package/src/components/VeCrules.vue +8 -1
  35. package/src/components/VeGrupBlngUsers.vue +37 -31
  36. package/src/components/VePemiBlngGrups.vue +37 -31
  37. package/src/plugins/mUI.mjs +20 -0
  38. package/src/plugins/reuseDeletedIds.mjs +64 -0
  39. package/src/plugins/submitGuard.mjs +91 -0
  40. package/dist/js/app-legacy.833f9566.js +0 -1
  41. package/dist/js/app.833f9566.js +0 -1
  42. package/tableTags-web-perm.json +0 -1
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>server/lockSave.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">server/lockSave.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+
51
+
52
+ /**
53
+ * 有副作用之寫入路徑(如清單儲存 updateUsersList / updateTokensList / updateIpsList)之雙擊防護(後端, ADR-025)
54
+ *
55
+ * 以「操作:操作者 id」於 wsemi cacheSt 原子占位執行 fn, 同一操作者之同一操作處理中再送出即 reject 錯誤 key
56
+ * (預設 'saveInProgress', 可以 opt.errKey 指定; 皆為 i18n key, 見 procLang); 不同操作者、不同操作互不影響;
57
+ * fn 之 reject 原樣 bubble; fn 結束(含失敗)即釋放.
58
+ * 第 2 次不排隊執行: 其內容與第 1 次相同, 排隊後再寫一次會把第 1 次剛建立之列當修改覆寫
59
+ * (依序重送另由各專案之資料層檢查擋, 如 SSO procCore.updateTabItems 之新增列檢查回 'saveNewRowExists').
60
+ * operatorId 可為操作者 id 或「操作者 id + 列識別」(只擋同一列之連發, 同批其他列不受影響).
61
+ *
62
+ * @param {Object} cst 輸入 wsemi cacheSt 實例
63
+ * @returns {Function} 回傳 async (op, operatorId, fn, opt = {}) => fn 之結果, opt.errKey 為占位衝突時 reject 之 key
64
+ */
65
+ function createLockSave(cst) {
66
+
67
+ let lockSave = async (op, operatorId, fn, opt = {}) => {
68
+
69
+ //errKey
70
+ let errKey = get(opt, 'errKey', '')
71
+ if (!isestr(errKey)) {
72
+ errKey = 'saveInProgress'
73
+ }
74
+
75
+ let key = `${op}:${operatorId}`
76
+ return await cst.setWithFree([key], fn)
77
+ .catch((err) => {
78
+ //remap setWithFree 之占位衝突 → i18n key; fn 內之 reject 原樣 bubble
79
+ if (err === `setWithFree: key in use: ${key}`) {
80
+ return Promise.reject(errKey)
81
+ }
82
+ return Promise.reject(err)
83
+ })
84
+ }
85
+
86
+ return lockSave
87
+ }
88
+
89
+
90
+ export default createLockSave
91
+ </code></pre>
92
+ </article>
93
+ </section>
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <br class="clear">
103
+
104
+ <footer>
105
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
106
+ </footer>
107
+
108
+ <script>prettyPrint();</script>
109
+ <script src="scripts/polyfill.js"></script>
110
+ <script src="scripts/linenumber.js"></script>
111
+
112
+
113
+
114
+ </body>
115
+ </html>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -163,7 +163,7 @@ export {
163
163
  <br class="clear">
164
164
 
165
165
  <footer>
166
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 27 2026 16:30:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
166
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
167
167
  </footer>
168
168
 
169
169
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -96,7 +96,7 @@ function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
96
96
  }
97
97
 
98
98
 
99
- function proc(woItems, procOrm, { srLog, kmx }) {
99
+ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
100
100
 
101
101
 
102
102
  //updateTabItems
@@ -106,8 +106,21 @@ function proc(woItems, procOrm, { srLog, kmx }) {
106
106
  //pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
107
107
  let pickKeysOnly = get(opt, 'pickKeysOnly', null)
108
108
 
109
+ //idsNew: 前端標為新增(transient 欄位 _isNew === true, 清單頁 addItem / copyItem 標記)之列 id, 供下方比對既有列 (ADR-025);
110
+ //_isNew 不在 schema keys 內, 下方 ltdtmapping 取欄時即剝除不入庫. pickKeysOnly 模式不採納新增列(見 mergePickKeysOnly), 故不收集
111
+ let idsNew = []
112
+ if (!isearr(pickKeysOnly)) {
113
+ each(rows, (r) => {
114
+ if (get(r, '_isNew') === true) {
115
+ idsNew.push(get(r, 'id', ''))
116
+ }
117
+ })
118
+ }
119
+
120
+ //雙擊防護(後端): 同一操作者之同表寫入處理中再送出即拒絕 'saveInProgress'(不排隊, 見 lockSave.mjs, ADR-025);
121
+ //占位包在 kmx 之外層, 不同操作者之並行寫入仍由 kmx 序列化、各自成功(ADR-012)
109
122
  //序列化同表之並行整表批次寫入: 防 lost update (同 keyTable 序列化, 不同表並行)
110
- return await kmx('updateTabItems:' + keyTable, async () => {
123
+ return await lockSave(`updateTabItems:${keyTable}`, userId, () => kmx('updateTabItems:' + keyTable, async () => {
111
124
 
112
125
  //pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
113
126
  if (isearr(pickKeysOnly)) {
@@ -184,6 +197,19 @@ function proc(woItems, procOrm, { srLog, kmx }) {
184
197
 
185
198
  //ltdtDiffByKey
186
199
  let ltdtOld = await woItems[keyTable].select()
200
+
201
+ //新增列已存在即拒絕 (ADR-025): 同一包重送且第 2 次於第 1 次完成後才處理時(占位已釋放), 第 1 次已建立之列會被當「修改」(diff),
202
+ //以前端之佔位字覆寫伺服器於新增時填入之建立者 / 建立時間(procOrm save 只覆寫 userIdUpdate / timeUpdate); 故整包拒絕、不寫入
203
+ if (size(idsNew) > 0) {
204
+ let kpIdOld = {}
205
+ each(ltdtOld, (v) => {
206
+ kpIdOld[get(v, 'id', '')] = true
207
+ })
208
+ if (idsNew.some((id) => kpIdOld[id] === true)) {
209
+ return Promise.reject('saveNewRowExists')
210
+ }
211
+ }
212
+
187
213
  let ltdtNew = rows
188
214
  let r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
189
215
  // console.log('ltdtDiffByKey r', r)
@@ -214,7 +240,7 @@ function proc(woItems, procOrm, { srLog, kmx }) {
214
240
 
215
241
  return ltdtNew
216
242
 
217
- })
243
+ }))
218
244
  }
219
245
 
220
246
 
@@ -298,7 +324,7 @@ export default proc
298
324
  <br class="clear">
299
325
 
300
326
  <footer>
301
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 27 2026 16:30:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
327
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
302
328
  </footer>
303
329
 
304
330
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -150,7 +150,7 @@ export default wrapInjected
150
150
  <br class="clear">
151
151
 
152
152
  <footer>
153
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 27 2026 16:30:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
153
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
154
  </footer>
155
155
 
156
156
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -126,7 +126,7 @@ export default fetchJson
126
126
  <br class="clear">
127
127
 
128
128
  <footer>
129
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 27 2026 16:30:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
129
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
130
130
  </footer>
131
131
 
132
132
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -212,7 +212,7 @@ export {
212
212
  <br class="clear">
213
213
 
214
214
  <footer>
215
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 27 2026 16:30:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
215
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
216
216
  </footer>
217
217
 
218
218
  <script>prettyPrint();</script>
@@ -0,0 +1,136 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/plugins/reuseDeletedIds.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/plugins/reuseDeletedIds.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import map from 'lodash-es/map.js'
50
+ import each from 'lodash-es/each.js'
51
+
52
+
53
+ /**
54
+ * 新增列沿用資料庫既有 id 時之正規化 (標的清單專用, 其 id 為可就地改寫之標的路徑; ADR-025)
55
+ *
56
+ * 同一次儲存中刪除(或把 id 改掉)某既有列, 又新增一列並把 id 改成該既有列之 id, 淨效果為「修改該列」:
57
+ * 該新增列改為一般列送出(_isNew 設 false), 建立者 / 建立時間沿用資料庫之值(修改者 / 修改時間由後端寫入).
58
+ * 否則後端之「新增列已存在即拒絕」(saveNewRowExists, 擋同一包重送) 會把它當重送擋下, 且重新整理後重做仍被擋;
59
+ * 亦避免以前端佔位字({待自動給予})覆寫建立者 / 建立時間.
60
+ * 只處理 id 於 rows 內唯一者; 重複 id 不處理, 由後端 ckKey 回 saveRowFieldDuplicate.
61
+ * 使用者 / 群組 / 權限清單之 id 為系統產生且不可編輯, 新增列不會與既有列同 id, 不需此處理.
62
+ *
63
+ * @param {Array} rows 輸入欲送出之列陣列(清單之 opt.rows), 不修改
64
+ * @param {Array} rowsDb 輸入資料庫之列陣列(前端 store 之同表資料)
65
+ * @returns {Array} 回傳正規化後之新陣列
66
+ */
67
+ function reuseDeletedIds(rows, rowsDb) {
68
+
69
+ //kpDb, kpCount: 以 Map 存放, 不以 lodash get 取值(標的 id 為路徑, 可能含「.」而被當成巢狀路徑)
70
+ let kpDb = new Map()
71
+ each(rowsDb, (r) => {
72
+ kpDb.set(get(r, 'id', ''), r)
73
+ })
74
+ let kpCount = new Map()
75
+ each(rows, (r) => {
76
+ let id = get(r, 'id', '')
77
+ kpCount.set(id, (kpCount.get(id) || 0) + 1)
78
+ })
79
+
80
+ let rs = map(rows, (r) => {
81
+ let id = get(r, 'id', '')
82
+
83
+ //非新增列
84
+ if (get(r, '_isNew') !== true) {
85
+ return r
86
+ }
87
+
88
+ //資料庫無此 id: 真正之新增
89
+ if (!kpDb.has(id)) {
90
+ return r
91
+ }
92
+ let rDb = kpDb.get(id)
93
+
94
+ //rows 內重複 id: 不處理
95
+ if (kpCount.get(id) !== 1) {
96
+ return r
97
+ }
98
+
99
+ return {
100
+ ...r,
101
+ _isNew: false,
102
+ userId: get(rDb, 'userId', ''),
103
+ timeCreate: get(rDb, 'timeCreate', ''),
104
+ }
105
+ })
106
+
107
+ return rs
108
+ }
109
+
110
+
111
+ export default reuseDeletedIds
112
+ </code></pre>
113
+ </article>
114
+ </section>
115
+
116
+
117
+
118
+
119
+
120
+
121
+ </div>
122
+
123
+ <br class="clear">
124
+
125
+ <footer>
126
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
127
+ </footer>
128
+
129
+ <script>prettyPrint();</script>
130
+ <script src="scripts/polyfill.js"></script>
131
+ <script src="scripts/linenumber.js"></script>
132
+
133
+
134
+
135
+ </body>
136
+ </html>
@@ -0,0 +1,163 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/plugins/submitGuard.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/plugins/submitGuard.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import each from 'lodash-es/each.js'
50
+ import isfun from 'wsemi/src/isfun.mjs'
51
+ import haskey from 'wsemi/src/haskey.mjs'
52
+
53
+
54
+ /**
55
+ * 送出類操作之前端雙擊防護核心 (與 Vue / store 無關, 供單元測試; 由 mUI 之 runSubmit 與 updateLoading 使用, ADR-025)
56
+ *
57
+ * - run(key, fn, opt): 同 key 之送出流程進行中(自觸發起至流程結束, 含結果訊息框開啟期間)再觸發即略過.
58
+ * 按鈕之滑鼠與鍵盤 Enter、輸入框之 Enter 等入口共用同一狀態.
59
+ * opt.pm 為 WButtonChip / WButtonCircle 之 promiseUnlock 鎖(click 事件之 msg.pm), 於 releaseBtnLocks()(全頁 loading 關閉, 即請求結束)
60
+ * 或流程結束時釋放; 被略過之觸發亦立即釋放其 pm, 否則該按鈕永久鎖住.
61
+ * fn 收到 unlock 函數: 流程需先開確認框者於開框前呼叫, 按鈕鎖只涵蓋請求期間(確認框背後之按鈕不顯示載入圖示), 重入仍由流程狀態擋.
62
+ * opt.hold 為函數, 輸入流程結果, 回傳 true 時保持占位與按鈕鎖(如登入成功轉址, 頁面即將離開).
63
+ * - releaseBtnLocks(): 釋放進行中流程所登記之按鈕鎖, 流程狀態不變(結果訊息框開啟期間之重入仍擋).
64
+ * - isRunning(key): 查詢 key 是否進行中.
65
+ *
66
+ * 為何需要: 全頁 loading(WDialog)不搶焦點, 只擋滑鼠, 鍵盤事件仍送達背後之按鈕與輸入框;
67
+ * 按鈕鎖若於 handler 第一行即解除, 鍵盤連按照樣送出(2026-09-29 實測).
68
+ *
69
+ * @returns {Object} 回傳 { run, releaseBtnLocks, isRunning }
70
+ */
71
+ function submitGuard() {
72
+
73
+ //kpRunning, 進行中流程: key → { unlock }
74
+ let kpRunning = {}
75
+
76
+ let run = (key, fn, opt = {}) => {
77
+
78
+ //unlock, 釋放本次觸發之按鈕鎖; pm.resolve 重複呼叫無作用
79
+ let pm = get(opt, 'pm', null)
80
+ let unlock = () => {
81
+ if (pm &amp;&amp; isfun(pm.resolve)) {
82
+ pm.resolve()
83
+ }
84
+ }
85
+
86
+ //同 key 進行中: 略過, 並立即釋放本次觸發之按鈕鎖
87
+ if (haskey(kpRunning, key)) {
88
+ unlock()
89
+ return Promise.resolve(null)
90
+ }
91
+
92
+ //占位
93
+ kpRunning[key] = { unlock }
94
+
95
+ //hold
96
+ let hold = get(opt, 'hold', null)
97
+
98
+ //end
99
+ let end = () => {
100
+ unlock()
101
+ delete kpRunning[key]
102
+ }
103
+
104
+ return Promise.resolve()
105
+ .then(() => {
106
+ return fn(unlock)
107
+ })
108
+ .then((r) => {
109
+ if (isfun(hold) &amp;&amp; hold(r) === true) {
110
+ return r //保持占位與按鈕鎖
111
+ }
112
+ end()
113
+ return r
114
+ }, (err) => {
115
+ end()
116
+ return Promise.reject(err)
117
+ })
118
+ }
119
+
120
+ let releaseBtnLocks = () => {
121
+ each(kpRunning, (v) => {
122
+ v.unlock()
123
+ })
124
+ }
125
+
126
+ let isRunning = (key) => {
127
+ return haskey(kpRunning, key)
128
+ }
129
+
130
+ return {
131
+ run,
132
+ releaseBtnLocks,
133
+ isRunning,
134
+ }
135
+ }
136
+
137
+
138
+ export default submitGuard
139
+ </code></pre>
140
+ </article>
141
+ </section>
142
+
143
+
144
+
145
+
146
+
147
+
148
+ </div>
149
+
150
+ <br class="clear">
151
+
152
+ <footer>
153
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
+ </footer>
155
+
156
+ <script>prettyPrint();</script>
157
+ <script src="scripts/polyfill.js"></script>
158
+ <script src="scripts/linenumber.js"></script>
159
+
160
+
161
+
162
+ </body>
163
+ </html>
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "w-web-perm",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "main": "dist/w-web-perm.umd.js",
5
5
  "dependencies": {
6
6
  "json5": "^2.2.3",
7
- "w-aggrid-vue": "^2.1.4",
8
- "w-component-vue": "^2.5.23",
9
- "w-data-collector": "^1.0.40",
10
- "w-echarts-vue": "^1.0.15",
11
- "w-serv-hapi": "^1.1.2",
12
- "w-serv-orm": "^1.1.3",
13
- "w-syslog": "^1.0.29",
14
- "w-ui-loginout": "^1.0.28",
15
- "wsemi": "^1.9.7"
7
+ "w-aggrid-vue": "^2.1.7",
8
+ "w-component-vue": "^2.5.27",
9
+ "w-data-collector": "^1.0.42",
10
+ "w-echarts-vue": "^1.0.17",
11
+ "w-serv-hapi": "^1.1.4",
12
+ "w-serv-orm": "^1.1.5",
13
+ "w-syslog": "^1.0.31",
14
+ "w-ui-loginout": "^1.0.30",
15
+ "wsemi": "^1.9.11"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@vue/cli-plugin-babel": "^5.0.8",
@@ -27,10 +27,10 @@
27
27
  "eslint-plugin-vue": "^9.10.0",
28
28
  "vue": "^2.7.16",
29
29
  "vuex": "^3.6.2",
30
- "w-orm-lmdb": "^1.0.34",
30
+ "w-orm-lmdb": "^1.0.36",
31
31
  "w-package-tools": "^1.1.16",
32
- "w-package-tools-e2e": "^1.0.1",
33
- "w-screenctl": "^1.0.22"
32
+ "w-package-tools-e2e": "^1.0.6",
33
+ "w-screenctl": "^1.0.24"
34
34
  },
35
35
  "scripts": {
36
36
  "serve": "vue-cli-service serve",