/**
* @module @unitybase/adminui-vue
*/
/* global SystemJS, Ext, $App */
const _ = require('lodash')
const UB = require('@unitybase/ub-pub')
// vuelidate internally use process.env.BUILD === 'web'
window.process = {
env: { BUILD: 'web' }
}
const IS_SYSTEM_JS = (typeof SystemJS !== 'undefined')
const isExt = (typeof window.Ext !== 'undefined')
/*
* The BOUNDLED_BY_WEBPACK variable is available only when a project is being built by a webpack.
* But not available in dev mode.
* Please note that BOUNDLED_BY_WEBPACK and window.BOUNDLED_BY_WEBPACK is not the same
* But if BOUNDLED_BY_WEBPACK is undefined app will use window.BOUNDLED_BY_WEBPACK
*/
window.BOUNDLED_BY_WEBPACK = false
// ------------- throttle-debounce --------------------
const throttleDebounce = require('throttle-debounce')
if (IS_SYSTEM_JS && !SystemJS.has('throttle-debounce')) SystemJS.set('throttle-debounce', SystemJS.newModule(throttleDebounce))
/**
* throttle-debounce see <a href=https://github.com/niksy/throttle-debounce>original doc</a>
* @type {{throttle: function, debounce: function}}
*/
module.exports.throttleDebounce = throttleDebounce
const Form = require('./utils/Form/Form')
/**
* Creates a new instance of UI module. See {@link module:Form.Form}
*/
module.exports.Form = Form.Form
const formHelpers = require('./utils/Form/helpers')
/**
* Helper functions for forms. See {@link module:formHelpers}.
* `mapInstanceFields` and `computedVuex` are aliased into `@unitybase/adminui-vue`
* @type {module:formHelpers}
*/
module.exports.formHelpers = formHelpers
const escapeHtml = require('./utils/escapeHtml')
/**
* Escape special HTML characters in the given string of text.
*
* @param {string} string The string to escape for inserting into HTML
* @returns {string}
* @function
*/
module.exports.escapeHtml = escapeHtml
/**
* See {@link module:formHelpers.mapInstanceFields}
*/
module.exports.mapInstanceFields = formHelpers.mapInstanceFields
/**
* See {@link module:formHelpers.computedVuex}
*/
module.exports.computedVuex = formHelpers.computedVuex
module.exports.SET = formHelpers.SET
/**
* Mount a Vue based component as a navbar tab, a modal form or inside other component (as a container).
* See {@link module:mountUtils mountUtils} module documentation for samples.
*
* @type {module:mountUtils}
*/
module.exports.mountUtils = require('./utils/Form/mount')
/**
* Open files using WebDav protocol (require @ub-e/web-daw to be added into application)
*
* @type {module:webDav}
*/
module.exports.webDav = require('./utils/webDav')
const magicLink = require('./utils/magicLinks')
/**
* MagicLinks instance. adminui-vue registers the following commands (using addCommand):
* - showList: runs an $App.doCommand({cmdType: 'showList', ...}
* - showForm: runs an $App.doCommand({cmdType: 'showForm', ...}
* - showReport: runs an $App.doCommand({cmdType: 'showReport', ...}
* - setFocus: sets a focus to specified HTML element
*
* Usage of setFocus: `<a href="#" data-cmd-type="setFocus" data-elm-id="my-html-element-id">focus other</a>`
*
* For usage examples for showList/Form/Repost see {@link module:magicLinks magicLinks} module documentation
*
* @type {module:magicLinks}
*/
module.exports.magicLink = magicLink
magicLink.install()
magicLink.addCommand('setFocus', magicLinkFocusCommand)
// -------------- Vue --------------------
if (window.Vue === undefined) {
window.Vue = require('vue')
}
const Vue = window.Vue
// next 2 lines for modules what use ES6 import `import Vue from 'vue' (not recommended for use)
Vue.__useDefault = Vue
Vue.default = Vue
if (IS_SYSTEM_JS && !SystemJS.has('vue')) SystemJS.set('vue', SystemJS.newModule(Vue))
// ------------- Vuex ------------------
const Vuex = require('vuex')
/** type {Vuex} */
window.Vuex = Vuex
// next 2 lines for modules what use ES6 import `import Vuex from 'vuex' (not recommended for use)
Vuex.__useDefault = Vuex
Vuex.default = Vuex
if (IS_SYSTEM_JS && !SystemJS.has('vuex')) SystemJS.set('vuex', SystemJS.newModule(Vuex))
Vue.use(Vuex)
// ------------ ElementUI ------------------
const ElementUI = require('element-ui') // adminui-pub maps element-ui -> element-ui/lib/index.js for SystemJS
window.ElementUI = ElementUI
if (IS_SYSTEM_JS && !SystemJS.has('element-ui')) SystemJS.set('element-ui', SystemJS.newModule(ElementUI))
Vue.use(ElementUI, {
size: 'small', // set element-ui default size
i18n: UB.i18n.bind(UB), // redirect ElementUI localization to UB.i18n
zIndex: 300000 // lat's Vue popovers always be above Ext
})
// ------------- Moment -------------------
const momentPlugin = require('./utils/moment-plugin')
Vue.use(momentPlugin)
// ------------- UB theme -----------------
require('normalize.css/normalize.css')
require('./dist/fonts.css')
require('./theme/icons/ub-icons.css')
require('./theme/ub-body.css')
if (BOUNDLED_BY_WEBPACK) {
// webpack MiniCssExtractPlugin extract all styles (for vue SFC), so we need to inject dist/adminui-vue.css
UB.inject('/clientRequire/@unitybase/adminui-vue/dist/adminui-vue.min.css').catch(e => {})
}
Vue.use(UB)
// ----------- UbComponents ----------------------
const UbComponents = require('./utils/install-ub-components')
Vue.use(UbComponents)
// ---------- Vuelidate ---------------------------
const Vuelidate = require('vuelidate').default
if (IS_SYSTEM_JS && !SystemJS.has('vuelidate')) SystemJS.set('vuelidate', SystemJS.newModule(Vuelidate))
Vue.use(Vuelidate)
const { validationMixin } = require('./utils/Form/validation')
/**
* Mixin for using in forms with own single-form validation. Mixin automatically creates
* and passes a validator instance for use in nested controls (UFormRow for example).
*/
module.exports.validationMixin = validationMixin
const clickOutsideDropdownMixin = require('./components/controls/mixins/clickOutsideDropdown')
/**
* Mixin for `USelectEntity`/`USelectMultiple` like components to close options dropdown
* on click outside
*/
module.exports.clickOutsideDropdownMixin = clickOutsideDropdownMixin
const filterTemplateMixin = require('./components/UTableEntity/filter-templates/mixinForFilter')
/**
* Mixin for reusing common logic needed for registering custom `UTableEntity` filter templates components
*/
module.exports.filterTemplateMixin = filterTemplateMixin
// ------------------ uDialogs -----------------
const uDialogs = require('./utils/uDialogs')
/**
* Modal uDialogs (message boxes) for showing errors, information and confirmation
* For usage examples see {@link module:uDialogs uDialogs} module documentation
*
* @type {module:uDialogs}
*/
module.exports.uDialogs = uDialogs
/**
* Show modal dialog with 3 optional button and text/html content, see {@link module:uDialogs.dialog uDialogs.dialog}
* @type {uDialogs.dialog}
*/
module.exports.dialog = uDialogs.dialog
/**
* Error dialog, see {@link module:uDialogs.dialogError uDialogs.dialogError}
* @type {uDialogs.dialogError}
*/
module.exports.dialogError = uDialogs.dialogError
/**
* Information dialog, see {@link module:uDialogs.dialogInfo uDialogs.dialogInfo}
* @type {uDialogs.dialogInfo}
*/
module.exports.dialogInfo = uDialogs.dialogInfo
/**
* Confirmation dialog, see {@link module:uDialogs.dialogYesNo uDialogs.dialogYesNo}
* @type {uDialogs.dialogYesNo}
*/
module.exports.dialogYesNo = uDialogs.dialogYesNo
/**
* Error reporter dialog, see {@link module:uDialogs.errorReporter uDialogs.errorReporter}
* @type {uDialogs.errorReporter}
*/
module.exports.errorReporter = uDialogs.errorReporter
// add $dialog* to Vue prototype
Vue.use(uDialogs)
UB.setErrorReporter(uDialogs.errorReporter)
// ---------------- lookups --------------------
const lookups = require('./utils/lookups')
/**
* A reactive (in terms of Vue reactivity) entities data cache.
* See examples in {@link module:lookups lookups} module documentation
* @type {module:lookups}
*/
module.exports.lookups = lookups
Vue.use(lookups)
// ---------------- fileActions --------------------
const fileActions = require('./components/controls/UFile/helpers/file-actions')
/**
* Helper functions for creating files in a specific way: with dictaphone or webcam help
*
* @type {module:fileActions}
*/
module.exports.fileActions = fileActions
// ----------- UI Settings Storage -----------
const uiSettingsStorage = require('./utils/uiSettingsStorage')
/**
* Storage for User Interface settings. Wrapper around `localStorage`
* @example
* // inside vue can be used as this.$uiSettings
* this.videoRatio = this.$uiSettings.get('UFileWebcamButton', 'videoRatio') ?? this.videoRatios[0]
* this.$uiSettings.put(this.videoRatios[0], 'UFileWebcamButton', 'videoRatio')
*
* // or from adminui-vue exports
* const App = require('@unitybase/adminui-vue')
* const isCollapsed = App.uiSettings.get('sidebar', 'isCollapsed')
* @type {module:uiSettings}
*/
module.exports.uiSettings = uiSettingsStorage
Vue.prototype.$uiSettings = uiSettingsStorage
// ---------------- Column Templates ------------------
/**
* The module provides column settings, cell templates,s and filter templates by
* UB data types or by the `customSettings.columnTemplate` value. Different types
* can have the same templates or settings.
*
* Entity attributes with `Text`, `BLOB`, `TimeLog` dataTypes do not have a default
* render template. If you need to render attributes values with these data types,
* register a custom column template for them or use column slots. You should decide
* to display this column type with great caution because this column can create large
* server requests
*
* @type {module:columnTemplates}
*/
module.exports.columnTemplates = require('./components/UTableEntity/column-template-provider')
if (isExt) {
const {
replaceExtJSDialogs,
replaceExtJSNavbar,
replaceExtJSMessageBarDialog,
replaceShowList
} = require('./utils/replaceExtJSWidgets')
$App.on('applicationReady', () => {
replaceExtJSDialogs()
replaceExtJSNavbar()
replaceExtJSMessageBarDialog()
replaceShowList()
})
UB.connection.on('ubm_navshortcut:changed', (execParams) => {
if (execParams && execParams.method !== 'delete') {
UB.core.UBStoreManager.updateNavshortcutCacheForItem(execParams.resultData, false)
}
})
}
const Sidebar = require('./components/sidebar/USidebar.vue').default
function addVueSidebar () {
const SidebarConstructor = Vue.extend(Sidebar)
// eslint-disable-next-line no-new
return new SidebarConstructor({
el: '#sidebar-placeholder'
})
}
module.exports.SidebarInstance = null
// Relogin.created calls UB.connection.setRequestAuthParamsFunction to define dialog for auth
const Relogin = require('./components/relogin/URelogin.vue').default
function replaceDefaultRelogin () {
const ReloginConstructor = Vue.extend(Relogin)
const instance = new ReloginConstructor()
const vm = instance.$mount()
document.body.appendChild(vm.$el)
}
// Request2fa.created calls UB.connection.setRequest2faFunction to define dialog for 2fa
const Request2fa = require('./components/relogin/URequest2fa.vue').default
function replaceDefaultRequest2fa () {
const Request2faConstructor = Vue.extend(Request2fa)
const instance = new Request2faConstructor()
const vm = instance.$mount()
document.body.appendChild(vm.$el)
}
function magicLinkAdminUiCommand (params) {
$App.doCommand(params)
}
/**
* Magic link to focus DOM/Ext element with specified id
* @example
<a href="#" data-cmd-type="setFocus" data-elm-id="my-html-element-id">focus other</a>
* @param {Object} params
* @param {string} params.elmId
* @param {EventTarget} target
*/
function magicLinkFocusCommand (params, target) {
const extCmp = isExt && Ext.getCmp(params.elmId)
if (extCmp) { // try Ext
Ext.callback(extCmp.focus, extCmp, [], 100)
} else { // try DOM
const domElm = document.getElementById(params.elmId)
if (domElm && domElm.focus) domElm.focus()
}
}
if (window.$App) {
magicLink.addCommand('showForm', magicLinkAdminUiCommand)
magicLink.addCommand('showList', magicLinkAdminUiCommand)
magicLink.addCommand('showReport', magicLinkAdminUiCommand)
window.$App.on('applicationReady', () => {
replaceDefaultRelogin()
replaceDefaultRequest2fa()
module.exports.SidebarInstance = addVueSidebar()
const UNavbarDefaultSlot = require('./components/navbarSlotDefault/UNavbarDefaultSlot.vue').default
/**
* Additional components can be added to the Sidebar and NavBar using this event
*
* @example
* window.$App.on('applicationReady', () => {
* const SidebarSlotExample = require('./samples/SidebarSlotExample.vue').default
* $App.fireEvent('portal:sidebar:defineSlot', SidebarSlotExample, { some attrs })
*
* const NavBarSlotExample = require('./samples/NavbarSlotExample.vue').default
* $App.fireEvent('portal:navbar:defineSlot', NavBarSlotExample, { some attrs })
* }
* @event 'portal:navbar:defineSlot'
*/
$App.fireEvent('portal:navbar:defineSlot', UNavbarDefaultSlot, {})
})
const loaderService = require('./utils/loaderService')
loaderService.subscribe()
}
if (isExt && window.$App && $App.connection.appConfig.uiSettings.adminUI.vueAutoForms) {
UB.core.UBCommand.showAutoForm = require('./utils/replaceExtJSWidgets').replaceAutoForms
}
/**
* Create fake (hidden) message and return it zIndex
* This hack is required to obtain current ElementUI zIndex
*/
Vue.prototype.$zIndex = () => {
const vm = Vue.prototype.$message({
customClass: 'ub-fake-notification'
})
return vm.$el.style.zIndex
}
Vue.config.warnHandler = (err, vm, trace) => {
// TODO - remove this hack when element-ui fixes https://github.com/ElemeFE/element/issues/21905
if (err && err.indexOf('"placement"') >= 0) return // mute ElDatePicker placement prop mutation
console.error(err, vm, trace)
const newErrText = '<b>THIS MESSAGE APPEARS ONLY IN DEBUG BUILD</b><br>' + err
window.onerror.apply(UB, [newErrText, trace, '', '', new UB.UBError(newErrText, trace)])
}
Vue.config.errorHandler = function (err, vm, trace) {
console.error(err, vm, trace)
window.onerror.apply(UB, ['', trace, '', '', err])
}
/**
* @deprecated Use $UB.formatter instead
* @type {{formatDate:function, formatNumber:function, setLang2LocaleHook:function, datePatterns: string[], numberPatterns: string[], setDefaultLang: function, collationCompare:function}}
*/
Vue.prototype.$formatByPattern = UB.formatter
/**
* Define custom merging strategy for the `validations` and `attributeCaptions` options.
* This allows reusing some code in `Form.validation()` for different forms.
* Now you can use mixins here with partial validations and not define validation
* for entity attributes with `notNull = true` that are defined by default
*/
Vue.config.optionMergeStrategies.validations = mergeReactiveOptions
Vue.config.optionMergeStrategies.attributeCaptions = mergeReactiveOptions
// Validate pasted input contains only printable characters
// \P{Cc} : Do not match control characters (includes \n and \t)
// \P{Cn} : Do not match unassigned characters.
// \P{Cs} : Do not match UTF-8-invalid characters.
// + : Make sure that something is found, i.e., this will also mean that "", the blank string, will not be considered printable.
// /g : Greedy match, exhaustively/greedily search the string for the character sets indicated.
// /u : The unicode regex operator for matching on unicode character points
const NON_PRINTABLE_RE = /[\p{Cc}\p{Cn}\p{Cs}]+/gu
document.addEventListener('paste', (e) => {
if (e.type !== 'paste') return
const pastingTxt = e.clipboardData.getData('text/plain')
if (!pastingTxt) return
const nonPrintableChars = pastingTxt.match(NON_PRINTABLE_RE) // example: [ "\u0002", "\n", "\t", "\n\n\t"]
if (Array.isArray(nonPrintableChars)) { // something found
let allNpChars = nonPrintableChars.join('').split('')
// allow \n \r for textarea and input (input automatically strip it) - and \n \r or \t for CodeMirror
const isCodeMirror = (e.target.tagName === 'TEXTAREA') && (e.target.style?.bottom === '-1em') // codemirror adds this specific style
allNpChars = allNpChars.filter(c => !((c === '\n') || (c === '\r') || (isCodeMirror && (c === '\t'))))
if (!allNpChars.length) return
const wrongCodes = allNpChars.map(c => '\\n' + c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')).join(', ')
e.preventDefault()
e.stopPropagation()
console.error('non printable paste', '"' + wrongCodes + '"')
throw new UB.UBError(UB.i18n('ERR_UNPRINTABLE_CHARS', wrongCodes))
// modify of clipboard data not works :(
// e.clipboardData.setData('text/plain', sanitizedText)
}
})
/**
* Helper function that merges validation config defined in mixins
* @param {object|function|undefined} a
* @param {object|function|undefined} b
* @returns {object}
*/
function mergeReactiveOptions (a, b) {
if (typeof a === 'function' || typeof b === 'function') {
return function () {
const aObj = typeof a === 'function' ? a.call(this) : a
const bObj = typeof b === 'function' ? b.call(this) : b
return _.merge(aObj, bObj)
}
}
return _.merge(a, b)
}
// register adminui-vue after all module.exports are defined - SystemJS.newModule memoryse an object props,
// so any new property added after call to SystemJS.newModule are not available to importers
if ((typeof SystemJS !== 'undefined') && !SystemJS.has('@unitybase/adminui-vue')) SystemJS.set('@unitybase/adminui-vue', SystemJS.newModule(module.exports))
// for CERT2 auth we must select crypto provider before connection, on this stage models is not available
// the only way to give pki() function access to capiSelectionDialog is global window object
const capiSelectionDialog = require('./views/capiSelectionDialog')
if (window) {
window.$AdminUiVue = module.exports
window.capiSelectionDialog = capiSelectionDialog
}
if (window.$App) {
$App.on('appInitialize', function serviceAccountGuard () {
if (UB.connection.userData('roles').split(',').includes('ServiceAccount')) {
throw new UB.UBError('Using of UI for members of "ServiceAccount" role is forbidden')
}
})
}