Skip to content

Commit

Permalink
VACMS-16575: Add extra validation to allow for combination of reusabl…
Browse files Browse the repository at this point in the history
…e and single page QAs. (#16838)

* VACMS-16575: Add Entity event listener to va_gov_clp module.

* VACMS-16575: Add new constrait for validating two paragraph fields together.

* VACMS-16575: Add the requiredErrorDisplayAsMessage option.

* VACMS-16575: Adds additional label properties to constraint.

* VACMS-16575: Uses new labels in validation.

* VACMS-16575: Adds some segment open/close commands to cypress.

* VACMS-16575: Updates CLP faq segment test.

* VACMS-16575: Ensures the FAQ tab is open before selecting elements inside.

* VACMS-16575: Updates to make sure the dropdowns can be toggled to remove paragraphs.

---------

Co-authored-by: Daniel Sasser <[email protected]>
  • Loading branch information
Becapa and dsasser authored Jan 16, 2024
1 parent 2eb630b commit cd4c1d5
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Drupal\va_gov_backend\Plugin\Validation\Constraint;

/**
* Checks that paragraphs a and b have been created as required.
*
* @Constraint(
* id = "RequiredParagraphAB",
* label = @Translation("Limit Paragraph A and B", context = "Validation"),
* type = "string"
* )
*/
class RequiredParagraphAB extends RequiredParagraph {

/**
* The field name of paragraph A.
*
* @var string
*/
public $fieldParagraphA;

/**
* The field name of paragraph B.
*
* @var string
*/
public $fieldParagraphB;

/**
* Displays validation error as Drupal message when no field values exist.
*
* @var bool
*/
public $requiredErrorDisplayAsMessage;

/**
* The plural label.
*
* @var string
*/
public $pluralLabel;

/**
* The panel label.
*
* @var string
*/
public $panelLabel;

/**
* The message that will be shown if the paragraph number is less than min.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $tooFew = 'Add %plurlLabel. A minimum of %min %readables is required.';

/**
* The message that will be shown if the paragraph number is more than max.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $tooMany = 'Remove %plurlLabel. A maximum of %max %readables is allowed.';

/**
* The message that will be shown if the paragraph is empty.
*
* @var string
* @see \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphABValidator
*/
public $required = 'A minimum of %min %readables is required when the %panelLabel page segment is enabled. Disable the FAQs page segment if there are no %readables to add.';

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Drupal\va_gov_backend\Plugin\Validation\Constraint;

use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
* Validates the RequiredParagraph constraint.
*/
class RequiredParagraphABValidator extends ConstraintValidator {

/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
assert($entity instanceof FieldableEntityInterface, 'Entity should inherit from FieldableEntityInterface.');
/** @var \Drupal\va_gov_backend\Plugin\Validation\Constraint\RequiredParagraphAB $constraint */
if (!$entity->hasField($constraint->toggle) && !$entity->hasField($constraint->fieldParagraphA) && !$entity->hasField($constraint->fieldParagraphB)) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$panel_enabled = $entity->get($constraint->toggle)->getString();
$countA = $this->getCountForParagraphField($constraint->fieldParagraphA);
$countB = $this->getCountForParagraphField($constraint->fieldParagraphB);
$number = $countA + $countB;
$paragraphAField = $this->getBaseField($constraint->fieldParagraphA);
$paragraphBField = $this->getBaseField($constraint->fieldParagraphB);
$errorPath = $countA ? $paragraphAField : $paragraphBField;
if ($panel_enabled && $number < $constraint->min && $number > 0) {
$this->context->buildViolation($constraint->tooFew, [
'%plurlLabel' => $constraint->pluralLabel,
'%readable' => $constraint->readable,
'%min' => $constraint->min,
])
->atPath($errorPath)
->addViolation();
}
elseif ($panel_enabled && $number > $constraint->max) {
$this->context->buildViolation($constraint->tooMany, [
'%plurlLabel' => $constraint->pluralLabel,
'%readable' => $constraint->readable,
'%max' => $constraint->max,
])
->atPath($errorPath)
->addViolation();
}
elseif ($panel_enabled && $number === 0) {
// Adding a violation in this way ensures that it is displayed even if
// paragraphA and paragraphB have no values.
$this->context->addViolation($constraint->required, [
'%min' => $constraint->min,
'%panelLabel' => $constraint->panelLabel,
'%readable' => $constraint->readable,
]);
}
}

/**
* Gets the item count from a paragraph field.
*
* To target a nested field (a field within a paragraph), specify the $field
* with a colon ":" between the parent and child field names. Only one level
* of nesting is supported. eg: field_faq_group:field_faq_items.
*
* @param string $field
* The field name to get the count from.
*
* @return int
* The item count for the number of nested items.
*/
private function getCountForParagraphField(string $field): int {
$count = 0;
$entity = $this->context->getRoot()->getEntity();
if (str_contains($field, ':')) {
$fields = explode(":", $field);
if (!empty($fields)) {
[$outerParagraphField, $innerParagraphField] = $fields;
if ($entity->hasField($outerParagraphField)) {
/** @var \Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem $item */
foreach ($entity->get($outerParagraphField) as $item) {
/** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
$paragraph = $item->entity;
if ($paragraph->hasField($innerParagraphField)) {
$count += $paragraph->get($innerParagraphField)->count();
}
}
}
}
}
else {
$count = $entity->get($field)->count();
}
return $count;
}

/**
* Get a base field from a given paragraph field identifier.
*
* Since fields can contain colon's (":") to separate parent:child, this
* method is used to get the base field.
*
* @return string
* The base field name.
*/
private function getBaseField(string $field): string {
if (str_contains($field, ':')) {
[$baseField] = explode(":", $field);
return $baseField;
}
else {
return $field;
}
}

}
9 changes: 0 additions & 9 deletions docroot/modules/custom/va_gov_backend/va_gov_backend.module
Original file line number Diff line number Diff line change
Expand Up @@ -1323,15 +1323,6 @@ function va_gov_backend_entity_bundle_field_info_alter(&$fields, EntityTypeInter
}
// Add paragraph checks on clp panels.
if ($entity_type->id() === 'node' && $bundle === 'campaign_landing_page') {
// Add range check on faq panel.
if (isset($fields['field_clp_faq_paragraphs'])) {
$fields['field_clp_faq_paragraphs']->addConstraint('RequiredParagraph', [
'toggle' => 'field_clp_faq_panel',
'readable' => 'Q&A',
'min' => 3,
'max' => 10,
]);
}
// Add range check on stories panel.
if (isset($fields['field_clp_stories_teasers'])) {
$fields['field_clp_stories_teasers']->addConstraint('RequiredParagraph', [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Drupal\va_gov_clp\EventSubscriber;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\core_event_dispatcher\EntityHookEvents;
use Drupal\core_event_dispatcher\Event\Entity\EntityTypeAlterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* VA.gov Campaign Landing Page Event Subscriber.
*/
class EntityEventSubscriber implements EventSubscriberInterface {

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
EntityHookEvents::ENTITY_TYPE_ALTER => 'entityTypeAlter',
];
}

/**
* Equivalent of hook_entity_type_alter().
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityTypeAlterEvent $event
* The event for entityTypeAlter.
*/
public function entityTypeAlter(EntityTypeAlterEvent $event): void {
$entity_types = $event->getEntityTypes();
if (!empty($entity_types['node'])) {
$nodeEntityType = $entity_types['node'];
$this->addConstraintsToClp($nodeEntityType);
}
}

/**
* Adds constraints to Campaign Landing Page Nodes.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type.
*/
public function addConstraintsToClp(EntityTypeInterface $entityType): void {
$entityType->addConstraint('RequiredParagraphAB', [
'toggle' => 'field_clp_faq_panel',
'readable' => 'Q&A',
'pluralLabel' => 'Page-Specific or Reusable Q&As',
'panelLabel' => 'FAQ',
'fieldParagraphA' => 'field_clp_faq_paragraphs',
'fieldParagraphB' => 'field_clp_reusable_q_a:field_q_as',
'requiredErrorDisplayAsMessage' => TRUE,
'min' => 3,
'max' => 10,
]);
}

}
5 changes: 5 additions & 0 deletions docroot/modules/custom/va_gov_clp/va_gov_clp.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
va_gov_clp.entity_event_subscriber:
class: Drupal\va_gov_clp\EventSubscriber\EntityEventSubscriber
tags:
- { name: event_subscriber }
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,46 @@ Feature: Content Type: Campaign Landing Page
And I can fill in "Text" field with fake text
And I should see "Add Reusable Q&A"
And I should see "Add a link to more FAQs"

Scenario: Test FAQ page segment requirements
Given I am logged in as a user with the "content_admin" role
Then I create a "campaign_landing_page" node and continue

# Test maximum FAQs cannot be exceeded.
When I click to expand "FAQs"
And I enable the page segment within selector "#edit-group-faqs"
And I click the "Add Page-Specific Q&A" button
And I fill in "Question" field with fake text
And I fill in ckeditor "edit-field-clp-faq-paragraphs-0-subform-field-answer-0-subform-field-wysiwyg-0-value" with "Adding Page-Specific Q&As..."
And I click the "Add Reusable Q&A Group" button
And I click to expand "Q&As"
And I select 10 items from the "Add Reusable Q&As" Entity Browser modal
And I wait "2" seconds
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see an element with the selector "#edit-field-clp-faq-paragraphs-0-subform-field-question-0-value.error"
And I should see "Remove Page-Specific or Reusable Q&As"

# Test fewer than minimum FAQs cannot be added.
When I click the button with selector "[data-drupal-selector='edit-field-clp-reusable-q-a-0-top'] .paragraphs-dropdown-toggle"
And I click the button with selector "[name='field_clp_reusable_q_a_0_remove']"
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see an element with the selector "#edit-field-clp-faq-paragraphs-0-subform-field-question-0-value.error"
And I should see "Add Page-Specific or Reusable Q&As"

# Test required Q&As if FAQ segment is enabled
When I click the button with selector "[data-drupal-selector='edit-field-clp-faq-paragraphs-0-top'] .paragraphs-dropdown-toggle"
And I click the button with selector "[name='field_clp_faq_paragraphs_0_remove']"
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then I should see "A minimum of 3 Q&As is required when the FAQ page segment is enabled. Disable the FAQs page segment if there are no Q&As to add."

# Test that no Q&A is required if the FAQ page segment is disabled
When I click to expand "FAQs"
And I disable the page segment
And I fill in field with selector "#edit-revision-log-0-value" with fake text
And I save the node
Then the element with selector ".messages__content" should contain "Campaign Landing Page"
And the element with selector ".messages__content" should contain "has been updated."

Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ import { Given } from "@badeball/cypress-cucumber-preprocessor";
Given("I enable the page segment", () => {
cy.findAllByLabelText("Enable this page segment").check({ force: true });
});

Given("I disable the page segment", () => {
cy.findAllByLabelText("Enable this page segment").uncheck({ force: true });
});

Given("I enable the page segment within selector {string}", (text) => {
cy.get(text)
.findAllByLabelText("Enable this page segment")
.check({ force: true });
});

Given("I disable the page segment within selector {string}", (text) => {
cy.get(text)
.findAllByLabelText("Enable this page segment")
.uncheck({ force: true });
});

0 comments on commit cd4c1d5

Please sign in to comment.