From aa83b1aa370dfdbdc35b06b0279fedbeb1e3fe7d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 8 Dec 2021 11:11:46 +0100 Subject: [PATCH] chg: [metaTemplate] Update system and conflict resolution interfaces - WiP --- src/Controller/Component/CRUDComponent.php | 17 +- .../Component/Navigation/MetaTemplates.php | 4 +- src/Controller/MetaTemplatesController.php | 160 +++- src/Model/Table/MetaFieldsTable.php | 7 +- src/Model/Table/MetaTemplateFieldsTable.php | 13 +- src/Model/Table/MetaTemplatesTable.php | 686 ++++++++++++++++-- .../get_meta_fields_to_update.php | 39 + templates/MetaTemplates/index.php | 35 +- ..._template_to_newest_version_for_entity.php | 92 +++ templates/MetaTemplates/update.php | 22 +- .../MetaTemplates/conflictResolution.php | 59 +- .../element/MetaTemplates/conflictTable.php | 26 +- .../migrationToNewVersionForm.php | 12 + .../genericElements/Form/metaTemplateForm.php | 73 ++ .../Form/metaTemplateScaffold.php | 82 +-- .../IndexTable/Fields/actions.php | 3 +- .../IndexTable/Fields/update_status.php | 9 - .../MetaTemplates/metaTemplateNav.php | 10 +- .../SingleViews/metafields_panel.php | 38 +- 19 files changed, 1188 insertions(+), 199 deletions(-) create mode 100644 templates/MetaTemplates/get_meta_fields_to_update.php create mode 100644 templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php create mode 100644 templates/element/MetaTemplates/migrationToNewVersionForm.php create mode 100644 templates/element/genericElements/Form/metaTemplateForm.php delete mode 100644 templates/element/genericElements/IndexTable/Fields/update_status.php diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 629549b..2ae4038 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -176,7 +176,6 @@ class CRUDComponent extends Component ->order(['is_default' => 'DESC']) ->where([ 'scope' => $metaFieldsBehavior->getScope(), - 'enabled' => 1 ]) ->contain('MetaTemplateFields') ->formatResults(function (\Cake\Collection\CollectionInterface $metaTemplates) { // Set meta-template && meta-template-fields indexed by their ID @@ -307,7 +306,7 @@ class CRUDComponent extends Component } // prune empty values and marshall fields - private function massageMetaFields($entity, $input, $allMetaTemplates=[]) + public function massageMetaFields($entity, $input, $allMetaTemplates=[]) { if (empty($input['MetaTemplates'] || !$this->metaFieldsSupported())) { return ['entity' => $entity, 'metafields_to_delete' => []]; @@ -548,11 +547,12 @@ class CRUDComponent extends Component return $data; } - public function attachMetaTemplates($data, $metaTemplates) + public function attachMetaTemplates($data, $metaTemplates, $pruneEmptyDisabled=true) { + $this->MetaTemplates = TableRegistry::getTableLocator()->get('MetaTemplates'); $metaFields = []; if (!empty($data->id)) { - $metaFields = $this->getMetaFields($data->id, $data); + $metaFields = $this->getMetaFields($data->id); } foreach ($metaTemplates as $i => $metaTemplate) { if (isset($metaFields[$metaTemplate->id])) { @@ -563,6 +563,14 @@ class CRUDComponent extends Component $metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = []; } } + } else { + if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) { + unset($metaTemplates[$i]); + } + } + $newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate); + if (!empty($newestTemplate) && !empty($metaTemplates[$i])) { + $metaTemplates[$i]['hasNewerVersion'] = $newestTemplate; } } $data['MetaTemplates'] = $metaTemplates; @@ -686,7 +694,6 @@ class CRUDComponent extends Component } $this->setResponseForController('delete', $bulkSuccesses, $message, $data, null, $additionalData); } - $this->Controller->set('metaGroup', 'ContactDB'); $this->Controller->set('scope', 'users'); $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/delete'); diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php index e088cc2..916fb25 100644 --- a/src/Controller/Component/Navigation/MetaTemplates.php +++ b/src/Controller/Component/Navigation/MetaTemplates.php @@ -80,8 +80,8 @@ class MetaTemplatesNavigation extends BaseNavigation if (empty($this->viewVars['updateableTemplate']['up-to-date'])) { $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [ 'label' => __('Update template'), - 'url' => '/metaTemplates/update/{{id}}', - 'url_vars' => ['id' => 'id'], + 'url' => '/metaTemplates/update/{{uuid}}', + 'url_vars' => ['uuid' => 'uuid'], 'variant' => 'warning', 'badge' => [ 'variant' => 'warning', diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index 42348e0..2548839 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -5,7 +5,11 @@ namespace App\Controller; use App\Controller\AppController; use Cake\Utility\Hash; use Cake\Utility\Text; +use Cake\Utility\Inflector; +use Cake\ORM\TableRegistry; use \Cake\Database\Expression\QueryExpression; +use Cake\Http\Exception\NotFoundException; +use Cake\Http\Exception\MethodNotAllowedException; class MetaTemplatesController extends AppController { @@ -13,22 +17,29 @@ class MetaTemplatesController extends AppController public $filterFields = ['name', 'uuid', 'scope', 'namespace']; public $containFields = ['MetaTemplateFields']; - public function update($template_id=false) + public function update($template_uuid=null) { - if (!empty($template_id)) { - $metaTemplate = $this->MetaTemplates->get($template_id); + $metaTemplate = false; + if (!is_null($template_uuid)) { + $metaTemplate = $this->MetaTemplates->find()->where([ + 'uuid' => $template_uuid + ])->first(); + if (empty($metaTemplate)) { + throw new NotFoundException(__('Invalid {0}.', $this->MetaTemplates->getAlias())); + } } if ($this->request->is('post')) { - $result = $this->MetaTemplates->update($template_id); + $updateStrategy = $this->request->getData('update_strategy', null); + $result = $this->MetaTemplates->update($template_uuid, $updateStrategy); if ($this->ParamHandler->isRest()) { return $this->RestResponse->viewData($result, 'json'); } else { if ($result['success']) { - $message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), $result['updated']); + $message = __n('{0} templates updated.', 'The template has been updated.', empty($template_uuid), $result['files_processed']); } else { - $message = __n('{0} templates could not be updated.', 'The template could not be updated.',empty($template_id), $result['updated']); + $message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_uuid), $result['files_processed']); } - $this->CRUD->setResponseForController('update', $result['success'], $message, $metaTemplate, $metaTemplate->getErrors(), ['redirect' => $this->referer()]); + $this->CRUD->setResponseForController('update', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -36,7 +47,7 @@ class MetaTemplatesController extends AppController } } else { if (!$this->ParamHandler->isRest()) { - if (!empty($template_id)) { + if (!is_null($template_uuid)) { $this->set('metaTemplate', $metaTemplate); $this->setUpdateStatus($metaTemplate->id); } else { @@ -50,6 +61,105 @@ class MetaTemplatesController extends AppController } } + public function getMetaFieldsToUpdate($template_id) + { + $metaTemplate = $this->MetaTemplates->get($template_id); + $newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate); + $entities = $this->MetaTemplates->getEntitiesWithMetaFieldsToUpdate($template_id); + $this->set('metaTemplate', $metaTemplate); + $this->set('newestMetaTemplate', $newestMetaTemplate); + $this->set('entities', $entities); + } + + public function migrateOldMetaTemplateToNewestVersionForEntity($template_id, $entity_id) + { + $metaTemplate = $this->MetaTemplates->get($template_id, [ + 'contain' => ['MetaTemplateFields'] + ]); + $newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate, true); + $entity = $this->MetaTemplates->migrateMetaTemplateToNewVersion($metaTemplate, $newestMetaTemplate, $entity_id); + $conditions = [ + 'MetaFields.meta_template_id IN' => [$metaTemplate->id, $newestMetaTemplate->id] + ]; + $keyedMetaFields = $this->MetaTemplates->getKeyedMetaFields($metaTemplate->scope, $entity_id, $conditions); + if (empty($keyedMetaFields[$metaTemplate->id])) { + throw new NotFoundException(__('Invalid {0}. This entities does not have meta-fields to be moved to a newer template.', $this->MetaTemplates->getAlias())); + } + $mergedMetaFields = $this->MetaTemplates->mergeMetaFieldsInMetaTemplate($keyedMetaFields, [$metaTemplate, $newestMetaTemplate]); + $entity['MetaTemplates'] = $mergedMetaFields; + if ($this->request->is('post') || $this->request->is('put')) { + $className = Inflector::camelize(Inflector::pluralize($newestMetaTemplate->scope)); + $entityTable = TableRegistry::getTableLocator()->get($className); + $inputData = $this->request->getData(); + $massagedData = $this->MetaTemplates->massageMetaFieldsBeforeSave($entity, $inputData, $newestMetaTemplate); + unset($inputData['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity + $data = $massagedData['entity']; + $metaFieldsToDelete = $massagedData['metafields_to_delete']; + foreach ($entity->meta_fields as $i => $metaField) { + if ($metaField->meta_template_id == $template_id) { + $metaFieldsToDelete[] = $entity->meta_fields[$i]; + } + } + $data = $entityTable->patchEntity($data, $inputData); + $savedData = $entityTable->save($data); + if ($savedData !== false) { + if (!empty($metaFieldsToDelete)) { + $entityTable->MetaFields->unlink($savedData, $metaFieldsToDelete); + } + $message = __('Data on old meta-template has been migrated to newest meta-template'); + } else { + $message = __('Could not migrate data to newest meta-template'); + } + $this->CRUD->setResponseForController( + 'migrateOldMetaTemplateToNewestVersionForEntity', + $savedData !== false, + $message, + $savedData, + [], + ['redirect' => [ + 'controller' => $className, + 'action' => 'view', $entity_id] + ] + ); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + $conflicts = $this->MetaTemplates->checkForMetaTemplateConflicts($metaTemplate, $newestMetaTemplate); + foreach ($conflicts as $conflict) { + $existingMetaTemplateField = $conflict['existing_meta_template_field']; + foreach ($existingMetaTemplateField->metaFields as $metafield_id => $metaField) { + $metaField->setError('value', implode(', ', $existingMetaTemplateField->conflicts)); + } + } + // automatically convert non-conflicting fields to new meta-template + $movedMetaTemplateFields = []; + foreach ($metaTemplate->meta_template_fields as $metaTemplateField) { + if (!empty($conflicts[$metaTemplateField->field]['conflicts'])) { + continue; + } + foreach ($newestMetaTemplate->meta_template_fields as $newMetaTemplateField) { + if ($metaTemplateField->field == $newMetaTemplateField->field && empty($newMetaTemplateField->metaFields)) { + $movedMetaTemplateFields[] = $metaTemplateField->id; + $copiedMetaFields = array_map(function ($e) use ($newMetaTemplateField) { + $e = $e->toArray(); + $e['meta_template_id'] = $newMetaTemplateField->meta_template_id; + $e['meta_template_field_id'] = $newMetaTemplateField->id; + unset($e['id']); + return $e; + }, $metaTemplateField->metaFields); + $newMetaTemplateField->metaFields = $this->MetaTemplates->MetaTemplateFields->MetaFields->newEntities($copiedMetaFields); + } + } + } + $this->set('oldMetaTemplate', $metaTemplate); + $this->set('newMetaTemplate', $newestMetaTemplate); + $this->set('entity', $entity); + $this->set('conflicts', $conflicts); + $this->set('movedMetaTemplateFields', $movedMetaTemplateFields); + } + public function index() { $updateableTemplate = $this->MetaTemplates->checkForUpdates(); @@ -69,7 +179,8 @@ class MetaTemplatesController extends AppController 'afterFind' => function($data) use ($updateableTemplate) { foreach ($data as $i => $metaTemplate) { if (!empty($updateableTemplate[$metaTemplate->uuid])) { - $metaTemplate->set('status', $this->MetaTemplates->getTemplateStatus($updateableTemplate[$metaTemplate->uuid])); + $updateStatusForTemplate = $this->MetaTemplates->checkUpdateForMetaTemplate($updateableTemplate[$metaTemplate->uuid]['template'], $metaTemplate); + $metaTemplate->set('status', $this->MetaTemplates->getTemplateStatus($updateStatusForTemplate, $metaTemplate)); } } return $data; @@ -81,6 +192,7 @@ class MetaTemplatesController extends AppController } $updateableTemplate = [ 'not_up_to_date' => $this->MetaTemplates->getNotUpToDateTemplates(), + 'can_be_removed' => $this->MetaTemplates->getCanBeRemovedTemplates(), 'new' => $this->MetaTemplates->getNewTemplates(), ]; $this->set('defaultTemplatePerScope', $this->MetaTemplates->getDefaultTemplatePerScope()); @@ -100,6 +212,20 @@ class MetaTemplatesController extends AppController $this->setUpdateStatus($id); } + public function delete($id) + { + $updateableTemplate = $this->getUpdateStatus($id); + if (empty($updateableTemplate['can_be_removed'])) { + throw MethodNotAllowedException(__('This meta-template cannot be removed')); + } + $this->set('deletionText', __('The meta-template "{0}" has no meta-field and can be safely removed.', h($updateableTemplate['existing_template']->name))); + $this->CRUD->delete($id); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + public function toggle($id, $fieldName = 'enabled') { if ($this->request->is('POST') && $fieldName == 'is_default') { @@ -115,11 +241,23 @@ class MetaTemplatesController extends AppController } } + private function getUpdateStatus($id): array + { + $metaTemplate = $this->MetaTemplates->get($id, [ + 'contain' => ['MetaTemplateFields'] + ]); + $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid); + $updateableTemplate = $this->MetaTemplates->checkUpdateForMetaTemplate($templateOnDisk, $metaTemplate); + return $updateableTemplate; + } + public function setUpdateStatus($id) { - $metaTemplate = $this->MetaTemplates->get($id); + $metaTemplate = $this->MetaTemplates->get($id, [ + 'contain' => ['MetaTemplateFields'] + ]); $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid); - $updateableTemplate = $this->MetaTemplates->checkUpdatesForTemplate($templateOnDisk); + $updateableTemplate = $this->MetaTemplates->checkUpdateForMetaTemplate($templateOnDisk, $metaTemplate); $this->set('updateableTemplate', $updateableTemplate); $this->set('templateOnDisk', $templateOnDisk); } diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index bc51baa..639b306 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -49,6 +49,11 @@ class MetaFieldsTable extends AppTable $metaFieldsTable = $context['providers']['table']; $entityData = $context['data']; $metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']); + return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField); + } + + public function isValidMetaFieldForMetaTemplateField($value, $metaTemplateField) + { $typeValid = $this->isValidType($value, $metaTemplateField['type']); if ($typeValid !== true) { return $typeValid; @@ -72,7 +77,7 @@ class MetaFieldsTable extends AppTable $re = $metaTemplateField['regex']; if (!preg_match("/^$re$/m", $value)) { - return __('Metafield value `{0}` for `{1}` doesn\'t pass regex validation', $value, $metaTemplateField->field); + return __('Metafield value `{0}` for `{1}` doesn\'t pass regex validation', $value, $metaTemplateField['field']); } return true; } diff --git a/src/Model/Table/MetaTemplateFieldsTable.php b/src/Model/Table/MetaTemplateFieldsTable.php index 9357b2d..39d0565 100644 --- a/src/Model/Table/MetaTemplateFieldsTable.php +++ b/src/Model/Table/MetaTemplateFieldsTable.php @@ -20,14 +20,21 @@ class MetaTemplateFieldsTable extends AppTable $this->setDisplayField('field'); } + public function beforeSave($event, $entity, $options) + { + if (empty($entity->meta_template_id)) { + $event->stopPropagation(); + $event->setResult(false); + return; + } + } + public function validationDefault(Validator $validator): Validator { $validator ->notEmptyString('field') ->notEmptyString('type') - ->numeric('meta_template_id') - ->notBlank('meta_template_id') - ->requirePresence(['meta_template_id', 'field', 'type'], 'create'); + ->requirePresence(['field', 'type'], 'create'); return $validator; } } diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php index 6b21c9c..0f75703 100644 --- a/src/Model/Table/MetaTemplatesTable.php +++ b/src/Model/Table/MetaTemplatesTable.php @@ -4,8 +4,11 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; +use Cake\ORM\TableRegistry; use Cake\Validation\Validator; use Cake\Utility\Hash; +use Cake\Utility\Inflector; +use Cake\Utility\Text; class MetaTemplatesTable extends AppTable { @@ -21,7 +24,10 @@ class MetaTemplatesTable extends AppTable $this->hasMany( 'MetaTemplateFields', [ - 'foreignKey' => 'meta_template_id' + 'foreignKey' => 'meta_template_id', + 'saveStrategy' => 'replace', + 'dependent' => true, + 'cascadeCallbacks' => true, ] ); $this->setDisplayField('name'); @@ -40,7 +46,7 @@ class MetaTemplatesTable extends AppTable return $validator; } - public function update(&$errors=[]) + public function update($template_uuid=null, $strategy=null) { $files_processed = []; // foreach (self::TEMPLATE_PATH as $path) { @@ -60,22 +66,54 @@ class MetaTemplatesTable extends AppTable $updatesErrors = []; $templates = $this->readTemplatesFromDisk($readErrors); foreach ($templates as $template) { - $preUpdateChecks[$template['uuid']] = $this->checkForUpdates($template); + $updateStatus = $this->checkForUpdates($template['uuid']); + $preUpdateChecks[$template['uuid']] = $updateStatus; + if (is_null($template_uuid) || $template_uuid == $template['uuid']) { + $errors = []; + $success = false; + if ($updateStatus['up-to-date']) { + $errors['message'] = __('Meta-template already up-to-date'); + $success = true; + } else if ($updateStatus['new']) { + $success = $this->saveNewMetaTemplate($template, $errors); + } else if ($updateStatus['updateable']) { + $success = $this->updateMetaTemplate($template, $errors); + } else if (!$updateStatus['up-to-date'] && is_null($strategy)) { + $errors['message'] = __('Cannot update meta-template, update strategy not provided'); + } else if (!$updateStatus['up-to-date'] && !is_null($strategy)) { + $success = $this->updateMetaTemplateWithStrategy($template, $strategy, $errors); + } else { + $errors['message'] = __('Could not update. Something went wrong.'); + } + if ($success) { + $files_processed[] = $template['uuid']; + } + if (!empty($errors)) { + $updatesErrors[] = $errors; + } + } } - $errors = [ + $results = [ 'read_errors' => $readErrors, 'pre_update_errors' => $preUpdateChecks, 'update_errors' => $updatesErrors, + 'files_processed' => $files_processed, + 'success' => !empty($files_processed), ]; - return $files_processed; + return $results; } - public function checkForUpdates(): array + public function checkForUpdates($template_uuid=null): array { $templates = $this->readTemplatesFromDisk($readErrors); $result = []; foreach ($templates as $template) { - $result[$template['uuid']] = $this->checkUpdatesForTemplate($template); + if (is_null($template_uuid)) { + $result[$template['uuid']] = $this->checkUpdatesForTemplate($template); + } else if ($template['uuid'] == $template_uuid) { + $result = $this->checkUpdatesForTemplate($template); + return $result; + } } return $result; } @@ -105,13 +143,26 @@ class MetaTemplatesTable extends AppTable return !$updateResult['updateable'] && !$updateResult['up-to-date'] && !$updateResult['new']; } - public function getTemplateStatus(array $updateResult): array + public function isUpdateableToExistingMetaTemplate($metaTemplate): bool + { + $newestTemplate = $this->getNewestVersion($metaTemplate); + return !empty($newestTemplate); + } + + public function isRemovable(array $updateResult): bool + { + return !empty($updateResult['can_be_removed']); + } + + public function getTemplateStatus(array $updateResult, $metaTemplate): array { return [ 'up_to_date' => $this->isUpToDate($updateResult), 'updateable' => $this->isUpdateable($updateResult), 'is_new' => $this->isNew($updateResult), 'has_conflict' => $this->hasConflict($updateResult), + 'to_existing' => $this->isUpdateableToExistingMetaTemplate($metaTemplate), + 'can_be_removed' => $this->isRemovable($updateResult), ]; } @@ -170,6 +221,32 @@ class MetaTemplatesTable extends AppTable return $result; } + public function getNewestVersion($metaTemplate, $full=false) + { + $query = $this->find()->where([ + 'uuid' => $metaTemplate->uuid, + 'id !=' => $metaTemplate->id, + 'version >=' => $metaTemplate->version, + ]) + ->order(['version' => 'DESC']); + if ($full) { + $query->contain(['MetaTemplateFields']); + } + $newestTemplate = $query->first(); + return $newestTemplate; + } + + public function getCanBeRemovedTemplates($result=null): array + { + $result = is_null($result) ? $this->checkForUpdates() : $result; + foreach ($result as $i => $updateResult) { + if (!$this->isRemovable($updateResult)) { + unset($result[$i]); + } + } + return $result; + } + public function readTemplatesFromDisk(&$errors=[]): array { $templates = []; @@ -232,6 +309,82 @@ class MetaTemplatesTable extends AppTable } } + public function getEntitiesWithMetaFieldsToUpdate(int $template_id): array + { + $metaTemplate = $this->get($template_id); + $queryParentEntities = $this->MetaTemplateFields->MetaFields->find(); + $queryParentEntities + ->select(['parent_id']) + ->where([ + 'meta_template_id' => $template_id + ]) + ->group(['parent_id']); + + $entitiesClassName = Inflector::camelize(Inflector::pluralize($metaTemplate->scope)); + $entitiesTable = TableRegistry::getTableLocator()->get($entitiesClassName); + $entityQuery = $entitiesTable->find() + ->where(['id IN' => $queryParentEntities]) + ->contain([ + 'MetaFields' => [ + 'conditions' => [ + 'meta_template_id' => $template_id + ] + ] + ]); + $entities = $entityQuery->all()->toList(); + return $entities; + } + + public function getKeyedMetaFields(string $scope, int $entity_id, array $conditions=[]) + { + $query = $this->MetaTemplateFields->MetaFields->find(); + $query->where(array_merge( + $conditions, + [ + 'MetaFields.scope' => $scope, + 'MetaFields.parent_id' => $entity_id + ] + )); + $metaFields = $query->all(); + $data = []; + foreach ($metaFields as $metaField) { + if (empty($data[$metaField->meta_template_id][$metaField->meta_template_field_id])) { + $data[$metaField->meta_template_id][$metaField->meta_template_field_id] = []; + } + $data[$metaField->meta_template_id][$metaField->meta_template_field_id][$metaField->id] = $metaField; + } + return $data; + } + + public function mergeMetaFieldsInMetaTemplate(array $keyedMetaFields, array $metaTemplates) + { + $merged = []; + foreach ($metaTemplates as $metaTemplate) { + $metaTemplate['meta_template_fields'] = Hash::combine($metaTemplate['meta_template_fields'], '{n}.id', '{n}'); + $merged[$metaTemplate->id] = $metaTemplate; + if (isset($keyedMetaFields[$metaTemplate->id])) { + foreach ($metaTemplate->meta_template_fields as $j => $meta_template_field) { + if (isset($keyedMetaFields[$metaTemplate->id][$meta_template_field->id])) { + $merged[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = $keyedMetaFields[$metaTemplate->id][$meta_template_field->id]; + } else { + $merged[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = []; + } + } + } + } + return $merged; + } + + public function migrateMetaTemplateToNewVersion(\App\Model\Entity\MetaTemplate $oldMetaTemplate, \App\Model\Entity\MetaTemplate $newMetaTemplate, int $entityId) + { + $entitiesClassName = Inflector::camelize(Inflector::pluralize($oldMetaTemplate->scope)); + $entitiesTable = TableRegistry::getTableLocator()->get($entitiesClassName); + $entity = $entitiesTable->get($entityId, [ + 'contain' => 'MetaFields' + ]); + return $entity; + } + public function getTemplate($id) { $query = $this->find(); @@ -265,88 +418,385 @@ class MetaTemplatesTable extends AppTable ); } - public function loadAndSaveMetaFile(String $filePath) + // public function loadAndSaveMetaFile(String $filePath) + // { + // if (file_exists($filePath)) { + // $contents = file_get_contents($filePath); + // $metaTemplate = json_decode($contents, true); + // if (empty($metaTemplate)) { + // return __('Could not load template file. Error while decoding the template\'s JSON'); + // } + // if (empty($metaTemplate['uuid']) || empty($metaTemplate['version'])) { + // return __('Could not load template file. Invalid template file. Missing template UUID or version'); + // } + // return $this->saveMetaFile($metaTemplate); + // } + // return __('Could not load template file. File does not exists'); + // } + + // public function saveMetaFile(array $newMetaTemplate) + // { + // $query = $this->find(); + // $query->contain('MetaTemplateFields')->where(['uuid' => $newMetaTemplate['uuid']]); + // $metaTemplate = $query->first(); + // if (empty($metaTemplate)) { + // $metaTemplate = $this->newEntity($newMetaTemplate); + // $result = $this->save($metaTemplate); + // if (!$result) { + // return __('Something went wrong, could not create the template.'); + // } + // } else { + // if ($metaTemplate->version >= $newMetaTemplate['version']) { + // return __('Could not update the template. Local version is newer.'); + // } + // // Take care of meta template fields + // $metaTemplate = $this->patchEntity($metaTemplate, $newMetaTemplate); + // $metaTemplate = $this->save($metaTemplate); + // if (!$metaTemplate) { + // return __('Something went wrong, could not update the template.'); + // } + // } + // if ($result) { + // $this->MetaTemplateFields->deleteAll(['meta_template_id' => $template->id]); + // foreach ($newMetaTemplate['metaFields'] as $metaField) { + // $metaField['meta_template_id'] = $template->id; + // $metaField = $this->MetaTemplateFields->newEntity($metaField); + // $this->MetaTemplateFields->save($metaField); + // } + // } + // } + + public function saveNewMetaTemplate(array $template, array &$errors=[], &$savedMetaTemplate=null): bool { - if (file_exists($filePath)) { - $contents = file_get_contents($filePath); - $metaTemplate = json_decode($contents, true); - if (empty($metaTemplate)) { - return __('Could not load template file. Error while decoding the template\'s JSON'); - } - if (empty($metaTemplate['uuid']) || empty($metaTemplate['version'])) { - return __('Could not load template file. Invalid template file. Missing template UUID or version'); - } - return $this->saveMetaFile($metaTemplate); + $template['meta_template_fields'] = $template['metaFields']; + unset($template['metaFields']); + $metaTemplate = $this->newEntity($template, [ + 'associated' => ['MetaTemplateFields'] + ]); + $tmp = $this->save($metaTemplate, [ + 'associated' => ['MetaTemplateFields'] + ]); + $error = null; + if (empty($tmp)) { + $error = new UpdateError(); + $error->success = false; + $error->message = __('Could not save the template.'); + $error->errors = $metaTemplate->getErrors(); + $errors[] = $error; } - return __('Could not load template file. File does not exists'); + $savedMetaTemplate = $tmp; + return !is_null($error); } - public function saveMetaFile(array $newMetaTemplate) + public function updateMetaTemplate(array $template, array &$errors=[]): bool { - $query = $this->find(); - $query->contain('MetaTemplateFields')->where(['uuid' => $newMetaTemplate['uuid']]); + $metaTemplate = $this->getMetaTemplateElligibleForUpdate($template); + if (is_string($metaTemplate)) { + $errors[] = new UpdateError(false, $metaTemplate); + return false; + } + $metaTemplate = $this->patchEntity($metaTemplate, $template, [ + 'associated' => ['MetaTemplateFields'] + ]); + $metaTemplate = $this->save($metaTemplate, [ + 'associated' => ['MetaTemplateFields'] + ]); + if (!empty($metaTemplate)) { + $errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors()); + return false; + } + return true; + } + + public function updateMetaTemplateWithStrategy(array $template, string $strategy, array $errors=[]): bool + { + $metaTemplate = $this->getMetaTemplateElligibleForUpdate($template); + if (is_string($metaTemplate)) { + $errors[] = new UpdateError(false, $metaTemplate); + return false; + } + $success = $this->executeUpdateStrategy($strategy, $template, $metaTemplate); + if (is_string($success)) { + $errors[] = new UpdateError(false, $success); + return false; + } + return true; + } + + public function getMetaTemplateElligibleForUpdate($template) + { + $query = $this->find() + ->contain('MetaTemplateFields')->where([ + 'uuid' => $template['uuid'] + ]); $metaTemplate = $query->first(); if (empty($metaTemplate)) { - $metaTemplate = $this->newEntity($newMetaTemplate); - $result = $this->save($metaTemplate); - if (!$result) { - return __('Something went wrong, could not create the template.'); + return __('Meta-template not found.'); + } + if ($metaTemplate->version >= $template['version']) { + return __('Could not update the template. Local version is newer.'); + } + return $metaTemplate; + } + + public function executeUpdateStrategy(string $strategy, array $template, \App\Model\Entity\MetaTemplate $metaTemplate) + { + if ($strategy == 'keep_both') { + $result = $this->executeStrategyKeep($template, $metaTemplate); + } else if ($strategy == 'delete_all') { + $result = $this->executeStrategyDeleteAll($template, $metaTemplate); + } else { + return __('Invalid strategy {0}', $strategy); + } + if (is_string($result)) { + return $result; + } + return true; + } + + // Old template remains untouched + // Create new template + // Migrate all non-conflicting meta-fields for one entity to the new template + // Keep all the conflicting meta-fields for one entity on the old template + public function executeStrategyKeep(array $template, \App\Model\Entity\MetaTemplate $metaTemplate) + { + $savedMetaTemplate = null; + $conflicts = $this->checkForMetaTemplateConflicts($metaTemplate, $template); + $blockingConflict = Hash::extract($conflicts, '{s}.conflicts'); + $errors = []; + if (empty($blockingConflict)) { // No conflict, everything can be updated without special care + $this->updateMetaTemplate($template, $errors); + return !empty($errors) ? $errors[0] : true; + } + $entities = $this->fetchEntitiesWithMetaFieldsForTemplate($metaTemplate); + + $conflictingEntities = []; + foreach ($entities as $entity) { + $conflicts = $this->checkMetaFieldsValidityUnderTemplate($entity['meta_fields'], $template); + if (!empty($conflicts)) { + $conflictingEntities[$entity->id] = $entity->id; + } + } + if (empty($conflictingEntities)) { + $this->updateMetaTemplate($template, $errors); + return !empty($errors) ? $errors[0] : true; + } + $template['is_default'] = $metaTemplate['is_default']; + $template['enabled'] = $metaTemplate['enabled']; + if ($metaTemplate->is_default) { + $metaTemplate->set('is_default', false); + $this->save($metaTemplate); + } + $success = $this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate); + if (!empty($savedMetaTemplate)) { // conflicting entities remain untouched + $savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}'); + foreach ($entities as $entity) { + if (empty($conflictingEntities[$entity->id])) { + foreach ($entity['meta_fields'] as $metaField) { + $savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field]; + $success = $this->replaceMetaTemplate($metaField, $savedMetaTemplateField); + } + } } } else { - if ($metaTemplate->version >= $newMetaTemplate['version']) { - return __('Could not update the template. Local version is newer.'); - } - // Take care of meta template fields - $metaTemplate = $this->patchEntity($metaTemplate, $newMetaTemplate); - $metaTemplate = $this->save($metaTemplate); - if (!$metaTemplate) { - return __('Something went wrong, could not update the template.'); - } - } - if ($result) { - $this->MetaTemplateFields->deleteAll(['meta_template_id' => $template->id]); - foreach ($newMetaTemplate['metaFields'] as $metaField) { - $metaField['meta_template_id'] = $template->id; - $metaField = $this->MetaTemplateFields->newEntity($metaField); - $this->MetaTemplateFields->save($metaField); - } + return $errors[0]->message; } + return true; } - public function handleMetaTemplateFieldUpdateEdgeCase($metaTemplateField, $newMetaTemplateField) + // Delete conflicting meta-fields + // Update template to the new version + public function executeStrategyDeleteAll($template, $metaTemplate) { + $errors = []; + $conflicts = $this->checkForMetaTemplateConflicts($metaTemplate, $template); + $blockingConflict = Hash::extract($conflicts, '{s}.conflicts'); + if (empty($blockingConflict)) { // No conflict, everything can be updated without special care + $this->updateMetaTemplate($template, $errors); + return !empty($errors) ? $errors[0] : true; + } + $entities = $this->fetchEntitiesWithMetaFieldsForTemplate($metaTemplate); + + foreach ($entities as $entity) { + $conflicts = $this->checkMetaFieldsValidityUnderTemplate($entity['meta_fields'], $template); + $result = $this->MetaTemplateFields->MetaFields->deleteAll([ + 'id IN' => $conflicts + ]); + } + $this->updateMetaTemplate($template, $errors); + return !empty($errors) ? $errors[0] : true; } - public function checkForMetaFieldConflicts(\App\Model\Entity\MetaTemplateField $metaField, array $templateField): array + public function replaceMetaTemplate(\App\Model\Entity\MetaField $metaField, \App\Model\Entity\MetaTemplateField $savedMetaTemplateField) + { + $metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id); + $metaField->set('meta_template_field_id', $savedMetaTemplateField->id); + $metaField = $this->MetaTemplateFields->MetaFields->save($metaField); + return !empty($metaField); + } + + public function checkMetaFieldsValidityUnderTemplate(array $metaFields, array $template): array + { + $conflicting = []; + $metaTemplateFieldByName = []; + foreach ($template['metaFields'] as $metaField) { + $metaTemplateFieldByName[$metaField['field']] = $this->MetaTemplateFields->newEntity($metaField); + } + foreach ($metaFields as $metaField) { + $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField( + $metaField->value, + $metaTemplateFieldByName[$metaField->field] + ); + if ($isValid !== true) { + $conflicting[] = $metaField; + } + } + return $conflicting; + } + + public function checkMetaFieldsValidityUnderExistingMetaTemplate(array $metaFields, \App\Model\Entity\MetaTemplate $metaTemplate): array + { + $conflicting = []; + $metaTemplateFieldByName = []; + foreach ($metaTemplate->meta_template_fields as $metaTemplateField) { + $metaTemplateFieldByName[$metaTemplateField->field] = $metaTemplateField; + } + foreach ($metaFields as $metaField) { + if ($metaField->meta_template_id != $metaTemplate->id) { + continue; + } + $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField( + $metaField->value, + $metaTemplateFieldByName[$metaField->field] + ); + if ($isValid !== true) { + $conflicting[] = $metaField; + } + } + return $conflicting; + } + + public function fetchEntitiesWithMetaFieldsForTemplate(\App\Model\Entity\MetaTemplate $metaTemplate): array + { + $entitiesIDWithMetaFields = $this->MetaTemplateFields->MetaFields->find() + ->select(['parent_id', 'scope']) + ->where(['MetaFields.meta_template_id' => $metaTemplate->id]) + ->group('parent_id') + ->all() + ->toList(); + $className = Inflector::camelize(Inflector::pluralize($entitiesIDWithMetaFields[0]->scope)); + + $table = TableRegistry::getTableLocator()->get($className); + $entities = $table->find() + ->where(['id IN' => Hash::extract($entitiesIDWithMetaFields, '{n}.parent_id')]) + ->contain([ + 'MetaFields' => [ + 'conditions' => [ + 'MetaFields.meta_template_id' => $metaTemplate->id + ] + ] + ]) + ->all()->toList(); + + return $entities; + } + + public function checkForMetaFieldConflicts(\App\Model\Entity\MetaTemplateField $metaTemplateField, array $templateField): array { $result = [ 'updateable' => true, 'conflicts' => [], + 'conflictingEntities' => [], ]; - if ($metaField->multiple && $templateField['multiple'] == false) { // Field is no longer multiple - $result['updateable'] = false; - $result['conflicts'][] = __('This field is no longer multiple'); + if ($metaTemplateField->multiple && $templateField['multiple'] == false) { // Field is no longer multiple + $query = $this->MetaTemplateFields->MetaFields->find(); + $query + ->enableHydration(false) + ->select([ + 'parent_id', + 'meta_template_field_id', + 'count' => $query->func()->count('meta_template_field_id'), + ]) + ->where([ + 'meta_template_field_id' => $metaTemplateField->id, + ]) + ->group(['parent_id']) + ->having(['count >' => 1]); + $conflictingStatus = $query->all()->toList(); + if (!empty($conflictingStatus)) { + $result['updateable'] = false; + $result['conflicts'][] = __('This field is no longer multiple'); + $result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id'); + } } - if (!empty($templateField['regex']) && $templateField['regex'] != $metaField->regex) { - // FIXME: Check if all meta-fields pass the new validation - $result['updateable'] = false; - $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore'); + if (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) { + $query = $this->MetaTemplateFields->MetaFields->find(); + $query + ->enableHydration(false) + ->select([ + 'parent_id', + 'scope', + 'meta_template_field_id', + ]) + ->where([ + 'meta_template_field_id' => $metaTemplateField->id, + ]); + $entitiesWithMetaField = $query->all()->toList(); + if (!empty($entitiesWithMetaField)) { + $className = Inflector::camelize(Inflector::pluralize($entitiesWithMetaField[0]['scope'])); + $table = TableRegistry::getTableLocator()->get($className); + $entities = $table->find() + ->where(['id IN' => Hash::extract($entitiesWithMetaField, '{n}.parent_id')]) + ->contain([ + 'MetaFields' => [ + 'conditions' => [ + 'MetaFields.meta_template_field_id' => $metaTemplateField->id + ] + ] + ]) + ->all()->toList(); + $conflictingEntities = []; + foreach ($entities as $entity) { + foreach ($entity['meta_fields'] as $metaField) { + $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField( + $metaField->value, + $templateField + ); + if ($isValid !== true) { + $conflictingEntities[] = $entity->id; + break; + } + } + } + + if (!empty($conflictingEntities)) { + $result['updateable'] = $result['updateable'] && false; + $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore'); + $result['conflictingEntities'] = $conflictingEntities; + } + } } return $result; } - public function checkForMetaTemplateConflicts(\App\Model\Entity\MetaTemplate $metaTemplate, array $template): array + public function checkForMetaTemplateConflicts(\App\Model\Entity\MetaTemplate $metaTemplate, $template): array { + $templateMetaFields = []; + if (!is_array($template) && get_class($template) == 'App\Model\Entity\MetaTemplate') { + $templateMetaFields = $template->meta_template_fields; + } else { + $templateMetaFields = $template['metaFields']; + } $conflicts = []; $existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field'); - foreach ($template['metaFields'] as $newMetaField) { + foreach ($templateMetaFields as $newMetaField) { foreach ($metaTemplate->meta_template_fields as $metaField) { if ($newMetaField['field'] == $metaField->field) { unset($existingMetaTemplateFields[$metaField->field]); - $templateConflicts = $this->checkForMetaFieldConflicts($metaField, $newMetaField); - if (!$templateConflicts['updateable']) { - $conflicts[$metaField->field] = $templateConflicts; - } + $templateConflicts = $this->checkForMetaFieldConflicts($metaField, !is_array($newMetaField) && get_class($newMetaField) == 'App\Model\Entity\MetaTemplateField' ? $newMetaField->toArray() : $newMetaField); + $conflicts[$metaField->field] = $templateConflicts; + $conflicts[$metaField->field]['existing_meta_template_field'] = $metaField; + $conflicts[$metaField->field]['existing_meta_template_field']['conflicts'] = $templateConflicts['conflicts']; } } } @@ -361,7 +811,7 @@ class MetaTemplatesTable extends AppTable return $conflicts; } - public function checkUpdatesForTemplate($template): array + public function checkUpdatesForTemplate($template, $metaTemplate=null): array { $result = [ 'new' => true, @@ -370,11 +820,15 @@ class MetaTemplatesTable extends AppTable 'conflicts' => [], 'template' => $template, ]; - $query = $this->find() - ->contain('MetaTemplateFields')->where([ - 'uuid' => $template['uuid'] - ]); - $metaTemplate = $query->first(); + if (is_null($metaTemplate)) { + $query = $this->find() + ->contain('MetaTemplateFields') + ->where([ + 'uuid' => $template['uuid'], + ]) + ->order(['version' => 'DESC']); + $metaTemplate = $query->first(); + } if (!empty($metaTemplate)) { $result['existing_template'] = $metaTemplate; $result['current_version'] = $metaTemplate->version; @@ -386,6 +840,7 @@ class MetaTemplatesTable extends AppTable $result['conflicts'][] = __('Could not update the template. Local version is equal or newer.'); return $result; } + $conflicts = $this->checkForMetaTemplateConflicts($metaTemplate, $template); if (!empty($conflicts)) { $result['conflicts'] = $conflicts; @@ -395,4 +850,105 @@ class MetaTemplatesTable extends AppTable } return $result; } + + public function checkUpdateForMetaTemplate($template, $metaTemplate): array + { + $result = $this->checkUpdatesForTemplate($template, $metaTemplate); + $result['meta_field_amount'] = $this->MetaTemplateFields->MetaFields->find()->where(['meta_template_id' => $metaTemplate->id])->count(); + $result['can_be_removed'] = empty($result['meta_field_amount']) && empty($result['to_existing']); + return $result; + } + + public function massageMetaFieldsBeforeSave($entity, $input, $metaTemplate) + { + $metaFieldsTable = $this->MetaTemplateFields->MetaFields; + $className = Inflector::camelize(Inflector::pluralize($metaTemplate->scope)); + $entityTable = TableRegistry::getTableLocator()->get($className); + $metaFieldsIndex = []; + if (!empty($entity->meta_fields)) { + foreach ($entity->meta_fields as $i => $metaField) { + $metaFieldsIndex[$metaField->id] = $i; + } + } else { + $entity->meta_fields = []; + } + + $metaFieldsToDelete = []; + foreach ($input['MetaTemplates'] as $template_id => $template) { + foreach ($template['meta_template_fields'] as $meta_template_field_id => $meta_template_field) { + $rawMetaTemplateField = $metaTemplate->meta_template_fields[$meta_template_field_id]; + foreach ($meta_template_field['metaFields'] as $meta_field_id => $meta_field) { + if ($meta_field_id == 'new') { // create new meta_field + $new_meta_fields = $meta_field; + foreach ($new_meta_fields as $new_value) { + if (!empty($new_value)) { + $metaField = $metaFieldsTable->newEmptyEntity(); + $metaFieldsTable->patchEntity($metaField, [ + 'value' => $new_value, + 'scope' => $entityTable->getBehavior('MetaFields')->getScope(), + 'field' => $rawMetaTemplateField->field, + 'meta_template_id' => $rawMetaTemplateField->meta_template_id, + 'meta_template_field_id' => $rawMetaTemplateField->id, + 'parent_id' => $entity->id, + 'uuid' => Text::uuid(), + ]); + $entity->meta_fields[] = $metaField; + $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField; + } + } + } else { + $new_value = $meta_field['value']; + if (!empty($new_value)) { // update meta_field and attach validation errors + if (!empty($metaFieldsIndex[$meta_field_id])) { + $index = $metaFieldsIndex[$meta_field_id]; + $metaFieldsTable->patchEntity($entity->meta_fields[$index], [ + 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id + ], ['value']); + $metaFieldsTable->patchEntity( + $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id], + ['value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id], + ['value'] + ); + } else { // metafield comes from a second post where the temporary entity has already been created + $metaField = $metaFieldsTable->newEmptyEntity(); + $metaFieldsTable->patchEntity($metaField, [ + 'value' => $new_value, + 'scope' => $entityTable->getBehavior('MetaFields')->getScope(), // get scope from behavior + 'field' => $rawMetaTemplateField->field, + 'meta_template_id' => $rawMetaTemplateField->meta_template_id, + 'meta_template_field_id' => $rawMetaTemplateField->id, + 'parent_id' => $entity->id, + 'uuid' => Text::uuid(), + ]); + $entity->meta_fields[] = $metaField; + $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField; + } + } else { // Metafield value is empty, indicating the field should be removed + $index = $metaFieldsIndex[$meta_field_id]; + $metaFieldsToDelete[] = $entity->meta_fields[$index]; + unset($entity->meta_fields[$index]); + unset($entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id]); + } + } + } + } + } + + $entity->setDirty('meta_fields', true); + return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete]; + } } + +class UpdateError +{ + public $success; + public $message = ''; + public $errors = []; + + public function __construct($success=false, $message='', $errors=[]) + { + $this->success = $success; + $this->message = $message; + $this->errors = $errors; + } +} \ No newline at end of file diff --git a/templates/MetaTemplates/get_meta_fields_to_update.php b/templates/MetaTemplates/get_meta_fields_to_update.php new file mode 100644 index 0000000..c0c3892 --- /dev/null +++ b/templates/MetaTemplates/get_meta_fields_to_update.php @@ -0,0 +1,39 @@ + + 'metaTemplates', + 'action' => 'view', + $newestMetaTemplate->id +]); + +$bodyHtml = ''; +$bodyHtml .= sprintf('
%s: %s
', __('Current version'), h($metaTemplate->version)); +$bodyHtml .= sprintf('
%s: %s
', __('Newest version'), $urlNewestMetaTemplate, h($newestMetaTemplate->version)); +$bodyHtml .= sprintf('

%s

', __('Entities with meta-fields to be updated:')); + +$bodyHtml .= ''; + +echo $this->Bootstrap->modal([ + 'titleHtml' => __('{0} is an old meta-template and has meta-fields to be updated', sprintf('%s', h($metaTemplate->name))), + 'bodyHtml' => $bodyHtml, + 'size' => 'lg', + 'type' => 'ok-only', +]); +?> \ No newline at end of file diff --git a/templates/MetaTemplates/index.php b/templates/MetaTemplates/index.php index 8bfb4ca..a6d67c7 100644 --- a/templates/MetaTemplates/index.php +++ b/templates/MetaTemplates/index.php @@ -159,11 +159,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'uuid', 'data_path' => 'uuid' ], - [ - 'name' => __('Updateable'), - 'data_path' => 'status', - 'element' => 'update_status', - ], ], 'title' => __('Meta Field Templates'), 'description' => __('The various templates used to enrich certain objects by a set of standardised fields.'), @@ -176,15 +171,39 @@ echo $this->element('genericElements/IndexTable/index_table', [ ], [ 'open_modal' => '/metaTemplates/update/[onclick_params_data_path]', - 'modal_params_data_path' => 'id', + 'modal_params_data_path' => 'uuid', 'title' => __('Update Meta-Template'), 'icon' => 'download', 'complex_requirement' => [ 'function' => function ($row, $options) { - return empty($row['status']['up_to_date']); + return empty($row['status']['up_to_date']) && empty($row['status']['to_existing']); } ] - ] + ], + [ + 'open_modal' => '/metaTemplates/getMetaFieldsToUpdate/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'), + 'icon' => 'exclamation-triangle', + 'variant' => 'warning', + 'complex_requirement' => [ + 'function' => function ($row, $options) { + return !empty($row['status']['to_existing']) && empty($row['status']['can_be_removed']); + } + ] + ], + [ + 'open_modal' => '/metaTemplates/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'), + 'icon' => 'trash', + 'variant' => 'success', + 'complex_requirement' => [ + 'function' => function ($row, $options) { + return !empty($row['status']['to_existing']) && !empty($row['status']['can_be_removed']); + } + ] + ], ] ] ]); diff --git a/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php b/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php new file mode 100644 index 0000000..a5b8b13 --- /dev/null +++ b/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php @@ -0,0 +1,92 @@ + +

name) ?>

+
+
+
+
+

+ version)) ?> + Bootstrap->badge([ + 'text' => __('Data to be migrated over'), + 'variant' => 'danger', + 'class' => 'fs-7' + ]) + ?> +

