mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-06-01 20:29:29 +00:00
Merge branch 'feature-34543/UX_Template'
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Controller using JSON
|
||||
*/
|
||||
class FHCAPI_Controller extends FHC_Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Response status
|
||||
* @see https://github.com/omniti-labs/jsend
|
||||
*/
|
||||
const STATUS_SUCCESS = 'success';
|
||||
const STATUS_FAIL = 'fail';
|
||||
const STATUS_ERROR = 'error';
|
||||
|
||||
/**
|
||||
* Error types
|
||||
*/
|
||||
const ERROR_TYPE_PHP = 'php'; // TODO(chris): php types from severity?
|
||||
const ERROR_TYPE_EXCEPTION = 'exception';
|
||||
const ERROR_TYPE_GENERAL = 'general';
|
||||
const ERROR_TYPE_404 = '404';
|
||||
const ERROR_TYPE_DB = 'db';
|
||||
const ERROR_TYPE_VALIDATION = 'validation';
|
||||
|
||||
/**
|
||||
* Return Object
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $returnObj = [];
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $requiredPermissions
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($requiredPermissions = [])
|
||||
{
|
||||
if (is_cli())
|
||||
show_404();
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->config->set_item('error_views_path', VIEWPATH.'errors'.DIRECTORY_SEPARATOR.'json'.DIRECTORY_SEPARATOR);
|
||||
|
||||
global $g_result;
|
||||
$g_result = $this;
|
||||
|
||||
ob_start(function ($content) {
|
||||
$http_response_code = http_response_code();
|
||||
// NOTE(chris): For security reasons 404 will be displayed the same everywhere
|
||||
if ($http_response_code == REST_Controller::HTTP_NOT_FOUND)
|
||||
return $content;
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!isset($this->returnObj['meta']) || !isset($this->returnObj['meta']['status'])) {
|
||||
switch ($http_response_code) {
|
||||
case 200:
|
||||
$this->setStatus(self::STATUS_SUCCESS);
|
||||
break;
|
||||
case 400:
|
||||
$this->setStatus(self::STATUS_FAIL);
|
||||
break;
|
||||
default:
|
||||
$this->setStatus(self::STATUS_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#$this->returnObj['test'] = implode('/n', headers_list());
|
||||
|
||||
return json_encode($this->returnObj);
|
||||
});
|
||||
|
||||
// Load libraries
|
||||
$this->load->library('AuthLib');
|
||||
$this->load->library('PermissionLib');
|
||||
|
||||
// Checks if the caller is allowed to access to this content
|
||||
$this->_isAllowed($requiredPermissions);
|
||||
|
||||
// For JSON Requests (as opposed to multipart/form-data) get the $_POST variable from the input stream instead
|
||||
if ($this->input->get_request_header('Content-Type', true) == 'application/json')
|
||||
$_POST = json_decode($this->security->xss_clean($this->input->raw_input_stream), true);
|
||||
elseif (isset($_POST['_jsondata'])) {
|
||||
$_POST = array_merge($_POST, json_decode($_POST['_jsondata'], true));
|
||||
unset($_POST['_jsondata']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Handle Output object
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string $type (optional)
|
||||
* @return void
|
||||
*/
|
||||
public function addError($data, $type = null)
|
||||
{
|
||||
if (!isset($this->returnObj['errors']))
|
||||
$this->returnObj['errors'] = [];
|
||||
|
||||
$error = [];
|
||||
|
||||
if (is_array($data)) {
|
||||
if ($type == self::ERROR_TYPE_VALIDATION)
|
||||
$error['messages'] = $data;
|
||||
else
|
||||
$error = $data;
|
||||
} else {
|
||||
$error['message'] = $data;
|
||||
}
|
||||
|
||||
if ($type)
|
||||
$error['type'] = $type;
|
||||
|
||||
$this->returnObj['errors'][] = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @return void
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->returnObj['data'] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
if (!isset($this->returnObj['meta']))
|
||||
$this->returnObj['meta'] = [];
|
||||
$this->returnObj['meta']['status'] = $status;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Handle Output object - Shortcut functions
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array $errors
|
||||
* @return void
|
||||
*/
|
||||
protected function terminateWithValidationErrors($errors)
|
||||
{
|
||||
$this->output->set_status_header(REST_Controller::HTTP_BAD_REQUEST);
|
||||
$this->addError($errors, self::ERROR_TYPE_VALIDATION);
|
||||
$this->setStatus(self::STATUS_FAIL);
|
||||
exit(EXIT_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data (optional)
|
||||
* @return void
|
||||
*/
|
||||
protected function terminateWithSuccess($data = null)
|
||||
{
|
||||
$this->setData($data);
|
||||
$this->setStatus(self::STATUS_SUCCESS);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $error
|
||||
* @param string $type (optional)
|
||||
* @return void
|
||||
*/
|
||||
protected function terminateWithError($error, $type = null)
|
||||
{
|
||||
$this->output->set_status_header(REST_Controller::HTTP_INTERNAL_SERVER_ERROR);
|
||||
$this->addError($error, $type);
|
||||
$this->setStatus(self::STATUS_ERROR);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdclass $result
|
||||
* @param string $errortype
|
||||
* @return void
|
||||
*/
|
||||
protected function checkForErrors($result, $errortype = self::ERROR_TYPE_GENERAL)
|
||||
{
|
||||
// TODO(chris): IMPLEMENT!
|
||||
if (isError($result)) {
|
||||
$this->terminateWithError(getError($result), $errortype);
|
||||
}
|
||||
return $result->retval;
|
||||
}
|
||||
|
||||
// TODO(chris): complete list
|
||||
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Security
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if the caller is allowed to access to this content with the given permissions
|
||||
* If it is not allowed will set the HTTP header with code 401
|
||||
* Wrapper for permissionlib->isEntitled
|
||||
*
|
||||
* @param array $requiredPermissions
|
||||
* @return void
|
||||
*/
|
||||
protected function _isAllowed($requiredPermissions)
|
||||
{
|
||||
// Checks if this user is entitled to access to this content
|
||||
if (!$this->permissionlib->isEntitled($requiredPermissions, $this->router->method))
|
||||
{
|
||||
$this->output->set_status_header(isLogged() ? REST_Controller::HTTP_FORBIDDEN : REST_Controller::HTTP_UNAUTHORIZED);
|
||||
|
||||
$this->addError([
|
||||
'message' => 'You are not allowed to access to this content',
|
||||
'controller' => $this->router->class,
|
||||
'method' => $this->router->method,
|
||||
'required_permissions' => $this->_rpsToString($requiredPermissions, $this->router->method)
|
||||
]);
|
||||
exit; // immediately terminate the execution
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of permissions to a string that contains them as a comma separated list
|
||||
* Ex: "<permission 1>, <permission 2>, <permission 3>"
|
||||
*
|
||||
* @param array $requiredPermissions
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
protected function _rpsToString($requiredPermissions, $method)
|
||||
{
|
||||
if (!isset($requiredPermissions[$method]))
|
||||
return '';
|
||||
|
||||
if (!is_array($requiredPermissions[$method]))
|
||||
return $requiredPermissions[$method];
|
||||
|
||||
return implode(', ', $requiredPermissions[$method]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
// NOTE(chris): For security reasons 404 will be displayed the same everywhere
|
||||
?><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>404 Page Not Found</title>
|
||||
<style type="text/css">
|
||||
|
||||
::selection { background-color: #E13300; color: white; }
|
||||
::-moz-selection { background-color: #E13300; color: white; }
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
margin: 40px;
|
||||
font: 13px/20px normal Helvetica, Arial, sans-serif;
|
||||
color: #4F5155;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #003399;
|
||||
background-color: transparent;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #444;
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid #D0D0D0;
|
||||
font-size: 19px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 14px 0;
|
||||
padding: 14px 15px 10px 15px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Consolas, Monaco, Courier New, Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #D0D0D0;
|
||||
color: #002166;
|
||||
display: block;
|
||||
margin: 14px 0 14px 0;
|
||||
padding: 12px 10px 12px 10px;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 10px;
|
||||
border: 1px solid #D0D0D0;
|
||||
box-shadow: 0 0 8px #D0D0D0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 15px 12px 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1><?php echo $heading; ?></h1>
|
||||
<?php echo $message; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
global $g_result;
|
||||
|
||||
// NOTE(chris): remove p tags from CI_Exceptions::show_error() function
|
||||
$msg = substr($message, 3);
|
||||
$msg = substr($msg, 0, -4);
|
||||
$msg = explode('</p><p>', $msg);
|
||||
|
||||
$msgs = [];
|
||||
|
||||
$error = [
|
||||
'heading' => $heading
|
||||
];
|
||||
|
||||
/** NOTE(chris): extract Error Number and SQL
|
||||
* @see: DB_driver.php:692
|
||||
*/
|
||||
if (substr(current($msg), 0, 14) == 'Error Number: ') {
|
||||
$code = substr(array_shift($msg), 14);
|
||||
if ($code)
|
||||
$error['code'] = (int)$code;
|
||||
$msgs[] = array_shift($msg);
|
||||
$error['sql'] = array_shift($msg);
|
||||
}
|
||||
|
||||
/** NOTE(chris): extract Line Number and Filename
|
||||
* @see: DB_driver.php:1782
|
||||
* @see: DB_driver.php:1783
|
||||
*/
|
||||
if (count($msg) >= 2) {
|
||||
if (substr(end($msg), 0, 13) == 'Line Number: ' && substr(prev($msg), 0, 10) == 'Filename: ') {
|
||||
$error['line'] = (int)substr(array_pop($msg), 13);
|
||||
$error['filename'] = substr(array_pop($msg), 10);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($msg as $m)
|
||||
$msgs[] = $m;
|
||||
|
||||
|
||||
if (count($msgs) == 1)
|
||||
$error['message'] = current($msgs);
|
||||
else
|
||||
$error['messages'] = $msgs;
|
||||
|
||||
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_DB);
|
||||
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
global $g_result;
|
||||
|
||||
$error = [
|
||||
'message' => $message,
|
||||
'class' => get_class($exception),
|
||||
'filename' => $exception->getFile(),
|
||||
'line' => $exception->getLine()
|
||||
];
|
||||
|
||||
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === true) {
|
||||
$error['backtrace'] = [];
|
||||
foreach (debug_backtrace() as $err) {
|
||||
if (isset($err['file']) && strpos($err['file'], realpath(BASEPATH)) !== 0) {
|
||||
$error['backtrace'][] = [
|
||||
'file' => $err['file'],
|
||||
'line' => $err['line'],
|
||||
'function' => $err['function']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_EXCEPTION);
|
||||
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
global $g_result;
|
||||
|
||||
// NOTE(chris): remove p tags from CI_Exceptions::show_error() function
|
||||
$msg = substr($message, 3);
|
||||
$msg = substr($msg, 0, -4);
|
||||
$msg = explode('</p><p>', $msg);
|
||||
|
||||
$error = [
|
||||
'heading' => $heading
|
||||
];
|
||||
if (count($msg) == 1)
|
||||
$error['message'] = current($msg);
|
||||
else
|
||||
$error['messages'] = $msg;
|
||||
|
||||
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_GENERAL);
|
||||
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
global $g_result;
|
||||
|
||||
$error = [
|
||||
'message' => $message,
|
||||
'severity' => $severity,
|
||||
'filename' => $filepath,
|
||||
'line' => $line
|
||||
];
|
||||
|
||||
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === true) {
|
||||
$error['backtrace'] = [];
|
||||
foreach (debug_backtrace() as $err) {
|
||||
if (isset($err['file']) && strpos($err['file'], realpath(BASEPATH)) !== 0) {
|
||||
$error['backtrace'][] = [
|
||||
'file' => $err['file'],
|
||||
'line' => $err['line'],
|
||||
'function' => $err['function']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(chris): change type with severity
|
||||
$g_result->addError($error, 'php');
|
||||
|
||||
if (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity) {
|
||||
$g_result->setStatus('error');
|
||||
}
|
||||
+81
-64
@@ -1,83 +1,100 @@
|
||||
.text-prewrap {
|
||||
.fhc-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.fhc-header > h1:first-child {
|
||||
font-size: calc(1.325rem + .9vw);
|
||||
}
|
||||
.fhc-header > :first-child > small {
|
||||
color: var(--bs-secondary);
|
||||
font-size: .65em;
|
||||
padding-inline-start: 1em;
|
||||
}
|
||||
|
||||
.fhc-alert.p-toast-center {
|
||||
width: 35rem;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.fhc-alert.p-toast-top-right {
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
.fhc-alert.p-toast-top-right .p-toast-detail,
|
||||
.fhc-alert.p-toast-center .p-toast-message-text .card {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text-preline {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.accordion-button-primary {
|
||||
background-color: #e7f1ff;
|
||||
color: #0c63e4;
|
||||
}
|
||||
.accordion-button-primary:not(.collapsed) {
|
||||
background-color: #cfe2ff;
|
||||
color: #0a58ca;
|
||||
.text-prewrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.accordion-button-secondary {
|
||||
background-color: #f0f1f2;
|
||||
color: #616971;
|
||||
}
|
||||
.accordion-button-secondary:not(.collapsed) {
|
||||
background-color: #e2e3e5;
|
||||
color: #565e64;
|
||||
.btn-p-0 {
|
||||
padding: 0 .375rem;
|
||||
}
|
||||
|
||||
.accordion-button-success {
|
||||
background-color: #e8f3ee;
|
||||
color: #177a4c;
|
||||
}
|
||||
.accordion-button-success:not(.collapsed) {
|
||||
background-color: #d1e7dd;
|
||||
color: #146c43;
|
||||
.z-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.accordion-button-info {
|
||||
background-color: #e7fafe;
|
||||
color: #0cb6d8;
|
||||
}
|
||||
.accordion-button-info:not(.collapsed) {
|
||||
background-color: #cff4fc;
|
||||
color: #0aa2c0;
|
||||
.input-group > .input-group-item {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.accordion-button-warning {
|
||||
background-color: #fff9e6;
|
||||
color: #e6ae06;
|
||||
}
|
||||
.accordion-button-warning:not(.collapsed) {
|
||||
background-color: #fff3cd;
|
||||
color: #cc9a06;
|
||||
.input-group > .input-group-item .form-control:focus,
|
||||
.input-group > .input-group-item .form-select:focus {
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.accordion-button-danger {
|
||||
background-color: #fcebec;
|
||||
color: #c6303e;
|
||||
}
|
||||
.accordion-button-danger:not(.collapsed) {
|
||||
background-color: #f8d7da;
|
||||
color: #b02a37;
|
||||
.input-group-lg > .input-group-item .form-control,
|
||||
.input-group-lg > .input-group-item .form-select {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1.25rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.accordion-button-light {
|
||||
background-color: #fefeff;
|
||||
color: #dfe0e1;
|
||||
}
|
||||
.accordion-button-light:not(.collapsed) {
|
||||
background-color: #fefefe;
|
||||
color: #c6c7c8;
|
||||
|
||||
.input-group-sm > .input-group-item .form-control,
|
||||
.input-group-sm > .input-group-item .form-select {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.accordion-button-dark {
|
||||
background-color: #e9e9ea;
|
||||
color: #1e2125;
|
||||
}
|
||||
.accordion-button-dark:not(.collapsed) {
|
||||
background-color: #d3d3d4;
|
||||
color: #1a1e21;
|
||||
.input-group-lg > .input-group-item .form-select,
|
||||
.input-group-sm > .input-group-item .form-select {
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.tabulator-edit-list .tabulator-edit-list-item {
|
||||
background-color: white;
|
||||
.input-group:not(.has-validation) > .input-group-item:not(:last-child) .form-control,
|
||||
.input-group:not(.has-validation) > .input-group-item:not(:last-child) .form-select {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group.has-validation > .input-group-item:nth-last-child(n+3) .form-control,
|
||||
.input-group.has-validation > .input-group-item:nth-last-child(n+3) .form-select {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group > .input-group-item:not(:first-child) .form-control,
|
||||
.input-group > .input-group-item:not(:first-child) .form-select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-color.is-invalid,
|
||||
.was-validated .form-control-color:invalid,
|
||||
.form-control-color.is-valid,
|
||||
.was-validated .form-control-color:valid {
|
||||
padding-right: .375rem;
|
||||
background-image: none;
|
||||
}
|
||||
.tabulator-edit-list .tabulator-edit-list-item:hover,
|
||||
.tabulator-edit-list .tabulator-edit-list-item.active {
|
||||
color: white;
|
||||
}
|
||||
@@ -64,3 +64,12 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tabulator {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tabulator-cell .btn {
|
||||
padding: 0 .375rem;
|
||||
font-size: .875rem;
|
||||
border-radius: .2rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
/*
|
||||
* To be moved outside
|
||||
*/
|
||||
.navbar.navbar-left-side ~ *,
|
||||
#content {
|
||||
position: inherit;
|
||||
margin: 0 0 0 250px;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.nav-item.nav-link:focus {
|
||||
box-shadow: 0 0 0 .24rem rgba(13,110,253,.25);
|
||||
z-index: 1;
|
||||
outline: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-item.nav-link:focus::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -.5rem;
|
||||
right: -.5rem;
|
||||
top: calc(100% + 1px);
|
||||
background: white;
|
||||
height: .25rem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
@import '../../../vendor/vuejs/vuedatepicker_css/main.css';
|
||||
:root {
|
||||
/*General*/
|
||||
--dp-font-family: var(--bs-body-font-family);
|
||||
--dp-border-radius: .25rem;
|
||||
--dp-cell-border-radius: .25rem; /*Specific border radius for the calendar cell*/
|
||||
|
||||
/*Sizing*/
|
||||
--dp-button-height: 2.1875rem; /*Size for buttons in overlays*/
|
||||
--dp-month-year-row-height: 2.1875rem; /*Height of the month-year select row*/
|
||||
--dp-month-year-row-button-size: 2.1875rem; /*Specific height for the next/previous buttons*/
|
||||
--dp-button-icon-height: 1.25rem; /*Icon sizing in buttons*/
|
||||
--dp-cell-size: 2.1875rem; /*Width and height of calendar cell*/
|
||||
--dp-cell-padding: .3125rem; /*Padding in the cell*/
|
||||
--dp-common-padding: .625rem; /*Common padding used*/
|
||||
--dp-input-icon-padding: 2.1875rem;
|
||||
--dp-input-padding: .375rem .75rem;
|
||||
--dp-action-buttons-padding: .125rem .3125rem; /*Adjust padding for the action buttons in action row*/
|
||||
--dp-row-margin: .3125rem 0; /*Adjust the spacing between rows in the calendar*/
|
||||
--dp-calendar-header-cell-padding: 0.5rem; /*Adjust padding in calendar header cells*/
|
||||
--dp-two-calendars-spacing: .625rem; /*Space between multiple calendars*/
|
||||
--dp-overlay-col-padding: .1875rem; /*Padding in the overlay column*/
|
||||
--dp-time-inc-dec-button-size: 2rem; /*Sizing for arrow buttons in the time picker*/
|
||||
--dp-menu-padding: .375rem .5rem; /*Menu padding*/
|
||||
}
|
||||
.dp__theme_light {
|
||||
--dp-text-color: var(--text-color);
|
||||
--dp-primary-color: var(--bs-primary);
|
||||
--dp-primary-disabled-color: rgba(var(--bs-primary-rgb), .65);
|
||||
--dp-primary-text-color: var(--primary-color-text);
|
||||
--dp-secondary-color: var(--bs-secondary);
|
||||
--dp-border-color: var(--bs-gray-400);
|
||||
--dp-menu-border-color: var(--bs-gray-400);
|
||||
--dp-border-color-hover: var(--bs-gray-400);
|
||||
--dp-icon-color: rgba(var(--bs-black-rgb), .5);
|
||||
--dp-hover-icon-color: rgba(var(--bs-black-rgb), .75);
|
||||
--dp-range-between-dates-text-color: var(--dp-hover-text-color, var(--text-color));
|
||||
}
|
||||
.dp__theme_light .form-control.is-invalid {
|
||||
--dp-border-color-hover: var(--bs-danger);
|
||||
}
|
||||
.dp__theme_light .form-control.is-valid {
|
||||
--dp-border-color-hover: var(--bs-success);
|
||||
}
|
||||
.form-control.is-invalid ~ .dp__clear_icon,
|
||||
.was-validated .form-control:invalid ~ .dp__clear_icon { margin-right: calc(1.5em + .75rem - 12px) }
|
||||
@@ -0,0 +1,123 @@
|
||||
import FhcFragment from "../Fragment.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FhcFragment
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
$registerToForm: component => {
|
||||
if (this.inputs.indexOf(component) < 0)
|
||||
this.inputs.push(component);
|
||||
},
|
||||
$clearValidationForName: this.clearValidationForName
|
||||
};
|
||||
},
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'form'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputs: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedInputs() {
|
||||
return this.inputs.reduce((a,c) => {
|
||||
let name = c.name || '_default';
|
||||
if (!a[name])
|
||||
a[name] = [];
|
||||
a[name].push(c);
|
||||
|
||||
if (c.lcType == 'checkbox' && name.substr(-1) == ']' && name.indexOf('[')) {
|
||||
name = name.substr(0, name.lastIndexOf('['));
|
||||
if (!a[name])
|
||||
a[name] = [];
|
||||
a[name].push(c);
|
||||
}
|
||||
|
||||
return a;
|
||||
}, {});
|
||||
},
|
||||
factory() {
|
||||
const factory = Object.create(Object.getPrototypeOf(this.$fhcApi.factory), Object.getOwnPropertyDescriptors(this.$fhcApi.factory));
|
||||
factory.$fhcApi = {
|
||||
get: this.get,
|
||||
post: this.post
|
||||
};
|
||||
return factory;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
get(...args) {
|
||||
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
|
||||
args[0] = this;
|
||||
else
|
||||
args.unshift(this);
|
||||
|
||||
return this.$fhcApi.get(...args);
|
||||
},
|
||||
post(...args) {
|
||||
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
|
||||
args[0] = this;
|
||||
else
|
||||
args.unshift(this);
|
||||
|
||||
return this.$fhcApi.post(...args);
|
||||
},
|
||||
_sendFeedbackToInput(inputs, feedback, valid) {
|
||||
if (inputs.length) {
|
||||
inputs.forEach(input => input.setFeedback(valid, feedback));
|
||||
return false;
|
||||
}
|
||||
if (this.$fhcAlert) {
|
||||
this.$fhcAlert[valid ? 'alertSuccess' : 'alertError'](feedback);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
setFeedback(valid, feedback) {
|
||||
if (Array.isArray(feedback)) {
|
||||
let remaining = feedback.filter(fb =>
|
||||
this._sendFeedbackToInput(
|
||||
this.sortedInputs['_default'] || [],
|
||||
fb,
|
||||
valid
|
||||
)
|
||||
);
|
||||
return remaining.length ? remaining : null;
|
||||
}
|
||||
if (typeof feedback === 'object') {
|
||||
let remaining = Object.entries(feedback).filter(([name, fb]) =>
|
||||
this._sendFeedbackToInput(
|
||||
this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [],
|
||||
fb,
|
||||
valid
|
||||
)
|
||||
);
|
||||
return remaining.length ? Object.fromEntries(remaining) : null;
|
||||
}
|
||||
|
||||
let remaining = this._sendFeedbackToInput(
|
||||
this.sortedInputs['_default'] || [],
|
||||
feedback,
|
||||
valid
|
||||
);
|
||||
return remaining ? feedback : null;
|
||||
},
|
||||
clearValidation() {
|
||||
this.inputs.forEach(input => input.clearValidation());
|
||||
},
|
||||
clearValidationForName(name) {
|
||||
(this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [])
|
||||
.forEach(input => input.clearValidation());
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<component :is="tag || 'FhcFragment'" v-bind="$attrs">
|
||||
<slot></slot>
|
||||
</component>`
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import FhcFragment from "../Fragment.js";
|
||||
|
||||
let _uuid = {};
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
components: {
|
||||
FhcFragment
|
||||
},
|
||||
inject: {
|
||||
registerToForm: {
|
||||
from: '$registerToForm',
|
||||
default: null
|
||||
},
|
||||
clearValidationForName: {
|
||||
from: '$clearValidationForName',
|
||||
default: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
bsFeedback: Boolean,
|
||||
noAutoClass: Boolean,
|
||||
noFeedback: Boolean,
|
||||
inputGroup: Boolean,
|
||||
type: String,
|
||||
name: String,
|
||||
containerClass: [String, Array, Object]
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valid: undefined,
|
||||
feedback: [],
|
||||
modelValueDummy: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasContainer() {
|
||||
if (!this.bsFeedback)
|
||||
return true;
|
||||
if (this.containerClass)
|
||||
return true;
|
||||
for (const prop in this.autoContainerClass)
|
||||
if (Object.hasOwn(this.autoContainerClass, prop))
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
acc() {
|
||||
if (!this.containerClass)
|
||||
return {};
|
||||
if (typeof this.containerClass === 'string' || this.containerClass instanceof String)
|
||||
return this.containerClass.split(' ').reduce((a,c) => {a[c] = true; return a}, {});
|
||||
if (Array.isArray(this.containerClass))
|
||||
return this.containerClass.reduce((a,c) => {a[c] = true; return a}, {});
|
||||
return this.containerClass;
|
||||
},
|
||||
autoContainerClass() {
|
||||
if (this.noAutoClass)
|
||||
return this.acc;
|
||||
|
||||
const acc = {...this.acc};
|
||||
|
||||
if (this.inputGroup)
|
||||
acc['input-group-item'] = true;
|
||||
|
||||
if (this.lcType == 'radio' || this.lcType == 'checkbox')
|
||||
acc['form-check'] = true;
|
||||
|
||||
if (this.inputGroup && acc['form-check']) {
|
||||
acc['input-group-item'] = false;
|
||||
acc['form-check'] = false;
|
||||
acc['input-group-text'] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
lcType() {
|
||||
if (!this.type)
|
||||
return 'text';
|
||||
return this.type.toLowerCase();
|
||||
},
|
||||
tag() {
|
||||
switch (this.lcType) {
|
||||
case 'textarea':
|
||||
case 'select':
|
||||
return this.lcType;
|
||||
case 'datepicker':
|
||||
return 'VueDatePicker';
|
||||
case 'autocomplete':
|
||||
return 'PvAutocomplete';
|
||||
case 'uploadimage':
|
||||
return 'UploadImage';
|
||||
case 'uploadfile':
|
||||
case 'uploaddms':
|
||||
return 'UploadDms';
|
||||
default:
|
||||
return 'input';
|
||||
}
|
||||
},
|
||||
validationClass() {
|
||||
const classes = [];
|
||||
if (this.valid)
|
||||
classes.push('is-valid');
|
||||
else if (this.valid === false)
|
||||
classes.push('is-invalid');
|
||||
|
||||
if (!this.noAutoClass) {
|
||||
let c = this.$attrs.class ? this.$attrs.class.split(' ') : [];
|
||||
switch (this.lcType) {
|
||||
// TODO(chris): complete list!
|
||||
case 'select':
|
||||
if (!c.includes('form-select'))
|
||||
classes.push('form-select');
|
||||
break;
|
||||
case 'range':
|
||||
if (!c.includes('form-range'))
|
||||
classes.push('form-range');
|
||||
break;
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
// TODO(chris): maybe different handling?
|
||||
if (!c.includes('form-check-input') && !c.includes('btn-check'))
|
||||
classes.push('form-check-input');
|
||||
break;
|
||||
case 'color':
|
||||
if (!c.includes('form-control-color'))
|
||||
classes.push('form-control-color');
|
||||
if (!c.includes('form-control'))
|
||||
classes.push('form-control');
|
||||
break;
|
||||
case 'autocomplete':
|
||||
case 'datepicker':
|
||||
classes.push('p-0');
|
||||
classes.push('border-0');
|
||||
if (!c.includes('form-control'))
|
||||
classes.push('form-control');
|
||||
break;
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'password':
|
||||
case 'textarea':
|
||||
if (!c.includes('form-control'))
|
||||
classes.push('form-control');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
feedbackClass() {
|
||||
if (!this.feedback || this.feedback === true)
|
||||
return '';
|
||||
if (!this.bsFeedback)
|
||||
return {
|
||||
'valid-tooltip': this.valid === true,
|
||||
'invalid-tooltip': this.valid === false
|
||||
};
|
||||
return {
|
||||
'valid-feedback': this.valid === true,
|
||||
'invalid-feedback': this.valid === false
|
||||
};
|
||||
},
|
||||
modelValueCmp: {
|
||||
get() {
|
||||
if (this.$attrs.modelValue === undefined)
|
||||
return this.modelValueDummy;
|
||||
return this.$attrs.modelValue;
|
||||
},
|
||||
set(v) {
|
||||
if (this.$attrs.modelValue === undefined)
|
||||
this.modelValueDummy = v;
|
||||
this.$emit('update:modelValue', v);
|
||||
}
|
||||
},
|
||||
idCmp() {
|
||||
let uuid = this.$attrs.id;
|
||||
if (this.lcType == 'datepicker')
|
||||
uuid = this.$attrs.uid;
|
||||
if (!uuid && this.$attrs.label)
|
||||
uuid = 'fhc-form-input';
|
||||
if (!uuid)
|
||||
return undefined;
|
||||
if (this.lcType == 'datepicker')
|
||||
uuid = 'dp-input-' + uuid;
|
||||
if (_uuid[uuid] === undefined)
|
||||
_uuid[uuid] = 0;
|
||||
return uuid + '-' + (_uuid[uuid]++);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearValidation() {
|
||||
this.valid = undefined;
|
||||
this.feedback = [];
|
||||
},
|
||||
clearValidationForThisName() {
|
||||
if (this.valid === undefined)
|
||||
return;
|
||||
if (this.clearValidationForName && this.name)
|
||||
this.clearValidationForName(this.name);
|
||||
else
|
||||
this.clearValidation();
|
||||
},
|
||||
setFeedback(valid, feedback) {
|
||||
if (!feedback)
|
||||
feedback = [];
|
||||
if (!Array.isArray(feedback))
|
||||
feedback = [feedback];
|
||||
this.valid = valid;
|
||||
// NOTE(chris): On a list of radios/checkboxes only add the feedback message to the last item
|
||||
if (this.name && (this.lcType == 'radio' || this.lcType == 'checkbox')) {
|
||||
const selector = 'input[type="' + this.lcType + '"][name="' + this.name + '"]';
|
||||
if ([...this.$el.parentNode.querySelectorAll(selector)].pop() != this.$refs.input)
|
||||
return;
|
||||
}
|
||||
this.feedback = feedback;
|
||||
},
|
||||
_loadComponents() {
|
||||
if (this.tag == 'VueDatePicker' && !this._.components.VueDatePicker) {
|
||||
this._.components.VueDatePicker = Vue.defineAsyncComponent(() => import("../vueDatepicker.js.php"));
|
||||
} else if (this.tag == 'PvAutocomplete' && !this._.components.PvAutocomplete) {
|
||||
this._.components.PvAutocomplete = Vue.defineAsyncComponent(() => import(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"));
|
||||
} else if (this.tag == 'UploadImage' && !this._.components.UploadImage) {
|
||||
this._.components.UploadImage = Vue.defineAsyncComponent(() => import("./Upload/Image.js"));
|
||||
} else if (this.tag == 'UploadDms' && !this._.components.UploadDms) {
|
||||
this._.components.UploadDms = Vue.defineAsyncComponent(() => import("./Upload/Dms.js"));
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this._loadComponents();
|
||||
},
|
||||
beforeUpdate() {
|
||||
this._loadComponents();
|
||||
},
|
||||
mounted() {
|
||||
if (this.registerToForm)
|
||||
this.registerToForm(this);
|
||||
},
|
||||
template: `
|
||||
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="autoContainerClass">
|
||||
<label v-if="$attrs.label && lcType != 'radio' && lcType != 'checkbox'" :for="idCmp">{{$attrs.label}}</label>
|
||||
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
|
||||
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)"></textarea>
|
||||
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
|
||||
<slot></slot>
|
||||
</select>
|
||||
<component
|
||||
v-else-if="tag == 'VueDatePicker'"
|
||||
ref="input"
|
||||
:is="tag"
|
||||
:type="type"
|
||||
v-model="modelValueCmp"
|
||||
v-bind="$attrs"
|
||||
:uid="idCmp ? idCmp.substr(9) : idCmp"
|
||||
:name="name"
|
||||
:class="validationClass"
|
||||
:input-class-name=
|
||||
"[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class-name'] ? $attrs['input-class-name'].split(' ') : [])].join(' ')"
|
||||
@update:model-value="clearValidationForThisName"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
<component
|
||||
v-else-if="tag == 'PvAutocomplete'"
|
||||
ref="input"
|
||||
:is="tag"
|
||||
:type="type"
|
||||
v-model="modelValueCmp"
|
||||
v-bind="$attrs"
|
||||
:id="idCmp"
|
||||
:input-props="{name}"
|
||||
:class="validationClass"
|
||||
:input-class="[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class'] ? $attrs['input-class'].split(' ') : [])].join(' ')"
|
||||
@update:model-value="clearValidationForThisName"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
<component
|
||||
v-else-if="tag == 'UploadDms'"
|
||||
ref="input"
|
||||
:is="tag"
|
||||
:type="type"
|
||||
v-model="modelValueCmp"
|
||||
v-bind="$attrs"
|
||||
:id="idCmp"
|
||||
:name="name"
|
||||
:class="validationClass"
|
||||
:input-class="validationClass"
|
||||
:no-list="inputGroup"
|
||||
@update:model-value="clearValidationForThisName"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
<component
|
||||
v-else
|
||||
ref="input"
|
||||
:is="tag"
|
||||
:type="type"
|
||||
v-model="modelValueCmp"
|
||||
v-bind="$attrs"
|
||||
:id="idCmp"
|
||||
:name="name"
|
||||
:class="validationClass"
|
||||
@update:model-value="clearValidationForThisName"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
<label v-if="$attrs.label && (lcType == 'radio' || lcType == 'checkbox')" :for="idCmp" :class="!noAutoClass && 'form-check-label'">{{$attrs.label}}</label>
|
||||
<div v-if="valid !== undefined && feedback.length && !noFeedback" :class="feedbackClass">
|
||||
<template v-for="(msg, i) in feedback" :key="i">
|
||||
<hr v-if="i" class="m-0">
|
||||
{{msg}}
|
||||
</template>
|
||||
</div>
|
||||
</component>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export default {
|
||||
emits: [
|
||||
'update:modelValue'
|
||||
],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [ FileList, Array ],
|
||||
required: true
|
||||
},
|
||||
multiple: Boolean,
|
||||
id: String,
|
||||
name: String,
|
||||
inputClass: [String, Array, Object],
|
||||
noList: Boolean
|
||||
},
|
||||
methods: {
|
||||
stringifyFile(file) {
|
||||
return JSON.stringify({
|
||||
lastModified: file.lastModified,
|
||||
lastModifiedDate: file.lastModifiedDate,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
});
|
||||
},
|
||||
addFiles(event) {
|
||||
if (!this.multiple)
|
||||
return this.$emit('update:modelValue', event.target.files);
|
||||
|
||||
const dt = new DataTransfer();
|
||||
const doubles = [];
|
||||
for (var file of this.modelValue) {
|
||||
dt.items.add(file);
|
||||
doubles.push(this.stringifyFile(file));
|
||||
}
|
||||
for (var file of event.target.files) {
|
||||
// NOTE(chris): deep check (with FileReader) would require an async function so we only check the basic attributes
|
||||
if (doubles.indexOf(this.stringifyFile(file)) < 0)
|
||||
dt.items.add(file);
|
||||
}
|
||||
this.$emit('update:modelValue', dt.files);
|
||||
},
|
||||
removeFile(id) {
|
||||
const fileToRemove = Array.from(this.modelValue)[id];
|
||||
|
||||
const dt = new DataTransfer();
|
||||
for (var file of this.modelValue) {
|
||||
if (file !== fileToRemove)
|
||||
dt.items.add(file);
|
||||
}
|
||||
this.$emit('update:modelValue', dt.files);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(n) {
|
||||
if (n instanceof FileList)
|
||||
return this.$refs.upload.files = n;
|
||||
|
||||
const dt = new DataTransfer();
|
||||
const dms = [];
|
||||
for (var file of n) {
|
||||
if (file instanceof File) {
|
||||
dt.items.add(file);
|
||||
} else {
|
||||
const dmsFile = new File([JSON.stringify(file)], file.name, {
|
||||
type: 'application/x.fhc-dms+json'
|
||||
});
|
||||
dt.items.add(dmsFile);
|
||||
}
|
||||
}
|
||||
this.$emit('update:modelValue', dt.files);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-upload-dms">
|
||||
<input ref="upload" class="form-control" :class="inputClass" :id="id" :name="name" :multiple="multiple" type="file" @change="addFiles">
|
||||
<ul v-if="modelValue.length && multiple && !noList" class="list-unstyled m-0">
|
||||
<li v-for="(file, index) in modelValue" :key="index" class="d-flex mx-1 mt-1 align-items-start">
|
||||
<span class="col-auto"><i class="fa fa-file me-1"></i></span>
|
||||
<span class="col">{{ file.name }}</span>
|
||||
<button class="col-auto btn btn-outline-secondary btn-p-0" @click="removeFile(index)">
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
export default {
|
||||
emits: [
|
||||
'update:modelValue'
|
||||
],
|
||||
props: {
|
||||
modelValue: String
|
||||
},
|
||||
computed: {
|
||||
valueAsBase64DataString() {
|
||||
if (!this.modelValue || this.modelValue.substring(0, 10) == 'data:image')
|
||||
return this.modelValue;
|
||||
return 'data:image/jpeg;charset=utf-8;base64,' + this.modelValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openUploadDialog() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
pickFile() {
|
||||
let file = this.$refs.fileInput.files;
|
||||
if (file && file[0]) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
this.$emit('update:modelValue', e.target.result);
|
||||
}
|
||||
reader.readAsDataURL(file[0]);
|
||||
}
|
||||
},
|
||||
deleteImage() {
|
||||
this.$emit('update:modelValue', '');
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-upload-image">
|
||||
<template v-if="modelValue">
|
||||
<img class="img-thumbnail" :src="valueAsBase64DataString" />
|
||||
<div class="fotobutton">
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="button" class="btn btn-outline-dark btn-sm" @click="deleteImage">
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
|
||||
<i class="fa fa-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot>
|
||||
<svg class="bd-placeholder-img img-thumbnail" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera: 200x200" preserveAspectRatio="xMidYMid slice" focusable="false"><title>A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera</title><rect width="100%" height="100%" fill="#868e96"></rect><text x="50%" y="50%" fill="#dee2e6" dy=".3em"></text></svg>
|
||||
</slot>
|
||||
<div class="fotobutton-visible">
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
|
||||
<i class="fa fa-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<input :id="$attrs.id" class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export default {
|
||||
inject: [
|
||||
'$registerToForm'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
feedback: {
|
||||
success: [],
|
||||
danger: []
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clearValidation() {
|
||||
this.feedback = {
|
||||
success: [],
|
||||
danger: []
|
||||
};
|
||||
},
|
||||
setFeedback(valid, feedback) {
|
||||
if (!feedback)
|
||||
feedback = [];
|
||||
if (!Array.isArray(feedback))
|
||||
feedback = [feedback];
|
||||
const ts = Date.now();
|
||||
this.feedback[valid ? 'success' : 'danger'] = feedback.map(msg => [msg, ts]);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$registerToForm)
|
||||
this.$registerToForm(this);
|
||||
},
|
||||
template: `
|
||||
<template v-for="(arr, key) in feedback" :key="key">
|
||||
<div v-for="[msg, ts] in arr" :key="ts + msg" class="alert alert-dismissible fade show" :class="'alert-' + key" role="alert">
|
||||
{{msg}}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
render() {
|
||||
return (this.$slots && this.$slots.default) ? this.$slots.default() : null;
|
||||
}
|
||||
};
|
||||
@@ -6,12 +6,19 @@ export default {
|
||||
accessibility
|
||||
},
|
||||
emits: [
|
||||
'update:modelValue'
|
||||
'update:modelValue',
|
||||
'change',
|
||||
'changed'
|
||||
],
|
||||
props: {
|
||||
configUrl: String,
|
||||
config: {
|
||||
type: [String, Object],
|
||||
required: true
|
||||
},
|
||||
default: String,
|
||||
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol]
|
||||
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol],
|
||||
vertical: Boolean,
|
||||
border: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -35,50 +42,84 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
CoreRESTClient
|
||||
.get(this.configUrl)
|
||||
.then(result => CoreRESTClient.getData(result.data))
|
||||
.then(result => {
|
||||
const tabs = {};
|
||||
// TODO(chris): check if result is array
|
||||
Object.entries(result).forEach(([key, config]) => {
|
||||
if (!config.component)
|
||||
watch: {
|
||||
config(n) {
|
||||
this.initConfig(n);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change(key) {
|
||||
this.$emit("change", key)
|
||||
this.current = key;
|
||||
this.$nextTick(() => this.$emit("changed", key));
|
||||
},
|
||||
initConfig(config) {
|
||||
if (!config)
|
||||
return;
|
||||
if (typeof config === 'string' || config instanceof String)
|
||||
return CoreRESTClient.get(config)
|
||||
.then(result => CoreRESTClient.getData(result.data))
|
||||
.then(this.initConfig)
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
|
||||
const tabs = {};
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
config.forEach((item, key) => {
|
||||
if (!item.component)
|
||||
return console.error('Component missing for ' + key);
|
||||
|
||||
tabs[key] = {
|
||||
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))),
|
||||
title: config.title || key,
|
||||
config: config.config,
|
||||
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
|
||||
title: item.title || key,
|
||||
config: item.config,
|
||||
key
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Object.entries(config).forEach(([key, item]) => {
|
||||
if (!item.component)
|
||||
return console.error('Component missing for ' + key);
|
||||
|
||||
tabs[key] = {
|
||||
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
|
||||
title: item.title || key,
|
||||
config: item.config,
|
||||
key
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.current === null || !tabs[this.current]) {
|
||||
if (tabs[this.default])
|
||||
this.current = this.default;
|
||||
else
|
||||
this.current = Object.keys(tabs)[0];
|
||||
this.tabs = tabs;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
this.tabs = tabs;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initConfig(this.config);
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-tabs d-flex flex-column">
|
||||
<div class="nav nav-tabs">
|
||||
<div class="fhc-tabs d-flex" :class="vertical ? 'align-items-stretch gap-3' : (border ? 'flex-column' : 'flex-column gap-3')" v-if="Object.keys(tabs).length">
|
||||
<div class="nav" :class="vertical ? 'nav-pills flex-column' : 'nav-tabs'">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="nav-item nav-link"
|
||||
:class="{active: tab.key == current}"
|
||||
@click="current=tab.key"
|
||||
@click="change(tab.key)"
|
||||
:aria-current="tab.key == current ? 'page' : ''"
|
||||
v-accessibility:tab
|
||||
v-accessibility:tab.[vertical]
|
||||
>
|
||||
{{tab.title}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1 1 0%; height: 0%" class="border-bottom border-start border-end overflow-auto p-3">
|
||||
<div :style="vertical ? '' : 'flex: 1 1 0%; height: 0%'" class="overflow-auto" :class="vertical || !border ? '' : 'p-3 border-bottom border-start border-end'">
|
||||
<keep-alive>
|
||||
<component :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
|
||||
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
@@ -136,9 +136,9 @@ export const CoreFilterCmpt = {
|
||||
for (let col of columns)
|
||||
{
|
||||
// If the column has to be displayed or not
|
||||
col.visible = selectedFields.indexOf(col.field) >= 0;
|
||||
if (col.formatter == 'rowSelection')
|
||||
col.visible = true;
|
||||
/* fields.indexOf(col.field) == -1; ensures displaying formatter colums
|
||||
e.g. column with rowSelection checkboxes or with custom formatted action buttons */
|
||||
col.visible = selectedFields.indexOf(col.field) >= 0 || fields.indexOf(col.field) == -1;
|
||||
|
||||
if (col.hasOwnProperty('resizable'))
|
||||
col.resizable = col.visible;
|
||||
@@ -185,13 +185,23 @@ export const CoreFilterCmpt = {
|
||||
else
|
||||
this.getFilter();
|
||||
},
|
||||
initTabulator() {
|
||||
async initTabulator() {
|
||||
let placeholder = '< Phrasen Plugin not loaded! >';
|
||||
if (this.$p) {
|
||||
await this.$p.loadCategory('ui');
|
||||
placeholder = this.$p.t('ui/keineDatenVorhanden');
|
||||
}
|
||||
// Define a default tabulator options in case it was not provided
|
||||
let tabulatorOptions = {...{
|
||||
height: 500,
|
||||
layout: "fitColumns",
|
||||
layout: "fitDataStretch",
|
||||
movableColumns: true,
|
||||
reactiveData: true
|
||||
columnDefaults:{
|
||||
tooltip: true,
|
||||
},
|
||||
placeholder,
|
||||
reactiveData: true,
|
||||
persistence: true
|
||||
}, ...(this.tabulatorOptions || {})};
|
||||
|
||||
if (!this.tableOnly) {
|
||||
@@ -265,6 +275,7 @@ export const CoreFilterCmpt = {
|
||||
this.filterName = data.filterName;
|
||||
this.dataset = data.dataset;
|
||||
this.datasetMetadata = data.datasetMetadata;
|
||||
|
||||
this.fields = data.fields;
|
||||
this.selectedFields = data.selectedFields;
|
||||
this.notSelectedFields = this.fields.filter(x => this.selectedFields.indexOf(x) === -1);
|
||||
@@ -435,7 +446,7 @@ export const CoreFilterCmpt = {
|
||||
*
|
||||
*/
|
||||
handlerRemoveCustomFilter: function(event) {
|
||||
filterId = event.currentTarget.getAttribute("href").substring(1);
|
||||
let filterId = event.currentTarget.getAttribute("href").substring(1);
|
||||
if (filterId === this.selectedFilter)
|
||||
this.selectedFilter = null;
|
||||
//
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (C) 2022 fhcomplete.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
mainCols: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
asideCols: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
mainGridCols() {
|
||||
return this.mainCols.length > 0 ? `col-md-${this.mainCols[0]}` : "col-md-12";
|
||||
},
|
||||
asideGridCols() {
|
||||
return this.asideCols.length > 0 ? `col-md-${this.asideCols[0]}` : "";
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="overflow-hidden">
|
||||
<header v-if="title">
|
||||
<h1 class="h2 mb-5">{{ title }}<span class="fhc-subtitle">{{ subtitle }}</span></h1>
|
||||
</header>
|
||||
<div class="row gx-5">
|
||||
<main :class="mainGridCols">
|
||||
<slot name="main">{{ mainGridCols }}</slot>
|
||||
</main>
|
||||
<aside v-if="asideCols.length > 0" :class="asideGridCols">
|
||||
<slot name="aside">{{ asideGridCols }}</slot>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -133,8 +133,8 @@ const helperApp = Vue.createApp({
|
||||
helperAppContainer.parentElement.removeChild(helperAppContainer);
|
||||
},
|
||||
template: `
|
||||
<pv-toast ref="toast" base-z-index="99999"></pv-toast>
|
||||
<pv-toast ref="alert" base-z-index="99999" position="center">
|
||||
<pv-toast ref="toast" class="fhc-alert" :base-z-index="99999"></pv-toast>
|
||||
<pv-toast ref="alert" class="fhc-alert" :base-z-index="99999" position="center">
|
||||
<template #message="slotProps">
|
||||
<i class="fa fa-circle-exclamation fa-2xl mt-3"></i>
|
||||
<div class="p-toast-message-text">
|
||||
@@ -161,7 +161,7 @@ const helperApp = Vue.createApp({
|
||||
</a>
|
||||
</div>
|
||||
<div ref="messageCard" :id="'fhcAlertCollapseMessageCard' + slotProps.message.id" class="collapse mt-3">
|
||||
<div class="card card-body text-body small" style="white-space: pre-wrap">
|
||||
<div class="card card-body text-body small">
|
||||
{{slotProps.message.detail}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@ export default {
|
||||
alertMultiple(messageArray, severity = 'info', title = 'Info', sticky = false){
|
||||
// Check, if array has only string values
|
||||
if (messageArray.every(message => typeof message === 'string')) {
|
||||
messageArray.every(message => this.alertDefault(severity, title, message, sticky));
|
||||
messageArray.forEach(message => this.alertDefault(severity, title, message, sticky));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -281,21 +281,21 @@ export default {
|
||||
handleSystemMessage(message) {
|
||||
// Message is string
|
||||
if (typeof message === 'string')
|
||||
return fhcerror.alertWarning(message);
|
||||
return $fhcAlert.alertWarning(message);
|
||||
|
||||
// Message is array of strings
|
||||
if (Array.isArray(message)) {
|
||||
// If Array has only Strings
|
||||
if (message.every(msg => typeof msg === 'string'))
|
||||
return message.every(fhcerror.alertWarning);
|
||||
return message.every($fhcAlert.alertWarning);
|
||||
|
||||
// If Array has only Objects
|
||||
if (message.every(msg => typeof msg === 'object') && msg !== null) {
|
||||
return message.every(msg => {
|
||||
if (msg.hasOwnProperty('data') && msg.data.hasOwnProperty('retval')) {
|
||||
fhcerror.alertWarning(JSON.stringify(msg.data.retval));
|
||||
$fhcAlert.alertWarning(JSON.stringify(msg.data.retval));
|
||||
} else {
|
||||
fhcerror.alertSystemError(JSON.stringify(msg));
|
||||
$fhcAlert.alertSystemError(JSON.stringify(msg));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -305,15 +305,15 @@ export default {
|
||||
if (typeof message === 'object' && message !== null){
|
||||
if (message.hasOwnProperty('data') && message.data.hasOwnProperty('retval')) {
|
||||
// NOTE(chris): changed: alertSystemError => alertWarning
|
||||
fhcerror.alertWarning(JSON.stringify(message.data.retval));
|
||||
$fhcAlert.alertWarning(JSON.stringify(message.data.retval));
|
||||
} else {
|
||||
fhcerror.alertSystemError(JSON.stringify(message));
|
||||
$fhcAlert.alertSystemError(JSON.stringify(message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
fhcerror.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method);
|
||||
$fhcAlert.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method);
|
||||
},
|
||||
resetFormValidation(form) {
|
||||
const event = new Event('fhc-form-reset');
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import FhcAlert from './FhcAlert.js';
|
||||
import FhcApiFactory from '../apps/api/fhcapifactory.js';
|
||||
|
||||
|
||||
export default {
|
||||
install: (app, options) => {
|
||||
app.use(FhcAlert);
|
||||
|
||||
function _get_config(form, uri, data, config) {
|
||||
if (typeof form == 'string' && config === undefined) {
|
||||
[uri, data, config] = [form, uri, data];
|
||||
form = undefined;
|
||||
} else if (form) {
|
||||
if (typeof form != 'object')
|
||||
throw new TypeError('Parameter 1 of _get_config must be an object or a string');
|
||||
if (uri === undefined && data === undefined && config === undefined) {
|
||||
config = form;
|
||||
form = undefined;
|
||||
}
|
||||
}
|
||||
if (form) {
|
||||
// NOTE(chris): check if form is fhc-form
|
||||
if (!form.clearValidation || !form.setFeedback)
|
||||
throw new TypeError("'form' is not a Form Component");
|
||||
|
||||
form = {
|
||||
clearValidation: form.clearValidation,
|
||||
setFeedback: form.setFeedback
|
||||
};
|
||||
|
||||
if (config)
|
||||
config.form = form;
|
||||
else
|
||||
config = {form};
|
||||
}
|
||||
|
||||
return [uri, data, config];
|
||||
}
|
||||
|
||||
function _clean_return_value(response) {
|
||||
const result = response.data;
|
||||
delete response.data;
|
||||
if (!result.meta)
|
||||
result.meta = {response};
|
||||
else
|
||||
result.meta.response = response;
|
||||
return result;
|
||||
}
|
||||
|
||||
const fhcApiAxios = axios.create({
|
||||
timeout: 5000,
|
||||
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/"
|
||||
});
|
||||
|
||||
fhcApiAxios.interceptors.request.use(config => {
|
||||
if (config.method != 'post' || !config.data)
|
||||
return config;
|
||||
|
||||
if (config.data instanceof FormData)
|
||||
return config;
|
||||
|
||||
if (!Object.values(config.data).every(item => {
|
||||
if (item instanceof FileList)
|
||||
return false;
|
||||
if (Array.isArray(item))
|
||||
return item.every(i => !(i instanceof File));
|
||||
return true;
|
||||
})) {
|
||||
const newData = Object.entries(config.data).reduce((nd, [key, item]) => {
|
||||
if (item instanceof FileList) {
|
||||
for (const file of item)
|
||||
nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file);
|
||||
} else if (Array.isArray(item)) {
|
||||
if (item.every(i => !(i instanceof File))) {
|
||||
nd.jsondata[key] = item;
|
||||
} else {
|
||||
item.forEach(file => nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file));
|
||||
}
|
||||
} else {
|
||||
nd.jsondata[key] = item;
|
||||
}
|
||||
return nd;
|
||||
}, {
|
||||
FormData: new FormData(),
|
||||
jsondata: {}
|
||||
});
|
||||
newData.FormData.append('_jsondata', JSON.stringify(newData.jsondata));
|
||||
config.data = newData.FormData;
|
||||
}
|
||||
|
||||
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.$fhcApi._defaultErrorHandlers[err.type])(err, response.config.form)
|
||||
);
|
||||
|
||||
return _clean_return_value(response);
|
||||
}, error => {
|
||||
if (error.code == 'ERR_CANCELED')
|
||||
return new Promise(() => {});
|
||||
|
||||
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 new Promise(() => {});
|
||||
}
|
||||
|
||||
// NOTE(chris): loop through errors
|
||||
error.response.data.errors = error.response.data.errors.filter(
|
||||
err => (error.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$fhcApi._defaultErrorHandlers[err.type])(err, error.config.form)
|
||||
);
|
||||
if (!error.response.data.errors.length)
|
||||
return new Promise(() => {});
|
||||
} else if (error.request) {
|
||||
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL);
|
||||
return new Promise(() => {});
|
||||
} else {
|
||||
app.config.globalProperties.$fhcAlert.alertError(error.message);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
app.config.globalProperties.$fhcApi = {
|
||||
get(form, uri, params, config) {
|
||||
[uri, params, config] = _get_config(form, uri, params, config);
|
||||
if (params) {
|
||||
if (config)
|
||||
config.params = params;
|
||||
else
|
||||
config = {params};
|
||||
}
|
||||
return fhcApiAxios.get(uri, config);
|
||||
},
|
||||
post(form, uri, data, config) {
|
||||
[uri, data, config] = _get_config(form, uri, data, config);
|
||||
return fhcApiAxios.post(uri, data, config);
|
||||
},
|
||||
_defaultErrorHandlers: {
|
||||
validation(error, form) {
|
||||
const $fhcAlert = app.config.globalProperties.$fhcAlert;
|
||||
|
||||
if (form) {
|
||||
form.clearValidation();
|
||||
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') {
|
||||
Object.entries(error.messages).forEach(
|
||||
([key, value]) => $fhcAlert.alertDefault('error', key, value, true)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
general(error, form) {
|
||||
const $fhcAlert = app.config.globalProperties.$fhcAlert;
|
||||
|
||||
if (form)
|
||||
form.setFeedback(false, error.message);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class FhcApiFactoryWrapper {
|
||||
constructor(factorypart, root) {
|
||||
if (root === undefined)
|
||||
this.$fhcApi = app.config.globalProperties.$fhcApi;
|
||||
else
|
||||
Object.defineProperty(this, '$fhcApi', {
|
||||
get() {
|
||||
return (root || this).$fhcApi;
|
||||
}
|
||||
})
|
||||
Object.keys(factorypart).forEach(key => {
|
||||
Object.defineProperty(this, key, {
|
||||
get() {
|
||||
if (typeof factorypart[key] == 'function')
|
||||
return factorypart[key].bind(this);
|
||||
return new FhcApiFactoryWrapper(factorypart[key], root || this);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
app.config.globalProperties.$fhcApi.factory = new FhcApiFactoryWrapper(FhcApiFactory);
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1279,6 +1279,32 @@ $filters = array(
|
||||
}
|
||||
',
|
||||
'oe_kurzbz' => null,
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'dataset_name' => 'exampledata',
|
||||
'filter_kurzbz' => 'exampledata',
|
||||
'description' => '{Beispieldaten Filter}',
|
||||
'sort' => 1,
|
||||
'default_filter' => true,
|
||||
'filter' => '
|
||||
{
|
||||
"name": "Alle Beispieldaten",
|
||||
"columns": [
|
||||
{"name": "uid"},
|
||||
{"name": "stringval"},
|
||||
{"name": "integerval"},
|
||||
{"name": "dateval"},
|
||||
{"name": "booleanval"},
|
||||
{"name": "moneyval"},
|
||||
{"name": "dokument_bezeichnung"},
|
||||
{"name": "textval"},
|
||||
{"name": "examplestatus_kurzbz"}
|
||||
],
|
||||
"filters": []
|
||||
}
|
||||
',
|
||||
'oe_kurzbz' => null
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -11395,6 +11395,26 @@ Any unusual occurrences
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'anrechnung',
|
||||
'phrase' => 'anrechnung',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Anrechnung',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Exemption',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'anrechnung',
|
||||
@@ -24196,7 +24216,227 @@ array(
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'geloescht',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Gelöscht',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Deleted',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'aenderungGespeichert',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Änderung gespeichert',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Saved changes',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'category' => 'global',
|
||||
'phrase' => 'datensatz',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Datensatz',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Dataset',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'category' => 'global',
|
||||
'phrase' => 'datensatzGenehmigen',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Datensatz genehmigen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Approve dataset',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'category' => 'global',
|
||||
'phrase' => 'datensatzAblehnen',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Datensatz ablehnen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Reject dataset',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'category' => 'global',
|
||||
'phrase' => 'datensatzAnlegen',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Datensatz anlegen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Add dataset',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'fhctemplate',
|
||||
'category' => 'global',
|
||||
'phrase' => 'datensatzBearbeiten',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Datensatz bearbeiten',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Edit dataset',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'alleGenehmigt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Alle genehmigt',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'All accepted',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'alleAbgelehnt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Alle abgelehnt',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'All rejected',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'dokument',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Dokument',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Document',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'global',
|
||||
'phrase' => 'aktionen',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Aktionen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Actions',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user