Compare commits

...

3 Commits

Author SHA1 Message Date
snipe 72d99b17de Changed retry behavior 2026-05-06 13:29:21 +01:00
snipe ea5b05b8df Show status code in error 2026-05-06 13:24:53 +01:00
snipe 87eca6b303 Fixed #18973 - Added error state renderer to show a different message if 500 is returned 2026-05-06 13:06:02 +01:00
2 changed files with 145 additions and 0 deletions
+2
View File
@@ -610,6 +610,8 @@ return [
'and' => 'and',
'action_source' => 'Action Source',
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to exclude, <code>is:value</code> for an exact match, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
'search_load_error_short' => 'Could not load records.',
'search_load_error_help' => 'An API error has occurred. Check your server logs for errors and confirm migrations are up to date.',
'or' => 'or',
'url' => 'URL',
'phone' => 'Phone',
@@ -497,6 +497,88 @@
return default_value;
}
var renderBootstrapTableLoadErrorState = function ($table) {
var $wrapper = $table.closest('.bootstrap-table');
if (!$wrapper.length) {
return;
}
var statusCode = parseInt($table.data('bs-table-load-error-status'), 10);
var statusSuffix = Number.isFinite(statusCode) && statusCode > 0 ? ' (HTTP ' + statusCode + ')' : '';
var errorHtml = '<div class="text-danger">'
+ '<i class="fas fa-triangle-exclamation" aria-hidden="true"></i> '
+ '{{ trans('general.search_load_error_short') }}' + statusSuffix
+ '<div class="text-muted" style="margin-top: 4px;">{{ trans('general.search_load_error_help') }}</div>'
+ '</div>';
var $targets = $wrapper.find('.fixed-table-body .no-records-found td, .fixed-table-body .fixed-table-loading');
if (!$targets.length) {
// On initial load errors, bootstrap-table can insert the no-records row
// after the load-error callback fires. Observe DOM changes briefly and
// render the error state once the row appears, then disconnect.
var existingObserver = $table.data('bs-table-load-error-observer');
if (existingObserver) {
return;
}
var wrapperNode = $wrapper.get(0);
if (!wrapperNode || typeof MutationObserver === 'undefined') {
return;
}
var observer = new MutationObserver(function () {
if ($table.data('bs-table-load-error') !== true) {
observer.disconnect();
$table.removeData('bs-table-load-error-observer');
return;
}
var $newTargets = $wrapper.find('.fixed-table-body .no-records-found td, .fixed-table-body .fixed-table-loading');
if ($newTargets.length) {
observer.disconnect();
$table.removeData('bs-table-load-error-observer');
renderBootstrapTableLoadErrorState($table);
}
});
observer.observe(wrapperNode, { childList: true, subtree: true });
$table.data('bs-table-load-error-observer', observer);
return;
}
var activeObserver = $table.data('bs-table-load-error-observer');
if (activeObserver) {
activeObserver.disconnect();
$table.removeData('bs-table-load-error-observer');
}
$targets.html(errorHtml);
};
var resolveHttpStatusFromLoadErrorArgs = function () {
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) {
continue;
}
if (typeof arg === 'number' && arg > 0) {
return arg;
}
if (typeof arg === 'object' && typeof arg.status === 'number' && arg.status > 0) {
return arg.status;
}
}
return null;
};
var initialAdvancedSearchOperator = getStoredAdvancedSearchOperator() || normalizeAdvancedSearchOperator(data_with_default('advanced-search-operator', defaultAdvancedSearchOperator));
@@ -607,8 +689,38 @@
exportOptions: export_options,
exportTypes: ['xlsx', 'excel', 'csv', 'pdf', 'json', 'xml', 'txt', 'sql', 'doc'],
onLoadSuccess: function () { // possible 'fixme'? this might be for contents, not for headers?
// Clear transport/server error state so genuine empty results show normal messaging.
$(this).data('bs-table-load-error', false);
$(this).removeData('bs-table-load-error-status');
var observer = $(this).data('bs-table-load-error-observer');
if (observer) {
observer.disconnect();
$(this).removeData('bs-table-load-error-observer');
}
$('[data-tooltip="true"]').tooltip(); // Needed to attach tooltips after ajax call
},
onLoadError: function () {
// Mark this table as failed so formatNoMatches can render an explicit error state
// instead of the misleading "no records" empty-state message.
var $table = $(this);
$table.data('bs-table-load-error', true);
var statusCode = resolveHttpStatusFromLoadErrorArgs.apply(null, arguments);
if (statusCode) {
$table.data('bs-table-load-error-status', statusCode);
}
// On first page load, bootstrap-table may already have rendered the default
// no-records row. Force-replace that content immediately after the error.
renderBootstrapTableLoadErrorState($table);
},
onPostBody: function () {
// Keep the error empty-state stable if bootstrap-table re-renders body rows
// while the current table state is still a transport/server error.
var $table = $(this);
if ($table.data('bs-table-load-error') === true) {
renderBootstrapTableLoadErrorState($table);
}
},
onPostHeader: function () {
var lookup = {};
var lookup_initialized = false;
@@ -666,11 +778,42 @@
},
formatNoMatches: function () {
if ($(this).data('bs-table-load-error') === true) {
var statusCode = parseInt($(this).data('bs-table-load-error-status'), 10);
var statusSuffix = Number.isFinite(statusCode) && statusCode > 0 ? ' (HTTP ' + statusCode + ')' : '';
return '<div class="text-danger" style="padding: 8px 0;">'
+ '<i class="fas fa-triangle-exclamation" aria-hidden="true" style="margin-right: 6px;"></i>'
+ '{{ trans('general.search_load_error_short') }}' + statusSuffix
+ '<div class="text-muted" style="margin-top: 4px;">{{ trans('general.search_load_error_help') }}</div>'
+ '</div>';
}
return '{{ trans('table.no_matching_records') }}';
}
});
// Event-driven fallback for initial-load timing/callback-context edge cases.
// In some bootstrap-table flows, onLoadError can race before the empty row exists
// or callback context may not be the raw table element. These events are emitted
// from the table node itself and are reliable across initial load and searches.
var $currentTable = $(this);
$currentTable.off('.snipeLoadState').on('load-success.bs.table.snipeLoadState', function () {
$currentTable.data('bs-table-load-error', false);
$currentTable.removeData('bs-table-load-error-status');
}).on('load-error.bs.table.snipeLoadState', function () {
$currentTable.data('bs-table-load-error', true);
var statusCode = resolveHttpStatusFromLoadErrorArgs.apply(null, Array.prototype.slice.call(arguments, 1));
if (statusCode) {
$currentTable.data('bs-table-load-error-status', statusCode);
}
renderBootstrapTableLoadErrorState($currentTable);
}).on('post-body.bs.table.snipeLoadState', function () {
if ($currentTable.data('bs-table-load-error') === true) {
renderBootstrapTableLoadErrorState($currentTable);
}
});
var bootstrapTableInstance = $(this).data('bootstrap.table');
if (bootstrapTableInstance && typeof bootstrapTableInstance.renderAdvancedSearchTags === 'function') {