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
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
|
|
129
129
|
<!-- 資料已被更新提示(R-PRINCIPLE-00 原則 7): 開框後他人或外部同步改了群組表, 本視窗之草稿與基準維持開框時之版本、不重建, 存檔將被伺服端拒收; 以 v-for 渲染(無提示時不渲染任何節點) -->
|
|
130
130
|
<div
|
|
131
|
-
v-for="ks in (staleNotice ? 1 : 0)"
|
|
131
|
+
v-for="ks in ((staleNotice || subjectNotice) ? 1 : 0)"
|
|
132
132
|
:key="`stale-${ks}`"
|
|
133
133
|
data-fmid="pemi-blng-grups-banner-stale"
|
|
134
134
|
style="padding:5px 15px; border-top:1px solid #ddd; background:#FFE0B2; color:#444; font-size:0.8rem; overflow-wrap:anywhere;"
|
|
@@ -221,7 +221,9 @@ import getKpLossy from '../js/getKpLossy.mjs'
|
|
|
221
221
|
import fmtLossyMsg from '../js/fmtLossyMsg.mjs'
|
|
222
222
|
import pairRowsToSnapshot from '../js/pairRowsToSnapshot.mjs'
|
|
223
223
|
import { makeBaseline } from '../js/rowDigest.mjs'
|
|
224
|
-
import { isDraftStale } from '../js/draftSync.mjs'
|
|
224
|
+
import { isDraftStale, planDialogSync, checkSubject } from '../js/draftSync.mjs'
|
|
225
|
+
import { applySavedRows } from '../js/applySaved.mjs'
|
|
226
|
+
import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
|
|
225
227
|
import { makeRowStates, getChangedIdx, buildRowsSend } from '../js/relDialogSend.mjs'
|
|
226
228
|
import { getRefState, getRawModeMap, putRefEntry } from '../js/editorRefRows.mjs'
|
|
227
229
|
import { getRefWarns, getRefWarnText } from '../js/getAuthWarn.mjs'
|
|
@@ -272,7 +274,10 @@ export default {
|
|
|
272
274
|
//baseline 為草稿所依據之那份群組列之逐列內容指紋, 存檔隨 updateGrups 第 2 參送伺服端; rowStates 為開框時各列之歸屬狀態, 存檔時判定使用者有無變更該列
|
|
273
275
|
baseline: null,
|
|
274
276
|
rowStates: null,
|
|
275
|
-
staleNotice: false,
|
|
277
|
+
staleNotice: false, //有未存編輯(含開啟中之儲存格編輯器)時收到推播、且群組表已異於本視窗載入時之基準(R-PRINCIPLE-00 原則 7、10): true 時顯示「資料已被更新」提示; 每次 show() 歸零
|
|
278
|
+
subject: null, //開框時之主體摘要 { id, name, saved }(第 114 項, R-SUBJECT-ID-01): 本對話框以**名稱**引用權限(群組 cpemis 的鍵是權限名稱), 存檔時以 id 驗主體仍是同一個實體(未被刪除、改名、刪除後以同名重建)
|
|
279
|
+
subjectNotice: false, //主體(權限)於開框後已被刪除、改名或以同名重建(id 不同): true 時顯示「資料已被更新」提示, 存檔將被拒收; 每次 show() 歸零
|
|
280
|
+
systemProcing: false, //程式端重建對話框內容期間為 true, 用於排除非使用者操作之 rowsChange (見 $ui.markDataReload; 開框中因推播重建時須用)
|
|
276
281
|
|
|
277
282
|
items: [],
|
|
278
283
|
itemsCheck: [],
|
|
@@ -304,14 +309,12 @@ export default {
|
|
|
304
309
|
|
|
305
310
|
},
|
|
306
311
|
watch: {
|
|
307
|
-
//store
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
vo.staleNotice = isDraftStale({ keys: vo.$ds.grups.keys, rows, baseline: vo.baseline })
|
|
312
|
+
//store 之群組表或權限(主體)表被推播更新(整表以新陣列取代): 處置見 syncStore
|
|
313
|
+
grups: function() {
|
|
314
|
+
this.syncStore()
|
|
315
|
+
},
|
|
316
|
+
pemis: function() {
|
|
317
|
+
this.syncStore()
|
|
315
318
|
},
|
|
316
319
|
},
|
|
317
320
|
computed: {
|
|
@@ -505,7 +508,7 @@ export default {
|
|
|
505
508
|
// console.log('rowsChange cloneDeep(vo.opt.rows)', cloneDeep(vo.opt.rows))
|
|
506
509
|
|
|
507
510
|
//check
|
|
508
|
-
if (!vo.syncState || vo.firstLoading || vo.firstSetting) {
|
|
511
|
+
if (!vo.syncState || vo.firstLoading || vo.firstSetting || vo.systemProcing) {
|
|
509
512
|
return
|
|
510
513
|
}
|
|
511
514
|
|
|
@@ -1034,17 +1037,93 @@ export default {
|
|
|
1034
1037
|
|
|
1035
1038
|
},
|
|
1036
1039
|
|
|
1037
|
-
|
|
1040
|
+
syncStore: function() {
|
|
1041
|
+
//store 之群組表或權限(主體)表被推播更新(整表以新陣列取代)時的處置(R-PRINCIPLE-00 原則 10; 規則帳本 R-DRAFT-KEEP-01 規則 11、12; draftSync.planDialogSync):
|
|
1042
|
+
// ① 無未存修改(含無開啟中之儲存格編輯器)而群組表已異於載入基準 → 以新資料重建對話框內容並更新基準(對話框開而未編輯時不停在過期資料上; 修 (b));
|
|
1043
|
+
// ② 有未存修改或開啟中之編輯器 → 保留草稿、凍結基準, 顯示「資料已被更新」提示, 存檔時由伺服端拒收(saveStale);
|
|
1044
|
+
// ③ 自己存檔進行中所致之推播不是他人更新 → 不重建、不提示;
|
|
1045
|
+
// ④ 主體(權限)已被刪除、改名或以同名重建(第 114 項) → 不重建(無從續編), 顯示同一提示, 存檔時前端與之比對 id 拒收.
|
|
1046
|
+
let vo = this
|
|
1047
|
+
if (!vo.bShow || !Array.isArray(vo.baseline)) {
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
let sc = checkSubject({ subject: vo.subject, rows: vo.pemis })
|
|
1051
|
+
vo.subjectNotice = sc !== 'ok'
|
|
1052
|
+
let act = planDialogSync({
|
|
1053
|
+
show: true,
|
|
1054
|
+
storeChanged: true,
|
|
1055
|
+
stale: isDraftStale({ keyTable: 'grups', rows: vo.grups, baseline: vo.baseline }),
|
|
1056
|
+
dirty: vo.isModified === true,
|
|
1057
|
+
editing: isCellEditing(get(vo, '$refs.rftable', null)),
|
|
1058
|
+
saving: vo.ownSaving === true,
|
|
1059
|
+
})
|
|
1060
|
+
if (act === 'rebuild') {
|
|
1061
|
+
if (sc === 'ok') {
|
|
1062
|
+
vo.rebuildDialog()
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
else if (act === 'keep') {
|
|
1066
|
+
vo.staleNotice = true
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
|
|
1070
|
+
rebuildDialog: function() {
|
|
1071
|
+
//開框中以目前 store 重建對話框內容與基準(無未存修改時; 同 show() 之內容組裝, 不動 pm/bShow/isEditable/subject)
|
|
1072
|
+
let vo = this
|
|
1073
|
+
let p = vo.pemis.find((r) => r.id === get(vo, 'subject.id', ''))
|
|
1074
|
+
let pemi = { ...vo.pemi, ...cloneDeep(p) }
|
|
1075
|
+
let grups = cloneDeep(vo.grups)
|
|
1076
|
+
let baseline = makeBaseline(grups, 'grups')
|
|
1077
|
+
let items = vo.genItems(pemi, grups)
|
|
1078
|
+
let rowStates = makeRowStates(items)
|
|
1079
|
+
|
|
1080
|
+
//markDataReload: 重建 opt 屬程式端寫入, 其後非同步觸發之 rowsChange 不得視為使用者變更
|
|
1081
|
+
vo.$ui.markDataReload(vo)
|
|
1082
|
+
|
|
1083
|
+
vo.pemi = pemi
|
|
1084
|
+
vo.items = items
|
|
1085
|
+
vo.baseline = baseline
|
|
1086
|
+
vo.rowStates = rowStates
|
|
1087
|
+
vo.isModified = false
|
|
1088
|
+
vo.staleNotice = false
|
|
1089
|
+
vo.genOpt()
|
|
1090
|
+
vo.$nextTick(() => {
|
|
1091
|
+
vo.bindEditing()
|
|
1092
|
+
})
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
bindEditing: function() {
|
|
1096
|
+
//第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(syncStore 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
|
|
1097
|
+
let vo = this
|
|
1098
|
+
bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
|
|
1099
|
+
vo.afterEditing()
|
|
1100
|
+
})
|
|
1101
|
+
},
|
|
1102
|
+
|
|
1103
|
+
afterEditing: function() {
|
|
1104
|
+
//儲存格編輯結束: 無未存修改而表於編輯期間已變新者, 補上一次同步處置(重建); 有未存修改者維持保留
|
|
1105
|
+
let vo = this
|
|
1106
|
+
if (vo.isModified !== true) {
|
|
1107
|
+
vo.syncStore()
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
|
|
1111
|
+
saveGrups: async function(rows, baseline, subject) {
|
|
1038
1112
|
// console.log('method saveGrups', rows, baseline)
|
|
1039
1113
|
|
|
1040
1114
|
let vo = this
|
|
1041
1115
|
|
|
1042
1116
|
//3) updateGrups, 各自 catch + okX 旗標短路 (canonical 五段結構; 空清單檢查已上移至 doSave core())
|
|
1043
|
-
//第 2 參 { baseline }:
|
|
1117
|
+
//第 2 參 { baseline, returnRows }: baseline 為開框時綁定之草稿基準(由 doSave 於流程開頭取得並傳入, 不於此讀 vo.baseline——存檔進行中若再次 show() 會換成新草稿之基準); 伺服端比對「本次將寫入之列」之庫內版本, 草稿之後被改過即整次拒收 saveStale(失敗路徑沿用下方 catch 顯示語系文字);
|
|
1118
|
+
//returnRows: 成功時回傳寫入後之列(鎖內新鮮讀取), 立即 commit 進 store(規格第 8 節; R-SAVED-ROWS-01), 其後再開此視窗時基準即為最新, 不會被自己上一次之存檔誤擋
|
|
1044
1119
|
let okSave = false
|
|
1045
1120
|
vo.ownSaving = true //自己之寫入所致之推播不提示(成功路徑維持至視窗關閉; show() 歸零)
|
|
1046
|
-
|
|
1047
|
-
|
|
1121
|
+
//subject: 開框時之主體(權限)摘要 { id, name, saved }(由 doSave 於流程開頭取得並傳入): 伺服端於權限表鎖內新鮮讀取後以同一判準(draftSync.checkSubject)再驗一次, 被刪除、改名、刪除後同名重建一律拒收 saveSubjectChanged(R-SUBJECT-ID-01 規則 4; 前端自檢先擋, 此為推播未抵達時之伺服端強制)
|
|
1122
|
+
await vo.$fapi.updateGrups(rows, { baseline, returnRows: true, subject })
|
|
1123
|
+
.then((res) => {
|
|
1124
|
+
okSave = true
|
|
1125
|
+
applySavedRows('grups', res, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
|
|
1126
|
+
})
|
|
1048
1127
|
.catch(async (err) => {
|
|
1049
1128
|
vo.ownSaving = false //失敗(含 saveStale 被拒): 視窗仍開著, 恢復監看
|
|
1050
1129
|
vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
|
|
@@ -1111,6 +1190,20 @@ export default {
|
|
|
1111
1190
|
//pemiName: 被編輯之權限名稱, 同樣於流程開頭(任何 await 之前)取得——等待後才讀 vo.pemi.name 會讀到新開啟之實體, 把舊草稿之歸屬寫成指派新實體(外審 sol R2)
|
|
1112
1191
|
let pemiName = get(vo, 'pemi.name', '')
|
|
1113
1192
|
|
|
1193
|
+
//subject: 開框時之主體摘要 { id, name, saved }, 同樣於流程開頭取得並持有. isSubjectOk: 主體(權限)於存檔當下仍是開框時那一個實體(第 114 項, R-SUBJECT-ID-01; 原則 3、6):
|
|
1194
|
+
//本對話框以名稱引用權限(群組 cpemis 的鍵是權限名稱)而不驗主體身分——開框後權限被刪除、改名, 或刪除後以同名重建(id 不同), 引用會靜默指向新實體(被同名新資料接管); 故以 id 比對存檔當下之權限表, 不符即整次拒存(零寫入)並提示重新載入
|
|
1195
|
+
let subject = vo.subject
|
|
1196
|
+
let isSubjectOk = () => checkSubject({ subject, rows: vo.pemis }) === 'ok'
|
|
1197
|
+
let rejectSubject = async () => {
|
|
1198
|
+
vo.$ui.updateLoading(false)
|
|
1199
|
+
await vo.$dg.showCheckYes(vo.$t('relSubjectChangedPemi').split('{name}').join(get(subject, 'name', '')))
|
|
1200
|
+
return false
|
|
1201
|
+
}
|
|
1202
|
+
if (!isSubjectOk()) {
|
|
1203
|
+
unlockBtn()
|
|
1204
|
+
return await rejectSubject()
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1114
1207
|
//1b) 無法無損表示之授權資料(R-AUTH-LOSSY-01, 主控複審 R4): 該列群組之 cpemis 含物件形式無法無損表示之格(getKpLossy)者, 本對話框不得改寫其字串——
|
|
1115
1208
|
//本對話框所管之歸屬/模式有變更 → 整次儲存拒絕並點名(原子, 其餘列亦不寫); 未變更 → 該列字串逐字元不動(見下方 kpLossyIdx)
|
|
1116
1209
|
//群組列之快照只取這一次: 有損檢查之值即下方寫入之值(外審 sol#2: 兩次讀取之間 store 可能被伺服器推送更新, 檢查與寫入看到不同資料)
|
|
@@ -1170,6 +1263,11 @@ export default {
|
|
|
1170
1263
|
//delay
|
|
1171
1264
|
await delay(300)
|
|
1172
1265
|
|
|
1266
|
+
//主體身分複驗(第 114 項): 開 loading 與等待期間 store 可能再被推播更新, 送出前再比對一次
|
|
1267
|
+
if (!isSubjectOk()) {
|
|
1268
|
+
return await rejectSubject()
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1173
1271
|
//grups: 沿用 1b 之快照(見上)
|
|
1174
1272
|
|
|
1175
1273
|
//kpNewCpemis: 列索引 → 該列新 cpemis 字串; 只收「使用者有變更(kpChanged)、非有損、且 putRefEntry 判定本項確有變更」者, 其餘一律不收(由 buildRowsSend 原樣帶回)
|
|
@@ -1217,7 +1315,7 @@ export default {
|
|
|
1217
1315
|
// }
|
|
1218
1316
|
|
|
1219
1317
|
//saveGrups, 失敗時回 false: 失敗提示已由 saveGrups 顯示, 此處不關閉對話框(保留未存變更供重按儲存)
|
|
1220
|
-
let okSave = await vo.saveGrups(grupsNew, baseline)
|
|
1318
|
+
let okSave = await vo.saveGrups(grupsNew, baseline, subject)
|
|
1221
1319
|
if (!okSave) {
|
|
1222
1320
|
return false
|
|
1223
1321
|
}
|
|
@@ -1288,6 +1386,7 @@ export default {
|
|
|
1288
1386
|
//default
|
|
1289
1387
|
vo.isModified = false
|
|
1290
1388
|
vo.staleNotice = false
|
|
1389
|
+
vo.subjectNotice = false
|
|
1291
1390
|
vo.ownSaving = false
|
|
1292
1391
|
|
|
1293
1392
|
//isEditable, pemi
|
|
@@ -1297,12 +1396,15 @@ export default {
|
|
|
1297
1396
|
//grup
|
|
1298
1397
|
pemi = cloneDeep(pemi)
|
|
1299
1398
|
|
|
1399
|
+
//subject: 開框時之主體摘要(第 114 項): 存檔時以 id 驗主體仍是同一個實體
|
|
1400
|
+
let subject = { id: get(pemi, 'id', ''), name: get(pemi, 'name', ''), saved: vo.pemis.some((r) => r.id === get(pemi, 'id', '')) }
|
|
1401
|
+
|
|
1300
1402
|
//grups
|
|
1301
1403
|
let grups = cloneDeep(vo.grups)
|
|
1302
1404
|
|
|
1303
|
-
//baseline(F-26, R-BASELINE-01; 外審 B.1): 於生成 items 之同一處, 以 items 所依據之那份群組列(grups)計算並綁定為本次草稿之載入基準; 之後 store
|
|
1304
|
-
|
|
1305
|
-
let baseline = makeBaseline(grups,
|
|
1405
|
+
//baseline(F-26, R-BASELINE-01; 外審 B.1): 於生成 items 之同一處, 以 items 所依據之那份群組列(grups)計算並綁定為本次草稿之載入基準; 之後 store 推播變新不得更新它,
|
|
1406
|
+
//除非無未存修改時 syncStore 整個重建(存檔當下才由最新 store 取基準, 證明的是「store 新」而非「待存之編輯內容新」), 每次 show() 重新綁定. 摘要口徑與伺服端同一(寫入契約 grups, 經 rowDigest.makeBaseline/w-serv-orm buildBaseline)
|
|
1407
|
+
let baseline = makeBaseline(grups, 'grups')
|
|
1306
1408
|
|
|
1307
1409
|
//items
|
|
1308
1410
|
let items = vo.genItems(pemi, grups)
|
|
@@ -1313,6 +1415,7 @@ export default {
|
|
|
1313
1415
|
//save
|
|
1314
1416
|
vo.isEditable = isEditable
|
|
1315
1417
|
vo.pemi = pemi
|
|
1418
|
+
vo.subject = subject
|
|
1316
1419
|
vo.items = items
|
|
1317
1420
|
vo.baseline = baseline
|
|
1318
1421
|
vo.rowStates = rowStates
|
|
@@ -1323,6 +1426,11 @@ export default {
|
|
|
1323
1426
|
//firstLoading
|
|
1324
1427
|
vo.firstLoading = false
|
|
1325
1428
|
|
|
1429
|
+
//第 115 項: 對話框內之 ag-grid 綁「儲存格編輯結束」監聽
|
|
1430
|
+
vo.$nextTick(() => {
|
|
1431
|
+
vo.bindEditing()
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1326
1434
|
//bShow
|
|
1327
1435
|
vo.bShow = true
|
|
1328
1436
|
|
package/src/getPermUserInfor.mjs
CHANGED
|
@@ -5,6 +5,16 @@ import isfun from 'wsemi/src/isfun.mjs'
|
|
|
5
5
|
import fetchJson from './fetchJson.mjs'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 由外部應用(node端)查詢指定使用者之權限資訊
|
|
10
|
+
*
|
|
11
|
+
* @param {String} url 輸入權限系統之getPermUserInfor網址字串,須含'token={sysToken}'與'userId={userId}'兩個佔位符,例如'http://localhost:11006/api/getPermUserInfor?token={sysToken}&userId={userId}'
|
|
12
|
+
* @param {String} tokenSelf 輸入系統介接用之app使用者token字串
|
|
13
|
+
* @param {String} userIdTar 輸入欲查詢之使用者id字串。權限系統mappingBy為'id'(預設)時,此為該使用者於外部系統之使用者id(與登入對應同一判準:外部同步列對應其來源鍵srcKey,後台列對應其id,不是權限系統配發之內部id;命中多筆時伺服端拒絕,HTTP回應msg為ambiguousUserFromPerm、本函數reject為cannotGetUserDataByUrl;查無使用者則回全停用之空權限);mappingBy為'email'或'name'時,此為權限系統users表內之內部id
|
|
14
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
15
|
+
* @param {Function} [opt.funConvertPerm] 輸入轉換函數,接收{user,rules},回傳物件(可為Promise)以取代預設回傳值,未給則原樣回傳
|
|
16
|
+
* @returns {Promise} 回傳Promise,resolve為{user,rules}物件(或funConvertPerm轉換後之物件),reject為錯誤鍵字串
|
|
17
|
+
*/
|
|
8
18
|
async function getPermUserInfor(url, tokenSelf, userIdTar, opt = {}) {
|
|
9
19
|
//url: http://localhost:11006/api/getPermUserInfor?token={sysToken}&userId={userId}
|
|
10
20
|
let errTemp = null
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//存檔成功後立即以伺服端回傳之「寫入後之列」更新前端 store(多人同時編輯規格第 8 節; R-PRINCIPLE-00 原則 10; 規則帳本 R-SAVED-ROWS-01; ADR-037 Update(perm-run01)): 純函數(不依賴 Vue/DOM/ORM).
|
|
2
|
+
//
|
|
3
|
+
//背景: 存檔成功後新基準須取自伺服端、且須立即取得——若只等推播/輪詢(約 2 秒)才更新 store 與基準, 使用者存檔成功後立即再編輯再存, 會被自己上一次之存檔誤擋(api-verify01 實測 8 次中 7 次; perm run12 同見).
|
|
4
|
+
//不得以「自己送出之值」當新基準: 寫入時 clearXSS 等會改寫部分字串(例 a<b 存為 a<b), 以送出值當基準, 下次存檔會被自己之基準誤拒. 故 updateXxx 於成功時(客戶端帶 returnRows:true)回傳 { msg, rows, others }:
|
|
5
|
+
//rows 為鎖內新鮮讀取之該表全部列, others 為引用連帶有改寫之引用端表(鍵為表名)之全部列. 前端把它們 commit 進 store(同 main.js recvData 之 UpdateTableData), 清單頁與對話框的重建照既有流程即以之為新資料與新基準; 其後抵達之同內容推播, 摘要相同, 不視為他人更新.
|
|
6
|
+
|
|
7
|
+
let TABLES = ['targets', 'pemis', 'grups', 'users']
|
|
8
|
+
let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
//isRows: 須為陣列且每列為物件、主鍵 id 為非空字串(正常資料恆是; 否則視為異常回傳而不套用, 退回等推播)
|
|
12
|
+
let isRows = (rs) => Array.isArray(rs) && rs.every((r) => r !== null && typeof r === 'object' && typeof r.id === 'string' && r.id !== '')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 取出存檔回傳中可套用之表資料: [{ tableName, data }, ...]; 回傳不是 { rows, others } 形狀(舊伺服端回字串、讀回失敗)或資料異常者回空陣列(呼叫端退回等推播)
|
|
17
|
+
*
|
|
18
|
+
* @param {String} keyTable 輸入本次存檔之表名(targets/pemis/grups/users)
|
|
19
|
+
* @param {*} res 輸入 updateXxx 之回傳(成功 msg key 字串, 或 { msg, rows, others })
|
|
20
|
+
* @returns {Array} 回傳 [{ tableName, data }, ...](本表在前, 引用端表在後; 表名不在白名單或資料異常者略過)
|
|
21
|
+
*/
|
|
22
|
+
function getSavedTables(keyTable, res) {
|
|
23
|
+
let out = []
|
|
24
|
+
if (res === null || typeof res !== 'object' || TABLES.indexOf(keyTable) < 0 || !isRows(res.rows)) {
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
out.push({ tableName: keyTable, data: res.rows })
|
|
28
|
+
let others = (res.others !== null && typeof res.others === 'object') ? res.others : {}
|
|
29
|
+
Object.keys(others).forEach((t) => {
|
|
30
|
+
if (t !== keyTable && TABLES.indexOf(t) >= 0 && hasOwn(others, t) && isRows(others[t])) {
|
|
31
|
+
out.push({ tableName: t, data: others[t] })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 存檔成功後以回傳之寫入後之列更新 store
|
|
40
|
+
*
|
|
41
|
+
* @param {String} keyTable 輸入本次存檔之表名
|
|
42
|
+
* @param {*} res 輸入 updateXxx 之回傳
|
|
43
|
+
* @param {Function} commitTable 輸入 (tableName, data) => void, 實際 commit UpdateTableData 之函數(同 main.js recvData)
|
|
44
|
+
* @returns {Boolean} 回傳是否已套用(false 為回傳不含可用之列, 前端退回等推播)
|
|
45
|
+
*/
|
|
46
|
+
function applySavedRows(keyTable, res, commitTable) {
|
|
47
|
+
let ts = getSavedTables(keyTable, res)
|
|
48
|
+
ts.forEach((t) => {
|
|
49
|
+
commitTable(t.tableName, t.data)
|
|
50
|
+
})
|
|
51
|
+
return ts.length > 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
export { getSavedTables, applySavedRows }
|
|
56
|
+
export default applySavedRows
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//儲存格編輯器開啟中視為有草稿(第 115 項; R-PRINCIPLE-00 原則 10「有未存修改時收到推播不重建」; 規則帳本 R-DRAFT-KEEP-01 規則 10): 以 ag-grid 的 API 判定, 四清單頁與兩對話框共用.
|
|
2
|
+
//
|
|
3
|
+
//背景: 使用者在儲存格輸入了文字、尚未按 Enter(或尚未失焦)時, 輸入只在編輯器內, 還沒進 rows(rowsChange 尚未觸發, isModified 仍為假); 此時推播使清單重建(setRowData)會直接丟掉編輯器與輸入, 且不會有任何提示.
|
|
4
|
+
//故編輯器開啟中視同有草稿: 不重建、凍結基準、提示「資料已被更新」; 編輯器關閉後(Enter 提交→isModified 為真續保留; Esc 或無變更→無草稿)若 store 於期間已變新, 補上一次重建.
|
|
5
|
+
//
|
|
6
|
+
//cmp 為 WAggridVue 元件實例(含 getApi(): ag-grid GridApi; w-aggrid-vue 2.1.9). 純函數: 不依賴 Vue; 取不到 api(尚未掛載、已銷毀)一律視為「未在編輯」.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
let apiOf = (cmp) => {
|
|
10
|
+
try {
|
|
11
|
+
let api = (cmp !== null && cmp !== undefined && typeof cmp.getApi === 'function') ? cmp.getApi() : null
|
|
12
|
+
return (api !== null && api !== undefined && typeof api.getEditingCells === 'function') ? api : null
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 儲存格編輯器是否開啟中(ag-grid getEditingCells 非空)
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} cmp 輸入 WAggridVue 元件實例(可為 null/undefined)
|
|
24
|
+
* @returns {Boolean} 回傳是否有儲存格正在編輯
|
|
25
|
+
*/
|
|
26
|
+
function isCellEditing(cmp) {
|
|
27
|
+
let api = apiOf(cmp)
|
|
28
|
+
if (api === null) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
let cs = api.getEditingCells()
|
|
33
|
+
return Array.isArray(cs) && cs.length > 0
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
let bound = new WeakSet()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 對 ag-grid api 註冊一次「儲存格編輯結束」監聽(同一個 api 只註冊一次; 元件重建即換新 api, 需再呼叫)
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} cmp 輸入 WAggridVue 元件實例
|
|
48
|
+
* @param {Function} onStop 輸入編輯結束(提交、Esc、失焦)後之回呼
|
|
49
|
+
* @returns {Boolean} 回傳是否已綁定(false 為 api 尚未可用, 呼叫端稍後重試)
|
|
50
|
+
*/
|
|
51
|
+
function bindCellEditingStop(cmp, onStop) {
|
|
52
|
+
let api = apiOf(cmp)
|
|
53
|
+
if (api === null || typeof api.addEventListener !== 'function') {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
if (bound.has(api)) {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
bound.add(api)
|
|
60
|
+
api.addEventListener('cellEditingStopped', () => {
|
|
61
|
+
try {
|
|
62
|
+
onStop()
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.log('cellEditingStopped', err)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 綁定編輯結束監聽, api 尚未可用時以短輪詢重試(元件於 v-if 內, 重建草稿後下一輪渲染才掛載)
|
|
74
|
+
*
|
|
75
|
+
* @param {Function} getCmp 輸入回傳 WAggridVue 元件實例之函數(每次重試重新取, 元件可能被重建)
|
|
76
|
+
* @param {Function} onStop 輸入編輯結束後之回呼
|
|
77
|
+
* @param {Number} [tries=30] 輸入最多重試次數(每次相隔 100 ms)
|
|
78
|
+
* @returns {void} 無回傳
|
|
79
|
+
*/
|
|
80
|
+
function bindCellEditingStopRetry(getCmp, onStop, tries = 30) {
|
|
81
|
+
if (bindCellEditingStop(getCmp(), onStop) || tries <= 0) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
bindCellEditingStopRetry(getCmp, onStop, tries - 1)
|
|
86
|
+
}, 100)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export { isCellEditing, bindCellEditingStop, bindCellEditingStopRetry }
|
|
91
|
+
export default isCellEditing
|
package/src/js/draftSync.mjs
CHANGED
|
@@ -56,15 +56,91 @@ function planDraftSync({ first, editChanged, storeChanged, hasDraft, saving }) {
|
|
|
56
56
|
* 草稿所依據之基準相對於目前 store 整表是否已過期(供顯示「資料已被更新」提示; 判準同伺服端 checkBaseline, 單一擁有者 rowDigest.compareBaseline)
|
|
57
57
|
*
|
|
58
58
|
* @param {Object} p 輸入參數物件
|
|
59
|
-
* @param {
|
|
59
|
+
* @param {String} p.keyTable 輸入表名(targets/pemis/grups/users; 依其寫入契約計算)
|
|
60
60
|
* @param {Array} p.rows 輸入目前 store 之該表列
|
|
61
61
|
* @param {Array} p.baseline 輸入草稿載入時之基準
|
|
62
|
-
* @returns {Boolean} 回傳是否過期(
|
|
62
|
+
* @returns {Boolean} 回傳是否過期(有任一列內容摘要不同、列被增刪或名次不同; 僅 order 數值改變而名次不變、或僅伺服器專有欄不同不算)
|
|
63
63
|
*/
|
|
64
|
-
function isDraftStale({
|
|
65
|
-
return isBaselineStale({
|
|
64
|
+
function isDraftStale({ keyTable, rows, baseline }) {
|
|
65
|
+
return isBaselineStale({ keyTable, rows, baseline })
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 是否有未存之草稿: 已提交之未存修改(isModified), 或**儲存格編輯器開啟中**(已輸入、未按 Enter 或未失焦——編輯器內的輸入尚未進 rows, 但重建清單會直接丟棄它; 第 115 項, R-DRAFT-KEEP-01 規則 10)
|
|
71
|
+
*
|
|
72
|
+
* 四清單頁(changeParams 的 hasDraft)與兩對話框(planDialogSync)共用, 口徑單一.
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} p 輸入參數物件
|
|
75
|
+
* @param {Boolean} p.dirty 輸入是否有已提交之未存修改(isModified 鏡像)
|
|
76
|
+
* @param {Boolean} p.editing 輸入儲存格編輯器是否開啟中(ag-grid getEditingCells 非空)
|
|
77
|
+
* @returns {Boolean} 回傳是否視為有草稿
|
|
78
|
+
*/
|
|
79
|
+
function hasDraftNow({ dirty, editing }) {
|
|
80
|
+
return dirty === true || editing === true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 反向關聯對話框(開著時)收到 store 推播之處置(第 114/115 項、修 (b); R-PRINCIPLE-00 原則 10「無未存修改時照舊重建」; 規則帳本 R-DRAFT-KEEP-01 規則 10~12)
|
|
86
|
+
*
|
|
87
|
+
* - 'none': 不須處置——未顯示、store 未變、表內容未異於載入基準(推播內容與載入時相同)、或此推播是自己存檔所致(saving)而無草稿
|
|
88
|
+
* - 'rebuild': 無未存修改(含無開啟中之儲存格編輯器)且表已異於載入基準 → 以新資料重建對話框內容並更新基準(對話框開而未編輯時不應停在過期的資料上)
|
|
89
|
+
* - 'keep': 有未存修改或開啟中之編輯器 → 保留草稿、凍結基準; 呼叫端依 stale 顯示「資料已被更新」提示
|
|
90
|
+
* - 'keepQuiet': 自己之存檔進行中(伺服端已寫入、回應未到)所致之推播, 不是他人更新 → 保留、不提示
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} p 輸入參數物件
|
|
93
|
+
* @param {Boolean} p.show 輸入對話框是否顯示中
|
|
94
|
+
* @param {Boolean} p.storeChanged 輸入來源 store 是否變新(參照改變)
|
|
95
|
+
* @param {Boolean} p.stale 輸入表是否已異於對話框載入時之基準(isDraftStale)
|
|
96
|
+
* @param {Boolean} p.dirty 輸入是否有已提交之未存修改
|
|
97
|
+
* @param {Boolean} p.editing 輸入儲存格編輯器是否開啟中
|
|
98
|
+
* @param {Boolean} p.saving 輸入自己之存檔是否進行中
|
|
99
|
+
* @returns {String} 回傳 'none'|'rebuild'|'keep'|'keepQuiet'
|
|
100
|
+
*/
|
|
101
|
+
function planDialogSync({ show, storeChanged, stale, dirty, editing, saving }) {
|
|
102
|
+
if (show !== true || storeChanged !== true) {
|
|
103
|
+
return 'none'
|
|
104
|
+
}
|
|
105
|
+
if (saving === true) {
|
|
106
|
+
return 'keepQuiet'
|
|
107
|
+
}
|
|
108
|
+
if (stale !== true) {
|
|
109
|
+
return 'none'
|
|
110
|
+
}
|
|
111
|
+
return hasDraftNow({ dirty, editing }) ? 'keep' : 'rebuild'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 驗證關聯對話框之主體(被編輯其歸屬之群組/權限)於存檔當下仍是開框時那一個實體(第 114 項; R-PRINCIPLE-00 原則 3、6; 規則帳本 R-SUBJECT-ID-01)
|
|
117
|
+
*
|
|
118
|
+
* 反向對話框以**名稱**引用主體(使用者 cgrups/群組 cpemis 的鍵是名稱), 不驗主體身分: 開框後主體被刪除並以同名重建(id 不同), 引用會靜默指向新實體(原則 6: 不得讓引用被同名新資料接管);
|
|
119
|
+
* 故以主體 id 與開框時之摘要(id、名稱、開框時是否已儲存)比對存檔當下的主體表: id 仍在、名稱未變才算同一實體. 不符一律拒收(收嚴, 原則 3), 提示重新載入.
|
|
120
|
+
* 開框時主體尚未儲存(store 查無該 id; 清單頁新增列未存即開對話框)者沿用既有行為(引用先指向尚不存在之名稱), 但其後 store 出現「同名而 id 不同」之另一實體時亦拒收(引用會被該實體接管).
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} p 輸入參數物件
|
|
123
|
+
* @param {Object} p.subject 輸入開框時之主體摘要 { id, name, saved }(saved: 開框時 store 已有該 id)
|
|
124
|
+
* @param {Array} p.rows 輸入存檔當下之主體表列(store 的 grups 或 pemis)
|
|
125
|
+
* @returns {String} 回傳 'ok'|'gone'(開框時已儲存之 id 現已不在: 已刪除或以同名重建)|'renamed'(id 仍在但名稱不同)|'taken'(開框時未儲存之主體, 現有同名而 id 不同之另一實體)|'noSubject'(未提供有效之開框摘要, 收嚴拒收)
|
|
126
|
+
*/
|
|
127
|
+
function checkSubject({ subject, rows }) {
|
|
128
|
+
let id = (subject !== null && typeof subject === 'object') ? subject.id : undefined
|
|
129
|
+
let name = (subject !== null && typeof subject === 'object') ? subject.name : undefined
|
|
130
|
+
if (typeof id !== 'string' || id === '' || typeof name !== 'string' || name === '') {
|
|
131
|
+
return 'noSubject'
|
|
132
|
+
}
|
|
133
|
+
let list = (Array.isArray(rows) ? rows : []).filter((r) => r !== null && typeof r === 'object')
|
|
134
|
+
let hit = list.find((r) => r.id === id)
|
|
135
|
+
if (hit !== undefined) {
|
|
136
|
+
return hit.name === name ? 'ok' : 'renamed'
|
|
137
|
+
}
|
|
138
|
+
if (subject.saved === true) {
|
|
139
|
+
return 'gone'
|
|
140
|
+
}
|
|
141
|
+
return list.some((r) => r.name === name) ? 'taken' : 'ok'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
export { refsChanged, planDraftSync, isDraftStale, hasDraftNow, planDialogSync, checkSubject }
|
|
70
146
|
export default planDraftSync
|
package/src/js/externalRows.mjs
CHANGED
|
@@ -24,6 +24,12 @@ let isObj = (v) => v !== null && typeof v === 'object'
|
|
|
24
24
|
let normV = (v) => JSON.stringify(v === undefined ? '' : v)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
//normC: 內容欄比對用——另視 null 與 ''/缺欄為同一空值(寫入契約之空值等價, 與基準 digestRow 同口徑; 0/false 仍異於空值).
|
|
28
|
+
// 基準已視 null≡'' 為相同而通過, 此處若仍視為不同, 會對合法存檔回 saveExternalRowChanged(外審 sol#2, 2026-10-23); 相同者亦不重寫, 庫內原表示(null)不被改成 ''.
|
|
29
|
+
// from 欄不適用: null 與 '' 之 from 有別(歷史後台列, 見 checkExternalWrite), 一律用 normV.
|
|
30
|
+
let normC = (v, k) => (k === 'from' ? normV(v) : JSON.stringify(v === undefined || v === null ? '' : v))
|
|
31
|
+
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* 判斷是否為外部列(from 為非空字串)
|
|
29
35
|
*
|
|
@@ -85,15 +91,17 @@ function restoreExternalOrder(rows, ltdtOld) {
|
|
|
85
91
|
* 將「外部列且僅伺服器管理欄(建立者/時間、更新者/時間)不同」之 diff 列移入 same(視為無變更, 不寫入)
|
|
86
92
|
*
|
|
87
93
|
* 外部同步每次整來源重寫, 使外部列之 timeUpdate/timeCreate/userId 隨之更新; 持舊快照之後台存檔若原樣送回該列, 其內容與庫內相同而僅伺服器管理欄較舊,
|
|
88
|
-
* 寫入會把建立者與建立時間回退為舊值(D2 設計稿 S2). 內容相同即無須寫入.
|
|
94
|
+
* 寫入會把建立者與建立時間回退為舊值(D2 設計稿 S2). 內容相同即無須寫入. 預設只處理外部列, 後台列之 diff 判定不變.
|
|
95
|
+
* all 為真時不分外部列或後台列一律處理(perm-run01: 寫入契約 serverOnly——伺服器專有欄位存檔不採用客戶端值, 規格第 4、6 節; R-CONTRACT-01).
|
|
89
96
|
*
|
|
90
97
|
* @param {Object} p 輸入參數物件
|
|
91
98
|
* @param {Array} p.keys 輸入該表 schema 欄位名陣列
|
|
92
99
|
* @param {Object} p.r 輸入 ltdtDiffByKey 結果 { add, del, diff, same }
|
|
93
100
|
* @param {Array} p.ltdtOld 輸入該表庫內全部列
|
|
101
|
+
* @param {Boolean} [p.all=false] 輸入是否不分外部列或後台列(預設只外部列)
|
|
94
102
|
* @returns {Object} 回傳新之 { add, del, diff, same }
|
|
95
103
|
*/
|
|
96
|
-
function dropServerOnlyDiff({ keys, r, ltdtOld }) {
|
|
104
|
+
function dropServerOnlyDiff({ keys, r, ltdtOld, all = false }) {
|
|
97
105
|
let oldById = new Map()
|
|
98
106
|
ltdtOld.forEach((o) => {
|
|
99
107
|
if (isObj(o) && isStr(o.id)) {
|
|
@@ -105,7 +113,7 @@ function dropServerOnlyDiff({ keys, r, ltdtOld }) {
|
|
|
105
113
|
let same = [...r.same]
|
|
106
114
|
r.diff.forEach((row) => {
|
|
107
115
|
let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
|
|
108
|
-
if (o !== undefined && isExternalRow(o) && fields.every((k) =>
|
|
116
|
+
if (o !== undefined && (all === true || isExternalRow(o)) && fields.every((k) => normC(row[k], k) === normC(o[k], k))) {
|
|
109
117
|
same.push(row)
|
|
110
118
|
}
|
|
111
119
|
else {
|
|
@@ -121,14 +129,16 @@ function dropServerOnlyDiff({ keys, r, ltdtOld }) {
|
|
|
121
129
|
*
|
|
122
130
|
* 伺服器管理欄由伺服器擁有(procOrm save 只覆寫 userIdUpdate/timeUpdate, 建立者與建立時間取自送入之列); 外部同步每次整來源重寫使外部列之建立者/建立時間更新,
|
|
123
131
|
* 持舊草稿(前端保留未存編輯, 見 draftSync)送回該列時, 基準因排除伺服器管理欄而通過(內容未變), 若原樣寫入就把較新之建立者與建立時間回退為舊值(R-EXT-OWN-01 S2 之同一缺陷, 外審 sol 第 3 點).
|
|
124
|
-
* 故改動之外部列一律以庫內之伺服器管理欄寫入.
|
|
132
|
+
* 故改動之外部列一律以庫內之伺服器管理欄寫入. 預設只處理庫內為外部列者(後台列之寫入不變); 庫內無該欄者(歷史列)不補、亦不採用客戶端送入之值(刪除該欄, 保留庫內值=仍無該欄; perm-run01 外審 sol#3).
|
|
133
|
+
* all 為真時不分外部列或後台列一律處理(perm-run01: 寫入契約 serverOnly, 見 dropServerOnlyDiff).
|
|
125
134
|
*
|
|
126
135
|
* @param {Object} p 輸入參數物件
|
|
127
136
|
* @param {Object} p.r 輸入 ltdtDiffByKey(及 dropServerOnlyDiff)結果 { add, del, diff, same }
|
|
128
137
|
* @param {Array} p.ltdtOld 輸入該表庫內全部列
|
|
138
|
+
* @param {Boolean} [p.all=false] 輸入是否不分外部列或後台列(預設只外部列)
|
|
129
139
|
* @returns {Object} 回傳新之 { add, del, diff, same }(不改輸入; 未改動之列為同一物件)
|
|
130
140
|
*/
|
|
131
|
-
function restoreServerFields({ r, ltdtOld }) {
|
|
141
|
+
function restoreServerFields({ r, ltdtOld, all = false }) {
|
|
132
142
|
let oldById = new Map()
|
|
133
143
|
ltdtOld.forEach((o) => {
|
|
134
144
|
if (isObj(o) && isStr(o.id)) {
|
|
@@ -137,7 +147,7 @@ function restoreServerFields({ r, ltdtOld }) {
|
|
|
137
147
|
})
|
|
138
148
|
let diff = r.diff.map((row) => {
|
|
139
149
|
let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
|
|
140
|
-
if (o === undefined || !isExternalRow(o)) {
|
|
150
|
+
if (o === undefined || (all !== true && !isExternalRow(o))) {
|
|
141
151
|
return row
|
|
142
152
|
}
|
|
143
153
|
let out = { ...row }
|
|
@@ -145,6 +155,10 @@ function restoreServerFields({ r, ltdtOld }) {
|
|
|
145
155
|
if (hasOwn(o, k)) {
|
|
146
156
|
out[k] = o[k]
|
|
147
157
|
}
|
|
158
|
+
else {
|
|
159
|
+
//庫內無該欄(歷史列): 不採用客戶端送入之值(寫入契約 serverOnly: 保留庫內值=仍無該欄; 外審 sol#3, 2026-10-23)
|
|
160
|
+
delete out[k]
|
|
161
|
+
}
|
|
148
162
|
})
|
|
149
163
|
return out
|
|
150
164
|
})
|
|
@@ -182,6 +196,9 @@ function checkExternalWrite({ keyTable, keys, r, ltdtOld, lockExternalRows = tru
|
|
|
182
196
|
bad.push({ key, reason, k, from: isStr(from) ? from : '' })
|
|
183
197
|
}
|
|
184
198
|
|
|
199
|
+
//srcKey: 外部匯入之來源鍵為身分欄(與 from 同; perm-run03, R-IMPORT-01), 只由匯入端點寫入, 後台不可新增帶值之列、不可改既有列之值(不論 lockExternalRows; 缺欄/null/'' 同一空值)
|
|
200
|
+
let normSrc = (v) => (v === undefined || v === null ? '' : v)
|
|
201
|
+
|
|
185
202
|
//add
|
|
186
203
|
r.add.forEach((row, k) => {
|
|
187
204
|
let f = isObj(row) ? normFrom(row.from) : ''
|
|
@@ -191,6 +208,9 @@ function checkExternalWrite({ keyTable, keys, r, ltdtOld, lockExternalRows = tru
|
|
|
191
208
|
else if (lockExternalRows && f !== '') {
|
|
192
209
|
push('saveFromNotAllowed', 'addFrom', k, f)
|
|
193
210
|
}
|
|
211
|
+
else if (isObj(row) && normSrc(row.srcKey) !== '') {
|
|
212
|
+
push('saveFromNotAllowed', 'addSrcKey', k, '')
|
|
213
|
+
}
|
|
194
214
|
})
|
|
195
215
|
|
|
196
216
|
//del
|
|
@@ -211,6 +231,10 @@ function checkExternalWrite({ keyTable, keys, r, ltdtOld, lockExternalRows = tru
|
|
|
211
231
|
push('saveFromNotAllowed', 'fromType', k, '')
|
|
212
232
|
return
|
|
213
233
|
}
|
|
234
|
+
if (o !== undefined && isObj(row) && normSrc(row.srcKey) !== normSrc(o.srcKey)) {
|
|
235
|
+
push(isExternalRow(o) ? 'saveExternalRowChanged' : 'saveFromNotAllowed', 'srcKeyChanged', k, isExternalRow(o) ? o.from : '')
|
|
236
|
+
return
|
|
237
|
+
}
|
|
214
238
|
if (!lockExternalRows || o === undefined) {
|
|
215
239
|
return
|
|
216
240
|
}
|
|
@@ -218,7 +242,7 @@ function checkExternalWrite({ keyTable, keys, r, ltdtOld, lockExternalRows = tru
|
|
|
218
242
|
if (nf !== o.from) {
|
|
219
243
|
push('saveExternalRowChanged', 'fromChanged', k, o.from)
|
|
220
244
|
}
|
|
221
|
-
else if (lockFields.some((f) =>
|
|
245
|
+
else if (lockFields.some((f) => normC(row[f], f) !== normC(o[f], f))) {
|
|
222
246
|
push('saveExternalRowChanged', 'fieldChanged', k, o.from)
|
|
223
247
|
}
|
|
224
248
|
}
|
|
@@ -262,5 +286,5 @@ function hasExternalDiff({ r, ltdtOld }) {
|
|
|
262
286
|
}
|
|
263
287
|
|
|
264
288
|
|
|
265
|
-
export { SERVER_FIELDS, ASSIGN_FIELD, isExternalRow, getLockFields, restoreExternalOrder, dropServerOnlyDiff, restoreServerFields, checkExternalWrite, hasExternalDiff }
|
|
289
|
+
export { SERVER_FIELDS, ASSIGN_FIELD, isExternalRow, getLockFields, restoreExternalOrder, dropServerOnlyDiff, restoreServerFields, checkExternalWrite, hasExternalDiff, normC }
|
|
266
290
|
export default isExternalRow
|