')
+ .click(function() {
+ const popover = bootstrap.Popover.getInstance($node[0])
+ popover.dispose()
+ })
+ const $buttonSubmit = $(`
`)
+ .click(function() {
+ options.confirm()
+ .then(function(result) {
+ promiseResolve(result)
+ })
+ .catch(function(error) {
+ promiseReject(error)
+ })
+ const popover = bootstrap.Popover.getInstance($node[0])
+ popover.dispose()
+ })
+ $container.append(`
`).append($buttonCancel, $buttonSubmit))
+ return $container
+ }
+
+ const thePopover = this.popover($node, popoverOptions)
+ thePopover.show()
+ return confirmPromise // have to return a promise to avoid closing the modal
+ }
}
/** Class representing a Toast */
@@ -257,6 +355,9 @@ class Toaster {
*/
constructor(options) {
this.options = Object.assign({}, Toaster.defaultOptions, options)
+ if (this.options.delay == 'auto') {
+ this.options.delay = this.computeDelay()
+ }
this.bsToastOptions = {
autohide: this.options.autohide,
delay: this.options.delay,
@@ -271,7 +372,7 @@ class Toaster {
* @property {string} body - The body's content of the toast
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast
* @property {boolean} autohide - If the toast show be hidden after some time defined by the delay
- * @property {number} delay - The number of milliseconds the toast should stay visible before being hidden
+ * @property {(number|string)} delay - The number of milliseconds the toast should stay visible before being hidden or 'auto' to deduce the delay based on the content
* @property {(jQuery|string)} titleHtml - The raw HTML title's content of the toast
* @property {(jQuery|string)} mutedHtml - The raw HTML muted's content of the toast
* @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast
@@ -284,7 +385,7 @@ class Toaster {
body: false,
variant: 'default',
autohide: true,
- delay: 5000,
+ delay: 'auto',
titleHtml: false,
mutedHtml: false,
bodyHtml: false,
@@ -350,18 +451,16 @@ class Toaster {
$toast.attr('id', options.id)
}
$toast.addClass('toast-' + options.variant)
- if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false) {
+ if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false || options.closeButton) {
var $toastHeader = $('')
$toastHeader.addClass('toast-' + options.variant)
- if (options.title !== false || options.titleHtml !== false) {
- var $toastHeaderText
- if (options.titleHtml !== false) {
- $toastHeaderText = $('
').html(options.titleHtml);
- } else {
- $toastHeaderText = $('
').text(options.title)
- }
- $toastHeader.append($toastHeaderText)
+ let $toastHeaderText = $('
')
+ if (options.titleHtml !== false) {
+ $toastHeaderText = $('
').html(options.titleHtml);
+ } else if (options.title !== false) {
+ $toastHeaderText = $('
').text(options.title)
}
+ $toastHeader.append($toastHeaderText)
if (options.muted !== false || options.mutedHtml !== false) {
var $toastHeaderMuted
if (options.mutedHtml !== false) {
@@ -391,6 +490,12 @@ class Toaster {
}
return $toast
}
+
+ computeDelay() {
+ return 3000
+ + 40*((this.options.title?.length ?? 0) + (this.options.body?.length ?? 0))
+ + (['danger', 'warning'].includes(this.options.variant) ? 5000 : 0)
+ }
}
/** Class representing a Modal */
@@ -402,15 +507,16 @@ class ModalFactory {
constructor(options) {
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
if (options.POSTSuccessCallback !== undefined) {
- if (this.options.rawHtml) {
- this.attachSubmitButtonListener = true
- } else {
+ if (!this.options.rawHtml) {
UI.toast({
variant: 'danger',
bodyHtml: '
POSTSuccessCallback can only be used in conjuction with the
rawHtml option. Instead, use the promise instead returned by the API call in
APIConfirm.'
})
}
}
+ if (this.options.rawHtml) {
+ this.attachSubmitButtonListener = true
+ }
if (options.type === undefined && options.cancel !== undefined) {
this.options.type = 'confirm'
}
@@ -428,13 +534,13 @@ class ModalFactory {
*/
/**
* @callback ModalFactory~confirm
- * @param {ModalFactory~closeModalFunction} closeFunction - A function that will close the modal if called
+ * @param {ModalFactory~confirm} closeFunction - A function that will close the modal if called
* @param {Object} modalFactory - The instance of the ModalFactory
* @param {Object} evt - The event that triggered the confirm operation
*/
/**
* @callback ModalFactory~cancel
- * @param {ModalFactory~closeModalFunction} closeFunction - A function that will close the modal if called
+ * @param {ModalFactory~cancel} closeFunction - A function that will close the modal if called
* @param {Object} modalFactory - The instance of the ModalFactory
* @param {Object} evt - The event that triggered the confirm operation
*/
@@ -621,6 +727,9 @@ class ModalFactory {
}
} else {
$modalDialog = $('
')
+ if (this.options.size !== false) {
+ $modalDialog.addClass(`modal-${this.options.size}`)
+ }
const $modalContent = $('
')
if (this.options.title !== false || this.options.titleHtml !== false) {
const $modalHeader = $('')
@@ -796,17 +905,25 @@ class ModalFactory {
$form = this.$modal.find('form')
}
if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') {
+ $submitButton[0].removeAttribute('onclick')
const clickHandler = window[$submitButton.data('confirmfunction')]
+ if (clickHandler === undefined) {
+ console.error(`Function \`${$submitButton.data('confirmfunction')}\` is not defined`)
+ }
this.options.APIConfirm = (tmpApi) => {
let clickResult = clickHandler(this, tmpApi)
if (clickResult !== undefined) {
return clickResult
.then((data) => {
- if (data.success) {
+ if (!data) {
this.options.POSTSuccessCallback([data, this])
- } else { // Validation error
- this.injectFormValidationFeedback(form, data.errors)
- return Promise.reject('Validation error');
+ } else {
+ if (data.success == undefined || data.success) {
+ this.options.POSTSuccessCallback([data, this])
+ } else { // Validation error
+ this.injectFormValidationFeedback(form, data.errors)
+ return Promise.reject('Validation error');
+ }
}
})
.catch((errorMessage) => {
@@ -816,23 +933,28 @@ class ModalFactory {
}
}
} else {
- $submitButton[0].removeAttribute('onclick')
- this.options.APIConfirm = (tmpApi) => {
- return tmpApi.postForm($form[0])
- .then((data) => {
- if (data.success) {
- // this.options.POSTSuccessCallback(data)
- this.options.POSTSuccessCallback([data, this])
- } else { // Validation error
- this.injectFormValidationFeedback(form, data.errors)
- return Promise.reject('Validation error');
- }
- })
- .catch((errorMessage) => {
- this.options.POSTFailCallback([errorMessage, this])
- // this.options.POSTFailCallback(errorMessage)
- return Promise.reject(errorMessage);
- })
+ if ($form[0]) {
+ // Submit the form via the AJAXApi
+ $submitButton[0].removeAttribute('onclick')
+ this.options.APIConfirm = (tmpApi) => {
+ return tmpApi.postForm($form[0])
+ .then((data) => {
+ if (!data) {
+ this.options.POSTSuccessCallback([data, this])
+ } else {
+ if (data.success == undefined || data.success) {
+ this.options.POSTSuccessCallback([data, this])
+ } else { // Validation error
+ this.injectFormValidationFeedback(form, data.errors)
+ return Promise.reject('Validation error');
+ }
+ }
+ })
+ .catch((errorMessage) => {
+ this.options.POSTFailCallback([errorMessage, this])
+ return Promise.reject(errorMessage);
+ })
+ }
}
}
$submitButton.click(this.getConfirmationHandlerFunction($submitButton))
@@ -879,7 +1001,7 @@ class OverlayFactory {
spinnerVariant: '',
spinnerSmall: false,
spinnerType: 'border',
- fallbackBoostrapVariant: '',
+ fallbackBootstrapVariant: '',
wrapperCSSDisplay: '',
}
@@ -978,7 +1100,7 @@ class OverlayFactory {
let classes = this.$node.attr('class')
if (classes !== undefined) {
classes = classes.split(' ')
- const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBoostrapVariant)
+ const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBootstrapVariant)
this.options.spinnerVariant = detectedVariant
}
}
@@ -987,7 +1109,7 @@ class OverlayFactory {
* Detect the bootstrap variant from a list of classes
* @param {Array} classes - A list of classes containg a bootstrap variant
*/
- static detectedBootstrapVariant(classes, fallback=OverlayFactory.defaultOptions.fallbackBoostrapVariant) {
+ static detectedBootstrapVariant(classes, fallback = OverlayFactory.defaultOptions.fallbackBootstrapVariant) {
const re = /^[a-zA-Z]+-(?
primary|success|danger|warning|info|light|dark|white|transparent)$/;
let result
for (let i=0; i')
@@ -1100,12 +1306,16 @@ class HtmlHelper {
if (options.variant) {
$table.addClass(`table-${options.variant}`)
}
+ if (options.fixed_layout) {
+ $table.css('table-layout', 'fixed')
+ }
if (options.tableClass) {
$table.addClass(options.tableClass)
}
- const $caption = $('')
+ let $caption = null
if (options.caption) {
+ $caption = $('')
if (options.caption instanceof jQuery) {
$caption = options.caption
} else {
@@ -1113,21 +1323,28 @@ class HtmlHelper {
}
}
- const $theadRow = $('
')
- head.forEach(head => {
- if (head instanceof jQuery) {
- $theadRow.append($(' | ').append(head))
- } else {
- $theadRow.append($(' | ').text(head))
- }
- })
- $thead.append($theadRow)
+ let $theadRow = null
+ if (head) {
+ $theadRow = $('
')
+ head.forEach(head => {
+ if (head instanceof jQuery) {
+ $theadRow.append($(' | ').append(head))
+ } else {
+ $theadRow.append($(' | ').text(head))
+ }
+ })
+ $thead.append($theadRow)
+ }
body.forEach(row => {
const $bodyRow = $('
')
row.forEach(item => {
if (item instanceof jQuery) {
- $bodyRow.append($(' | ').append(item))
+ if (item.is('td')) {
+ $bodyRow.append(item)
+ } else {
+ $bodyRow.append($(' | ').append(item))
+ }
} else {
$bodyRow.append($(' | ').text(item))
}
diff --git a/webroot/js/main.js b/webroot/js/main.js
index 420d1bf46..f79c4eb6e 100644
--- a/webroot/js/main.js
+++ b/webroot/js/main.js
@@ -1,5 +1,5 @@
-function executePagination(randomValue, url) {
- UI.reload(url, $(`#table-container-${randomValue}`), $(`#table-container-${randomValue} table.table`))
+function executePagination(selector, url) {
+ UI.reload(url, $(selector), $(selector).find('table.table'))
}
function executeStateDependencyChecks(dependenceSourceSelector) {
@@ -55,11 +55,8 @@ function attachTestConnectionResultHtml(result, $container) {
$testResultDiv.append(getKVHtml('Internal error', result, ['text-danger fw-bold']))
} else {
if (result['error']) {
- if (result['ping']) {
- $testResultDiv.append('Status', 'OK', ['text-danger'], `${result['ping']} ms`);
- }
$testResultDiv.append(
- getKVHtml('Status', `Error: ${result['error']}`, ['text-danger']),
+ getKVHtml('Status', `Error: ${result['error']}`, ['text-danger'], (result['ping'] ? `${result['ping']} ms` : '')),
getKVHtml('Reason', result['reason'], ['text-danger'])
)
} else {
@@ -209,6 +206,24 @@ function deleteBookmark(bookmark, forSidebar=false) {
}).catch((e) => { })
}
+function downloadIndexTable(downloadButton, filename) {
+ const $dropdownMenu = $(downloadButton).closest('.dropdown')
+ const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
+ const $container = $dropdownMenu.closest('div[id^="table-container-"]')
+ const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
+ const $filterButton = $(`#toggleFilterButton-${tableRandomValue}`)
+ const activeFilters = $filterButton.data('activeFilters')
+ const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
+ const searchParam = jQuery.param(activeFilters);
+ const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
+ let options = {}
+ const downloadPromise = AJAXApi.quickFetchJSON(url, options)
+ UI.overlayUntilResolve($dropdownMenu, downloadPromise)
+ downloadPromise.then((data) => {
+ download(filename, JSON.stringify(data, undefined, 4))
+ })
+}
+
function overloadBSDropdown() {
// Inspired from https://jsfiddle.net/dallaslu/mvk4uhzL/
(function ($bs) {
diff --git a/webroot/js/utils.js b/webroot/js/utils.js
index 00ac04ffa..1a04fb02d 100644
--- a/webroot/js/utils.js
+++ b/webroot/js/utils.js
@@ -189,3 +189,21 @@ function mergeDeep(target, ...sources) {
return mergeDeep(target, ...sources);
}
+
+function download(filename, data, type='application/json') {
+ const blob = new Blob([data], {type: type})
+ const a = window.document.createElement('a')
+ const objectURL = URL.createObjectURL(blob)
+ a.href = objectURL
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(objectURL)
+}
+
+function sanitize(unsafeText) {
+ const decoder = $('')
+ decoder.text(unsafeText)
+ return decoder.html()
+}
\ No newline at end of file