diff --git a/.ddev/.env.example b/.ddev/.env.example index df5aa81207..910941075a 100644 --- a/.ddev/.env.example +++ b/.ddev/.env.example @@ -81,3 +81,8 @@ AWS_BEDROCK_ROLE_SESSION_NAME='' AWS_BEDROCK_ACCESS_KEY_ID='' AWS_BEDROCK_SECRET_ACCESS_KEY='' AWS_BEDROCK_REGION=us-east-1 + +## Feedback Submission Lambda API endpoint +## Local dev: Use local Docker endpoint or tunneled Lambda +## Example for local testing: http://localhost:3000/feedback/submit, after you run the Lambda from ETL repo (this can be outdated). +FEEDBACK_SUBMISSION_API_ENDPOINT='' diff --git a/changelogs/DP-43340.yml b/changelogs/DP-43340.yml new file mode 100644 index 0000000000..ed9b8157ff --- /dev/null +++ b/changelogs/DP-43340.yml @@ -0,0 +1,41 @@ +# +# Write your changelog entry here. Every pull request must have a changelog yml file. +# +# Change types: +# ############################################################################# +# You can use one of the following types: +# - Added: For new features. +# - Changed: For changes to existing functionality. +# - Deprecated: For soon-to-be removed features. +# - Removed: For removed features. +# - Fixed: For any bug fixes. +# - Security: In case of vulnerabilities. +# +# Format +# ############################################################################# +# The format is crucial. Please follow the examples below. For reference, the requirements are: +# - All 3 parts are required and you must include "Type", "description" and "issue". +# - "Type" must be left aligned and followed by a colon. +# - "description" must be indented with 2 spaces followed by a colon +# - "issue" must be indented with 4 spaces followed by a colon. +# - "issue" is for the Jira ticket number only e.g. DP-1234 +# - No extra spaces, indents, or blank lines are allowed. +# +# Example: +# ############################################################################# +# Fixed: +# - description: Fixes scrolling on edit pages in Safari. +# issue: DP-13314 +# +# You may add more than 1 description & issue for each type using the following format: +# Changed: +# - description: Automating the release branch. +# issue: DP-10166 +# - description: Second change item that needs a description. +# issue: DP-19875 +# - description: Third change item that needs a description along with an issue. +# issue: DP-19843 +# +Changed: + - description: Switch to custom feedback backend, remove useless code, improve feedback form for nodes without organization. + issue: DP-43340 diff --git a/docroot/modules/custom/mass_feedback_form/js/mass-feedback-form.behaviors.js b/docroot/modules/custom/mass_feedback_form/js/mass-feedback-form.behaviors.js index 61025ffb1a..b72878d732 100644 --- a/docroot/modules/custom/mass_feedback_form/js/mass-feedback-form.behaviors.js +++ b/docroot/modules/custom/mass_feedback_form/js/mass-feedback-form.behaviors.js @@ -1,107 +1,163 @@ /** * @file * Provides JavaScript for Mass Feedback Forms. + * Handles submission to the Lambda feedback API instead of Formstack. */ -/* global dataLayer */ - -(function ($) { +(function ($, once) { 'use strict'; /** - * Support a multi-step Feedback form. + * Support feedback form submission to Lambda API. */ Drupal.behaviors.massFeedbackForm = { attach: function (context) { - // This field is used by the feedback manager to join the survey (second) with the first submission - var MG_FEEDBACK_ID = 'field68557708'; - - // This field is used to set a unique device ID to form submissions. - var UNIQUE_ID_FIELD = 'field68798989'; + // Process feedback forms using Drupal's once() function + once('massFeedbackForm', '.ma__mass-feedback-form', context).forEach(function (element) { + const $self = $(element); + const $form = $self.find('form').not('has-error'); - // For certain form inputs, use a value from the data layer. - $('.data-layer-substitute', context).each(function (index) { - var $this = $(this); - var property = $this.val(); - var sub = ''; + if (!$form.length) { + return; + } - for (var i = 0; i < dataLayer.length; i++) { - if (typeof dataLayer[i][property] !== 'undefined') { - sub = dataLayer[i][property]; + const feedback = $self[0]; + const $success = $self.find('#success-screen'); + const $submitBtn = $('input[type="submit"]', $form); + const formAction = $form.attr('action'); + let isSubmitting = false; + + // Prevent double-click form submission + $submitBtn.on('click', function (e) { + if (isSubmitting) { + e.preventDefault(); + return false; } - } + }); - if (sub !== '' && typeof sub === 'string') { - $this.val(sub); - } - $this.removeClass('data-layer-substitute'); - }); + // Handle form submission + $form.on('submit', function (e) { + e.preventDefault(); - // Process the multistep form. - $('.ma__mass-feedback-form', context).each(function (index) { - var $self = $(this); - var feedback = $self[0]; - - var $formOriginal = $self.find('form').not('has-error'); - // Checks to avoid bots submitting the form - // On the first form, the Feedback manager lambda will populate this field, - // so a populated field was a bot, we honey potted him/her/they - if ($formOriginal.find('#field68798989').length && $('#field68798989').val()) { - // We don't need to show the bot anything - return false; - } + if (isSubmitting) { + return false; + } - var $form = $self.find('form').not('has-error'); - var $success = $self.find('#success-screen'); - // This is to stop a double click submitting the form twice - var $submitBtn = $('input[type="submit"]', $form); + isSubmitting = true; + $submitBtn.prop('disabled', true); - // Use device ID set in docroot/themes/custom/mass_theme/overrides/js/device.js. - var massgovDeviceId = localStorage.getItem('massgovDeviceId') || ''; - $form.find('input[name="' + UNIQUE_ID_FIELD + '"]').val(massgovDeviceId); + // Submit feedback. + submitFeedback($form, formAction, $success, feedback, $submitBtn, function () { + isSubmitting = false; + }); - $form.submit(function () { - $submitBtn.prop('disabled', true); - }); - $form.on('submit', function (e) { - $form.addClass('hidden'); - $success.removeClass('hidden'); - feedback.scrollIntoView(); + return false; }); + }); - $form.ajaxForm({ - data: {jsonp: 1}, - dataType: 'script' + /** + * Submit feedback to Lambda API. + * + * @param {jQuery} $form The form element. + * @param {string} formAction The API endpoint URL. + * @param {jQuery} $success The success message element. + * @param {Element} feedback The feedback container element. + * @param {jQuery} $submitBtn The submit button element. + * @param {Function} onComplete Callback when submission is complete. + */ + function submitFeedback($form, formAction, $success, feedback, $submitBtn, onComplete) { + const formData = new FormData($form[0]); + + // Get explain field - handle both visible and hidden textareas with same name + // The form has two textareas with name="explain" (positive and negative feedback) + // FormData.get() only returns the first one, so we need to get the visible one + let explainField = ''; + const explainInputs = $form.find('textarea[name="explain"]'); + explainInputs.each(function () { + const $textarea = $(this); + // Check if textarea is visible (not hidden by CSS display:none or parent hidden class) + if ($textarea.is(':visible') && !$textarea.closest('.feedback-response').hasClass('hidden')) { + explainField = $textarea.val() || ''; + } }); - window['form' + $form.attr('id')] = { - onPostSubmit: function (message) { - // If MG_FEEDBACK_ID is 'uniqueId', then we are submitting the first (feedback) form - // so we now need to set the MG_FEEDBACK_ID value with the ID returned from formstack. - var submissionId = message.submission; - if ($('#' + MG_FEEDBACK_ID).val() === 'uniqueId') { - $('#' + MG_FEEDBACK_ID).val(submissionId); - } - feedback.scrollIntoView(); - }, - onSubmitError: function (err) { - var message = 'Submission Failure: ' + err.error; - getMessaging($form).html(message); - } + // Get unique device ID from localStorage for spam detection + // This ID is created by device.js and persists across sessions + const mgFeedbackId = localStorage.getItem('massgovDeviceId') || null; + + const payload = { + node_id: parseInt(formData.get('node_id')) || 0, + info_found: formData.get('info_found') === 'Yes', + explain: explainField, + url: window.location.href, + timestamp: new Date().toISOString(), + mg_feedback_id: mgFeedbackId }; - }); - // Handle the creation and management of the form messages. - function getMessaging($form) { - var $messages = $('.messages', $form); + fetch(formAction, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Accept-Language': navigator.language || 'en-US', + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': navigator.userAgent + }, + body: JSON.stringify(payload) + }) + .then(function (response) { + if (!response.ok) { + return response.json().then(function (data) { + throw { + status: response.status, + data: data + }; + }); + } + return response.json(); + }) + .then(function (data) { + // Show success screen and keep it visible + $form.addClass('hidden'); + $success.removeClass('hidden'); + feedback.scrollIntoView({behavior: 'smooth'}); + }) + .catch(function (error) { + console.error('Feedback submission failed:', error); + + var errorMessage = 'Unable to submit your feedback. Please try again later.'; + if (error.status === 400 && error.data && error.data.errors) { + errorMessage = 'Submission error: ' + error.data.errors.join(', '); + } + + showErrorMessage($form, errorMessage); + $submitBtn.prop('disabled', false); + }) + .finally(function () { + onComplete(); + }); + } + + /** + * Display error message in the form. + * + * @param {jQuery} $form The form element. + * @param {string} message The error message to display. + */ + function showErrorMessage($form, message) { + let $messages = $form.find('.messages'); if (!$messages.length) { - $form.find('input[type="submit"]').parent().prepend('
'); - $messages = $('.messages', $form); + $form.prepend('
'); + $messages = $form.find('.messages'); } - return $messages; - } + $messages.html(message).show(); + // Auto-hide after 5 seconds + setTimeout(function () { + $messages.fadeOut(); + }, 5000); + } } }; -})(jQuery); +})(jQuery, once); diff --git a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.libraries.yml b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.libraries.yml index 00717813c8..9cbcd0bca1 100644 --- a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.libraries.yml +++ b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.libraries.yml @@ -1,7 +1,8 @@ feedback-form-behaviors: - version: 1.x + version: 1.1.0 js: js/mass-feedback-form.behaviors.js: {} dependencies: - core/jquery - core/internal.jquery.form + - core/once diff --git a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module index 997b641658..9d258cd7c1 100644 --- a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module +++ b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module @@ -29,16 +29,11 @@ function mass_feedback_form_help($route_name, RouteMatchInterface $route_match) /** * Implements hook_theme(). * - * Defines twig templates for two feedback form blocks, one called by PageFeedbackForm and the other by NodeFeedbackContainer. + * Defines twig templates for two feedback form blocks, one called by + * PageFeedbackForm and the other by NodeFeedbackContainer. */ function mass_feedback_form_theme() { return [ - 'mass_feedback_form_without_organization' => [ - 'render element' => NULL, - ], - 'mass_feedback_form' => [ - 'variables' => ['node_id' => NULL], - ], 'block__node_feedback_container' => [ 'render element' => 'elements', 'template' => 'block--node-feedback-container', @@ -47,6 +42,15 @@ function mass_feedback_form_theme() { ]; } +/** + * Implements hook_preprocess(). + */ +function mass_feedback_form_preprocess(&$variables, $hook) { + if (in_array($hook, ['node', 'node__mass_feedback_form_without_organization'])) { + $variables['feedback_submission_api_endpoint'] = getenv('FEEDBACK_SUBMISSION_API_ENDPOINT'); + } +} + /** * Implements hook_node_update() */ @@ -130,9 +134,8 @@ function mass_feedback_form_node_view(array &$build, EntityInterface $node, Enti $org_node_view_feedback = $view_builder->view($node, 'feedback'); } elseif ($node->bundle() === 'topic_page' && $node->get('field_organizations')->isEmpty()) { - $org_node_view_feedback = [ - '#theme' => 'mass_feedback_form_without_organization', - ]; + $org_node_view_feedback = $view_builder->view($node, 'feedback'); + $org_node_view_feedback['#theme'] = 'node__mass_feedback_form_without_organization'; } else { if ($node->field_organizations->count() > 0 && !empty($node->field_organizations[0]->entity)) { diff --git a/docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form.html.twig b/docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form.html.twig deleted file mode 100644 index 42590866cc..0000000000 --- a/docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form.html.twig +++ /dev/null @@ -1,134 +0,0 @@ -{{ attach_library('mass_feedback_form/feedback-form') }} -
-

