Merge branch 'feature-63359/StV_API_error_clustering'

This commit is contained in:
Andreas Österreicher
2025-09-04 14:40:24 +02:00
3 changed files with 437 additions and 247 deletions
@@ -151,7 +151,17 @@ export default {
ApiStvStatus.addStudent(prestudent_id, data),
{ errorHeader: prestudent_id }
]))
.then(() => {
.then(result => {
const messagesSuccessful = result.filter(res => res.status == 'fulfilled');
if (messagesSuccessful.length) {
this.$fhcAlert.alertDefault(
'info',
'Feedback',
messagesSuccessful.length + " erfolgreiche Statusänderung(en) durchgeführt", // TODO(chris): translate
false,
true
);
}
this.$emit('reloadTable');
this.$reloadList();
});
+406 -246
View File
@@ -10,7 +10,232 @@ export default {
if (!app.config.globalProperties.$fhcAlert)
app.use(FhcAlert);
function _get_config(form, uri, data, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
function _send_array_or_object(errors, func) {
if (Array.isArray(errors))
errors.forEach(func);
else
Object.entries(errors).forEach(
([title, errs]) => errs.forEach(
error => func(error, title)
)
);
}
let DEFAULT_ERROR_CONFIG = {
success: true,
fail: true,
combine: {
form: ['validation', 'general'],
toast: ['validation', 'general', 'not_found', 'site_failed']
},
handler: {
form(form, errors) {
form.clearValidation();
errors.forEach(err => form.setFeedback(
false,
err.messages || err.message
));
},
async toast(errors) {
const $p = app.config.globalProperties.$p;
if (!$p)
return Promise.reject('Phrasen plugin not loaded!');
async function _format_toast(errors) {
errors = errors.reduce((result, err) => {
switch (err.type) {
case 'not_found':
case 'site_failed':
if (err.message)
result[err.message] = [err.url];
else
result._default = [err.url];
break;
case 'general':
if (!result._default)
result._default = [];
result._default.push(err.message);
break;
case 'validation':
Object.entries(err.messages)
.forEach(([field, msg]) => {
if (!result[field])
result[field] = [];
if (Array.isArray(msg))
result[field].push(...msg);
else
result[field].push(msg);
});
break;
}
return result;
}, {});
let counter = 0;
const msgs = await Promise.all(Object.entries(errors)
.sort((a, b) => ['_default'].indexOf(b[0]) - ['_default'].indexOf(a[0])) // sort _default first
.map(async ([field, msgs]) => {
if (field == '_default') {
await $p.loadCategory('dashboard');
const general = $p.t('dashboard/general');
field = '<dt class="d-none">' + general + '</dt>';
} else {
field = '<dt>' + field + '</dt>';
}
counter += msgs.length;
return field
+ '<dd>'
+ msgs.join('</dd><dd>')
+ '</dd>';
}));
return {
counter,
msgs
}
}
let counter, msgs;
if (Array.isArray(errors)) {
({ counter, msgs } = await _format_toast(errors));
} else {
({ counter, msgs } = await Object.entries(errors)
.reduce(async (res, [title, errs]) => {
const result = await res;
const { counter, msgs } = await _format_toast(errs);
result.counter += counter;
result.msgs.push('<dt>'
+ title
+ '</dt><dd><dl>'
+ msgs.join('')
+ '</dl></dd>');
return result;
}, Promise.resolve({ counter: 0, msgs: []})));
}
await $p.loadCategory('ui');
const n_errors = $p.t('ui/n_errors', { n: counter });
$fhcAlert.alertDefault(
'error',
n_errors,
'<dl>' + msgs.join('') + '</dl>',
true,
true
);
},
php(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
switch (error.severity) {
case 'Warning':
case 'Core Warning':
case 'Compile Warning':
case 'User Warning':
if (title)
title += ': PHP ' + error.severity;
else
title = 'PHP ' + error.severity;
$fhcAlert.alertDefault('warn', title, message, true);
break;
case 'Notice':
case 'User Notice':
case 'Runtime Notice':
if (title)
title += ': PHP ' + error.severity;
else
title = 'PHP ' + error.severity;
$fhcAlert.alertDefault('info', title, message, true);
break;
default:
message = 'Type: PHP ' + error.severity + '\n\n' + message;
if (title)
message = title + '\n\n' + message;
$fhcAlert.alertSystemError(message);
break;
}
});
},
exception(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
if (title)
message += title + '\n\n';
message += 'Type: ' + error.class + '\n\n';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
$fhcAlert.alertSystemError(message);
});
},
db(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
if (title)
message += title + '\n\n';
if (error.heading !== undefined)
message += error.heading + '\n\n';
if (error.code !== undefined)
message += 'Code: ' + error.code + '\n\n';
if (error.sql !== undefined)
message += 'SQL: ' + error.sql + '\n\n';
if (error.message !== undefined)
message += 'Message: ' + error.message + '\n\n';
else if (error.messages !== undefined)
message += 'Messages: ' + error.messages.join('\n\t') + '\n\n';
if (error.filename !== undefined)
message += 'Filename: ' + error.filename + '\n';
if (error.line !== undefined)
message += 'Line Number: ' + error.line + '\n';
$fhcAlert.alertSystemError(message);
});
},
auth(errors) {
_send_array_or_object(errors, (error, title) => {
if (title)
title += ': ' + error.message;
else
title = error.message;
var message = '';
message += 'Controller name: ' + error.controller + '\n';
message += 'Method name: ' + error.method + '\n';
message += 'Required permissions: ' + error.required_permissions;
$fhcAlert.alertDefault(
'error',
title,
message,
true
);
});
}
}
};
if (options?.errorHandling !== undefined)
DEFAULT_ERROR_CONFIG = _merge_error_config(options.errorHandling);
function get_config(form, uri, data, config) {
if (typeof form == 'string' && config === undefined) {
[uri, data, config] = [form, uri, data];
form = undefined;
@@ -40,10 +265,9 @@ export default {
return [uri, data, config];
}
function _clean_return_value(response) {
function clean_return_value(response) {
if (typeof response.data === 'string' || response.data instanceof String)
return _clean_return_value({ data: response });
return clean_return_value({ data: response });
const result = response.data;
delete response.data;
@@ -55,10 +279,111 @@ export default {
result.meta.response = response;
return result;
}
const baseURL = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/";
function _merge_error_config(config) {
if (config === false || config === 'off')
return { ...DEFAULT_ERROR_CONFIG, success: false, fail: false };
if (!config || config === true)
return { ...DEFAULT_ERROR_CONFIG };
if (config === 'success')
return { ...DEFAULT_ERROR_CONFIG, fail: false };
if (config === 'fail')
return { ...DEFAULT_ERROR_CONFIG, success: false };
const { success, fail, handler, combine } = config;
config = { ...DEFAULT_ERROR_CONFIG };
Object.entries({ fail, success }).forEach(([key, value]) => {
if (value !== undefined)
config[key] = value;
});
Object.entries({ handler, combine }).forEach(([key, value]) => {
if (value !== undefined)
config[key] = { ...config[key], ...value };
});
return config;
}
function get_error_handler(config) {
const result = _merge_error_config(config?.errorHandling);
if (!config?.form) {
result.combine = { ...result.combine, form: [] };
} else {
const formHandler = result.handler.form;
result.handler = { ...result.handler, form: errors => formHandler(config.form, errors) };
}
return result;
}
function get_error_list(error) {
if (error.response) {
if (error.response.status == 404) {
return [{
type: 'not_found',
message: error.message,
url: error.request.responseURL
}];
} else {
return error.response.data.errors;
}
} else if (error.request) {
return [{
type: 'site_failed',
message: error.message,
url: error.request.responseURL
}];
} else {
return [{
type: 'script',
message: error.message
}];
}
}
function popHandleableErrors(errorHandling, errors) {
const result = {};
const copy = [];
while (errors.length)
copy.push(errors.pop());
for (var error of copy) {
let type = error.type;
let newType = null;
for (var t in errorHandling.combine) {
let newTypeCombinesType = errorHandling
.combine[t]
.includes(type);
let newTypeHasHandler = errorHandling.handler[t];
if (newTypeCombinesType && newTypeHasHandler) {
newType = t;
if (newType == 'form')
break;
}
}
if (newType)
type = newType;
const handler = errorHandling.handler[type];
if (handler) {
if (!result[type])
result[type] = [];
if (Array.isArray(error))
result[type].push(...error);
else
result[type].push(error);
continue;
}
errors.push(error);
}
return result;
}
const fhcApiAxios = axios.create({
timeout: 500000,
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/"
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ "/"
});
fhcApiAxios.interceptors.request.use(config => {
@@ -100,57 +425,51 @@ export default {
return config;
});
fhcApiAxios.interceptors.response.use(response => {
if (response.config?.errorHandling == 'off'
|| response.config?.errorHandling === false
|| response.config?.errorHandling == 'fail')
return _clean_return_value(response);
// NOTE(chris): loop through errors
if (response.data.errors)
response.data.errors = response.data.errors.filter(
err => (response.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, response.config)
);
return _clean_return_value(response);
}, error => {
if (error.code == 'ERR_CANCELED')
return Promise.reject({...{handled: true}, ...error});
if (error.config?.errorHandling == 'off'
|| error.config?.errorHandling === false
|| error.config?.errorHandling == 'success')
return Promise.reject(error);
if (error.response) {
if (error.response.status == 404) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL, true);
return Promise.reject({...{handled: true}, ...error});
}
fhcApiAxios.interceptors.response.use(
response => {
if (response.config?.errorHandling == 'off'
|| response.config?.errorHandling === false
|| response.config?.errorHandling == 'fail')
return clean_return_value(response);
// NOTE(chris): loop through errors
error.response.data.errors = error.response.data.errors.filter(
err => (error.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, error.config)
);
if (!error.response.data.errors.length)
return Promise.reject({...{handled: true}, ...error});
} else if (error.request) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL);
return Promise.reject({...{handled: true}, ...error});
} else {
app.config.globalProperties.$fhcAlert.alertError(error.message);
return Promise.reject({...{handled: true}, ...error});
}
if (response.data.errors)
response.data.errors = response.data.errors.filter(
err => (response.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, response.config)
);
return Promise.reject(error);
});
return clean_return_value(response);
},
error => {
if (error.code == 'ERR_CANCELED')
return Promise.reject({ handled: true, ...error });
const errorConfig = get_error_handler(error.config);
if (!errorConfig.fail)
return Promise.reject(error);
const remaining = get_error_list(error);
const errors = popHandleableErrors(errorConfig, remaining);
for (var type in errors) {
errorConfig.handler[type](errors[type]);
}
if (remaining.length)
return Promise.reject(error);
return Promise.reject({ handled: true, ...error });
}
);
app.config.globalProperties.$api = {
getUri(url) {
return fhcApiAxios.getUri({url});
},
get(form, uri, params, config) {
[uri, params, config] = _get_config(form, uri, params, config);
[uri, params, config] = get_config(form, uri, params, config);
if (params) {
if (config)
config.params = params;
@@ -160,82 +479,58 @@ export default {
return fhcApiAxios.get(uri, config);
},
post(form, uri, data, config) {
[uri, data, config] = _get_config(form, uri, data, config);
[uri, data, config] = get_config(form, uri, data, config);
return fhcApiAxios.post(uri, data, config);
},
call(factory, configoverwrite, form) {
if (Array.isArray(factory)) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
const $api = app.config.globalProperties.$api;
Promise
return Promise
.allSettled(factory.map((config, index) => {
if (Array.isArray(config))
return $api.call(config[1], {
errorHeader: config[0],
errorHandling: false
});
else
return $api.call(config, {
errorHeader: '#' + index,
errorHandling: false
});
}))
.then(res => {
// TODO(chris): obey form & configoverwrite
let messagesError = [];
let messagesSuccessful = [];
res.forEach(result => {
if (result.status === 'fulfilled') {
//console.log(JSON.parse(result.value.data));
const successTitle = "<dt>" + result.value.data + "</dt>";
messagesSuccessful.push(successTitle + "ok");
} else {
const errorTitle = "<dt>" + result.reason.config.errorHeader + "</dt>";
const errorMsg = JSON.parse(result.reason.request.response);
const fullMessage = errorMsg.errors.map(error => {
if (error.type == 'validation') {
// TODO(chris): do we want the keys?
return '<dd>' + Object.values(error.messages).join("</dd><dd>") + '</dd>';
}
// TODO(chris): other types
if (error.message)
return '<dd>' + error.message + '</dd>';
if (error.messages)
return '<dd>' + error.messages.join("\n") + '</dd>';
// TODO(chris): what to do here
return '<dd>' + "Generic Error" + '</dd>'; // TODO(chris): translate
}).join("\n");
messagesError.push(errorTitle + fullMessage);
}
if (!Array.isArray(config))
config = ['#' + index, config];
return $api.call(config[1], {
errorHeader: config[0],
errorHandling: false
});
}))
.then(result => {
const [ , , config ] = get_config(form, undefined, undefined, configoverwrite || {});
const errorConfig = get_error_handler(config);
if (messagesError.length)
{
const test = document.createElement('b');
$fhcAlert.alertDefault(
'error',
messagesError.length + " Fehler", // TODO(chris): translate
'<dl>' + messagesError.join("") + '</dl>',
true,
true
);
if (!errorConfig.success && !errorConfig.fail) {
return result;
}
if (messagesSuccessful.length)
{
const test = document.createElement('b');
$fhcAlert.alertDefault(
'info',
'Feedback',
messagesSuccessful.length + " erfolgreiche Statusänderung(en) durchgeführt", // TODO(chris): translate
false,
true
);
const typedErrors = {};
for (var res of result) {
const [ allowed, item ] = res.status === 'fulfilled'
? [ errorConfig.success, res.value ]
: [ errorConfig.fail, res.reason ];
if (!allowed)
return;
const errors = popHandleableErrors(errorConfig, get_error_list(item));
for (var type in errors) {
if (!typedErrors[type])
typedErrors[type] = {
[item.config.errorHeader]: errors[type]
};
else
typedErrors[type][item.config.errorHeader] = errors[type];
}
};
for (var errType in typedErrors) {
errorConfig.handler[errType](typedErrors[errType]);
}
return result;
});
}
let {method, url, params, config} = factory;
let { method, url, params, config } = factory;
if (configoverwrite !== undefined) {
config = configoverwrite;
}
@@ -251,141 +546,6 @@ export default {
} else {
console.error("FhcApi: method not allowed:", method);
}
},
_defaultErrorHandlers: {
validation(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (config?.form) {
config.form.clearValidation();
config.form.setFeedback(false, error.messages);
return false;
}
if (Array.isArray(error.messages)) {
error.messages.forEach($fhcAlert.alertError);
return false;
} else if (typeof error.messages == 'object') {
if (config?.errorHeader)
Object.values(error.messages).forEach(
value => $fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
value,
true
)
);
else
Object.entries(error.messages).forEach(
([key, value]) => $fhcAlert.alertDefault('error', key, value, true)
);
return false;
}
return true;
},
general(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (config?.form)
config.form.setFeedback(false, error.message);
else if (config?.errorHeader)
$fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
error.message,
true
);
else
$fhcAlert.alertError(error.message);
},
php(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
switch (error.severity) {
case 'Warning':
case 'Core Warning':
case 'Compile Warning':
case 'User Warning':
$fhcAlert.alertDefault('warn', 'PHP ' + error.severity, message, true);
break;
case 'Notice':
case 'User Notice':
case 'Runtime Notice':
$fhcAlert.alertDefault('info', 'PHP ' + error.severity, message, true);
break;
default:
message = 'Type: PHP ' + error.severity + '\n\n' + message;
$fhcAlert.alertSystemError(message);
break;
}
},
exception(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Type: ' + error.class + '\n\n';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
$fhcAlert.alertSystemError(message);
},
db(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
if (error.heading !== undefined)
message += error.heading + '\n\n';
if (error.code !== undefined)
message += 'Code: ' + error.code + '\n\n';
if (error.sql !== undefined)
message += 'SQL: ' + error.sql + '\n\n';
if (error.message !== undefined)
message += 'Message: ' + error.message + '\n\n';
else if (error.messages !== undefined)
message += 'Messages: ' + error.messages.join('\n\t') + '\n\n';
if (error.filename !== undefined)
message += 'Filename: ' + error.filename + '\n';
if (error.line !== undefined)
message += 'Line Number: ' + error.line + '\n';
$fhcAlert.alertSystemError(message);
},
auth(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Controller name: ' + error.controller + '\n';
message += 'Method name: ' + error.method + '\n';
message += 'Required permissions: ' + error.required_permissions;
if (config?.errorHeader)
$fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
error.message,
true
);
else
$fhcAlert.alertDefault('error', error.message, message);
}
}
};
+20
View File
@@ -1475,6 +1475,26 @@ $phrases = array(
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'n_errors',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => '{n} Fehler',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => '{n} Error(s)',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',