w-web-perm 1.1.13 → 1.1.15
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 +98 -7
- package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.307bef4f.js +1 -0
- package/dist/js/app.307bef4f.js +1 -0
- package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.6cc4d11e.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.6cc4d11e.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 +15 -7
- package/docs/global.html +11733 -5078
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +154 -345
- package/docs/server_adminCheck.mjs.html +92 -0
- package/docs/server_cascadeWrite.mjs.html +5 -26
- package/docs/server_dataChannel.mjs.html +4 -4
- package/docs/server_keyCheck.mjs.html +150 -23
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +261 -65
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncCore.mjs.html +533 -0
- package/docs/server_syncKeep.mjs.html +27 -20
- package/docs/server_tools_authRefsAudit.mjs.html +30 -10
- package/docs/server_tools_authRefsClean.mjs.html +2 -2
- package/docs/server_tools_fromTools.mjs.html +2 -2
- package/docs/server_tools_isMain.mjs.html +2 -2
- package/docs/server_tools_nameDupReport.mjs.html +40 -7
- package/docs/server_validateRows.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +2 -2
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_getPermUserInfor.mjs.html +175 -0
- package/docs/src_js_applySaved.mjs.html +128 -0
- package/docs/src_js_cellEditing.mjs.html +163 -0
- package/docs/src_js_cvKpToArr.mjs.html +2 -2
- package/docs/src_js_cvKpToObj.mjs.html +2 -2
- package/docs/src_js_draftSync.mjs.html +83 -7
- package/docs/src_js_eachOwn.mjs.html +2 -2
- package/docs/src_js_editorRefRows.mjs.html +2 -2
- package/docs/src_js_externalRows.mjs.html +34 -10
- package/docs/src_js_externalUi.mjs.html +4 -8
- package/docs/src_js_findRefRows.mjs.html +2 -2
- package/docs/src_js_findTouchedDup.mjs.html +2 -2
- package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
- package/docs/src_js_fromAudit.mjs.html +2 -2
- package/docs/src_js_getAddImpact.mjs.html +2 -2
- package/docs/src_js_getAuthWarn.mjs.html +2 -2
- package/docs/src_js_getCascadePreview.mjs.html +2 -2
- package/docs/src_js_getDelRefCount.mjs.html +2 -2
- package/docs/src_js_getEffMode.mjs.html +2 -2
- package/docs/src_js_getKpLossy.mjs.html +2 -2
- package/docs/src_js_getNameErrs.mjs.html +2 -2
- package/docs/src_js_getOwn.mjs.html +2 -2
- package/docs/src_js_getPropKey.mjs.html +2 -2
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +2 -2
- package/docs/src_js_joinNames.mjs.html +2 -2
- package/docs/src_js_legendFit.mjs.html +2 -2
- package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
- package/docs/src_js_refAudit.mjs.html +2 -2
- package/docs/src_js_refCascade.mjs.html +2 -2
- package/docs/src_js_refIdentity.mjs.html +500 -0
- package/docs/src_js_relDialogSend.mjs.html +2 -2
- package/docs/src_js_rowDigest.mjs.html +75 -145
- package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_provideGrups.mjs +1 -1
- package/g_providePemis.mjs +1 -1
- package/g_provideTargets.mjs +1 -1
- package/g_provideUsers.mjs +1 -1
- package/package.json +2 -2
- package/server/WWebPerm.mjs +152 -343
- package/server/adminCheck.mjs +20 -0
- package/server/cascadeWrite.mjs +3 -24
- package/server/dataChannel.mjs +2 -2
- package/server/keyCheck.mjs +148 -21
- package/server/procCore.mjs +259 -63
- package/server/procLang.mjs +78 -4
- package/server/provideTabs.mjs +6 -4
- package/server/syncCore.mjs +461 -0
- package/server/syncKeep.mjs +25 -18
- package/server/tools/authRefsAudit.mjs +28 -8
- package/server/tools/nameDupReport.mjs +38 -5
- package/src/App.vue +6 -1
- package/src/components/LayoutContentGrups.vue +57 -8
- package/src/components/LayoutContentPemis.vue +57 -8
- package/src/components/LayoutContentTargets.vue +38 -7
- package/src/components/LayoutContentUsers.vue +57 -8
- package/src/components/LayoutState.vue +12 -0
- package/src/components/VeGrupBlngUsers.vue +128 -20
- package/src/components/VePemiBlngGrups.vue +128 -20
- package/src/getPermUserInfor.mjs +10 -0
- package/src/js/applySaved.mjs +56 -0
- package/src/js/cellEditing.mjs +91 -0
- package/src/js/draftSync.mjs +81 -5
- package/src/js/externalRows.mjs +32 -8
- package/src/js/externalUi.mjs +2 -6
- package/src/js/refIdentity.mjs +428 -0
- package/src/js/rowDigest.mjs +73 -143
- package/src/plugins/mUI.mjs +5 -0
- package/src/schema/contracts/grups.mjs +8 -0
- package/src/schema/contracts/index.mjs +17 -0
- package/src/schema/contracts/pemis.mjs +8 -0
- package/src/schema/contracts/targets.mjs +8 -0
- package/src/schema/contracts/users.mjs +13 -0
- package/src/schema/tables/grups.mjs +4 -0
- package/src/schema/tables/pemis.mjs +4 -0
- package/src/schema/tables/targets.mjs +4 -0
- package/src/schema/tables/users.mjs +4 -0
- package/src/store/mutations.mjs +1 -1
- package/srv.mjs +43 -0
- package/dist/js/app-legacy.8af87052.js +0 -1
- package/dist/js/app.8af87052.js +0 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// node server/tools/nameDupReport.mjs --audit <匯出檔.json> [--mapping-by id|email|name] [--json] (同上; 與 authMergeReport.mjs --audit 寫法一致)
|
|
6
6
|
//
|
|
7
7
|
// 匯出檔內容 { users, grups, pemis, targets }(各為列陣列, 如以資料通道 select 之結果或資料庫匯出)。
|
|
8
|
-
// --mapping-by 省略時 email 與 name 皆報告(部署之 mappingBy 設定不在資料庫內); 給 id
|
|
8
|
+
// --mapping-by 省略時 email 與 name 皆報告(部署之 mappingBy 設定不在資料庫內); 給 id 則查 id 缺漏與「登入識別撞值」(perm-run05; 見下方報告內容 2).
|
|
9
9
|
// --json 輸出結構化結果(供其他工具), 省略則輸出文字報告.
|
|
10
10
|
//
|
|
11
11
|
//報告內容:
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
// 已知邊界(增量外審 mmd-N8, 未處理): 陣列形式之引用元素缺 name 時, 伺服端 arrFinder 以 get(vv,'name','') 取得 ''(可對應名稱為 '' 之列), 本工具解碼後名稱為索引數字, 不計入該引用;
|
|
15
15
|
// 須同時存在「名稱為空字串之群組/權限」與「陣列形式且缺 name 之引用」, 空名稱者已另由 authMergeReport --audit 列出, 請一併人工檢視.
|
|
16
16
|
// 2. users 對應鍵: isActive==='y' 之列, 對應鍵(email/name/id)為空/空白/非字串者, 以及值重複者(精確 ===; 與 getTokenUser 之 select 一致), 附各列 id/name/from.
|
|
17
|
+
// mappingBy 為 id 時(perm-run05, 規則帳本 R-MAPKEY-05): 登入識別值=後台列(from 空)之 id、外部列(from 非空)之 srcKey(遷移前無 srcKey 之舊外部列為其 id);
|
|
18
|
+
// 列出每一組「同一登入識別值有 ≥ 2 列」者(登入時命中多筆即一律拒絕 ambiguousUserFromPerm), **不論 isActive、含他來源與同一來源**; 每組標 scope:
|
|
19
|
+
// crossSource=匯入/後台存檔會擋之撞值組合(後台列↔外部列、他來源外部列↔外部列、同來源一有 srcKey 一為舊形之 claimTwice 形; 判準 keyCheck.findLoginIdClashDetail),
|
|
20
|
+
// sameSource=同一來源之重複(皆有 srcKey 之 srcKeyDuplicate/srcKeyConflict 形, 或同主鍵), 同步端本就拒絕, 需於來源端或以 g_purgeFrom.mjs 清理.
|
|
21
|
+
// 各組附各列 id/name/from/isActive. 升版後這些歷史撞值不阻擋後台存檔(才可修復), 但登入一律被拒——請依本報告在後台刪除或改用可唯一對應之鍵、並請外部來源改用不撞值之 srcKey
|
|
22
|
+
// (同時須讓部署方之 getUserByToken 回報值隨之改變, 見 README Upgrade Notes 第 4 節), 處理後重跑確認為 0 組.
|
|
17
23
|
//
|
|
18
24
|
//【清理須知】引用以「名稱」比對, 改名級聯(F-07)尚未實作: 同名組中改名或刪除其一, 引用該名稱者會全部指向剩下的那一筆或失效.
|
|
19
25
|
// 請先以本報告列出引用者, 清理後逐一確認授權(getPermUserInfor/getPerm 之結果), 再處理下一組.
|
|
@@ -23,6 +29,7 @@ import fs from 'fs'
|
|
|
23
29
|
import isMain from './isMain.mjs'
|
|
24
30
|
import JSON5 from 'json5'
|
|
25
31
|
import cvKpToArr from '../../src/js/cvKpToArr.mjs'
|
|
32
|
+
import { loginIdOf, findLoginIdClashDetail } from '../keyCheck.mjs'
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
let isStr = (v) => typeof v === 'string'
|
|
@@ -104,6 +111,27 @@ function mapKeyIssues(users, key) {
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
|
|
114
|
+
//loginIdDups: mappingBy='id' 之登入識別撞值組(perm-run05): 依 loginIdOf 分組, 凡同一登入識別值有 ≥ 2 列者皆列出(不論 isActive、含他來源與同一來源——登入時命中多筆即一律拒絕, 故稽核須與登入同口徑,
|
|
115
|
+
// 外審 sol 第一輪#5: 同來源重複〔其一停用〕亦使該使用者不能登入、來源同步回 srcKeyConflict); scope 標示: 'crossSource'=匯入/後台存檔會擋之撞值組合(findLoginIdClashDetail, 判準單一擁有者在 keyCheck.mjs),
|
|
116
|
+
// 'sameSource'=同一來源之重複(皆有 srcKey 之 srcKeyDuplicate/srcKeyConflict 形,或同主鍵), 需在來源端或以 g_purgeFrom.mjs 清理
|
|
117
|
+
function loginIdDups(users) {
|
|
118
|
+
let kp = new Map()
|
|
119
|
+
for (let u of arr(users)) {
|
|
120
|
+
let v = loginIdOf(u)
|
|
121
|
+
if (v === '') {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (!kp.has(v)) {
|
|
125
|
+
kp.set(v, [])
|
|
126
|
+
}
|
|
127
|
+
kp.get(v).push(u)
|
|
128
|
+
}
|
|
129
|
+
return [...kp]
|
|
130
|
+
.filter(([, rs]) => rs.length >= 2)
|
|
131
|
+
.map(([value, rs]) => ({ value, scope: (findLoginIdClashDetail(rs, () => true) !== null) ? 'crossSource' : 'sameSource', rows: rs.map((u) => ({ id: u.id, name: u.name, from: u.from, isActive: u.isActive })) }))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
107
135
|
/**
|
|
108
136
|
* 建立稽核結果(純函數, 不讀寫檔案)
|
|
109
137
|
*
|
|
@@ -138,7 +166,7 @@ function buildReport(data, opt = {}) {
|
|
|
138
166
|
let mapKey = {}
|
|
139
167
|
for (let key of keys) {
|
|
140
168
|
mapKey[key] = (key === 'id')
|
|
141
|
-
? { empty: arr(d.users).filter((u) => u && u.isActive === 'y' && !(isStr(u.id) && u.id !== '')).map((u) => ({ id: u.id, name: u.name, from: u.from })), dup:
|
|
169
|
+
? { empty: arr(d.users).filter((u) => u && u.isActive === 'y' && !(isStr(u.id) && u.id !== '')).map((u) => ({ id: u.id, name: u.name, from: u.from })), dup: loginIdDups(d.users) }
|
|
142
170
|
: mapKeyIssues(d.users, key)
|
|
143
171
|
}
|
|
144
172
|
//壞 JSON 去重(同一列被多個重複名稱之解碼重複記錄)
|
|
@@ -174,11 +202,16 @@ function formatReport(rp) {
|
|
|
174
202
|
}
|
|
175
203
|
for (let [key, v] of Object.entries(rp.mapKey)) {
|
|
176
204
|
L.push('')
|
|
177
|
-
|
|
205
|
+
if (key === 'id') {
|
|
206
|
+
L.push(`== users 對應鍵 id(後台列=id、外部列=srcKey; 登入識別撞值不論 isActive、含他來源): 無 id 之有效列 ${v.empty.length} 筆, 撞值 ${v.dup.length} 組 ==`)
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
L.push(`== users 對應鍵 ${key}(僅 isActive='y'): 空/空白/非字串 ${v.empty.length} 筆, 值重複 ${v.dup.length} 組 ==`)
|
|
210
|
+
}
|
|
178
211
|
v.empty.forEach((u) => L.push(`- 空: id=${u.id} name=${JSON.stringify(u.name)} from=${JSON.stringify(u.from)}`))
|
|
179
212
|
for (let g of v.dup) {
|
|
180
|
-
L.push(`-
|
|
181
|
-
g.rows.forEach((u) => L.push(` id=${u.id} name=${JSON.stringify(u.name)} from=${JSON.stringify(u.from)}`))
|
|
213
|
+
L.push(`- ${key === 'id' ? (g.scope === 'sameSource' ? '同來源重複' : '跨來源撞值') : '重複'}「${g.value}」${g.rows.length} 筆`)
|
|
214
|
+
g.rows.forEach((u) => L.push(` id=${u.id} name=${JSON.stringify(u.name)} from=${JSON.stringify(u.from)}${('isActive' in u) ? ` isActive=${JSON.stringify(u.isActive)}` : ''}`))
|
|
182
215
|
}
|
|
183
216
|
}
|
|
184
217
|
L.push('')
|
package/src/App.vue
CHANGED
|
@@ -83,8 +83,13 @@ export default {
|
|
|
83
83
|
|
|
84
84
|
function loginError(data) {
|
|
85
85
|
console.log('login error', cloneDeep(data))
|
|
86
|
-
|
|
86
|
+
//登入者不是系統管理員(後端 userNotAdmin; 即使部署方放行亦拒絕, R-ADMIN-ONLY-01): 顯示「無權進入權限後台」並停留, 不轉址(轉走則使用者看不到原因); 一般使用者查本人權限樹走 /api/getPerm
|
|
87
|
+
let isNotAdmin = get(data, 'msg') === 'userNotAdmin'
|
|
88
|
+
vo.$ui.updateConnState(isNotAdmin ? 'csErrNotAdmin' : 'csErrLogin')
|
|
87
89
|
vo.$ui.updateUserToken('')
|
|
90
|
+
if (isNotAdmin) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
88
93
|
let urlRedirect = get(window, '___pmwperm___.urlRedirect', '')
|
|
89
94
|
if (!isestr(urlRedirect)) {
|
|
90
95
|
console.log('urlRedirect', urlRedirect)
|
|
@@ -307,7 +307,10 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
|
|
|
307
307
|
import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
|
|
308
308
|
import { isExternalRow } from '../js/externalRows.mjs'
|
|
309
309
|
import { makeBaseline } from '../js/rowDigest.mjs'
|
|
310
|
-
import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
|
|
310
|
+
import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
|
|
311
|
+
import { applySavedRows } from '../js/applySaved.mjs'
|
|
312
|
+
import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
|
|
313
|
+
import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
|
|
311
314
|
import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
|
|
312
315
|
import WIcon from 'w-component-vue/src/components/WIcon.vue'
|
|
313
316
|
import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
|
|
@@ -410,6 +413,8 @@ export default {
|
|
|
410
413
|
this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
|
|
411
414
|
//saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
|
|
412
415
|
this.saveMark = { saving: false }
|
|
416
|
+
//refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(權限名稱) → 選取當時對應之權限 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
|
|
417
|
+
this.refPicks = new Map()
|
|
413
418
|
},
|
|
414
419
|
mounted: function() {
|
|
415
420
|
// console.log('mounted')
|
|
@@ -594,14 +599,14 @@ export default {
|
|
|
594
599
|
first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
|
|
595
600
|
editChanged: vo.syncMark.editable !== isEditable,
|
|
596
601
|
storeChanged: refsChanged(vo.syncMark.refs, [rows, kpUse]),
|
|
597
|
-
hasDraft: () => vo.syncMark.dirty === true,
|
|
602
|
+
hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
|
|
598
603
|
saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
|
|
599
604
|
})
|
|
600
605
|
if (act === 'rebuild') {
|
|
601
606
|
vo.rebuildDraft(rows, kpUse, isEditable)
|
|
602
607
|
}
|
|
603
608
|
else if (act === 'keep') {
|
|
604
|
-
vo.staleNotice = isDraftStale({
|
|
609
|
+
vo.staleNotice = isDraftStale({ keyTable: 'grups', rows, baseline: vo.syncMark.baseline })
|
|
605
610
|
}
|
|
606
611
|
|
|
607
612
|
return ''
|
|
@@ -779,7 +784,7 @@ export default {
|
|
|
779
784
|
let kpUseGrupUser = cloneDeep(kpUse)
|
|
780
785
|
|
|
781
786
|
//baseline, 與 items 同一處由同一份來源列(grups, genItems 之前)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
|
|
782
|
-
let baseline = makeBaseline(grups,
|
|
787
|
+
let baseline = makeBaseline(grups, 'grups')
|
|
783
788
|
|
|
784
789
|
//items
|
|
785
790
|
let items = vo.genItems(grups, kpUseGrupUser)
|
|
@@ -791,6 +796,7 @@ export default {
|
|
|
791
796
|
vo.isModified = false
|
|
792
797
|
vo.staleNotice = false
|
|
793
798
|
vo.syncMark = { refs: [rows, kpUse], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
|
|
799
|
+
vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
|
|
794
800
|
|
|
795
801
|
//genOpt
|
|
796
802
|
vo.genOpt({ isEditable })
|
|
@@ -798,6 +804,11 @@ export default {
|
|
|
798
804
|
//firstLoading
|
|
799
805
|
vo.firstLoading = false
|
|
800
806
|
|
|
807
|
+
//第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
|
|
808
|
+
vo.$nextTick(() => {
|
|
809
|
+
vo.bindEditing()
|
|
810
|
+
})
|
|
811
|
+
|
|
801
812
|
},
|
|
802
813
|
|
|
803
814
|
curRefs: function() {
|
|
@@ -806,6 +817,22 @@ export default {
|
|
|
806
817
|
return [vo.grups, vo.kpUseGrupUser]
|
|
807
818
|
},
|
|
808
819
|
|
|
820
|
+
bindEditing: function() {
|
|
821
|
+
//第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
|
|
822
|
+
let vo = this
|
|
823
|
+
bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
|
|
824
|
+
vo.afterEditing()
|
|
825
|
+
})
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
afterEditing: function() {
|
|
829
|
+
//儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
|
|
830
|
+
let vo = this
|
|
831
|
+
if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
|
|
832
|
+
vo.reloadDraft()
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
|
|
809
836
|
reloadDraft: function() {
|
|
810
837
|
//「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
|
|
811
838
|
//genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
|
|
@@ -1106,6 +1133,7 @@ export default {
|
|
|
1106
1133
|
let _r = vo.$ds.grups.funNew()
|
|
1107
1134
|
r.id = _r.id //使用新建方式預先產生id避免重複
|
|
1108
1135
|
r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
|
|
1136
|
+
r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
|
|
1109
1137
|
r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('grupCopyNameNew'))
|
|
1110
1138
|
r.userId = `{${vo.$t('grupAddIdNew')}}`
|
|
1111
1139
|
r.timeCreate = `{${vo.$t('grupAddIdNew')}}`
|
|
@@ -1393,6 +1421,8 @@ export default {
|
|
|
1393
1421
|
//編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
|
|
1394
1422
|
//父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
|
|
1395
1423
|
let baseline0 = cloneDeep(vo.baseline)
|
|
1424
|
+
//refSnap: 開啟編輯器當下之「權限名稱 → 權限 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的權限即此刻 store 之權限, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍是同一實體(被刪除後同名重建〔id 不同〕即拒收)
|
|
1425
|
+
let refSnap = snapRefIds('grups', vo.pemis)
|
|
1396
1426
|
//showVeCpemis
|
|
1397
1427
|
vo.$dg.showVeCpemis({
|
|
1398
1428
|
isEditable: vo.isEditable,
|
|
@@ -1415,11 +1445,14 @@ export default {
|
|
|
1415
1445
|
}
|
|
1416
1446
|
|
|
1417
1447
|
//草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
|
|
1418
|
-
if (isDraftStale({
|
|
1448
|
+
if (isDraftStale({ keyTable: 'grups', rows: vo.baseRows, baseline: baseline0 })) {
|
|
1419
1449
|
vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
|
|
1420
1450
|
return
|
|
1421
1451
|
}
|
|
1422
1452
|
|
|
1453
|
+
//選取記錄: 本次新增之啟用引用項(權限名稱)在編輯器開啟當下對應之權限 id(查無記空陣列)
|
|
1454
|
+
recordPicks({ keyTable: 'grups', strOld: get(vo, `opt.rows[${krNow}].cpemis`, ''), strNew: cpemis, snap: refSnap, picks: vo.refPicks })
|
|
1455
|
+
|
|
1423
1456
|
//set
|
|
1424
1457
|
set(vo, `opt.rows[${krNow}].cpemis`, cpemis)
|
|
1425
1458
|
// console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
|
|
@@ -1476,6 +1509,15 @@ export default {
|
|
|
1476
1509
|
return
|
|
1477
1510
|
}
|
|
1478
1511
|
|
|
1512
|
+
//正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(權限名稱)所指之權限,須仍是使用者選取當時的同一實體(同 id);
|
|
1513
|
+
//被刪除或刪除後以同名重建(id 不同)即不送出(零寫入)並提示重新載入,不得把授權寫到同名新權限. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於權限表鎖內以新鮮讀取再驗一次
|
|
1514
|
+
let refs = parseRefDecl(buildRefDecl({ keyTable: 'grups', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.pemis }))
|
|
1515
|
+
if (countRefMismatch({ keyTable: 'grups', decl: refs, refRows: vo.pemis }) > 0) {
|
|
1516
|
+
unlockBtn()
|
|
1517
|
+
await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
|
|
1518
|
+
return
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1479
1521
|
//新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
|
|
1480
1522
|
//(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
|
|
1481
1523
|
|
|
@@ -1519,15 +1561,17 @@ export default {
|
|
|
1519
1561
|
|
|
1520
1562
|
//4) updateGrups, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
|
|
1521
1563
|
let okSave = false
|
|
1564
|
+
let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
|
|
1522
1565
|
vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
|
|
1523
|
-
await vo.$fapi.updateGrups(rows, { baseline })
|
|
1524
|
-
.then(() => {
|
|
1566
|
+
await vo.$fapi.updateGrups(rows, { baseline, returnRows: true, refs })
|
|
1567
|
+
.then((res) => {
|
|
1525
1568
|
okSave = true
|
|
1569
|
+
savedRes = res
|
|
1526
1570
|
vo.saveMark.saving = false
|
|
1527
1571
|
})
|
|
1528
1572
|
.catch(async (err) => {
|
|
1529
1573
|
vo.saveMark.saving = false
|
|
1530
|
-
vo.staleNotice = isDraftStale({
|
|
1574
|
+
vo.staleNotice = isDraftStale({ keyTable: 'grups', rows: vo.grups, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
|
|
1531
1575
|
vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
|
|
1532
1576
|
await vo.$dg.showCheckYes(`${vo.$t('grupSaveGrupsFail')}: ${vo.$tErr(err)}`)
|
|
1533
1577
|
})
|
|
@@ -1536,6 +1580,11 @@ export default {
|
|
|
1536
1580
|
//isModified; postSaving 自此至成功訊息框關閉後 300ms 內排除 rowsChange(見 data 註解)
|
|
1537
1581
|
vo.postSaving = true
|
|
1538
1582
|
vo.isModified = false
|
|
1583
|
+
vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
|
|
1584
|
+
|
|
1585
|
+
//存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
|
|
1586
|
+
//其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
|
|
1587
|
+
applySavedRows('grups', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
|
|
1539
1588
|
|
|
1540
1589
|
//alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
|
|
1541
1590
|
vo.$ui.updateLoading(false)
|
|
@@ -308,7 +308,10 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
|
|
|
308
308
|
import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
|
|
309
309
|
import { isExternalRow } from '../js/externalRows.mjs'
|
|
310
310
|
import { makeBaseline } from '../js/rowDigest.mjs'
|
|
311
|
-
import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
|
|
311
|
+
import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
|
|
312
|
+
import { applySavedRows } from '../js/applySaved.mjs'
|
|
313
|
+
import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
|
|
314
|
+
import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
|
|
312
315
|
import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
|
|
313
316
|
import WIcon from 'w-component-vue/src/components/WIcon.vue'
|
|
314
317
|
import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
|
|
@@ -411,6 +414,8 @@ export default {
|
|
|
411
414
|
this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
|
|
412
415
|
//saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
|
|
413
416
|
this.saveMark = { saving: false }
|
|
417
|
+
//refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(標的 id) → 選取當時對應之標的 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
|
|
418
|
+
this.refPicks = new Map()
|
|
414
419
|
},
|
|
415
420
|
mounted: function() {
|
|
416
421
|
// console.log('mounted')
|
|
@@ -595,14 +600,14 @@ export default {
|
|
|
595
600
|
first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
|
|
596
601
|
editChanged: vo.syncMark.editable !== isEditable,
|
|
597
602
|
storeChanged: refsChanged(vo.syncMark.refs, [rows, kpUse]),
|
|
598
|
-
hasDraft: () => vo.syncMark.dirty === true,
|
|
603
|
+
hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
|
|
599
604
|
saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
|
|
600
605
|
})
|
|
601
606
|
if (act === 'rebuild') {
|
|
602
607
|
vo.rebuildDraft(rows, kpUse, isEditable)
|
|
603
608
|
}
|
|
604
609
|
else if (act === 'keep') {
|
|
605
|
-
vo.staleNotice = isDraftStale({
|
|
610
|
+
vo.staleNotice = isDraftStale({ keyTable: 'pemis', rows, baseline: vo.syncMark.baseline })
|
|
606
611
|
}
|
|
607
612
|
|
|
608
613
|
return ''
|
|
@@ -779,7 +784,7 @@ export default {
|
|
|
779
784
|
let kpUsePemiGrup = cloneDeep(kpUse)
|
|
780
785
|
|
|
781
786
|
//baseline, 與 items 同一處由同一份來源列(pemis, genItems 之前)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
|
|
782
|
-
let baseline = makeBaseline(pemis,
|
|
787
|
+
let baseline = makeBaseline(pemis, 'pemis')
|
|
783
788
|
|
|
784
789
|
//items
|
|
785
790
|
let items = vo.genItems(pemis, kpUsePemiGrup)
|
|
@@ -791,6 +796,7 @@ export default {
|
|
|
791
796
|
vo.isModified = false
|
|
792
797
|
vo.staleNotice = false
|
|
793
798
|
vo.syncMark = { refs: [rows, kpUse], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
|
|
799
|
+
vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
|
|
794
800
|
|
|
795
801
|
//genOpt
|
|
796
802
|
vo.genOpt({ isEditable })
|
|
@@ -798,6 +804,11 @@ export default {
|
|
|
798
804
|
//firstLoading
|
|
799
805
|
vo.firstLoading = false
|
|
800
806
|
|
|
807
|
+
//第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
|
|
808
|
+
vo.$nextTick(() => {
|
|
809
|
+
vo.bindEditing()
|
|
810
|
+
})
|
|
811
|
+
|
|
801
812
|
},
|
|
802
813
|
|
|
803
814
|
curRefs: function() {
|
|
@@ -806,6 +817,22 @@ export default {
|
|
|
806
817
|
return [vo.pemis, vo.kpUsePemiGrup]
|
|
807
818
|
},
|
|
808
819
|
|
|
820
|
+
bindEditing: function() {
|
|
821
|
+
//第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
|
|
822
|
+
let vo = this
|
|
823
|
+
bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
|
|
824
|
+
vo.afterEditing()
|
|
825
|
+
})
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
afterEditing: function() {
|
|
829
|
+
//儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
|
|
830
|
+
let vo = this
|
|
831
|
+
if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
|
|
832
|
+
vo.reloadDraft()
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
|
|
809
836
|
reloadDraft: function() {
|
|
810
837
|
//「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
|
|
811
838
|
//genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
|
|
@@ -1107,6 +1134,7 @@ export default {
|
|
|
1107
1134
|
let _r = vo.$ds.pemis.funNew()
|
|
1108
1135
|
r.id = _r.id //使用新建方式預先產生id避免重複
|
|
1109
1136
|
r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
|
|
1137
|
+
r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
|
|
1110
1138
|
r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('pemiCopyNameNew'))
|
|
1111
1139
|
r.userId = `{${vo.$t('pemiAddIdNew')}}`
|
|
1112
1140
|
r.timeCreate = `{${vo.$t('pemiAddIdNew')}}`
|
|
@@ -1395,6 +1423,8 @@ export default {
|
|
|
1395
1423
|
//編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
|
|
1396
1424
|
//父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
|
|
1397
1425
|
let baseline0 = cloneDeep(vo.baseline)
|
|
1426
|
+
//refSnap: 開啟編輯器當下之「標的名稱 → 標的 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的標的即此刻 store 之標的, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍存在(標的 id 即名稱, 被刪除即拒收)
|
|
1427
|
+
let refSnap = snapRefIds('pemis', vo.targets)
|
|
1398
1428
|
//showVeCrules
|
|
1399
1429
|
vo.$dg.showVeCrules({
|
|
1400
1430
|
isEditable: vo.isEditable,
|
|
@@ -1417,11 +1447,14 @@ export default {
|
|
|
1417
1447
|
}
|
|
1418
1448
|
|
|
1419
1449
|
//草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
|
|
1420
|
-
if (isDraftStale({
|
|
1450
|
+
if (isDraftStale({ keyTable: 'pemis', rows: vo.baseRows, baseline: baseline0 })) {
|
|
1421
1451
|
vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
|
|
1422
1452
|
return
|
|
1423
1453
|
}
|
|
1424
1454
|
|
|
1455
|
+
//選取記錄: 本次新增之啟用引用項(標的 id)在編輯器開啟當下對應之標的 id(查無記空陣列)
|
|
1456
|
+
recordPicks({ keyTable: 'pemis', strOld: get(vo, `opt.rows[${krNow}].crules`, ''), strNew: crules, snap: refSnap, picks: vo.refPicks })
|
|
1457
|
+
|
|
1425
1458
|
//set
|
|
1426
1459
|
set(vo, `opt.rows[${krNow}].crules`, crules)
|
|
1427
1460
|
// console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
|
|
@@ -1478,6 +1511,15 @@ export default {
|
|
|
1478
1511
|
return
|
|
1479
1512
|
}
|
|
1480
1513
|
|
|
1514
|
+
//正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(標的 id)所指之標的,須仍存在(標的 id 即名稱,身分定義即 id,見 R-REF-IDENTITY-01 規則 5);
|
|
1515
|
+
//已被刪除即不送出(零寫入)並提示重新載入. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於標的表鎖內以新鮮讀取再驗一次
|
|
1516
|
+
let refs = parseRefDecl(buildRefDecl({ keyTable: 'pemis', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.targets }))
|
|
1517
|
+
if (countRefMismatch({ keyTable: 'pemis', decl: refs, refRows: vo.targets }) > 0) {
|
|
1518
|
+
unlockBtn()
|
|
1519
|
+
await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
|
|
1520
|
+
return
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1481
1523
|
//新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
|
|
1482
1524
|
//(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
|
|
1483
1525
|
|
|
@@ -1521,15 +1563,17 @@ export default {
|
|
|
1521
1563
|
|
|
1522
1564
|
//4) updatePemis, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
|
|
1523
1565
|
let okSave = false
|
|
1566
|
+
let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
|
|
1524
1567
|
vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
|
|
1525
|
-
await vo.$fapi.updatePemis(rows, { baseline })
|
|
1526
|
-
.then(() => {
|
|
1568
|
+
await vo.$fapi.updatePemis(rows, { baseline, returnRows: true, refs })
|
|
1569
|
+
.then((res) => {
|
|
1527
1570
|
okSave = true
|
|
1571
|
+
savedRes = res
|
|
1528
1572
|
vo.saveMark.saving = false
|
|
1529
1573
|
})
|
|
1530
1574
|
.catch(async (err) => {
|
|
1531
1575
|
vo.saveMark.saving = false
|
|
1532
|
-
vo.staleNotice = isDraftStale({
|
|
1576
|
+
vo.staleNotice = isDraftStale({ keyTable: 'pemis', rows: vo.pemis, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
|
|
1533
1577
|
vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
|
|
1534
1578
|
await vo.$dg.showCheckYes(`${vo.$t('pemiSavePemisFail')}: ${vo.$tErr(err)}`)
|
|
1535
1579
|
})
|
|
@@ -1538,6 +1582,11 @@ export default {
|
|
|
1538
1582
|
//isModified; postSaving 自此至成功訊息框關閉後 300ms 內排除 rowsChange(見 data 註解)
|
|
1539
1583
|
vo.postSaving = true
|
|
1540
1584
|
vo.isModified = false
|
|
1585
|
+
vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
|
|
1586
|
+
|
|
1587
|
+
//存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
|
|
1588
|
+
//其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
|
|
1589
|
+
applySavedRows('pemis', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
|
|
1541
1590
|
|
|
1542
1591
|
//alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
|
|
1543
1592
|
vo.$ui.updateLoading(false)
|
|
@@ -280,7 +280,9 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
|
|
|
280
280
|
import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
|
|
281
281
|
import { isExternalRow } from '../js/externalRows.mjs'
|
|
282
282
|
import { makeBaseline } from '../js/rowDigest.mjs'
|
|
283
|
-
import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
|
|
283
|
+
import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
|
|
284
|
+
import { applySavedRows } from '../js/applySaved.mjs'
|
|
285
|
+
import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
|
|
284
286
|
import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
|
|
285
287
|
import WIcon from 'w-component-vue/src/components/WIcon.vue'
|
|
286
288
|
import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
|
|
@@ -480,14 +482,14 @@ export default {
|
|
|
480
482
|
first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
|
|
481
483
|
editChanged: vo.syncMark.editable !== isEditable,
|
|
482
484
|
storeChanged: refsChanged(vo.syncMark.refs, [rows]),
|
|
483
|
-
hasDraft: () => vo.syncMark.dirty === true,
|
|
485
|
+
hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
|
|
484
486
|
saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
|
|
485
487
|
})
|
|
486
488
|
if (act === 'rebuild') {
|
|
487
489
|
vo.rebuildDraft(rows, isEditable)
|
|
488
490
|
}
|
|
489
491
|
else if (act === 'keep') {
|
|
490
|
-
vo.staleNotice = isDraftStale({
|
|
492
|
+
vo.staleNotice = isDraftStale({ keyTable: 'targets', rows, baseline: vo.syncMark.baseline })
|
|
491
493
|
}
|
|
492
494
|
|
|
493
495
|
return ''
|
|
@@ -660,7 +662,7 @@ export default {
|
|
|
660
662
|
let items = cloneDeep(rows)
|
|
661
663
|
|
|
662
664
|
//baseline, 與 items 同一處由同一份來源列(items)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
|
|
663
|
-
let baseline = makeBaseline(items,
|
|
665
|
+
let baseline = makeBaseline(items, 'targets')
|
|
664
666
|
|
|
665
667
|
//save; 重建即放棄未存編輯(isModified 歸零)與過期提示, syncMark 記下本次所依據之 store 參照與編輯模式
|
|
666
668
|
vo.items = items
|
|
@@ -676,6 +678,11 @@ export default {
|
|
|
676
678
|
//firstLoading
|
|
677
679
|
vo.firstLoading = false
|
|
678
680
|
|
|
681
|
+
//第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
|
|
682
|
+
vo.$nextTick(() => {
|
|
683
|
+
vo.bindEditing()
|
|
684
|
+
})
|
|
685
|
+
|
|
679
686
|
},
|
|
680
687
|
|
|
681
688
|
curRefs: function() {
|
|
@@ -684,6 +691,22 @@ export default {
|
|
|
684
691
|
return [vo.targets]
|
|
685
692
|
},
|
|
686
693
|
|
|
694
|
+
bindEditing: function() {
|
|
695
|
+
//第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
|
|
696
|
+
let vo = this
|
|
697
|
+
bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
|
|
698
|
+
vo.afterEditing()
|
|
699
|
+
})
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
afterEditing: function() {
|
|
703
|
+
//儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
|
|
704
|
+
let vo = this
|
|
705
|
+
if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
|
|
706
|
+
vo.reloadDraft()
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
|
|
687
710
|
reloadDraft: function() {
|
|
688
711
|
//「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
|
|
689
712
|
//genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
|
|
@@ -916,6 +939,7 @@ export default {
|
|
|
916
939
|
r.id = _r.id //使用新建方式預先產生id避免重複
|
|
917
940
|
r.id = vo.$s.getNameNew(rows, 'id', nameOld, vo.$t('targetCopyNameNew'))
|
|
918
941
|
r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
|
|
942
|
+
r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
|
|
919
943
|
r.userId = `{${vo.$t('targetAddIdNew')}}`
|
|
920
944
|
r.timeCreate = `{${vo.$t('targetAddIdNew')}}`
|
|
921
945
|
r.userIdUpdate = `{${vo.$t('targetAddIdNew')}}`
|
|
@@ -1090,15 +1114,17 @@ export default {
|
|
|
1090
1114
|
|
|
1091
1115
|
//4) updateTargets, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
|
|
1092
1116
|
let okSave = false
|
|
1117
|
+
let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
|
|
1093
1118
|
vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
|
|
1094
|
-
await vo.$fapi.updateTargets(rowsSend, { baseline })
|
|
1095
|
-
.then(() => {
|
|
1119
|
+
await vo.$fapi.updateTargets(rowsSend, { baseline, returnRows: true })
|
|
1120
|
+
.then((res) => {
|
|
1096
1121
|
okSave = true
|
|
1122
|
+
savedRes = res
|
|
1097
1123
|
vo.saveMark.saving = false
|
|
1098
1124
|
})
|
|
1099
1125
|
.catch(async (err) => {
|
|
1100
1126
|
vo.saveMark.saving = false
|
|
1101
|
-
vo.staleNotice = isDraftStale({
|
|
1127
|
+
vo.staleNotice = isDraftStale({ keyTable: 'targets', rows: vo.targets, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
|
|
1102
1128
|
vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
|
|
1103
1129
|
await vo.$dg.showCheckYes(`${vo.$t('targetSaveTargetsFail')}: ${vo.$tErr(err)}`)
|
|
1104
1130
|
})
|
|
@@ -1106,6 +1132,11 @@ export default {
|
|
|
1106
1132
|
|
|
1107
1133
|
//isModified
|
|
1108
1134
|
vo.isModified = false
|
|
1135
|
+
vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
|
|
1136
|
+
|
|
1137
|
+
//存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
|
|
1138
|
+
//其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
|
|
1139
|
+
applySavedRows('targets', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
|
|
1109
1140
|
|
|
1110
1141
|
//alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
|
|
1111
1142
|
vo.$ui.updateLoading(false)
|