diff --git a/application/controllers/api/frontend/v1/Udf.php b/application/controllers/api/frontend/v1/Udf.php new file mode 100644 index 000000000..99ed2a10d --- /dev/null +++ b/application/controllers/api/frontend/v1/Udf.php @@ -0,0 +1,142 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the UDFLib (back-end) + * Provides data to the ajax get calls about the Udf component + * Listens to ajax post calls to change the Udf data + * This controller works with JSON calls on the HTTP GET or POST and the output is always JSON + */ +class Udf extends FHCAPI_Controller +{ + /** + * Calls the parent's constructor and prepares the UDFLib + */ + public function __construct() + { + // TODO(chris): permissions + // NOTE: UdfLib has its own permissions checks + parent::__construct([ + 'load' => 'admin:r',#self::PERM_LOGGED, + 'save' => 'admin:r'#self::PERM_LOGGED + ]); + + // Libraries + $this->load->library('form_validation'); + $this->load->library('UDFLib'); + + // Models + $this->load->model($this->getTargetModelPath(), 'TargetModel'); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * Load all UDFs for a dataset + * + * @return void + */ + public function load() + { + $pks = $this->TargetModel->getPks(); + foreach ($pks as $id) + $this->form_validation->set_rules($id, $id, 'required'); + + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + + $id = []; + foreach ($pks as $pk) + $id[$pk] = $this->input->post($pk); + if (!is_array($this->TargetModel->getPk())) + $id = current($id); + + $result = $this->udflib->getFieldArray($this->TargetModel, $id); + + #$fields = $this->getDataOrTerminateWithError($result); + if (isError($result)) + $this->terminateWithError(getError($result)); + $fields = $result->retval; + + $this->terminateWithSuccess($fields); + } + + /** + * Saves UDFs to a dataset + * + * @return void + */ + public function save() + { + $pks = $this->TargetModel->getPks(); + foreach ($pks as $id) + $this->form_validation->set_rules($id, $id, 'required'); + + $result = $this->udflib->getCiValidations($this->TargetModel, $this->input->post()); + + #$fieldValidations = $this->getDataOrTerminateWithError($result); + if (isError($result)) + $this->terminateWithError(getError($result)); + $fieldvalidations = $result->retval; + + $this->form_validation->set_rules($fieldvalidations); + + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + + $id = []; + $fields = $this->input->post(); + foreach ($pks as $pk) { + $id[$pk] = $fields[$pk]; + unset($fields[$pk]); + } + if (!is_array($this->TargetModel->getPk())) + $id = current($id); + + $result = $this->TargetModel->update($id, $fields); + + #$this->getDataOrTerminateWithError($result); + if (isError($result)) + $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); + + $this->terminateWithSuccess(array_fill_keys(array_keys($fields), '')); + } + + //------------------------------------------------------------------------------------------------------------------ + // Private methods + + /** + * Get the path to the target model from the url + * + * @return string + */ + private function getTargetModelPath() + { + $ci_model_path = array_slice($this->uri->rsegments, 2); + if ($ci_model_path) + $ci_model_path[] = ucfirst(array_pop($ci_model_path)) . '_model'; + return implode(DIRECTORY_SEPARATOR, $ci_model_path); + } +} \ No newline at end of file diff --git a/application/core/DB_Model.php b/application/core/DB_Model.php index 79040dc66..c799de516 100644 --- a/application/core/DB_Model.php +++ b/application/core/DB_Model.php @@ -827,6 +827,23 @@ class DB_Model extends CI_Model return $result; } + public function getDbTable() + { + return $this->dbTable; + } + + public function getPk() + { + return $this->pk; + } + + public function getPks() + { + if (is_array($this->pk)) + return $this->pk; + return [$this->pk]; + } + // ------------------------------------------------------------------------------------------ // Protected methods diff --git a/application/libraries/UDFLib.php b/application/libraries/UDFLib.php index c5f0d3e98..2fa5dd0d4 100644 --- a/application/libraries/UDFLib.php +++ b/application/libraries/UDFLib.php @@ -65,6 +65,8 @@ class UDFLib private $_udfUniqueId; // Property that contains the UDF widget unique id + private $_definition_cache = []; + /** * Gets CI instance */ @@ -613,6 +615,160 @@ class UDFLib ); } + /** + * Gets the UDF definitions for a model + * + * @param DB_Model $targetModel + * + * @return stdClass + */ + public function getDefinitionForModel($targetModel) + { + $dbTable = $targetModel->getDbTable(); + if (!isset($this->_definition_cache[$dbTable])) { + $this->_ci->load->model('system/UDF_model', 'UDFModel'); + list($schema, $table) = explode('.', $dbTable); + $result = $this->_ci->UDFModel->loadWhere([ + 'schema' => $schema, + 'table' => $table + ]); + if (isError($result)) + return $result; + + $this->_definition_cache[$dbTable] = json_decode(current($result->retval)->jsons, true); + } + return success($this->_definition_cache[$dbTable]); + } + + /** + * Gets the UDFs for db entry with translated params and resolved listValues for dropdowns + * + * @param DB_Model $targetModel + * @param mixed $id + * + * @return stdClass + */ + public function getFieldArray($targetModel, $id) + { + // Load Libraries + $this->_ci->load->library('PhrasesLib'); + $this->_ci->load->library('PermissionLib'); + + $result = $this->getDefinitionForModel($targetModel); + if (isError($result)) + return $result; + $definitions = getData($result); + + usort($definitions, function ($a, $b) { + return $a[self::SORT] - $b[self::SORT]; + }); + + $values = $targetModel->getUDFs($id); + + $fields = []; + foreach ($definitions as $field) { + // check read permissions + if (!$this->_ci->permissionlib->hasAtLeastOne( + $field[self::REQUIRED_PERMISSIONS_PARAMETER], + self::PERMISSION_TABLE_METHOD, + self::PERMISSION_TYPE_READ + )) + continue; + + // set value + if (isset($values[$field[self::NAME]])) { + $field['value'] = $values[$field[self::NAME]]; + } elseif (isset($field['defaultValue'])) { + $field['value'] = $field['defaultValue']; + } elseif (isset($field[self::TYPE]) && $field[self::TYPE] == 'checkbox') { + $field['value'] = false; + } else { + $field['value'] = ''; + } + + // translate params + foreach ([self::LABEL, self::TITLE, self::PLACEHOLDER] as $key) { + if (isset($field[$key])) { + $res = $this->_ci->phraseslib->getPhrases(self::PHRASES_APP_NAME, getUserLanguage(), $field[$key], null, null, 'no'); + if (hasData($res)) + $field[$key] = current(getData($res))->text; + } + } + + // check write permissions + $field['disabled'] = !$this->_ci->permissionlib->hasAtLeastOne( + $field[self::REQUIRED_PERMISSIONS_PARAMETER], + self::PERMISSION_TABLE_METHOD, + self::PERMISSION_TYPE_WRITE + ); + + // set listValues for dropdowns + if (isset($field[self::LIST_VALUES])) { + if (isset($field[self::LIST_VALUES]['enum'])) { + $field['options'] = $field[self::LIST_VALUES]['enum']; + } elseif (isset($field[self::LIST_VALUES]['sql'])) { + $res = $this->_ci->UDFModel->execReadOnlyQuery($field[self::LIST_VALUES]['sql']); + $field['options'] = hasData($res) ? getData($res) : []; + } + } + + // add to array + $fields[] = $field; + } + return success($fields); + } + + /** + * Gets a validation config array for CI form_validation + * + * @param DB_Model $targetModel + * @param array (optional) $filter + * + * @return stdClass + */ + public function getCiValidations($targetModel, $filter = null) + { + $result = $this->getDefinitionForModel($targetModel); + if (isError($result)) + return $result; + $definitions = getData($result); + + $result = []; + foreach ($definitions as $def) { + if ($filter && !isset($filter[$def['name']])) + continue; + $validations = []; + if (isset($def['requiredPermissions'])) + $validations[] = 'has_write_permissions[' . implode(',', $def['requiredPermissions']) . ']'; + if (isset($def['required'])) + $validations[] = 'required'; + if (isset($def['validation'])) { + if (isset($def['validation']['max-value'])) + $validations[] = 'less_than_equal_to[' . $def['validation']['max-value'] . ']'; + if (isset($def['validation']['min-value'])) + $validations[] = 'greater_than_equal_to[' . $def['validation']['min-value'] . ']'; + if (isset($def['validation']['max-length'])) + $validations[] = 'max_length[' . $def['validation']['max-length'] . ']'; + if (isset($def['validation']['min-length'])) + $validations[] = 'min_length[' . $def['validation']['min-length'] . ']'; + if (isset($def['validation']['regex']) && is_array($def['validation']['regex'])) { + foreach ($def['validation']['regex'] as $regex) { + if ($regex['language'] == 'php') { + $validations[] = 'regex_match[' . $regex['expression'] . ']'; + } + } + } + } + if ($validations) + $result[] = [ + 'field' => $def['name'], + 'label' => $def['title'], + 'rules' => $validations + ]; + } + return success($result); + } + // ------------------------------------------------------------------------------------------------- // Private methods // diff --git a/public/js/components/Udf/Udf.js b/public/js/components/Udf/Udf.js new file mode 100644 index 000000000..e9cc3c60a --- /dev/null +++ b/public/js/components/Udf/Udf.js @@ -0,0 +1,156 @@ +import { CoreFetchCmpt } from '../Fetch.js'; +import FormInput from '../Form/Input.js'; + + +export default { + components: { + CoreFetchCmpt, + FormInput + }, + emits: [ + 'update:modelValue', + 'load' + ], + props: { + // CodeIgniter model (eg: crm/prestudent) + ciModel: { + type: String, + required: true + }, + // Primarykey(s) of the record (eg: {prestudent_id: 12345}) + pk: { + type: Object, + required: true + }, + // The values as associative array + modelValue: Object, + // Show only fields with a name that exists in the filter + filter: [String, Array] + }, + data() { + return { + fields: [], + backupModelValue: {} + }; + }, + computed: { + filterArray() { + if (!this.filter || Array.isArray(this.filter)) + return this.filter; + return [this.filter]; + }, + filteredFields() { + if (!this.filterArray) + return this.fields; + return this.fields.filter(el => this.filterArray.includes(el.name)); + }, + filteredValues() { + return this.filteredFields.reduce((r,e) => (r[e.name] = this.internModelValue[e.name], r), {}); + }, + internModelValue: { + get() { + return this.modelValue || this.backupModelValue; + }, + set(value) { + this.backupModelValue = value; + this.$emit('update:modelValue', value); + this.originalValues + } + } + }, + watch: { + pk(n, o) { + if (!this.$refs.fetch) + return; // NOTE(chris): no initial load yet + + if (Object.keys(o).length == Object.keys(n).length + && Object.keys(o).every(key => n.hasOwnProperty(key) && o[key] === n[key])) + return; // NOTE(chris): old and new are the same + + this.$nextTick(this.$refs.fetch.fetchData); + } + }, + methods: { + loadF(params) { + // TODO(chris): move to fhcapi.factory + return this.$fhcApi + .post('/api/frontend/v1/udf/load/' + params.ciModel, params.pk); + }, + init(result) { + const fields = result.map(el => { + switch (el.type) { + case 'textfield': + el.type = 'text'; + break; + case 'date': + el.type = 'Datepicker'; + el.clearable = el.hasOwnProperty('clearable') ? el.clearable : false + el.autoApply = el.hasOwnProperty('autoApply') ? el.autoApply : true + el.enableTimePicker = el.hasOwnProperty('enableTimePicker') ? el.enableTimePicker : false + el.format = el.hasOwnProperty('format') ? el.format : "dd.MM.yyyy" + el.previewFormat = el.hasOwnProperty('previewFormat') ? el.previewFormat : "dd.MM.yyyy" + el.teleport = el.hasOwnProperty('teleport') ? el.teleport : true + break; + case 'multipledropdown': + el.multiple = true; + case 'dropdown': + el.type = 'select'; + el.options = el.options.map(item => { + if (Array.isArray(item)) + return { + id: item[0], + description: item[1] + }; + if (typeof item === 'object') + return item; + return { + id: item, + description: item + }; + }); + break; + } + return el; + }); + const values = fields.reduce((a,c) => { + a[c.name] = c.value; + return a; + }, {}); + + this.internModelValue = {...this.internModelValue, ...values}; + this.fields = fields; + this.$emit('load', values); + } + }, + template: ` +
+ + + +
` +} \ No newline at end of file