module.exports = createProcessingModule
const UB = require('@unitybase/ub-pub')
const uDialogs = require('../uDialogs')
const Vue = require('vue')
// eslint-disable-next-line no-unused-vars
const Vuex = require('vuex') // required to see a Vuex.d.ts
const { Notification: $notify } = require('element-ui')
const {
buildExecParams,
buildDeleteRequest,
enrichFieldList,
SET,
isEmpty,
change,
markAsTouched,
prepareCopyAddNewExecParams
} = require('./helpers')
/**
* @callback UbVuexStoreCollectionRequestBuilder
* @param {{ collection: UbVuexStoreCollectionInfo, execParams: Object, fieldList: Array<string>, item: VuexTrackedObject}} params
* @returns {object}
*/
/**
* @callback UbVuexStoreCollectionResponseHandler
* @param {{ collection: UbVuexStoreCollectionInfo, response: Object}} params
*/
/**
* @callback UbVuexStoreCollectionDeleteRequestBuilder
* @param {{ collection: UbVuexStoreCollectionInfo, item: VuexTrackedObject}} params
* @returns {object}
*/
/**
* @callback UbVuexStoreRepositoryBuilder
* @param {Vuex.Store} store
* @returns {ClientRepository}
*/
/**
* @typedef {object} UbVuexStoreCollectionInfo
* Metadata what describes a detail collection what edited on the form.
*
* @property {UbVuexStoreRepositoryBuilder} repository
* @property {boolean} lazy
* An flag, indicating that collection shall not be loaded right away, but on demand
* @property {UbVuexStoreCollectionRequestBuilder} [buildRequest]
* @property {UbVuexStoreCollectionResponseHandler} [handleResponse]
* @property {UbVuexStoreCollectionDeleteRequestBuilder} [buildDeleteRequest]
*/
/**
* Create unified object for tracking edited object state.
*
* The state consists of the following properties:
* - data: it is an object with actual (to be shown on UI) data values, regardless if values are untouched by user
* or already edited.
* - originalData: this object is initially empty, but as user starts editing, it is filled by original values, as
* they loaded from DB, so that it would be always possible to say if a certain attribute was changed or not.
* If after some editing, value returned to its original state, value is deleted from this object.
* When this object is has no attributes, we know there is nothing to save.
* - collections: this is a property for complex object, objects which consist of one master record and collection or
* multiple collections of detail records.
* Each collection tracks added, changed and deleted items, so that we know if there is any change to save
* in the collection.
* Collection item is tracked just like the master record, using the same technique -
* "data" and "originalData" properties for item. Item also has "isNew" property, indicating if item was added
* after original loading of collection or not.
* The "deleted"
*
* Also creates Vuex store object with basic processing actions:
* - isNew status for master record
* - list of pending loadings
* - master record entity information (name, fieldList, schema etc.)
* - canDelete, canSave, canRefresh getters
* - CRUD actions
*
* @param {object} pmCfg
* @param {string} pmCfg.entity Name of entity for master record
* @param {Array<string>} pmCfg.fieldList Master request fieldList. If unset will set all fields in an entity
* @param {Object<string, UbVuexStoreCollectionInfo>} pmCfg.collections Collections requests map
* @param {function(Validator?)} pmCfg.validator Validator
* @param {number} pmCfg.instanceID instanceID
* @param {object} [pmCfg.parentContext] Optional values for main instance attributes passed to addNew method
* @param {UBEntity} pmCfg.entitySchema Entity schema
* @param {Function} [pmCfg.beforeCreate] Async callback. Called (with await) before creating
* @param {Function} [pmCfg.created] Async callback. Called (with await) when data was created
* @param {Function} [pmCfg.beforeLoad] Async callback. Called (with await) before loading
* @param {Function} [pmCfg.loaded] Async callback. Called (with await) when data was loaded
* @param {Function} [pmCfg.beforeInit] Async callback that emits before init
* @param {Function} [pmCfg.inited] Async callback. Called (with await) when data is initialized
* @param {Function} [pmCfg.beforeSave] Async callback. Called (with await) before save
* @param {Function} [pmCfg.saved] Async callback. Called (with await) when data was saved, receive a method name `insert/update` as a second argument
* @param {Function} [pmCfg.beforeDelete] Async callback. Called (with await) before delete
* @param {Function} [pmCfg.deleted] Async callback. Called (with await) when data was deleted
* @param {Function} [pmCfg.beforeCopy] Async callback. Called (with await) before copy of existing record
* @param {Function} [pmCfg.copied] Async callback. Called (with await) when data was copied from existing record
* @param {Function} [pmCfg.saveNotification] Callback that can show custom save notification (instead of 'successfullySaved')
* @param {Function} [pmCfg.errorNotification] Callback that can show custom error notification (instead of UB.showErrorWindow(err) ). Accept error as parameter
* @param {Function} [pmCfg.transformSaveRequest] Function that can manipulate requests before sending to server: order, patch, etc.
* @param {boolean} [pmCfg.isCopy] Flag that used to create a new record with data of existing record
* @param {boolean} [pmCfg.isModal] Is parent opened from modal. Used to provide modal state to the child
* @returns {Vuex.StoreOptions} Vue store cfg
*/
function createProcessingModule ({
entity: masterEntityName,
fieldList,
collections: initCollectionsRequests,
validator,
instanceID,
parentContext,
entitySchema,
beforeInit,
inited,
beforeCreate,
created,
beforeLoad,
loaded,
beforeSave,
saved,
beforeDelete,
deleted,
beforeCopy,
copied,
afterValidation,
saveNotification,
errorNotification,
transformSaveRequest,
isCopy,
isModal
}) {
const autoLoadedCollections = Object.entries(initCollectionsRequests)
.filter(([, collData]) => !collData.lazy)
.map(([coll]) => coll)
const isLockable = function () { return entitySchema.hasMixin('softLock') }
return {
/**
* @type {VuexTrackedInstance}
*/
state: {
/**
* Whether master instance was loaded or it is newly created
*/
isNew: false,
/**
* Properties as they are in DB.
*/
data: {},
/**
* This contains old (originally loaded) values of updated properties.
*/
originalData: {},
/**
* Detailed collections (if any)
*/
collections: {},
/**
* result of previous lock() operation (in case softLock mixin assigned to entity)
*/
lockInfo: {},
/**
* result of needAls misc operation (in case als mixin assigned to entity)
*/
alsInfo: {},
pendings: [],
/**
* Whether master instance was copy of existing record
*/
isCopy,
isModal,
/**
* Flag, indicating that after save, user won't have access to the record anymore.
*/
isLostAccess: false,
/**
* Form services allows closing forms or change form titles.
* Always check if value is defined before use!
*/
$formServices: undefined
},
getters: {
/**
* @param {VuexTrackedInstance} state
* @returns {boolean}
*/
isDirty (state) {
if (!isEmpty(state.originalData)) {
return true
}
for (const collection of Object.values(state.collections)) {
if (collection.deleted.length) {
return true
}
for (const item of collection.items) {
if (item.isNew || !isEmpty(item.originalData)) {
return true
}
}
}
return false
},
/**
* The "loading" status
*
* @returns {boolean}
*/
loading (state) {
return state.pendings.length > 0
},
canDelete (state) {
return !state.isNew && entitySchema.haveAccessToMethod('delete')
},
canInsertOrUpdate (state, getters) {
return entitySchema.haveAccessToAnyMethods(['insert', 'update'])
},
canSave (state, getters) {
return (getters.isDirty || state.isCopy) && getters.canInsertOrUpdate
},
canRefresh (state, getters) {
return !state.isNew
},
isLocked (state) {
return !!state.lockInfo.lockExists
},
isLockedByMe (state) {
return state.lockInfo.lockExists && (state.lockInfo.lockUser === UB.connection.userLogin())
},
lockInfoMessage (state) {
if (!state.lockInfo.lockExists) {
return UB.i18n('recordNotLocked')
} else if ((state.lockInfo.lockUser === UB.connection.userLogin())) {
if (state.lockInfo.lockType === 'Temp') {
return UB.i18n('recordLockedThisUserByTempLock')
} else {
return UB.i18n('entityLockedOwn')
}
} else { // locked by another user
if (state.lockInfo.lockType === 'Temp') {
return UB.i18n('tempSoftLockInfo', state.lockInfo.lockUser)
} else {
return UB.i18n('softLockInfo', state.lockInfo.lockUser, UB.formatter.formatDate(state.lockInfo.lockTime, 'dateTimeFull'))
}
}
}
},
mutations: {
SET,
/**
* Load initial state of tracked master entity, all at once.
*
* @param {VuexTrackedInstance} state
* @param {object} loadedState
*/
LOAD_DATA (state, loadedState) {
if (loadedState) {
state.data = loadedState
} else if (!state.isLostAccess) {
// Server cannot return state when access to the record is lost,
// so throw an error only when "isLostAccess" flag is not set
throw new UB.UBAbortError('documentNotFound')
}
Vue.set(state, 'originalData', {})
},
/**
* Load initial state of tracked detail entity, all at once for specified item.
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection collection
* @param {number} payload.index index in collection
* @param {object} payload.loadedState loaded state
*/
LOAD_COLLECTION_DATA (state, { collection, index, loadedState }) {
if (!(collection in state.collections)) {
throw new Error(`Collection "${collection}" was not loaded or created!`)
}
if (!loadedState) {
throw new UB.UBAbortError('documentNotFound')
}
const collectionInstance = state.collections[collection]
if (!(index in collectionInstance.items)) {
throw new Error(`Collection "${collection}" does not have index: ${index}!`)
}
const stateToChange = collectionInstance.items[index]
stateToChange.data = loadedState
stateToChange.isNew = false
Vue.set(stateToChange, 'originalData', {})
},
/**
* After insert, update or other server calls, which update entity, need to inform module about new server state.
*
* @param {VuexTrackedInstance} state
* @param {object} loadedState
*/
LOAD_DATA_PARTIAL (state, loadedState) {
for (const [key, value] of Object.entries(loadedState)) {
change(state, key, value)
Vue.delete(state.originalData, key)
}
},
/**
* Update value of attribute for master record or a record of collection item details.
* The mutation uses "data" and "originalData" object to correctly track object state.
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} [payload.collection] Name of collection, optional
* @param {number} [payload.index] Index of item, optional, shall only be specified, if collection is specified.
* @param {string} payload.key Key of changed attribute
* @param {string} [payload.path] Path (for JSON attributes) of the value
* @param {*} payload.value Value attribute is changed to.
*/
SET_DATA (state, { collection, index, key, value, path }) {
if (typeof collection !== 'string') {
// Change the Master record
change(state, key, value, path)
return
}
// Item of a detail collection
if (!(collection in state.collections)) {
throw new Error(`Collection "${collection}" was not loaded or created!`)
}
const collectionInstance = state.collections[collection]
if (!(index in collectionInstance.items)) {
throw new Error(`Collection "${collection}" does not have index: ${index}!`)
}
change(collectionInstance.items[index], key, value, path)
},
/**
* Mark attribute as touched (without actual modification).
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.key Key of touched attribute
* @constructor
*/
MARK_AS_TOUCHED (state, { key }) {
markAsTouched(state, key)
},
/**
* Just like "SET_DATA", but assign multiple values at once passed as an object.
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {object} [payload.collection] optional collection (if not passed update master store)
* @param {object} [payload.index] optional collection item index. required in case collection is passed
* @param {object} payload.data
* @param {object} payload.loadedState Deprecated!!! Use "data" instead
*/
ASSIGN_DATA (state, { collection, index, data, loadedState }) {
if (!data && loadedState) {
data = loadedState
}
let stateToChange
if (collection) {
if (!(collection in state.collections)) {
throw new Error(`Collection "${collection}" was not loaded or created!`)
}
const collectionInstance = state.collections[collection]
if (!(index in collectionInstance.items)) {
throw new Error(`Collection "${collection}" does not have index: ${index}!`)
}
stateToChange = collectionInstance.items[index]
} else {
stateToChange = state
}
for (const [key, value] of Object.entries(data)) {
change(stateToChange, key, value)
}
},
/**
* Set original state of collection items
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection
* @param {VuexTrackedObject[]} payload.items
* @param {string} payload.entity
*/
LOAD_COLLECTION (state, { collection, items: itemStates, entity }) {
if (!itemStates) {
throw new UB.UBAbortError('documentNotFound')
}
const items = itemStates.map(item => ({
data: item,
originalData: {}
}))
const collectionObj = { items, deleted: [], key: collection, entity }
Vue.set(state.collections, collection, collectionObj)
},
/**
* Update collection data.
* Removed originalData for props which updated
* Remove isNew status.
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection collection
* @param {number} payload.index index in collection
* @param {object} payload.loadedState loaded state
*/
LOAD_COLLECTION_PARTIAL (state, { collection, index, loadedState }) {
const collectionInstance = state.collections[collection]
for (const [key, value] of Object.entries(loadedState)) {
change(collectionInstance.items[index], key, value)
Vue.delete(collectionInstance.items[index].originalData, key)
collectionInstance.items[index].isNew = false
}
},
/**
* Add a new item to a collection. Added item is marked as "isNew".
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection Collection name
* @param {object} payload.item Item state (a regular JS object)
*/
ADD_COLLECTION_ITEM (state, { collection, item: itemState }) {
if (!(collection in state.collections)) {
// Lazy create collection
Vue.set(state.collections, collection, { items: [], deleted: [] })
}
state.collections[collection].items.push({ data: itemState, originalData: {}, isNew: true })
},
/**
* Remove an item from a collection.
* If remove an added item, no need to track the deletion.
* If remove originally loaded record, remember the
* deletion to track it as a change.
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection Collection name
* @param {number} payload.index Index of item inside a collection to remove
*/
DELETE_COLLECTION_ITEM (state, { collection, index }) {
if (collection in state.collections) {
const removedItem = state.collections[collection].items.splice(index, 1)[0]
if (removedItem && !removedItem.isNew) {
state.collections[collection].deleted.push(removedItem)
}
}
},
/**
* Remove an item from a collection and do NOT track its deletion.
* Could be useful for entities deleted by cascade
*
* @param {VuexTrackedInstance} state
* @param {object} payload
* @param {string} payload.collection Collection name
* @param {number} payload.index Index of item inside a collection to remove
*/
DELETE_COLLECTION_ITEM_WITHOUT_TRACKING (state, { collection, index }) {
if (collection in state.collections) {
state.collections[collection].items.splice(index, 1)
}
},
/**
* Clear deleted items in all collections, after sending removal requests
*
* @param {VuexTrackedInstance} state
*/
CLEAR_ALL_DELETED_ITEMS (state) {
for (const collection of Object.keys(state.collections)) {
Vue.set(state.collections[collection], 'deleted', [])
}
},
/**
* Remove all items from a collection.
* If remove an added item, no need to track the deletion.
* If remove originally loaded record, remember the
* deletion to track it as a change.
*
* @param {object} state
* @param {string} collectionName Name of collection
*/
DELETE_ALL_COLLECTION_ITEMS (state, collectionName) {
if (collectionName in state.collections) {
const collection = state.collections[collectionName]
const deleted = collection.items
.splice(0, collection.items.length)
.filter(i => !i.isNew)
collection.deleted.push(...deleted)
}
},
/**
* Set "IsNew" flag for the master record.
*
* @param {Vuex.state} state
* @param {boolean} isNew
*/
IS_NEW (state, isNew) {
state.isNew = isNew
},
/**
* Set "IsCopy" flag.
*
* @param {Vuex.state} state
* @param {boolean} isCopy
*/
IS_COPY (state, isCopy) {
state.isCopy = isCopy
},
/**
* Add or delete loading pending for some action
*
* @param {object} state
* @param {object} payload
* @param {boolean} payload.isLoading add/remove action from pending
* @param {string} payload.target name of pending action
*/
LOADING (state, { isLoading, target }) {
const index = state.pendings.indexOf(target)
if (isLoading) {
if (index === -1) {
state.pendings.push(target)
}
} else {
if (index !== -1) {
state.pendings.splice(index, 1)
}
}
},
/**
* Add info about als to store state.
*
* @param {object} state
* @param {object} resultAls - result of `alsNeed` misc in repository
*/
SET_ALS_INFO (state, resultAls) {
state.alsInfo = resultAls
},
/**
* Set "lost access" flag value, which used on "save" action.
*
* @param {object} state
* @param {boolean} isLostAccess
*/
LOST_ACCESS (state, isLostAccess) {
state.isLostAccess = isLostAccess
},
/**
* Set the $formServices, so that store might accomplish actions like force form closing.
*
* @param {object} state
* @param {object} $formServices
*/
SET_FORM_SERVICES (state, $formServices) {
state.$formServices = $formServices
}
},
actions: {
/**
* Initialize store:
* - sets isNew
* - creates empty collections which passed on init processing module
* - dispatch `create` or `load` action
* @param {Vuex.Store} store
*/
async init (store) {
for (const [key, collection] of Object.entries(initCollectionsRequests)) {
store.commit('LOAD_COLLECTION', {
collection: key,
items: [],
entity: collection.repository(store).entityName
})
}
if (beforeInit) {
await beforeInit(store)
}
store.commit('IS_NEW', !instanceID || store.state.isCopy)
if (store.state.isCopy) {
await store.dispatch('copyExisting')
} else if (store.state.isNew) {
await store.dispatch('create')
} else {
await store.dispatch('loadWithCollections', {
collectionKeys: autoLoadedCollections
})
}
if (inited) {
await inited(store)
}
},
/**
* Send add new request and load to instance props
* that are response by the server
* @param {Vuex.Store} store
*/
async create ({ commit }) {
if (beforeCreate) {
await beforeCreate()
}
commit('LOADING', {
isLoading: true,
target: 'create'
})
const data = await UB.connection.addNewAsObject({
entity: masterEntityName,
fieldList,
execParams: parentContext
})
commit('ASSIGN_DATA', { data })
if (created) {
await created()
}
commit('LOADING', {
isLoading: false,
target: 'create'
})
},
/**
* Load instance data by record ID or newInstanceID in case this record is just created
*
* @param {Vuex.Store} store
* @param {number} [newInstanceID] optional row id to load. If omitted instanceID will be used
*/
async load ({ commit }, newInstanceID) {
commit('LOADING', {
isLoading: true,
target: 'loadMaster'
})
const repo = UB.connection
.Repository(masterEntityName)
.attrs(fieldList)
.misc({
ID: instanceID || newInstanceID, // Add top level ID to bypass caching, soft deletion and history
__openForm: true
})
.miscIf(isLockable(), { lockType: 'None' }) // get lock info
.miscIf(entitySchema.hasMixin('als'), { alsNeed: true }) // get als info
const data = await repo.selectById(instanceID || newInstanceID)
commit('LOAD_DATA', data)
if (isLockable()) {
const rl = repo.rawResult.resultLock
commit('SET', { // TODO - create mutation SET_LOCK_RESULT
key: 'lockInfo',
value: rl.success
? rl.lockInfo
: { // normalize response - ub api is ugly here
lockExists: true,
lockType: rl.lockType,
lockUser: rl.lockUser,
lockTime: rl.lockTime,
lockValue: rl.lockInfo.lockValue
}
})
}
if (entitySchema.hasMixin('als')) {
commit('SET_ALS_INFO', repo.rawResult.resultAls)
}
commit('LOADING', {
isLoading: false,
target: 'loadMaster'
})
},
/**
* Check if record not new
* then check if collections inited when processing module is created
* then fetch data from server for each collection
*
* @param {Vuex.Store} store
* @param {string[]} collectionKeys Collections keys
*/
async loadCollections (store, collectionKeys) {
if (store.state.isNew) {
return
}
for (const key of collectionKeys) {
const inCollection = key in initCollectionsRequests
if (!inCollection) {
console.error(`${key} not included in the collections, please check initCollectionsRequests param`)
return
}
}
store.commit('LOADING', {
isLoading: true,
target: 'loadCollections'
})
const collectionsData = await Promise.all(
collectionKeys.map(key => {
const req = initCollectionsRequests[key].repository(store)
req.fieldList = enrichFieldList(
UB.connection.domain.get(req.entityName),
req.fieldList,
['ID', 'mi_modifyDate', 'mi_createDate']
)
return req.select()
})
)
collectionsData.forEach((collectionData, index) => {
const collection = collectionKeys[index]
store.commit('LOAD_COLLECTION', {
collection,
items: collectionData,
entity: initCollectionsRequests[collection].repository(store).entityName
})
})
store.commit('LOADING', {
isLoading: false,
target: 'loadCollections'
})
},
/**
* Load instance data by record ID or newInstanceID and load collections by collectionKeys
*
* @param {Vuex.Store} store
* @param {object} payload
* @param {object} payload.collectionKeys Collections keys
* @param {string} [payload.newInstanceID] optional row id to load. If omitted instanceID will be used
*/
async loadWithCollections ({ dispatch }, payload) {
const { collectionKeys, newInstanceID } = payload
if (beforeLoad) {
await beforeLoad()
}
await dispatch('load', newInstanceID)
await dispatch('loadCollections', collectionKeys)
if (loaded) {
await loaded()
}
},
/**
* Create copy of master record and all collections
*
* @param {Vuex.Store} store
* @returns {Promise<void>}
*/
async copyExisting (store) {
const collections = Object.keys(initCollectionsRequests)
store.commit('LOADING', {
isLoading: true,
target: 'createCopy'
})
if (beforeCopy) {
await beforeCopy()
}
// load master record
const copiedRecord = await UB.connection
.Repository(masterEntityName)
.attrs(
fieldList
.filter(attrCode => {
// exclude UB attributes with dataType 'Document'
const attr = entitySchema.getEntityAttribute(attrCode)
if (attr) {
return attr.dataType !== UB.connection.domain.ubDataTypes.Document
}
return true
})
)
.selectById(instanceID)
store.commit('LOAD_DATA', copiedRecord) // need for load collections because collections maps to data of master record
// load collections
const collectionsResponse = await Promise.all(
collections.map(collectionKey => {
const collectionDefinition = initCollectionsRequests[collectionKey]
const req = collectionDefinition.repository(store)
req.fieldList = enrichFieldList(
UB.connection.domain.get(req.entityName),
req.fieldList,
['ID', 'mi_modifyDate', 'mi_createDate']
)
return req.select()
})
)
const newRecord = await UB.connection.addNewAsObject({
entity: masterEntityName,
fieldList,
execParams: prepareCopyAddNewExecParams(copiedRecord, masterEntityName)
})
store.commit('LOAD_DATA', newRecord)
await Promise.all(
collectionsResponse.flatMap((collectionData, index) => {
const collection = collections[index]
const collectionDefinition = initCollectionsRequests[collection]
const entityName = collectionDefinition.repository(store).entityName
return collectionData.map(collectionItem => {
const execParams = { ...collectionItem }
delete execParams.ID
// get attributes that point to the master entity record
UB.connection.domain.get(entityName).eachAttribute(attr => {
if (
(attr.associatedEntity === masterEntityName) &&
(execParams[attr.code] === copiedRecord.ID)
) {
// replace associated attributes for current entity
execParams[attr.code] = newRecord.ID
}
})
return store.dispatch('addCollectionItem', {
collection,
execParams
})
})
})
)
if (copied) {
const copiedCollectionsIds = Object.fromEntries(
collections.map((collectionKey, index) => [collectionKey, collectionsResponse[index].map(i => i.ID)])
)
await copied(copiedRecord.ID, copiedCollectionsIds)
}
store.commit('LOADING', {
isLoading: false,
target: 'createCopy'
})
},
/**
* Check validation then
* build requests for master and collections records
*
* @param {Vuex.Store} store
* @param {Function} [closeForm]
* For using action in the "Save and Close" actions, pass the function, which will close the form
* @returns {Promise<void>}
*/
async save (store, closeForm) {
if (!store.state.isNew && !store.getters.isDirty) {
return
}
if (beforeSave) {
const answer = await beforeSave()
if (answer === false) {
return -1
}
}
if (validator()) {
validator().validateForm()
}
store.commit('LOADING', {
isLoading: true,
target: 'save'
})
if (afterValidation) {
// This stands initially for let Blob editors upload data into temporary storage
// just after successful validation, before actual saving
await afterValidation()
}
let requests = []
const masterExecParams = buildExecParams(store.state, masterEntityName)
const method = store.state.isNew ? 'insert' : 'update'
if (masterExecParams) {
const ubql = {
entity: masterEntityName,
method,
execParams: masterExecParams,
fieldList
}
if (entitySchema.hasMixin('als')) {
ubql.alsNeed = true
}
const handler = response => store.commit('LOAD_DATA', response.resultData)
requests.push({ ubql, handler })
}
// Iterate in reverse order to delete child before master in case of master-detail relation between
// collections definition should be ordered from master to details (as documented in createProcessingModule)
for (const [collectionKey, collectionInfo] of Object.entries(initCollectionsRequests).reverse()) {
const collection = store.state.collections[collectionKey]
if (!collection) continue
const req = collectionInfo.repository(store)
const collectionEntityName = req.entityName
for (const deletedItem of collection.deleted || []) {
const ubql = typeof collectionInfo.buildDeleteRequest === 'function'
? collectionInfo.buildDeleteRequest({ ...store, collection, item: deletedItem })
: buildDeleteRequest(collectionEntityName, deletedItem.data.ID)
// Deleted items are cleared all at once using CLEAR_ALL_DELETED_ITEMS mutation
const handler = () => {}
requests.push({ ubql, handler })
}
}
for (const [collectionKey, collectionInfo] of Object.entries(initCollectionsRequests)) {
const collection = store.state.collections[collectionKey]
if (!collection) continue
const req = collectionInfo.repository(store)
const collectionEntityName = req.entityName
const collectionFieldList = enrichFieldList(
UB.connection.domain.get(collectionEntityName),
req.fieldList,
['ID', 'mi_modifyDate', 'mi_createDate']
)
for (const item of collection.items || []) {
const execParams = buildExecParams(item, collectionEntityName)
if (execParams) {
const ubql = typeof collectionInfo.buildRequest === 'function'
? collectionInfo.buildRequest({
...store,
collection,
execParams,
fieldList: collectionFieldList,
item
})
: {
entity: collectionEntityName,
method: item.isNew ? 'insert' : 'update',
execParams,
fieldList: collectionFieldList
}
const handler = response => {
const loadedState = response.resultData
if (loadedState) {
if (typeof collectionInfo.handleResponse === 'function') {
collectionInfo.handleResponse({ ...store, collection, response })
} else if (Number.isInteger(loadedState.ID)) {
const index = collection.items.findIndex(i => i.data.ID === loadedState.ID)
if (index !== -1) {
store.commit('LOAD_COLLECTION_DATA', {
collection: collectionKey,
index,
loadedState
})
}
}
}
}
requests.push({ ubql, handler })
}
}
}
try {
if (typeof transformSaveRequest === 'function') {
requests = transformSaveRequest(requests)
}
const responses = await UB.connection.runTransAsObject(requests.map(r => r.ubql))
const responseHandlers = requests.map(r => r.handler)
for (let i = 0, count = Math.min(responses.length, responseHandlers.length); i < count; i++) {
const response = responses[i]
const responseHandler = responseHandlers[i]
responseHandler(response)
if (response?.entity === masterEntityName && response.resultAls) {
store.commit('SET_ALS_INFO', response.resultAls)
}
}
store.commit('CLEAR_ALL_DELETED_ITEMS')
for (const response of responses) {
UB.connection.emitEntityChanged(response.entity, response)
}
if (store.state.isNew) {
store.commit('IS_NEW', false)
}
if (store.state.isCopy) {
store.commit('IS_COPY', false)
}
if (typeof saveNotification === 'function') {
saveNotification()
} else {
$notify.success(UB.i18n('successfullySaved'))
}
if (store.state.isLostAccess && store.state.$formServices) {
store.state.$formServices.forceClose()
// After lost access, record is not visible for the user, so
// notification "delete" is required, so that forms with tables know they should remove the row
UB.connection.emitEntityChanged(masterEntityName, {
entity: masterEntityName,
method: 'delete',
resultData: { ID: store.state.data.ID }
})
}
if (saved) {
await saved(method)
}
if (closeForm) {
closeForm()
}
} catch (err) {
if (typeof errorNotification === 'function') {
errorNotification(err)
} else {
UB.showErrorWindow(err)
throw new UB.UBAbortError(err)
}
} finally {
store.commit('LOADING', {
isLoading: false,
target: 'save'
})
}
},
/**
* Send reload request for master record and all collections record that already loaded by `loadCollections` action
*
* In case form dirty - show confirmation dialog for loosing changes
* @fires entity_name:refresh
*
* @param {object} [options]
* @param {boolean} options.skipNotify Do not show notification message on refresh operation
*/
async refresh ({ state, getters, commit, dispatch }, options) {
if (getters.isDirty) {
const result = await uDialogs.dialogYesNo('refresh', 'formWasChanged')
if (!result) return
}
commit('LOADING', {
isLoading: true,
target: 'master'
})
await dispatch('loadWithCollections', {
newInstanceID: state.data.ID,
collectionKeys: Object.keys(state.collections)
})
commit('LOADING', {
isLoading: false,
target: 'master'
})
if (validator()) {
validator().reset()
}
/**
* Fires just after form is refreshed using `processing.refresh()`
* @example
// @param {THTTPRequest} req
UB.connection.on('uba_user:refresh', function (data) {
console.log(`Someone call refresh for User with ID ${data.ID}`
})
* @event entity_name:refresh
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
* @param {object} payload
* @param {number} payload.ID and ID of entity_name instance what refreshed
*/
UB.connection.emit(`${masterEntityName}:refresh`, { ID: state.data.ID })
if (!options?.skipNotify) {
$notify.success(UB.i18n('formWasRefreshed'))
}
},
/**
* Asks for user confirmation and sends delete request for master record
*
* @param {Vuex.Store} store
* @param {Function} closeForm Close form without confirmation
*/
async deleteInstance ({ state, getters, commit }, closeForm = () => {}) {
if (beforeDelete) {
const answer = await beforeDelete()
if (answer === false) {
return
}
}
const answer = await uDialogs.dialogDeleteRecord(masterEntityName, state.data)
if (answer) {
commit('LOADING', {
isLoading: true,
target: 'delete'
})
try {
await UB.connection.doDelete({
entity: masterEntityName,
execParams: { ID: state.data.ID }
})
UB.connection.emitEntityChanged(masterEntityName, {
entity: masterEntityName,
method: 'delete',
resultData: { ID: state.data.ID }
})
closeForm()
$notify.success(UB.i18n('recordDeletedSuccessfully'))
if (deleted) {
await deleted()
}
} catch (err) {
UB.showErrorWindow(err)
} finally {
commit('LOADING', {
isLoading: false,
target: 'delete'
})
}
}
},
/**
* Sends addNew request then fetch default params and push it in collection
*
* @param {Vuex.Store} store
* @param {object} payload
* @param {string} payload.collection Collection name
* @param {object} payload.execParams if we need to create new item with specified params
*/
async addCollectionItem (store, { collection, execParams }) {
const repo = initCollectionsRequests[collection].repository(store)
const entity = repo.entityName
const fieldList = repo.fieldList
const item = await UB.connection.addNewAsObject({
entity,
fieldList,
execParams
})
store.commit('ADD_COLLECTION_ITEM', { collection, item })
},
/**
* Sends addNew request without fetching default params and push it in collection
*
* @param {Store} store
* @param {object} payload
* @param {string} payload.collection Collection name
* @param {object} payload.execParams if we need to create new item with specified params
*/
async addCollectionItemWithoutDefaultValues (store, { collection, execParams }) {
const { commit } = store
const repo = initCollectionsRequests[collection].repository(store)
const entity = repo.entityName
const { ID } = await UB.connection.addNewAsObject({
entity,
fieldList: ['ID']
})
commit('ADD_COLLECTION_ITEM', { collection, item: { ID, ...execParams } })
},
/**
* Lock entity. Applicable for entities with "softLock" mixin
* @param {Vuex.Store} store
* @param {boolean} [persistentLock=false] Lock with persistent locking type
* @return {Promise<void>}
*/
lockEntity ({ state, commit }, persistentLock = false) {
return UB.connection.query({
entity: masterEntityName,
method: 'lock',
lockType: persistentLock ? 'Persist' : 'Temp',
ID: state.data.ID
}).then(resp => {
const resultLock = resp.resultLock
if (resultLock.success) {
commit('SET', { // TODO - create mutation SET_LOCK_RESULT
key: 'lockInfo',
value: { ...resultLock.lockInfo, ownLock: resultLock.ownLock }
})
$notify.success(UB.i18n('lockSuccessCreated'))
} else {
return uDialogs.dialogError(UB.i18n('softLockInfo', resultLock.lockInfo.lockUser, UB.formatter.formatDate(resultLock.lockInfo.lockTime, 'dateTimeFull')))
}
}).catch(e => {
UB.showErrorWindow(e)
})
},
/**
* Unlock entity. Applicable for entities with "softLock" mixin
* @param {Vuex.Store} store
* @return {Promise<void>}
*/
unlockEntity ({ state, commit }) {
return UB.connection.query({
entity: masterEntityName,
method: 'unlock',
lockType: state.lockInfo.lockType,
lockID: state.lockInfo.lockValue // MPV - why not lockID ?
}).then(resp => {
if (resp.resultLock.success) {
commit('SET', { // TODO - create mutation SET_LOCK_RESULT
key: 'lockInfo',
value: {}
})
$notify.success(UB.i18n('lockSuccessDeleted'))
}
}).catch(e => {
UB.showErrorWindow(e)
})
},
/**
* Get lock information. Applicable for entities with "softLock" mixin
* @param {Vuex.Store} store
* @return {Promise<void>}
*/
retrieveLockInfo ({ state, commit }) {
return UB.connection.query({
entity: masterEntityName,
method: 'isLocked',
ID: state.data.ID
}).then(resp => {
commit('SET', { // TODO - create mutation SET_LOCK_RESULT
key: 'lockInfo',
value: resp.lockInfo.isLocked ? resp.lockInfo : {}
})
}).catch(e => {
UB.showErrorWindow(e)
})
}
}
}
}