{{ 'Feedback'|t }}

-
- - - - - - - - - - -{# ONLY THE FIRST MESSAGE IS IN USE WITH THIS FEEDBACK FORM. - SINCE NO USE FOR THE OTHER MESSAGE. - IT SHOULD BE PRETTY SAFE TO REMOVE THEM. -#} - -{# -
Please fill in all required (marked with *) fields.
- - - - - - - - - - - - - - - - -#} - -{# - MIGHT NEED TO SET UP: - AFTER A SUBMITTION WITH EITHER "YES" OR "NO", "Thank you for your feedback" IS DISPLAYED, BUT IT'S NOT SET IN THE MESSAGES ABOVE. - -#} - -{# - -NOTE: - 1. wrapper/layout container
s are replaced with
. - 2. name for and - {# character counter is added to DOM. #} -
We use your feedback to help us improve this site but we are not able to respond directly. Please do not include personal or contact information. If you need a response, please locate the contact information elsewhere on this page or in the footer.
-
- - {# SHOW THIS WHEN "YES" IS SELECTED ABOVE #} -
- {# No .fsLabel necessary, this is optinal field. #} - Is there anything else you would like to tell us? - -
We use your feedback to help us improve this site but we are not able to respond directly. Please do not include personal or contact information. If you need a response, please locate the contact information elsewhere on this page or in the footer.
-
- -

If you need to report child abuse, any other kind of abuse, or need urgent assistance, please click here.

- -{# THESE BUTTON ARE NOT USED AT ALL: THEY SHOULD BE SAFE TO REMOVE #} -{# - - #} - - {# SUBMIT BUTTON IN USE FOR THIS FORM #} -
{# not necessaary, but for layout/styling purpose #} - -
- -
diff --git a/docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form-without-organization.html.twig b/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig similarity index 60% rename from docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form-without-organization.html.twig rename to docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig index 0049155a6d..9534ec0c1d 100644 --- a/docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form-without-organization.html.twig +++ b/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig @@ -1,69 +1,13 @@ {% set reportMessage = false %} {% set showHelp = false %} -{% set showHelp = false %} {% set warnmsg = false %} {% set helpText = "The feedback will only be used for improving the website." %} - {% set hiddenElements = [ { - "id": "field47056299", - "name": "field47056299", - "value": url(''), - "class": "fsField" - }, - { - "id": "field58154059", - "name": "field58154059", - "value": "entityIdentifier", - "class": "fsField data-layer-substitute" - }, - { - "id": "field57432673", - "name": "field57432673", - "value": "entityIdentifier", - "class": "fsField data-layer-substitute" - }, - { - "id": "field68798989", - "name": "field68798989", - "value": "", - "class": "fsField unique-id-substitute" - }, - { - "id": "field97986104", - "name": "field97986104", - "value": "9999", - "class": "fsField" - }, - { - "id": "form2521317", - "name": "form", - "value": "2521317" - }, - { - "id": "viewkeyvx39GBYJhi", - "name": "viewkey", - "value": "vx39GBYJhi" - }, - { - "id": "hidden_fields2521317", - "name": "hidden_fields", - "value": "" - }, - { - "id": "submit2521317", - "name": "_submit", - "value": "1" - }, - { - "id": "style_version2521317", - "name": "style_version", - "value": "3" - }, - { - "id": "viewparam", - "name": "viewparam", - "value": "524744" + "id": "node-id-hidden", + "name": "node_id", + "value": node.id, + "class": "ma-feedback-hidden-field" } ] %} @@ -71,8 +15,8 @@ {% include "@organisms/feedback/mass-feedback.twig" with { - "formAction": 'https://www.formstack.com/forms/index.php', - "formId": "2521317", + "formAction": feedback_submission_api_endpoint, + "formId": "feedback-form", "heading": "Help Us Improve Mass.gov", "title": { "value": "Did you find what you were looking for on this webpage?", @@ -84,15 +28,15 @@ "inline": true, "items": [ { - "name": "field47054416", + "name": "info_found", "value": "Yes", "label": "Yes", - "id": "field47054416_1", + "id": "info_found-yes", "checked": false, }, { - "name": "field47054416", - "id": "field47054416_2", + "name": "info_found", + "id": "info_found-no", "value": "No", "label": "No", "checked": false, @@ -106,8 +50,8 @@ "affirmativeTextarea": { "required": false, "maxlength": 500, - "id": "field52940022", - "name": "field52940022", + "id": "explain-affirmative", + "name": "explain", "describedBy": "helptext-feedback-no-response", "errorIds": "affirmative-textarea-error-alert" }, @@ -118,8 +62,8 @@ "negativeTextarea": { "required": true, "maxlength": 500, - "id": "field47054414", - "name": "field47054414", + "id": "explain-negative", + "name": "explain", "errorMsg": "Please let us know how we can improve this page.", "describedBy": "helptext-feedback-no-response", "errorIds": "negative-textarea-error-alert" diff --git a/docroot/themes/custom/mass_theme/templates/content/node--org-page--feedback.html.twig b/docroot/themes/custom/mass_theme/templates/content/node--org-page--feedback.html.twig index bb49ca90b1..5cec97c748 100644 --- a/docroot/themes/custom/mass_theme/templates/content/node--org-page--feedback.html.twig +++ b/docroot/themes/custom/mass_theme/templates/content/node--org-page--feedback.html.twig @@ -13,64 +13,10 @@ {% set hiddenElements = [ { - "id": "field47056299", - "name": "field47056299", - "value": url(''), - "class": "fsField" - }, - { - "id": "field58154059", - "name": "field58154059", - "value": "entityIdentifier", - "class": "fsField data-layer-substitute" - }, - { - "id": "field57432673", - "name": "field57432673", - "value": "entityIdentifier", - "class": "fsField data-layer-substitute" - }, - { - "id": "field68798989", - "name": "field68798989", - "value": "", - "class": "fsField unique-id-substitute" - }, - { - "id": "field97986104", - "name": "field97986104", - "value": "9999", - "class": "fsField" - }, - { - "id": "form2521317", - "name": "form", - "value": "2521317" - }, - { - "id": "viewkeyvx39GBYJhi", - "name": "viewkey", - "value": "vx39GBYJhi" - }, - { - "id": "hidden_fields2521317", - "name": "hidden_fields", - "value": "" - }, - { - "id": "submit2521317", - "name": "_submit", - "value": "1" - }, - { - "id": "style_version2521317", - "name": "style_version", - "value": "3" - }, - { - "id": "viewparam", - "name": "viewparam", - "value": "524744" + "id": "node-id-hidden", + "name": "node_id", + "value": node.id, + "class": "ma-feedback-hidden-field" } ] %} @@ -100,8 +46,8 @@ {% include "@organisms/feedback/mass-feedback.twig" with { - "formAction": 'https://www.formstack.com/forms/index.php', - "formId": "2521317", + "formAction": feedback_submission_api_endpoint, + "formId": "feedback-form", "heading": "Help Us Improve Mass.gov", "title": { "value": "Did you find what you were looking for on this webpage?", @@ -113,15 +59,15 @@ "inline": true, "items": [ { - "name": "field47054416", + "name": "info_found", "value": "Yes", "label": "Yes", - "id": "field47054416_1", + "id": "info_found-yes", "checked": false, }, { - "name": "field47054416", - "id": "field47054416_2", + "name": "info_found", + "id": "info_found-no", "value": "No", "label": "No", "checked": false, @@ -135,8 +81,8 @@ "affirmativeTextarea": { "required": false, "maxlength": 500, - "id": "field52940022", - "name": "field52940022", + "id": "explain-affirmative", + "name": "explain", "describedBy": "helptext-feedback-no-response", "errorIds": "affirmative-textarea-error-alert" }, @@ -147,8 +93,8 @@ "negativeTextarea": { "required": true, "maxlength": 500, - "id": "field47054414", - "name": "field47054414", + "id": "explain-negative", + "name": "explain", "errorMsg": "Please let us know how we can improve this page.", "describedBy": "helptext-feedback-no-response", "errorIds": "negative-textarea-error-alert"