From 39953e0937a27e36c4f2c7416c1bdd6df84d5be0 Mon Sep 17 00:00:00 2001 From: Benjamin Rasmussen Date: Tue, 17 Dec 2024 22:14:57 +0100 Subject: [PATCH] BNF: Library import forms. DDFHER-166 Allowing the editor to input either a UUID or URL to a content. We will then display a preview, where they can see the title. If they wish to continue, they can import, and the node will be created, and they will be redirected to the edit form. --- web/modules/custom/bnf/bnf.module | 26 +++ .../bnf/bnf_client/bnf_client.links.menu.yml | 6 + .../bnf/bnf_client/bnf_client.permissions.yml | 2 + .../bnf/bnf_client/bnf_client.routing.yml | 16 ++ .../src/Form/BnfImportConfirmForm.php | 100 +++++++++++ .../bnf/bnf_client/src/Form/BnfImportForm.php | 94 ++++++++++ .../custom/bnf/src/Services/BnfImporter.php | 161 +++++------------- 7 files changed, 285 insertions(+), 120 deletions(-) create mode 100644 web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml create mode 100644 web/modules/custom/bnf/bnf_client/bnf_client.routing.yml create mode 100644 web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php create mode 100644 web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php diff --git a/web/modules/custom/bnf/bnf.module b/web/modules/custom/bnf/bnf.module index d19fd7ed8..11e83cce5 100644 --- a/web/modules/custom/bnf/bnf.module +++ b/web/modules/custom/bnf/bnf.module @@ -3,6 +3,7 @@ use Drupal\bnf\BnfStateEnum; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\node\NodeInterface; /** * Implements hook_entity_base_field_info(). @@ -41,3 +42,28 @@ function bnf_get_bnf_state_allowed_values(): array { } return $values; } + +/** + * Implements theme_preprocess_html(). + * + * Adding the node UUID as a metatag, that we can use when the user submits + * a URL to the BNF import form. + */ +function bnf_preprocess_html(array &$variables): void { + $route = \Drupal::routeMatch(); + $node = $route->getParameter('node'); + + if ($route->getRouteName() !== 'entity.node.canonical' || !($node instanceof NodeInterface)) { + return; + } + + $uuid_metatag = [ + '#tag' => 'meta', + '#attributes' => [ + 'name' => 'uuid', + 'content' => $node->uuid(), + ], + ]; + + $variables['page']['#attached']['html_head'][] = [$uuid_metatag, 'node-uuid']; +} diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml b/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml new file mode 100644 index 000000000..5bce3976d --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml @@ -0,0 +1,6 @@ +--- +bnf_client.import_content: + title: 'Import BNF content' + parent: system.admin_content + route_name: bnf_client.import_form + weight: 10 diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml b/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml index e8a3704a5..b3a94242f 100644 --- a/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml +++ b/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml @@ -1,2 +1,4 @@ bnf client export nodes: title: 'Can export nodes to BNF' +bnf client import nodes: + title: 'Can import nodes from BNF' diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml b/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml new file mode 100644 index 000000000..6d3987e66 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml @@ -0,0 +1,16 @@ +--- +bnf_client.import_form: + path: '/admin/bnf/import' + defaults: + _form: '\Drupal\bnf_client\Form\BnfImportForm' + _title: 'Import' + requirements: + _permission: 'bnf client import nodes' + +bnf_client.import_confirm_form: + path: '/admin/bnf/import/{uuid}' + defaults: + _form: '\Drupal\bnf_client\Form\BnfImportConfirmForm' + _title: 'Confirm import' + requirements: + _permission: 'bnf client import nodes' diff --git a/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php new file mode 100644 index 000000000..1a8130673 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php @@ -0,0 +1,100 @@ +get('current_route_match'), + $container->get('bnf.importer') + ); + } + + /** + * {@inheritDoc} + */ + public function getFormId(): string { + return 'bnf_import_form_form'; + } + + /** + * {@inheritDoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['#title'] = $this->t('Confirm import of BNF content', [], ['context' => 'BNF']); + + $uuid = $this->routeMatch->getParameter('uuid'); + $bnfServer = (string) getenv('BNF_SERVER_GRAPHQL_ENDPOINT'); + + $form_state->set('uuid', $uuid); + $form_state->set('bnfServer', $bnfServer); + + $nodeData = $this->bnfImporter->loadNodeData($uuid, $bnfServer); + + $form['uuid'] = [ + '#title' => 'UUID', + '#type' => 'textfield', + '#default_value' => $uuid, + '#disabled' => TRUE, + ]; + + $form['label'] = [ + '#title' => $this->t('Content label', [], ['context' => 'BNF']), + '#type' => 'textfield', + '#default_value' => $nodeData['title'] ?? NULL, + '#disabled' => TRUE, + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Import content'), + ]; + + return $form; + } + + /** + * {@inheritDoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + + } + + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $uuid = $form_state->get('uuid'); + $bnfServer = $form_state->get('bnfServer'); + $node = $this->bnfImporter->importNode($uuid, $bnfServer); + $form_state->setRedirect('entity.node.edit_form', ['node' => $node->id()]); + } + +} diff --git a/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php new file mode 100644 index 000000000..9c07ca218 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php @@ -0,0 +1,94 @@ +t('Import nodes from BNF', [], ['context' => 'BNF']); + $form['reference'] = [ + '#type' => 'textfield', + '#title' => $this->t('URL or UUID of content'), + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Preview content'), + ]; + + return $form; + } + + /** + * {@inheritDoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + $reference = $form_state->getValue('reference'); + $uuid = $this->parseAndValidateUuid($reference); + + if (empty($uuid)) { + $form_state->setErrorByName( + 'reference', + $this->t('Invalid URL or UUID.', [], ['context' => 'BNF']) + ); + } + + $form_state->set('uuid', $uuid); + } + + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $uuid = $form_state->get('uuid'); + $form_state->setRedirect('bnf_client.import_confirm_form', ['uuid' => $uuid]); + } + + /** + * Getting and validate a UUID from string or URL. + */ + protected function parseAndValidateUuid(string $reference): string|false { + try { + // Detect if reference is a URL. + if (filter_var($reference, FILTER_VALIDATE_URL)) { + // Finding the metatag that contains the UUID. + $meta_tags = get_meta_tags($reference); + $reference = $meta_tags['uuid'] ?? NULL; + } + + return Uuid::isValid((string) $reference) ? $reference : FALSE; + } + catch (\Exception) { + return FALSE; + } + } + +} diff --git a/web/modules/custom/bnf/src/Services/BnfImporter.php b/web/modules/custom/bnf/src/Services/BnfImporter.php index 53325dc0f..dfcb4b516 100644 --- a/web/modules/custom/bnf/src/Services/BnfImporter.php +++ b/web/modules/custom/bnf/src/Services/BnfImporter.php @@ -4,11 +4,8 @@ use Drupal\bnf\BnfStateEnum; use Drupal\bnf\Exception\AlreadyExistsException; -use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\node\NodeInterface; use Drupal\paragraphs\ParagraphInterface; use GuzzleHttp\ClientInterface; @@ -27,8 +24,7 @@ class BnfImporter { const ALLOWED_PARAGRAPHS = [ - 'ParagraphTextBody' => 'text_body', - 'ParagraphAccordion' => 'accordion', + 'text_body', ]; /** @@ -36,129 +32,33 @@ class BnfImporter { */ public function __construct( protected ClientInterface $httpClient, - protected EntityFieldManagerInterface $entityFieldManager, protected EntityTypeManagerInterface $entityTypeManager, protected TranslationInterface $translation, protected LoggerInterface $logger, ) {} /** - * Loading the columns of a field, that we use to ask GraphQL for data. - * - * E.g. a WYSIWYG field will have both a "value" and a "format" that we want - * to pull out. - * - * @return mixed[] - * Return an array of fields, along with their column keys. + * Building the query we use to get data from source. */ - protected function getFieldColumns(string $entityType, string $bundle): array { - $values = []; - $fields = []; - $fieldDefinitions = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle); - - foreach ($fieldDefinitions as $fieldKey => $fieldDefinition) { - if ($fieldDefinition instanceof FieldConfig) { - $fields[] = $fieldKey; - } - } - - foreach ($fields as $fieldKey) { - $field = $this->entityTypeManager->getStorage('field_storage_config')->load("$entityType.$fieldKey"); - - if ($field instanceof FieldStorageConfig) { - $values[$fieldKey] = array_keys($field->getColumns()); - } - } - - return $values; - } - - /** - * Builds the query used to get data from the source. - */ - public function getQuery(string $uuid, string $queryName): string { - // Start building the GraphQL query. - $query = << $drupalBundle) { - $query .= <<getFieldColumns('paragraph', $drupalBundle); - foreach ($fieldColumns as $fieldKey => $columns) { - $fieldKey = $this->drupalFieldToGraphField($fieldKey); - - $columnsString = implode("\r\n ", $columns); - $query .= <<graphqlTypeToBundle($type); - if (empty($bundleName)) { + if (!in_array($bundleName, self::ALLOWED_PARAGRAPHS)) { continue; } @@ -197,7 +97,7 @@ protected function parseGraphParagraphs(array $nodeData) { } // Assume Drupal uses field names like "field_{key}". - $drupalFieldName = $this->graphFieldToDrupalField($key); + $drupalFieldName = 'field_' . $key; $paragraph[$drupalFieldName] = $value; } @@ -217,7 +117,7 @@ protected function parseGraphParagraphs(array $nodeData) { * The paragraph entities. */ protected function getParagraphs(array $nodeData): array { - $parsedParagraphs = $this->parseGraphParagraphs($nodeData); + $parsedParagraphs = $this->parseParagraphs($nodeData); $storage = $this->entityTypeManager->getStorage('paragraph'); $paragraphs = []; foreach ($parsedParagraphs as $paragraphData) { @@ -232,6 +132,26 @@ protected function getParagraphs(array $nodeData): array { return $paragraphs; } + /** + * Converts a GraphQL typename to a Drupal paragraph bundle name. + * + * @param string $typeName + * The GraphQL typename (e.g., ParagraphTextBody). + * + * @return string + * The Drupal paragraph bundle name (e.g., text_body). + */ + protected function graphqlTypeToBundle(string $typeName): string { + // Removing 'Paragraph' prefix. + $typeName = preg_replace('/^Paragraph/', '', $typeName); + + // Converting CamelCase to snake_case. + $pattern = '/(?<=\\w)(?=[A-Z])|(?<=[a-z])(?=[0-9])/'; + $typeName = preg_replace($pattern, '_', $typeName); + + return strtolower($typeName); + } + /** * Loading the node data from a GraphQL endpoint. * @@ -266,7 +186,7 @@ public function loadNodeData(string $uuid, string $endpointUrl, string $nodeType throw new \InvalidArgumentException('The provided callback URL must use HTTPS.'); } - $query = $this->getQuery($uuid, $queryName); + $query = $this->getQuery($queryName, $uuid); $response = $this->httpClient->request('post', $endpointUrl, [ 'headers' => [ @@ -284,13 +204,11 @@ public function loadNodeData(string $uuid, string $endpointUrl, string $nodeType $nodeData = $data['data'][$queryName] ?? NULL; if (empty($nodeData)) { - $this->logger->error( - 'Could not find any node data in GraphQL response. @query', - ['@query' => $query] - ); + $this->logger->error('Could not find any node data in GraphQL response.'); throw new \Exception('Could not retrieve content values.'); } + return $nodeData; } @@ -300,12 +218,14 @@ public function loadNodeData(string $uuid, string $endpointUrl, string $nodeType */ public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): NodeInterface { $nodeStorage = $this->entityTypeManager->getStorage('node'); + try { $nodeData = $this->loadNodeData($uuid, $endpointUrl, $nodeType); + $nodeData['type'] = $nodeType; $nodeData['uuid'] = $uuid; - $nodeData['field_paragraphs'] = $this->getParagraphs($nodeData); $nodeData['status'] = NodeInterface::NOT_PUBLISHED; + $nodeData['field_paragraphs'] = $this->getParagraphs($nodeData); /** @var \Drupal\node\NodeInterface $node */ $node = $nodeStorage->create($nodeData); @@ -329,6 +249,7 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = ]); return $node; + } }