+
+ element('MetaTemplates/migrationToNewVersionForm', [ + 'metaTemplate' => $oldMetaTemplate, + 'entity' => $entity, + ]) + ?> +
+
+
+
+ Bootstrap->icon('arrow-alt-circle-right') ?> +
+
+
+

+ version)) ?> + Bootstrap->badge([ + 'text' => __('Data to be saved'), + 'variant' => 'success', + 'class' => 'fs-7' + ]) + ?> +

+
+ element('MetaTemplates/migrationToNewVersionForm', [ + 'metaTemplate' => $newMetaTemplate, + 'entity' => $entity, + ]) + ?> +
+
+
+
+
+ Bootstrap->button([ + 'text' => __('Update to version {0}', h($newMetaTemplate->version)), + 'variant' => 'success', + 'params' => [ + 'onclick' => 'submitMigration()' + ] + ]) + ?> +
+
+ +Html->scriptBlock(sprintf( + 'var csrfToken = %s;', + json_encode($this->request->getAttribute('csrfToken')) +)); +?> + + \ No newline at end of file diff --git a/templates/MetaTemplates/update.php b/templates/MetaTemplates/update.php index de3f040..bd57231 100644 --- a/templates/MetaTemplates/update.php +++ b/templates/MetaTemplates/update.php @@ -17,6 +17,26 @@ if ($updateableTemplate['up-to-date']) { 'html' => __('This meta-template can be updated to version {0} (current: {1}).', sprintf('%s', h($templateOnDisk['version'])), h($metaTemplate->version)), 'dismissible' => false, ]); + $form = $this->element('genericElements/Form/genericForm', [ + 'entity' => null, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'model' => 'MetaTemplate', + 'fields' => [ + [ + 'field' => 'update_strategy', + 'type' => 'checkbox', + 'value' => 'update', + 'checked' => true, + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ], + ] + ]); + $bodyHtml .= sprintf('
%s
', $form); } else { $modalSize = 'xl'; $bodyHtml .= $this->Bootstrap->alert([ @@ -47,7 +67,7 @@ echo $this->Bootstrap->modal([ 'size' => $modalSize, 'type' => $modalType, 'confirmText' => __('Update meta-templates'), - 'confirmFunction' => 'updateMetaTemplate', + // 'confirmFunction' => 'updateMetaTemplate', ]); ?> diff --git a/templates/element/MetaTemplates/conflictResolution.php b/templates/element/MetaTemplates/conflictResolution.php index ceb836a..ebe326f 100644 --- a/templates/element/MetaTemplates/conflictResolution.php +++ b/templates/element/MetaTemplates/conflictResolution.php @@ -1,7 +1,31 @@ -
+element('genericElements/Form/genericForm', [ + 'entity' => null, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'model' => 'MetaTemplate', + 'fields' => [ + [ + 'field' => 'update_strategy', + 'type' => 'radio', + 'options' => [ + ['value' => 'keep_both', 'text' => 'keep_both', 'id' => 'radio_keep_both'], + ['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'], + ], + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ], + ] +]); +?> + +
- + - +
- \ No newline at end of file +
+ +
+ +
+ + \ No newline at end of file diff --git a/templates/element/MetaTemplates/conflictTable.php b/templates/element/MetaTemplates/conflictTable.php index b47cf8f..045b858 100644 --- a/templates/element/MetaTemplates/conflictTable.php +++ b/templates/element/MetaTemplates/conflictTable.php @@ -1,9 +1,14 @@ + + - + @@ -16,10 +21,21 @@ diff --git a/templates/element/MetaTemplates/migrationToNewVersionForm.php b/templates/element/MetaTemplates/migrationToNewVersionForm.php new file mode 100644 index 0000000..7bbbfaa --- /dev/null +++ b/templates/element/MetaTemplates/migrationToNewVersionForm.php @@ -0,0 +1,12 @@ +Form->create($entity, ['id' => 'form-' . $formRandomValue]); +echo $this->element( + 'genericElements/Form/metaTemplateForm', + [ + 'metaTemplate' => $metaTemplate, + ] +); +echo $this->Form->end(); +?> \ No newline at end of file diff --git a/templates/element/genericElements/Form/metaTemplateForm.php b/templates/element/genericElements/Form/metaTemplateForm.php new file mode 100644 index 0000000..c70996d --- /dev/null +++ b/templates/element/genericElements/Form/metaTemplateForm.php @@ -0,0 +1,73 @@ + '
{{content}}
', + 'inputContainerError' => '
{{content}}
', + 'formGroup' => '
{{input}}{{error}}
', + 'error' => '
{{content}}
', + 'errorList' => '
    {{content}}
', + 'errorItem' => '
  • {{text}}
  • ', +]; +$this->Form->setTemplates($default_template); +$backupTemplates = $this->Form->getTemplates(); + +$fieldsHtml = ''; +foreach ($metaTemplate->meta_template_fields as $metaTemplateField) { + $metaTemplateField->label = Inflector::humanize($metaTemplateField->field); + if (!empty($metaTemplateField->metaFields)) { + if (!empty($metaTemplateField->multiple)) { + $fieldsHtml .= $this->element( + 'genericElements/Form/multiFieldScaffold', + [ + 'metaFieldsEntities' => $metaTemplateField->metaFields, + 'metaTemplateField' => $metaTemplateField, + 'multiple' => !empty($metaTemplateField->multiple), + 'form' => $this->Form, + ] + ); + } else { + $metaField = reset($metaTemplateField->metaFields); + $fieldData = [ + 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id), + 'label' => $metaTemplateField->label, + ]; + $this->Form->setTemplates($backupTemplates); + $fieldsHtml .= $this->element( + 'genericElements/Form/fieldScaffold', + [ + 'fieldData' => $fieldData, + 'metaTemplateField' => $metaTemplateField, + 'form' => $this->Form + ] + ); + } + } else { + if (!empty($metaTemplateField->multiple)) { + $fieldsHtml .= $this->element( + 'genericElements/Form/multiFieldScaffold', + [ + 'metaFieldsEntities' => [], + 'metaTemplateField' => $metaTemplateField, + 'multiple' => !empty($metaTemplateField->multiple), + 'form' => $this->Form, + ] + ); + } else { + $this->Form->setTemplates($backupTemplates); + $fieldData = [ + 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id), + 'label' => $metaTemplateField->label, + ]; + $fieldsHtml .= $this->element( + 'genericElements/Form/fieldScaffold', + [ + 'fieldData' => $fieldData, + 'form' => $this->Form + ] + ); + } + } +} +echo $fieldsHtml; \ No newline at end of file diff --git a/templates/element/genericElements/Form/metaTemplateScaffold.php b/templates/element/genericElements/Form/metaTemplateScaffold.php index f7d1ef1..00dc9be 100644 --- a/templates/element/genericElements/Form/metaTemplateScaffold.php +++ b/templates/element/genericElements/Form/metaTemplateScaffold.php @@ -1,82 +1,18 @@ '
    {{content}}
    ', - 'inputContainerError' => '
    {{content}}
    ', - 'formGroup' => '
    {{input}}{{error}}
    ', -]; -$this->Form->setTemplates($default_template); $backupTemplates = $this->Form->getTemplates(); $tabData = []; foreach ($entity->MetaTemplates as $i => $metaTemplate) { - if ($metaTemplate->is_default) { - $tabData['navs'][$i] = [ - 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate]) - ]; - } else { - $tabData['navs'][$i] = [ - 'text' => $metaTemplate->name - ]; - } + $tabData['navs'][$i] = [ + 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate]) + ]; $fieldsHtml = ''; - foreach ($metaTemplate->meta_template_fields as $metaTemplateField) { - $metaTemplateField->label = Inflector::humanize($metaTemplateField->field); - if (!empty($metaTemplateField->metaFields)) { - if (!empty($metaTemplateField->multiple)) { - $fieldsHtml .= $this->element( - 'genericElements/Form/multiFieldScaffold', - [ - 'metaFieldsEntities' => $metaTemplateField->metaFields, - 'metaTemplateField' => $metaTemplateField, - 'multiple' => !empty($metaTemplateField->multiple), - 'form' => $this->Form, - ] - ); - } else { - $metaField = reset($metaTemplateField->metaFields); - $fieldData = [ - 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id), - 'label' => $metaTemplateField->label, - ]; - $this->Form->setTemplates($backupTemplates); - $fieldsHtml .= $this->element( - 'genericElements/Form/fieldScaffold', - [ - 'fieldData' => $fieldData, - 'metaTemplateField' => $metaTemplateField, - 'form' => $this->Form - ] - ); - } - } else { - if (!empty($metaTemplateField->multiple)) { - $fieldsHtml .= $this->element( - 'genericElements/Form/multiFieldScaffold', - [ - 'metaFieldsEntities' => [], - 'metaTemplateField' => $metaTemplateField, - 'multiple' => !empty($metaTemplateField->multiple), - 'form' => $this->Form, - ] - ); - } else { - $this->Form->setTemplates($backupTemplates); - $fieldData = [ - 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id), - 'label' => $metaTemplateField->label, - ]; - $fieldsHtml .= $this->element( - 'genericElements/Form/fieldScaffold', - [ - 'fieldData' => $fieldData, - 'form' => $this->Form - ] - ); - } - } - } + $fieldsHtml .= $this->element( + 'genericElements/Form/metaTemplateForm', + [ + 'metaTemplate' => $metaTemplate, + ] + ); $tabData['content'][$i] = $fieldsHtml; } $this->Form->setTemplates($backupTemplates); diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php index c280aad..543cb2e 100644 --- a/templates/element/genericElements/IndexTable/Fields/actions.php +++ b/templates/element/genericElements/IndexTable/Fields/actions.php @@ -101,12 +101,13 @@ $action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue); } echo sprintf( - ' ', + ' ', $url, empty($action['title']) ? '' : h($action['title']), empty($action['title']) ? '' : h($action['title']), empty($action['dbclickAction']) ? '' : 'class="dblclickActionElement"', empty($action['onclick']) ? '' : sprintf('onClick="%s"', $action['onclick']), + empty($action['variant']) ? 'outline-dark' : h($action['variant']), $this->FontAwesome->getClass($action['icon']) ); } diff --git a/templates/element/genericElements/IndexTable/Fields/update_status.php b/templates/element/genericElements/IndexTable/Fields/update_status.php deleted file mode 100644 index 8f266fc..0000000 --- a/templates/element/genericElements/IndexTable/Fields/update_status.php +++ /dev/null @@ -1,9 +0,0 @@ -Bootstrap->icon($icon); -?> \ No newline at end of file diff --git a/templates/element/genericElements/MetaTemplates/metaTemplateNav.php b/templates/element/genericElements/MetaTemplates/metaTemplateNav.php index 9f9e36d..11c16c1 100644 --- a/templates/element/genericElements/MetaTemplates/metaTemplateNav.php +++ b/templates/element/genericElements/MetaTemplates/metaTemplateNav.php @@ -1,4 +1,12 @@ name) ?> - + Bootstrap->badge([ + 'variant' => !empty($metaTemplate['hasNewerVersion']) ? 'warning' : 'primary', + 'text' => sprintf('v%s', h($metaTemplate->version)) + ]) + ?> + is_default)): ?> + + \ No newline at end of file diff --git a/templates/element/genericElements/SingleViews/metafields_panel.php b/templates/element/genericElements/SingleViews/metafields_panel.php index 2a70d39..753a5c7 100644 --- a/templates/element/genericElements/SingleViews/metafields_panel.php +++ b/templates/element/genericElements/SingleViews/metafields_panel.php @@ -1,19 +1,15 @@ - [], 'content' => [] ]; foreach($data['MetaTemplates'] as $metaTemplate) { if (!empty($metaTemplate->meta_template_fields)) { - if ($metaTemplate->is_default) { - $tabData['navs'][] = [ - 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate]) - ]; - } else { - $tabData['navs'][] = [ - 'text' => $metaTemplate->name - ]; - } + $tabData['navs'][] = [ + 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate]) + ]; $fields = []; foreach ($metaTemplate->meta_template_fields as $metaTemplateField) { $labelPrintedOnce = false; @@ -40,6 +36,28 @@ foreach($data['MetaTemplates'] as $metaTemplate) { count($fields) ) ]); + if (!empty($metaTemplate['hasNewerVersion'])) { + $listTable = $this->Bootstrap->alert([ + 'html' => sprintf( + '
    %s
    %s
    ', + __('These meta-fields are registered under an outdated template. Newest template is {0}, current is {1}.', $metaTemplate['hasNewerVersion']->version, $metaTemplate->version), + $this->Bootstrap->button([ + 'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version), + 'variant' => 'success', + 'nodeType' => 'a', + 'params' => [ + 'href' => Router::url([ + 'controller' => 'metaTemplates', + 'action' => 'migrateOldMetaTemplateToNewestVersionForEntity', + $metaTemplate->id, + $data->id, + ]) + ] + ]) + ), + 'variant' => 'warning', + ]) . $listTable; + } $tabData['content'][] = $listTable; } }
    Bootstrap->badge([ - 'text' => __('Affected meta-fields will be removed'), - 'variant' => 'danger', - ]) + foreach ($fieldConflict['conflictingEntities'] as $i => $id) { + if ($i > 0) { + echo ', '; + } + if ($i > 10) { + echo sprintf('%s', __('{0} more', count($fieldConflict['conflictingEntities'])-$i)); + break; + } + $url = Router::url([ + 'controller' => Inflector::pluralize($updateableTemplate['existing_template']->scope), + 'action' => 'view', + $id + ]); + echo sprintf('%s', $url, __('{0} #{1}', h(Inflector::humanize($updateableTemplate['existing_template']->scope)), h($id))); + } ?>