From 20eebd097de43d434d6391d80e3eedd7e47e3edf Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 14 Feb 2023 14:42:35 +0100 Subject: [PATCH] new: [meta-template] Improvement of the update system - Changed default update strategy from `create_new` to `update_existing` - Added mechanism to automatically migrate meta-fields to newest template - Improved validation and conflict detection strategies - Fixed various UI bugs and improved QoL --- .../Component/Navigation/MetaTemplates.php | 2 +- src/Controller/MetaTemplatesController.php | 49 +++ src/Model/Table/MetaFieldsTable.php | 19 +- src/Model/Table/MetaTemplatesTable.php | 290 ++++++++++++++---- templates/MetaTemplates/index.php | 14 +- .../migrate_metafields_to_newest_template.php | 102 ++++++ templates/MetaTemplates/update.php | 16 +- templates/MetaTemplates/update_all.php | 21 +- .../MetaTemplates/conflictResolution.php | 46 +-- .../element/MetaTemplates/conflictTable.php | 6 +- 10 files changed, 459 insertions(+), 106 deletions(-) create mode 100644 templates/MetaTemplates/migrate_metafields_to_newest_template.php diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php index d18140e..87bd4e0 100644 --- a/src/Controller/Component/Navigation/MetaTemplates.php +++ b/src/Controller/Component/Navigation/MetaTemplates.php @@ -91,7 +91,7 @@ class MetaTemplatesNavigation extends BaseNavigation 'url' => '/metaTemplates/prune_outdated_template', ]); - if (empty($this->viewVars['updateableTemplates']['up-to-date'])) { + if (empty($this->viewVars['templateStatus']['up-to-date'])) { $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [ 'label' => __('Update template'), 'url' => '/metaTemplates/update/{{id}}', diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index 2595385..a695351 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -244,6 +244,55 @@ class MetaTemplatesController extends AppController $this->set('movedMetaTemplateFields', $movedMetaTemplateFields); } + public function migrateMetafieldsToNewestTemplate(int $template_id) + { + $oldMetaTemplate = $this->MetaTemplates->find()->where([ + 'id' => $template_id + ])->contain(['MetaTemplateFields'])->first(); + + if (empty($oldMetaTemplate)) { + throw new NotFoundException(__('Invalid {0} {1}.', $this->MetaTemplates->getAlias(), $template_id)); + } + $newestMetaTemplate = $this->MetaTemplates->getNewestVersion($oldMetaTemplate, true); + if ($oldMetaTemplate->id == $newestMetaTemplate->id) { + throw new NotFoundException(__('Invalid {0} {1}. Template already the newest version', $this->MetaTemplates->getAlias(), $template_id)); + } + + if ($this->request->is('post')) { + $result = $this->MetaTemplates->migrateMetafieldsToNewestTemplate($oldMetaTemplate, $newestMetaTemplate); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($result, 'json'); + } else { + if ($result['success']) { + $message = __('{0} entities updated. {1} entities could not be automatically migrated.', $result['migrated_count'], $result['conflicting_entities']); + } else { + $message = __('{0} entities updated. {1} entities could not be automatically migrated. {2} entities could not be updated due to errors', $result['migrated_count'], $result['conflicting_entities'], $result['migration_errors']); + } + $this->CRUD->setResponseForController('update', $result['success'], $message, $result, $result['migration_errors'], ['redirect' => $this->referer()]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + } else { + $entities = $this->MetaTemplates->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null); + $conflictingEntities = []; + foreach ($entities as $entity) { + $conflicts = $this->MetaTemplates->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate); + if (!empty($conflicts)) { + $conflictingEntities[] = $entity; + } + } + + if (!$this->ParamHandler->isRest()) { + $this->set('oldMetaTemplate', $oldMetaTemplate); + $this->set('newestMetaTemplate', $newestMetaTemplate); + $this->set('conflictingEntities', $conflictingEntities); + $this->set('entityCount', count($entities)); + } + } + } + public function index() { $templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates(); diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index ee0004c..92ebf83 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -39,8 +39,6 @@ class MetaFieldsTable extends AppTable ->notEmptyString('value') ->notEmptyString('meta_template_id') ->notEmptyString('meta_template_field_id') - // ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); - // ->requirePresence(['scope', 'field', 'value', 'uuid',], 'create'); ->notEmptyString('meta_template_directory_id') ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create'); @@ -53,10 +51,9 @@ class MetaFieldsTable extends AppTable return $validator; } - public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) - { - if (!isset($data['meta_template_directory_id'])) { - $data['meta_template_directory_id'] = $this->getTemplateDirectoryIdFromMetaTemplate($data['meta_template_id']); + public function afterMarshal(EventInterface $event, \App\Model\Entity\MetaField $entity, ArrayObject $data, ArrayObject $options) { + if (!isset($entity->meta_template_directory_id)) { + $entity->set('meta_template_directory_id', $this->getTemplateDirectoryIdFromMetaTemplate($entity->meta_template_id)); } } @@ -89,6 +86,9 @@ class MetaFieldsTable extends AppTable if (!empty($metaTemplateField['regex'])) { return $this->isValidRegex($value, $metaTemplateField); } + if (!empty($metaTemplateField['values_list'])) { + return $this->isValidValuesList($value, $metaTemplateField); + } return true; } @@ -118,4 +118,11 @@ class MetaFieldsTable extends AppTable } return true; } + + public function isValidValuesList($value, $metaTemplateField) + { + + $valuesList = $metaTemplateField['values_list']; + return in_array($value, $valuesList); + } } diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php index 76d82e9..0353e2b 100644 --- a/src/Model/Table/MetaTemplatesTable.php +++ b/src/Model/Table/MetaTemplatesTable.php @@ -24,8 +24,12 @@ class MetaTemplatesTable extends AppTable public const UPDATE_STRATEGY_KEEP_BOTH = 'keep_both'; public const UPDATE_STRATEGY_DELETE = 'delete_all'; - public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW; - public const ALLOWED_STRATEGIES = [MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW]; + public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING; + public const ALLOWED_STRATEGIES = [ + MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW, + MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING, + MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH, + ]; private $templatesOnDisk = null; @@ -73,6 +77,41 @@ class MetaTemplatesTable extends AppTable * @return array The update result containing potential errors and the successes */ public function updateAllTemplates(): array + { + // Create new template found on the disk + $newTemplateResult = $this->createAllNewTemplates(); + $files_processed = $newTemplateResult['files_processed']; + $updatesErrors = $newTemplateResult['update_errors']; + + $templatesUpdateStatus = $this->getUpdateStatusForTemplates(); + + // Update all existing templates + foreach ($templatesUpdateStatus as $uuid => $templateUpdateStatus) { + if (!empty($templateUpdateStatus['existing_template'])) { + $metaTemplate = $templateUpdateStatus['existing_template']; + $result = $this->update($metaTemplate, null); + if ($result['success']) { + $files_processed[] = $metaTemplate->uuid; + } + if (!empty($result['errors'])) { + $updatesErrors[] = $result['errors']; + } + } + } + $results = [ + 'update_errors' => $updatesErrors, + 'files_processed' => $files_processed, + 'success' => !empty($files_processed), + ]; + return $results; + } + + /** + * Load the templates stored on the disk update and create them in the database without touching at the existing ones + * + * @return array The update result containing potential errors and the successes + */ + public function createAllNewTemplates(): array { $updatesErrors = []; $files_processed = []; @@ -106,7 +145,7 @@ class MetaTemplatesTable extends AppTable * @param string|null $strategy The strategy to be used when updating templates with conflicts * @return array The update result containing potential errors and the successes */ - public function update($metaTemplate, $strategy = null): array + public function update(\App\Model\Entity\MetaTemplate $metaTemplate, $strategy = null): array { $files_processed = []; $updatesErrors = []; @@ -114,6 +153,25 @@ class MetaTemplatesTable extends AppTable $templateStatus = $this->getStatusForMetaTemplate($templateOnDisk, $metaTemplate); $updateStatus = $this->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate); $errors = []; + + $result = $this->doUpdate($updateStatus, $templateOnDisk, $metaTemplate, $strategy); + if ($result['success']) { + $files_processed[] = $templateOnDisk['uuid']; + } + if (!empty($result['errors'])) { + $updatesErrors[] = $errors; + } + $results = [ + 'update_errors' => $updatesErrors, + 'files_processed' => $files_processed, + 'success' => !empty($files_processed), + ]; + return $results; + } + + private function doUpdate(array $updateStatus, array $templateOnDisk, \App\Model\Entity\MetaTemplate $metaTemplate, string $strategy = null): array + { + $errors = []; $success = false; if ($updateStatus['up-to-date']) { $errors['message'] = __('Meta-template already up-to-date'); @@ -126,22 +184,17 @@ class MetaTemplatesTable extends AppTable $errors['message'] = __('Cannot update meta-template, update strategy not allowed'); } else if (!$updateStatus['up-to-date']) { $strategy = is_null($strategy) ? MetaTemplatesTable::DEFAULT_STRATEGY : $strategy; + if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING && !$updateStatus['automatically-updateable']) { + $strategy = MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH; + } $success = $this->updateMetaTemplateWithStrategyRouter($metaTemplate, $templateOnDisk, $strategy, $errors); } else { $errors['message'] = __('Could not update. Something went wrong.'); } - if ($success) { - $files_processed[] = $templateOnDisk['uuid']; - } - if (!empty($errors)) { - $updatesErrors[] = $errors; - } - $results = [ - 'update_errors' => $updatesErrors, - 'files_processed' => $files_processed, - 'success' => !empty($files_processed), + return [ + 'success' => $success, + 'errors' => $errors, ]; - return $results; } /** @@ -764,13 +817,27 @@ class MetaTemplatesTable extends AppTable $errors[] = new UpdateError(false, $metaTemplate); return false; } - $metaTemplate = $this->patchEntity($metaTemplate, $template, [ - 'associated' => ['MetaTemplateFields'] - ]); + $metaTemplate = $this->patchEntity($metaTemplate, $template); + foreach ($template['metaFields'] as $newMetaField) { + $newMetaField['__patched'] = true; + foreach($metaTemplate->meta_template_fields as $i => $oldMetaField) { + if ($oldMetaField->field == $newMetaField['field']) { + $metaTemplate->meta_template_fields[$i] = $this->MetaTemplateFields->patchEntity($oldMetaField, $newMetaField); + continue 2; + } + } + $metaTemplate->meta_template_fields[] = $this->MetaTemplateFields->newEntity($newMetaField); + } + $metaTemplate->setDirty('meta_template_fields', true); $metaTemplate = $this->save($metaTemplate, [ 'associated' => ['MetaTemplateFields'] ]); - if (!empty($metaTemplate)) { + foreach ($metaTemplate->meta_template_fields as $savedMetafield) { + if (empty($savedMetafield['__patched'])) { + $this->MetaTemplateFields->delete($savedMetafield); + } + } + if (empty($metaTemplate)) { $errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors()); return false; } @@ -797,8 +864,8 @@ class MetaTemplatesTable extends AppTable } if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH) { $result = $this->executeStrategyKeep($template, $metaTemplate); - } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_DELETE) { - $result = $this->executeStrategyDeleteAll($template, $metaTemplate); + } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING) { + $result = $this->updateMetaTemplate($metaTemplate, $template); } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) { $result = $this->executeStrategyCreateNew($template, $metaTemplate); } else { @@ -830,6 +897,7 @@ class MetaTemplatesTable extends AppTable $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']); } $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template); + $conflictingEntities = Hash::combine($conflicts, '{s}.conflictingEntities.{n}.parent_id', '{s}.conflictingEntities.{n}.parent_id'); $blockingConflict = Hash::extract($conflicts, '{s}.conflicts'); $errors = []; if (empty($blockingConflict)) { // No conflict, everything can be updated without special care @@ -838,7 +906,6 @@ class MetaTemplatesTable extends AppTable } $entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null); - $conflictingEntities = []; foreach ($entities as $entity) { $conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template); if (!empty($conflicts)) { @@ -849,8 +916,9 @@ class MetaTemplatesTable extends AppTable $this->updateMetaTemplate($metaTemplate, $template, $errors); return !empty($errors) ? $errors[0] : true; } - $template['is_default'] = $metaTemplate['is_default']; - $template['enabled'] = $metaTemplate['enabled']; + $template['is_default'] = $metaTemplate->is_default; + $template['enabled'] = $metaTemplate->enabled; + $metaTemplate->set('enabled', false); if ($metaTemplate->is_default) { $metaTemplate->set('is_default', false); $this->save($metaTemplate); @@ -858,13 +926,9 @@ class MetaTemplatesTable extends AppTable $savedMetaTemplate = null; $this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate); if (!empty($savedMetaTemplate)) { - $savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}'); foreach ($entities as $entity) { if (empty($conflictingEntities[$entity->id])) { // conflicting entities remain untouched - foreach ($entity['meta_fields'] as $metaField) { - $savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field]; - $this->supersedeMetaFieldWithMetaTemplateField($metaField, $savedMetaTemplateField); - } + $this->supersedeMetaFieldsWithMetaTemplateField($entity['meta_fields'], $savedMetaTemplate); } } } else { @@ -873,6 +937,44 @@ class MetaTemplatesTable extends AppTable return true; } + public function migrateMetafieldsToNewestTemplate(\App\Model\Entity\MetaTemplate $oldMetaTemplate, \App\Model\Entity\MetaTemplate $newestMetaTemplate): array + { + $result = [ + 'success' => true, + 'migrated_count' => 0, + 'conflicting_entities' => 0, + 'migration_errors' => 0, + ]; + + $entities = $this->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null); + if (empty($entities)) { + return $result; + } + + $successfullyMigratedEntities = 0; + $migrationErrors = 0; + $conflictingEntities = []; + foreach ($entities as $entity) { + $conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate); + if (!empty($conflicts)) { + $conflictingEntities[] = $entity->id; + } else { + $success = $this->supersedeMetaFieldsWithMetaTemplateField($entity->meta_fields, $newestMetaTemplate); + if ($success) { + $successfullyMigratedEntities += 1; + } else { + $migrationErrors += 1; + } + } + } + + $result['success'] = $migrationErrors == 0; + $result['migrated_count'] = $successfullyMigratedEntities; + $result['conflicting_entities'] = count($conflictingEntities); + $result['migration_errors'] = $migrationErrors; + return $result; + } + /** * Execute the `delete_all` update strategy by updating the meta-template and deleting all conflicting meta-fields. * Strategy: @@ -941,16 +1043,20 @@ class MetaTemplatesTable extends AppTable /** * Supersede a meta-fields's meta-template-field with the provided one. * - * @param \App\Model\Entity\MetaField $metaField + * @param array $metaFields * @param \App\Model\Entity\MetaTemplateField $savedMetaTemplateField * @return bool True if the replacement was a success, False otherwise */ - public function supersedeMetaFieldWithMetaTemplateField(\App\Model\Entity\MetaField $metaField, \App\Model\Entity\MetaTemplateField $savedMetaTemplateField): bool + public function supersedeMetaFieldsWithMetaTemplateField(array $metaFields, \App\Model\Entity\MetaTemplate $savedMetaTemplate): bool { - $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); + $savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}'); + foreach ($metaFields as $i => $metaField) { + $savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field]; + $metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id); + $metaField->set('meta_template_field_id', $savedMetaTemplateField->id); + } + $entities = $this->MetaTemplateFields->MetaFields->saveMany($metaFields); + return !empty($entities); } /** @@ -977,13 +1083,14 @@ class MetaTemplatesTable extends AppTable $metaTemplateFieldByName[$metaTemplateField['field']] = $this->MetaTemplateFields->newEntity($metaTemplateField); } foreach ($metaFields as $metaField) { - if ($existingMetaTemplate && $metaField->meta_template_id != $template->id) { - continue; + if (empty($metaTemplateFieldByName[$metaField->field])) { // Meta-field was removed from the template + $isValid = false; + } else { + $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField( + $metaField->value, + $metaTemplateFieldByName[$metaField->field] + ); } - $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField( - $metaField->value, - $metaTemplateFieldByName[$metaField->field] - ); if ($isValid !== true) { $conflicting[] = $metaField; } @@ -1027,27 +1134,12 @@ class MetaTemplatesTable extends AppTable $result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id'); } } - if (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) { - $entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find(); - $entitiesWithMetaFieldQuery - ->enableHydration(false) - ->select([ - 'parent_id', - ]) - ->where([ - 'meta_template_field_id' => $metaTemplateField->id, - ]); - $entitiesTable = $this->getTableForMetaTemplateScope($scope); - $entities = $entitiesTable->find() - ->where(['id IN' => $entitiesWithMetaFieldQuery]) - ->contain([ - 'MetaFields' => [ - 'conditions' => [ - 'MetaFields.meta_template_field_id' => $metaTemplateField->id - ] - ] - ]) - ->all()->toList(); + + if ( + (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) || + !empty($templateField['values_list']) + ) { + $entities = $this->getEntitiesForMetaTemplateField($scope, $metaTemplateField->id, true); $conflictingEntities = []; foreach ($entities as $entity) { foreach ($entity['meta_fields'] as $metaField) { @@ -1056,7 +1148,10 @@ class MetaTemplatesTable extends AppTable $templateField ); if ($isValid !== true) { - $conflictingEntities[] = $entity->id; + $conflictingEntities[] = [ + 'parent_id' => $entity->id, + 'meta_template_field_id' => $metaTemplateField->id, + ]; break; } } @@ -1064,13 +1159,50 @@ class MetaTemplatesTable extends AppTable if (!empty($conflictingEntities)) { $result['automatically-updateable'] = $result['automatically-updateable'] && false; - $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore'); + $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore.'); $result['conflictingEntities'] = array_merge($result['conflictingEntities'], $conflictingEntities); } } return $result; } + /** + * Return all entities having the meta-fields using the provided meta-template-field. + * + * @param string $scope + * @param integer $metaTemplateFieldID The ID of the matching meta-template-field + * @param boolean $includeMatchingMetafields Should the entities also include the matching meta-fields + * @return array + */ + private function getEntitiesForMetaTemplateField(string $scope, int $metaTemplateFieldID, bool $includeMatchingMetafields=true): array + { + $entitiesTable = $this->getTableForMetaTemplateScope($scope); + $entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find(); + $entitiesWithMetaFieldQuery + ->enableHydration(false) + ->select([ + 'parent_id', + ]) + ->where([ + 'meta_template_field_id' => $metaTemplateFieldID, + ]); + + $entitiesQuery = $entitiesTable->find() + ->where(['id IN' => $entitiesWithMetaFieldQuery]); + + if ($includeMatchingMetafields) { + $entitiesQuery->contain([ + 'MetaFields' => [ + 'conditions' => [ + 'MetaFields.meta_template_field_id' => $metaTemplateFieldID, + ] + ] + ]); + } + + return $entitiesQuery->all()->toList(); + } + /** * Check the conflict that would be introduced if the metaTemplate would be updated to the provided template * @@ -1087,7 +1219,7 @@ class MetaTemplatesTable extends AppTable $templateMetaFields = $template['metaFields']; } $conflicts = []; - $existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field'); + $existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field', 'meta_template_fields.{n}'); foreach ($templateMetaFields as $newMetaField) { foreach ($metaTemplate->meta_template_fields as $metaField) { if ($newMetaField['field'] == $metaField->field) { @@ -1103,10 +1235,23 @@ class MetaTemplatesTable extends AppTable } } if (!empty($existingMetaTemplateFields)) { - foreach ($existingMetaTemplateFields as $field => $tmp) { - $conflicts[$field] = [ + foreach ($existingMetaTemplateFields as $metaTemplateField) { + $query = $this->MetaTemplateFields->MetaFields->find(); + $query + ->enableHydration(false) + ->select([ + 'parent_id', + 'meta_template_field_id', + ]) + ->where([ + 'meta_template_field_id' => $metaTemplateField['id'], + ]) + ->group(['parent_id']); + $entityWithMetafieldToBeRemoved = $query->all()->toList(); + $conflicts[$metaTemplateField['field']] = [ 'automatically-updateable' => false, 'conflicts' => [__('This field is intended to be removed')], + 'conflictingEntities' => $entityWithMetafieldToBeRemoved, ]; } } @@ -1159,9 +1304,9 @@ class MetaTemplatesTable extends AppTable $updateStatus['current_version'] = $metaTemplate->version; $updateStatus['next_version'] = $template['version']; $updateStatus['new'] = false; + $updateStatus['automatically-updateable'] = false; if ($metaTemplate->version >= $template['version']) { $updateStatus['up-to-date'] = true; - $updateStatus['automatically-updateable'] = false; $updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.'); return $updateStatus; } @@ -1169,6 +1314,17 @@ class MetaTemplatesTable extends AppTable $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template); if (!empty($conflicts)) { $updateStatus['conflicts'] = $conflicts; + $updateStatus['automatically-updateable'] = false; + $emptySum = 0; + foreach ($conflicts as $fieldname => $fieldStatus) { + if (!empty($fieldStatus['conflictingEntities'])) { + break; + } + $emptySum += 1; + } + if ($emptySum == count($conflicts)) { + $updateStatus['automatically-updateable'] = true; + } } else { $updateStatus['automatically-updateable'] = true; } diff --git a/templates/MetaTemplates/index.php b/templates/MetaTemplates/index.php index d67d859..1ac384f 100644 --- a/templates/MetaTemplates/index.php +++ b/templates/MetaTemplates/index.php @@ -198,10 +198,22 @@ echo $this->element('genericElements/IndexTable/index_table', [ } ] ], + [ + 'open_modal' => '/metaTemplates/migrateMetafieldsToNewestTemplate/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'title' => __('Update meta-fields to the newest version of this meta-template'), + 'icon' => 'arrow-circle-up', + 'variant' => 'success', + 'complex_requirement' => [ + 'function' => function ($row, $options) { + return !empty($row['updateStatus']['to-existing']) && empty($row['updateStatus']['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'), + 'title' => __('This meta-template doesn\'t have any meta-fields and can be safely removed.'), 'icon' => 'trash', 'variant' => 'success', 'complex_requirement' => [ diff --git a/templates/MetaTemplates/migrate_metafields_to_newest_template.php b/templates/MetaTemplates/migrate_metafields_to_newest_template.php new file mode 100644 index 0000000..09a7c3b --- /dev/null +++ b/templates/MetaTemplates/migrate_metafields_to_newest_template.php @@ -0,0 +1,102 @@ + 'metaTemplates', + 'action' => 'view', + $newestMetaTemplate->id +]); + +$bodyHtml = ''; +$bodyHtml .= sprintf('
%s: %s
', __('Current version'), h($oldMetaTemplate->version)); +$bodyHtml .= sprintf('
%s: %s
', __('Newest version'), $urlNewestMetaTemplate, h($newestMetaTemplate->version)); +$bodyHtml .= sprintf('

%s

', __('{0} Entities with meta-fields for the meta-template version {1}', h($entityCount), h($oldMetaTemplate->version))); + +// debug($conflictingEntities); + +if (empty($conflictingEntities)) { + $bodyHtml .= $this->Bootstrap->alert([ + 'text' => __('All entities can updated automatically', count($conflictingEntities)), + 'variant' => 'success', + 'dismissible' => false, + ]); +} else { + $bodyHtml .= $this->Bootstrap->alert([ + 'html' => sprintf( + '', + $this->Bootstrap->node('li', [], __('{0} entities can be updated automatically', $entityCount - count($conflictingEntities))), + $this->Bootstrap->node('li', [], __('{0} entities cannot be updated automatically and require manual migration', count($conflictingEntities))) + ), + 'variant' => 'warning', + 'dismissible' => false, + ]); + $bodyHtml .= ''; +} +$form = sprintf( + '
%s%s
', + $this->Form->create(null, [ + 'url' => [ + 'controller' => 'MetaTemplates', + 'action' => 'migrateMetafieldsToNewestTemplate', + $oldMetaTemplate->id, + ] + ]), + $this->Form->end() +); +$bodyHtml .= $form; + +$title = __('{0} has a new meta-template and meta-fields to be updated', sprintf('%s', h($oldMetaTemplate->name))); +if (!empty($ajax)) { + echo $this->Bootstrap->modal([ + 'titleHtml' => $title, + 'bodyHtml' => $bodyHtml, + 'size' => 'lg', + 'type' => 'confirm', + 'confirmButton' => [ + 'text' => __('Migrate meta-fields'), + 'variant' => 'success', + ], + 'confirmFunction' => 'migrateMetafieldsToNewestTemplate', + ]); +} else { + echo $this->Bootstrap->node('h1', [], $title); + echo $bodyHtml; + echo $this->Bootstrap->button([ + 'text' => __('Migrate meta-fields'), + 'variant' => 'success', + 'onclick' => '$(".hidden-form-container form").submit()', + ]); +} +?> + + \ No newline at end of file diff --git a/templates/MetaTemplates/update.php b/templates/MetaTemplates/update.php index 473f29a..a1c1821 100644 --- a/templates/MetaTemplates/update.php +++ b/templates/MetaTemplates/update.php @@ -18,6 +18,11 @@ if ($updateStatus['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, ]); + $bodyHtml .= $this->Bootstrap->alert([ + 'variant' => 'success', + 'text' => __('All meta-fields will be migrated to the newest version.'), + 'dismissible' => false, + ]); $form = $this->element('genericElements/Form/genericForm', [ 'entity' => null, 'ajax' => false, @@ -28,9 +33,9 @@ if ($updateStatus['up-to-date']) { [ 'field' => 'update_strategy', 'type' => 'checkbox', - 'value' => MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW, + 'value' => MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING, 'checked' => true, - ] + ], ], 'submit' => [ 'action' => $this->request->getParam('action') @@ -42,7 +47,7 @@ if ($updateStatus['up-to-date']) { $modalSize = 'xl'; $bodyHtml .= $this->Bootstrap->alert([ 'variant' => 'warning', - 'text' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', h($templateOnDisk['version'])), + 'html' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', sprintf('%s', h($templateOnDisk['version']))), 'dismissible' => false, ]); $conflictTable = $this->element('MetaTemplates/conflictTable', [ @@ -50,8 +55,11 @@ if ($updateStatus['up-to-date']) { 'metaTemplate' => $metaTemplate, 'templateOnDisk' => $templateOnDisk, ]); + $conflictCount = array_reduce($templateStatus['conflicts'], function ($carry, $conflict) { + return $carry + count($conflict['conflictingEntities']); + }, 0); $bodyHtml .= $this->Bootstrap->collapse([ - 'text' => __('View conflicts'), + 'text' => __('View conflicts ({0})', $conflictCount), 'open' => false ], $conflictTable); $bodyHtml .= $this->element('MetaTemplates/conflictResolution', [ diff --git a/templates/MetaTemplates/update_all.php b/templates/MetaTemplates/update_all.php index 141cca5..95d04af 100644 --- a/templates/MetaTemplates/update_all.php +++ b/templates/MetaTemplates/update_all.php @@ -40,12 +40,19 @@ foreach ($templatesUpdateStatus as $uuid => $status) { if (!empty($status['new'])) { $tableHtml .= sprintf('%s', __('N/A')); } else { - $tableHtml .= sprintf( - '%s %s %s', - h($status['current_version']), - $this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']), - h($status['next_version']) - ); + if ($status['current_version'] == $status['next_version']) { + $tableHtml .= sprintf( + '%s', + h($status['current_version']) + ); + } else { + $tableHtml .= sprintf( + '%s %s %s', + h($status['current_version']), + $this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']), + h($status['next_version']) + ); + } } if (!empty($status['new'])) { $numberOfUpdates += 1; @@ -60,6 +67,8 @@ foreach ($templatesUpdateStatus as $uuid => $status) { } if (!empty($status['new'])) { $tableHtml .= sprintf('%s', __('N/A')); + } elseif (!empty($status['up-to-date'])) { + $tableHtml .= sprintf('%s', __('N/A')); } else { $tableHtml .= sprintf('%s', !empty($status['conflicts']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times')); } diff --git a/templates/element/MetaTemplates/conflictResolution.php b/templates/element/MetaTemplates/conflictResolution.php index a0d1adc..cf50b58 100644 --- a/templates/element/MetaTemplates/conflictResolution.php +++ b/templates/element/MetaTemplates/conflictResolution.php @@ -1,9 +1,10 @@ element('genericElements/Form/genericForm', [ 'entity' => null, @@ -17,7 +18,7 @@ $form = $this->element('genericElements/Form/genericForm', [ 'type' => 'radio', 'options' => [ ['value' => 'create_new', 'text' => 'create_new', 'id' => 'radio_create_new'], - ['value' => 'keep_both', 'text' => 'keep_both', 'id' => 'radio_keep_both'], + ['value' => 'update_existing', 'text' => 'update', 'id' => 'radio_update'], ['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'], ], ] @@ -33,10 +34,13 @@ $form = $this->element('genericElements/Form/genericForm', [
- -