diff --git a/composer.json b/composer.json index 57d646e10..724d8e7d8 100644 --- a/composer.json +++ b/composer.json @@ -79,7 +79,8 @@ "respect/validation": "2.3.1", "giggsey/libphonenumber-for-php": "^8.13", "drupal/site_alert": "^1.3", - "lcobucci/clock": "3.0.0" + "lcobucci/clock": "3.0.0", + "drupal/term_reference_tree": "^2.0" }, "repositories": { "drupal": { diff --git a/config/install/field.storage.node.field_content_category.yml b/config/install/field.storage.node.field_content_category.yml new file mode 100644 index 000000000..2b7530800 --- /dev/null +++ b/config/install/field.storage.node.field_content_category.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + module: + - field_permissions + - node + - taxonomy +third_party_settings: + field_permissions: + permission_type: public +id: node.field_content_category +field_name: field_content_category +entity_type: node +type: entity_reference +settings: + target_type: taxonomy_term +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/install/taxonomy.vocabulary.content_category.yml b/config/install/taxonomy.vocabulary.content_category.yml new file mode 100644 index 000000000..50602faed --- /dev/null +++ b/config/install/taxonomy.vocabulary.content_category.yml @@ -0,0 +1,7 @@ +langcode: en +status: true +dependencies: { } +name: 'Content category' +vid: content_category +description: 'Categories assigned to all content to assist with filtering in content collection and search' +weight: 0 diff --git a/css/pseudo_required_field.css b/css/pseudo_required_field.css new file mode 100644 index 000000000..2ea2bddac --- /dev/null +++ b/css/pseudo_required_field.css @@ -0,0 +1,3 @@ +.pseudo-required-field { + color: #dc2323; +} diff --git a/includes/helpers.inc b/includes/helpers.inc index 3ac380d97..6fce1f59b 100644 --- a/includes/helpers.inc +++ b/includes/helpers.inc @@ -9,6 +9,8 @@ use Drupal\Core\Config\FileStorage; use Drupal\Core\Serialization\Yaml; use Drupal\Core\Site\Settings; use Drupal\paragraphs\Entity\ParagraphsType; +use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\Entity\Vocabulary; /** * Helper to read configuration from provided locations. @@ -199,3 +201,188 @@ function _tide_extract_id_for_storage_usage($target, $config, $include) { } return $matches; } + +/** + * Adds taxonomy with nested terms up to a specified depth and order. + * + * @param array $terms + * An associative array of terms where keys are term names and values are + * either arrays of child term names or associative arrays with nested terms. + * Example structure for $terms: + * $terms = [ + * 'Events' => [ + * 'Events', + * ], + * 'Policy and legislation' => [ + * 'Legislation', + * 'Regulation' => [ + * 'hello' => [ + * 'world', + * 'new' => [ + * 'era', + * ], + * ], + * ], + * ], + * ]; + * This represents a hierarchy with 'Events' and 'Policy and legislation' + * as top-level terms, and nested children underneath them. + * @param array $vocabulary_details + * An associative array with vocabulary details. Example: + * $vocabulary_details = [ + * 'vid' => 'content_category', + * 'description' => 'Categories for content filtering and search', + * 'name' => 'Content category', + * ];. + * @param int $max_depth + * The maximum hierarchy depth to create, defaulting to 4. Deeper nested + * terms beyond this depth will be ignored. + */ +function _tide_core_adding_default_taxonomy(array $terms, array $vocabulary_details, $max_depth = 4) { + $vocabulary_id = $vocabulary_details['vid']; + // Load or create the vocabulary. + $vocabulary = Vocabulary::load($vocabulary_id); + if (!$vocabulary) { + $vocabulary = Vocabulary::create($vocabulary_details); + $vocabulary->save(); + } + + // Function to recursively add terms to the vocabulary. + $add_terms_recursively = function ($parent_tid, array $terms, $weight, $depth) use (&$add_terms_recursively, $vocabulary_id, $max_depth) { + if ($depth > $max_depth) { + return; + } + + foreach ($terms as $term_name => $child_terms) { + if (is_array($child_terms)) { + $parent_term = _create_or_load_term($vocabulary_id, $term_name, $parent_tid, $weight++); + $add_terms_recursively($parent_term->id(), $child_terms, 0, $depth + 1); + } + else { + _create_or_load_term($vocabulary_id, $child_terms, $parent_tid, $weight++); + } + } + }; + + $add_terms_recursively(0, $terms, 0, 1); +} + +/** + * Create or retrieve a Term. + * + * This function checks if a taxonomy term already exists within a specified + * vocabulary. If the term exists, it is returned. If not, a new term is created + * with the given details and then returned. + * + * @param string $vocabulary_id + * The machine name of the vocabulary. + * @param string $term_name + * The name of the term to be created or retrieved. + * @param int $parent_tid + * The term ID of the parent term. If set to 0, it has no parent. + * @param int $weight + * The weight of the term, used for sorting purposes. + * + * @return \Drupal\taxonomy\Entity\Term + * The taxonomy term object. + */ +function _create_or_load_term($vocabulary_id, $term_name, $parent_tid = 0, $weight = 0) { + // If a parent term is specified, search for a term with the same name + // and parent. + if ($parent_tid != 0) { + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties([ + 'name' => $term_name, + 'vid' => $vocabulary_id, + 'parent' => $parent_tid, + ]); + } + // If no parent is specified, search for a term with the same name in + // the vocabulary. + else { + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties([ + 'name' => $term_name, + 'vid' => $vocabulary_id, + ]); + } + // If the term exists, return it. + $term = reset($terms); + if ($term) { + return $term; + } + // If the term does not exist, create a new one with the specified details. + $term = Term::create([ + 'vid' => $vocabulary_id, + 'name' => $term_name, + 'parent' => [$parent_tid], + 'weight' => $weight, + ]); + // Save the newly created term. + $term->save(); + // Return the new term. + return $term; +} + +/** + * Returns predefined content_category terms for tide_core module. + */ +function _content_category_terms() { + return [ + 'Events' => [ + 'Event', + ], + 'Grants' => [ + 'Grant', + ], + 'Other' => [ + 'Other', + ], + 'News and media' => [ + 'News article', + 'Media release', + ], + 'Policy and legislation' => [ + 'Legislation', + 'Regulation', + 'Policy', + 'Ministerial order', + 'Notice', + 'Briefing', + ], + 'Profiles' => [ + 'Individual profile', + 'Organisation profile', + ], + 'Programs and reports' => [ + 'Report', + 'Program', + 'Initiative', + 'Campaign', + 'Inquiry', + 'Strategy', + ], + 'Resources and guidance' => [ + 'Guide', + 'Handbook', + 'Manual', + 'Fact sheet', + 'Brochure', + 'Map', + 'Framework', + 'Poster', + 'Specification', + 'Plan', + ], + 'Tools and templates' => [ + 'Tool', + 'Quote template', + 'Template', + 'Calculator', + 'Contract template', + 'Worksheet', + 'Form', + 'Letter', + 'Checklist', + 'Memo', + ], + ]; +} diff --git a/includes/updates.inc b/includes/updates.inc new file mode 100644 index 000000000..a33e0f0b1 --- /dev/null +++ b/includes/updates.inc @@ -0,0 +1,76 @@ +condition('name', $term_name) + ->condition('vid', 'content_category') + ->condition('parent', 0, '<>') + ->accessCheck(TRUE); + + $results = $query->execute(); + if (!empty($results)) { + $tid = reset($results); + $uuid = Term::load($tid)->uuid(); + if (!empty($uuid)) { + $default_values[] = ['target_uuid' => $uuid]; + } + } + } + + if (!empty($default_values)) { + /** @var \Drupal\field\Entity\FieldConfig $config */ + $config = FieldConfig::loadByName('node', $bundle, 'field_content_category'); + $config->set('default_value', $default_values)->save(); + } +} + +/** + * Set form display for field_content_category field. + */ +function _tide_core_content_category_form_display(string $bundle) { + $entity_form_display = EntityFormDisplay::load('node.' . $bundle . '.default'); + $detail = $entity_form_display->getComponent('field_tags'); + $weight = $detail['weight']; + $content = [ + "type" => "term_reference_tree", + "weight" => $weight + 1, + "region" => "content", + "settings" => [ + "start_minimized" => TRUE, + "leaves_only" => TRUE, + "select_parents" => FALSE, + "cascading_selection" => 0, + "max_depth" => 0, + ], + "third_party_settings" => [], + ]; + $field_content_category_component = $entity_form_display->getComponent('field_content_category'); + if ($field_content_category_component === NULL) { + $entity_form_display->setComponent('field_content_category', $content)->save(); + } +} diff --git a/src/TideCoreOperation.php b/src/TideCoreOperation.php index 499b07efd..9ca516149 100644 --- a/src/TideCoreOperation.php +++ b/src/TideCoreOperation.php @@ -233,4 +233,17 @@ public function chagneDiffSettings() { } } + /** + * Creates terms for content_category vocabulary. + */ + public function addContentCategoryVocabulary() { + $vocabulary_details = [ + 'vid' => 'content_category', + 'description' => 'Categories assigned to all content to assist with filtering in content collection and search', + 'name' => 'Content category', + ]; + \Drupal::moduleHandler()->loadInclude('tide_core', 'inc', 'includes/helpers'); + _tide_core_adding_default_taxonomy(_content_category_terms(), $vocabulary_details); + } + } diff --git a/tide_core.info.yml b/tide_core.info.yml index 548ab30cc..c9ad1292e 100644 --- a/tide_core.info.yml +++ b/tide_core.info.yml @@ -84,6 +84,7 @@ dependencies: - field_permissions:field_permissions - content_lock:content_lock - ckeditor_templates:ckeditor_templates + - term_reference_tree:term_reference_tree themes: - claro config_devel: diff --git a/tide_core.install b/tide_core.install index 69d7774ad..21221bf63 100644 --- a/tide_core.install +++ b/tide_core.install @@ -34,6 +34,9 @@ function tide_core_install() { // Creates terms for Topic vocabulary. $tideCoreOperation->createTopicTermsVocabulary(); + // Creates terms for content_category vocabulary. + $tideCoreOperation->addContentCategoryVocabulary(); + // Update default Editorial workflow of Content Moderation. $tideCoreOperation->updateEditorialWorkflow(); @@ -144,9 +147,38 @@ function tide_core_update_10004() { } /** - * Fixes dialog can no longer use custom data- attributes in CKEditor 5. + * Run _add_default_content_category_taxonomy(). */ function tide_core_update_10005() { + \Drupal::moduleHandler()->loadInclude('tide_core', 'inc', 'includes/helpers'); + $config_location = [\Drupal::service('extension.list.module')->getPath('tide_core') . '/config/install']; + $config_read = _tide_read_config('field.storage.node.field_content_category', $config_location, TRUE); + $storage = \Drupal::entityTypeManager()->getStorage('field_storage_config'); + if ($storage->load('node.field_content_category') === NULL) { + $config_entity = $storage->createFromStorageRecord($config_read); + $config_entity->save(); + } + if (\Drupal::moduleHandler()->moduleExists('term_reference_tree') === FALSE) { + \Drupal::service('module_installer')->install(['term_reference_tree']); + } + $config_read = _tide_read_config('taxonomy.vocabulary.content_category', $config_location, TRUE); + $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary'); + if ($storage->load('content_category') === NULL) { + $config_entity = $storage->createFromStorageRecord($config_read); + $config_entity->save(); + } + $vocabulary_details = [ + 'vid' => 'content_category', + 'description' => 'Categories assigned to all content to assist with filtering in content collection and search', + 'name' => 'Content category', + ]; + _tide_core_adding_default_taxonomy(_content_category_terms(), $vocabulary_details); +} + +/** + * Fixes dialog can no longer use custom data- attributes in CKEditor 5. + */ +function tide_core_update_10006() { $config_factory = \Drupal::configFactory(); $filter_ids = [ 'filter.format.admin_text', diff --git a/tide_core.libraries.yml b/tide_core.libraries.yml index 2c5be7e9e..5dba2bd1b 100644 --- a/tide_core.libraries.yml +++ b/tide_core.libraries.yml @@ -36,3 +36,9 @@ claro_layout: css: theme: css/claro_layout.css: {} + +pseudo_required_field: + version: 1.x + css: + theme: + css/pseudo_required_field.css: {} diff --git a/tide_core.module b/tide_core.module index 8eb14af6f..f0df7bb19 100644 --- a/tide_core.module +++ b/tide_core.module @@ -419,13 +419,34 @@ function tide_core_form_node_form_alter(&$form, FormStateInterface $form_state, $form['#attached']['library'][] = 'tide_core/node_iframe'; $form['#attached']['library'][] = 'tide_core/sticky_node_form_sidebar'; $form['#process'][] = '_tide_core_form_node_form_process'; - - // Add comment log message field. + $form['#attached']['library'][] = 'tide_core/pseudo_required_field'; $node = $form_state->getFormObject()->getEntity(); + // Modify field_content_category help text. + if ($node->hasField('field_content_category')) { + $newLabelDescription = [ + '#type' => 'inline_template', + '#template' => '{{ title }} + {{ asterisk }} +
{{ description }}
', + '#context' => [ + 'title' => (isset($form['field_content_category']['widget']['#required'])) ? $form['field_content_category']['widget']['#title'] : '', + 'description' => (isset($form['field_content_category']['widget']['#description'])) ? $form['field_content_category']['widget']['#description'] : '', + 'asterisk' => (isset($form['field_content_category']['widget']['#required']) && $form['field_content_category']['widget']['#required'] == TRUE) ? '*' : '', + ], + ]; + + $form['field_content_category']['#prefix'] = \Drupal::service('renderer')->renderPlain($newLabelDescription); + + if (isset($form['field_content_category']['widget']['#description'])) { + $form['field_content_category']['widget']['#description'] = ''; + } + $form['field_content_category']['widget']['#title_display'] = 'invisible'; + } // Apply to edit form only, tide_workflow_notification_form_node_form_alter. if ($node->isNew()) { return; } + // Add comment log message field. if (isset($form['revision_log']) && $form['revision_log']['#access']) { $form['moderation_state']['comment_log_message'] = _tide_core_revision_log_form_label_text() + [ '#type' => 'textarea',