From 00fe62ff56c0a9b04689d43cdc4eddfde142070f Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Wed, 26 Nov 2025 14:42:33 -0600 Subject: [PATCH 1/8] DP-43340: Update feedback form to use Lambda API and add geolocation support. Signed-off-by: Dmytro Storozhuk --- .ddev/.env.example | 5 + .../js/mass-feedback-form.behaviors.js | 269 +++++++++++++----- .../mass_feedback_form.libraries.yml | 3 +- .../mass_feedback_form.module | 12 +- .../node--org-page--feedback.html.twig | 84 ++---- 5 files changed, 237 insertions(+), 136 deletions(-) diff --git a/.ddev/.env.example b/.ddev/.env.example index df5aa81207..01f692c6ca 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 labda from ETL repo (this can be outdated). +FEEDBACK_SUBMISSION_API_ENDPOINT='' 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..4de5f5d043 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,234 @@ /** * @file * Provides JavaScript for Mass Feedback Forms. + * Handles submission to the Lambda feedback API instead of Formstack. */ -/* global dataLayer */ +/* global dataLayer, drupalSettings, once */ (function ($) { 'use strict'; /** - * Support a multi-step Feedback form. + * Support feedback form submission to Lambda API with geolocation support. */ 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'; - - // 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 = ''; - - for (var i = 0; i < dataLayer.length; i++) { - if (typeof dataLayer[i][property] !== 'undefined') { - sub = dataLayer[i][property]; - } - } - - if (sub !== '' && typeof sub === 'string') { - $this.val(sub); + // Cache for geolocation promise (to avoid multiple requests) + var geoLocationPromise = null; + var geoLocationPromiseStarted = false; + + /** + * Get or create the geolocation promise. + * Only requests geolocation once, subsequent calls return the same promise. + */ + function getGeolocationPromise() { + if (!geoLocationPromiseStarted) { + geoLocationPromiseStarted = true; + geoLocationPromise = getMassgovGeolocation(); + // Ensure any promise rejection is handled to prevent unhandled rejection errors + geoLocationPromise.catch(function (error) { + console.warn('Geolocation request failed:', error.message || error); + // Don't rethrow - let consumers handle it + return {}; + }); } - $this.removeClass('data-layer-substitute'); - }); + return geoLocationPromise; + } - // Process the multistep form. - $('.ma__mass-feedback-form', context).each(function (index) { - var $self = $(this); - var feedback = $self[0]; + // Process feedback forms using Drupal's once() function + once('massFeedbackForm', '.ma__mass-feedback-form', context).forEach(function (element) { + var $self = $(element); + var $form = $self.find('form').not('has-error'); - 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 (!$form.length) { + return; } - var $form = $self.find('form').not('has-error'); + var feedback = $self[0]; var $success = $self.find('#success-screen'); - // This is to stop a double click submitting the form twice var $submitBtn = $('input[type="submit"]', $form); + var formAction = $form.attr('action'); + var isSubmitting = false; + + // Prevent double-click form submission + $submitBtn.on('click', function (e) { + if (isSubmitting) { + e.preventDefault(); + return false; + } + }); - // 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); + // Handle form submission + $form.on('submit', function (e) { + e.preventDefault(); - $form.submit(function () { + if (isSubmitting) { + return false; + } + + isSubmitting = true; $submitBtn.prop('disabled', true); + + // Wait for geolocation to complete (or fail), then submit + var geoPromise = getGeolocationPromise(); + geoPromise.then(function (geoData) { + submitFeedback($form, formAction, geoData, $success, feedback, $submitBtn, function () { + isSubmitting = false; + }); + }).catch(function (error) { + console.warn('Geolocation error, submitting without coordinates:', error); + submitFeedback($form, formAction, {}, $success, feedback, $submitBtn, function () { + isSubmitting = false; + }); + }); + + return false; }); - $form.on('submit', function (e) { - $form.addClass('hidden'); - $success.removeClass('hidden'); - feedback.scrollIntoView(); - }); + }); + + /** + * Get user's geolocation if available. + * Returns a promise that resolves with {latitude, longitude} or rejects. + */ + function getMassgovGeolocation() { + return new Promise(function (resolve, reject) { + if (!navigator.geolocation) { + reject(new Error('Geolocation not supported')); + return; + } - $form.ajaxForm({ - data: {jsonp: 1}, - dataType: 'script' + var timeoutId = setTimeout(function () { + reject(new Error('Geolocation timeout')); + }, 10000); // 10 second timeout + + navigator.geolocation.getCurrentPosition( + function (position) { + clearTimeout(timeoutId); + resolve({ + latitude: position.coords.latitude.toString(), + longitude: position.coords.longitude.toString(), + }); + }, + function (error) { + clearTimeout(timeoutId); + reject(error); + }, + { + enableHighAccuracy: false, + timeout: 10000, + maximumAge: 300000, // 5 minutes cache + }, + ); }); + } - 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); + /** + * Submit feedback to Lambda API. + */ + function submitFeedback($form, formAction, geoData, $success, feedback, $submitBtn, onComplete) { + var 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 + var explainField = ''; + var explainInputs = $form.find('textarea[name="explain"]'); + explainInputs.each(function () { + var $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() || ''; } + }); + + var 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(), }; - }); - // Handle the creation and management of the form messages. - function getMessaging($form) { - var $messages = $('.messages', $form); - if (!$messages.length) { - $form.find('input[type="submit"]').parent().prepend('
'); - $messages = $('.messages', $form); + // Add geolocation if available + if (geoData && geoData.latitude && geoData.longitude) { + payload.latitude = geoData.latitude; + payload.longitude = geoData.longitude; } - return $messages; + + console.log('Submitting feedback:', payload); + + 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) { + console.log('Feedback submitted successfully:', data); + + // Show success screen + $form.addClass('hidden'); + $success.removeClass('hidden'); + feedback.scrollIntoView({ behavior: 'smooth' }); + + // Reset form after delay + setTimeout(function () { + $form.removeClass('hidden'); + $success.addClass('hidden'); + $form[0].reset(); + $submitBtn.prop('disabled', false); + }, 5000); + }) + .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. + */ + function showErrorMessage($form, message) { + var $messages = $form.find('.messages'); + if (!$messages.length) { + $form.prepend('
'); + $messages = $form.find('.messages'); + } + $messages.html(message).show(); + + // Auto-hide after 5 seconds + setTimeout(function () { + $messages.fadeOut(); + }, 5000); + } + }, }; })(jQuery); 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..2d2f9b59d3 100644 --- a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module +++ b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module @@ -29,7 +29,8 @@ 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 [ @@ -47,6 +48,15 @@ function mass_feedback_form_theme() { ]; } +/** + * Implements hook_preprocess(). + */ +function mass_feedback_form_preprocess(&$variables, $hook) { + if (in_array($hook, ['mass_feedback_form_without_organization', 'mass_feedback_form', 'block__node_feedback_container', 'node']) ) { + $variables['feedback_submission_api_endpoint'] = getenv('FEEDBACK_SUBMISSION_API_ENDPOINT'); + } +} + /** * Implements hook_node_update() */ 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..261b12c993 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,22 @@ {% set hiddenElements = [ { - "id": "field47056299", - "name": "field47056299", - "value": url(''), - "class": "fsField" + "id": "node-id-hidden", + "name": "node_id", + "value": node.id, + "class": "ma-feedback-hidden-field" }, { - "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", + "id": "latitude-hidden", + "name": "latitude", "value": "", - "class": "fsField unique-id-substitute" + "class": "ma-feedback-hidden-field ma-feedback-latitude" }, { - "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": "longitude-hidden", + "name": "longitude", + "value": "", + "class": "ma-feedback-hidden-field ma-feedback-longitude" } ] %} @@ -100,8 +58,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 +71,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 +93,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 +105,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" From ddd186cc309d1416b97cfb27b3050cf40ece1bb2 Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Wed, 26 Nov 2025 18:33:58 -0600 Subject: [PATCH 2/8] DP-43340: Replace feedback API backend and improve form handling for nodes without organization Signed-off-by: Dmytro Storozhuk --- changelogs/DP-43340.yml | 41 ++++++ .../mass_feedback_form.module | 13 +- .../templates/mass-feedback-form.html.twig | 134 ------------------ ...dback-form-without-organization.html.twig} | 86 +++-------- 4 files changed, 65 insertions(+), 209 deletions(-) create mode 100644 changelogs/DP-43340.yml delete mode 100644 docroot/modules/custom/mass_feedback_form/templates/mass-feedback-form.html.twig rename docroot/{modules/custom/mass_feedback_form/templates/mass-feedback-form-without-organization.html.twig => themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig} (61%) diff --git a/changelogs/DP-43340.yml b/changelogs/DP-43340.yml new file mode 100644 index 0000000000..143202fc1b --- /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 withour organization. + issue: DP-43340 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 2d2f9b59d3..9d258cd7c1 100644 --- a/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module +++ b/docroot/modules/custom/mass_feedback_form/mass_feedback_form.module @@ -34,12 +34,6 @@ function mass_feedback_form_help($route_name, RouteMatchInterface $route_match) */ 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', @@ -52,7 +46,7 @@ function mass_feedback_form_theme() { * Implements hook_preprocess(). */ function mass_feedback_form_preprocess(&$variables, $hook) { - if (in_array($hook, ['mass_feedback_form_without_organization', 'mass_feedback_form', 'block__node_feedback_container', 'node']) ) { + if (in_array($hook, ['node', 'node__mass_feedback_form_without_organization'])) { $variables['feedback_submission_api_endpoint'] = getenv('FEEDBACK_SUBMISSION_API_ENDPOINT'); } } @@ -140,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 61% 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..443755a62c 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,25 @@ {% 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": "node-id-hidden", + "name": "node_id", + "value": node.id, + "class": "ma-feedback-hidden-field" }, { - "id": "field68798989", - "name": "field68798989", + "id": "latitude-hidden", + "name": "latitude", "value": "", - "class": "fsField unique-id-substitute" - }, - { - "id": "field97986104", - "name": "field97986104", - "value": "9999", - "class": "fsField" - }, - { - "id": "form2521317", - "name": "form", - "value": "2521317" + "class": "ma-feedback-hidden-field ma-feedback-latitude" }, { - "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": "longitude-hidden", + "name": "longitude", + "value": "", + "class": "ma-feedback-hidden-field ma-feedback-longitude" } ] %} @@ -71,8 +27,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 +40,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 +62,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 +74,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" From beb891670208dc61415b84dd010fe08e36c84c03 Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Wed, 26 Nov 2025 19:13:17 -0600 Subject: [PATCH 3/8] DP-43340: Code validation fixes. Signed-off-by: Dmytro Storozhuk --- .../js/mass-feedback-form.behaviors.js | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) 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 4de5f5d043..538d499644 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 @@ -4,9 +4,7 @@ * Handles submission to the Lambda feedback API instead of Formstack. */ -/* global dataLayer, drupalSettings, once */ - -(function ($) { +(function ($, once) { 'use strict'; /** @@ -16,12 +14,14 @@ attach: function (context) { // Cache for geolocation promise (to avoid multiple requests) - var geoLocationPromise = null; - var geoLocationPromiseStarted = false; + let geoLocationPromise = null; + let geoLocationPromiseStarted = false; /** * Get or create the geolocation promise. * Only requests geolocation once, subsequent calls return the same promise. + * + * @return {Promise} Promise resolving with geolocation data or rejection. */ function getGeolocationPromise() { if (!geoLocationPromiseStarted) { @@ -39,18 +39,18 @@ // Process feedback forms using Drupal's once() function once('massFeedbackForm', '.ma__mass-feedback-form', context).forEach(function (element) { - var $self = $(element); - var $form = $self.find('form').not('has-error'); + const $self = $(element); + const $form = $self.find('form').not('has-error'); if (!$form.length) { return; } - var feedback = $self[0]; - var $success = $self.find('#success-screen'); - var $submitBtn = $('input[type="submit"]', $form); - var formAction = $form.attr('action'); - var isSubmitting = false; + 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) { @@ -91,6 +91,8 @@ /** * Get user's geolocation if available. * Returns a promise that resolves with {latitude, longitude} or rejects. + * + * @return {Promise} Promise resolving with {latitude, longitude} object or rejection. */ function getMassgovGeolocation() { return new Promise(function (resolve, reject) { @@ -108,7 +110,7 @@ clearTimeout(timeoutId); resolve({ latitude: position.coords.latitude.toString(), - longitude: position.coords.longitude.toString(), + longitude: position.coords.longitude.toString() }); }, function (error) { @@ -118,32 +120,40 @@ { enableHighAccuracy: false, timeout: 10000, - maximumAge: 300000, // 5 minutes cache - }, + maximumAge: 300000 // 5 minutes cache + } ); }); } /** * Submit feedback to Lambda API. + * + * @param {jQuery} $form The form element. + * @param {string} formAction The API endpoint URL. + * @param {Object} geoData The geolocation data {latitude, longitude}. + * @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, geoData, $success, feedback, $submitBtn, onComplete) { - var formData = new FormData($form[0]); + 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 - var explainField = ''; - var explainInputs = $form.find('textarea[name="explain"]'); + let explainField = ''; + const explainInputs = $form.find('textarea[name="explain"]'); explainInputs.each(function () { - var $textarea = $(this); + 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() || ''; } }); - var payload = { + const payload = { node_id: parseInt(formData.get('node_id')) || 0, info_found: formData.get('info_found') === 'Yes', explain: explainField, @@ -157,8 +167,6 @@ payload.longitude = geoData.longitude; } - console.log('Submitting feedback:', payload); - fetch(formAction, { method: 'POST', headers: { @@ -166,24 +174,22 @@ 'Accept': 'application/json', 'Accept-Language': navigator.language || 'en-US', 'X-Requested-With': 'XMLHttpRequest', - 'User-Agent': navigator.userAgent, + 'User-Agent': navigator.userAgent }, - body: JSON.stringify(payload), + body: JSON.stringify(payload) }) .then(function (response) { if (!response.ok) { return response.json().then(function (data) { throw { status: response.status, - data: data, + data: data }; }); } return response.json(); }) .then(function (data) { - console.log('Feedback submitted successfully:', data); - // Show success screen $form.addClass('hidden'); $success.removeClass('hidden'); @@ -215,9 +221,12 @@ /** * Display error message in the form. + * + * @param {jQuery} $form The form element. + * @param {string} message The error message to display. */ function showErrorMessage($form, message) { - var $messages = $form.find('.messages'); + let $messages = $form.find('.messages'); if (!$messages.length) { $form.prepend('
'); $messages = $form.find('.messages'); @@ -229,6 +238,6 @@ $messages.fadeOut(); }, 5000); } - }, + } }; -})(jQuery); +})(jQuery, once); From 31b138c2b54babaace97173369a63f1e8de349f4 Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Mon, 1 Dec 2025 13:27:32 -0600 Subject: [PATCH 4/8] DP-43340: Fix code formatting in mass-feedback-form.behaviors.js Signed-off-by: Dmytro Storozhuk --- .../mass_feedback_form/js/mass-feedback-form.behaviors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 538d499644..5fa67902d7 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 @@ -158,7 +158,7 @@ info_found: formData.get('info_found') === 'Yes', explain: explainField, url: window.location.href, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; // Add geolocation if available @@ -193,7 +193,7 @@ // Show success screen $form.addClass('hidden'); $success.removeClass('hidden'); - feedback.scrollIntoView({ behavior: 'smooth' }); + feedback.scrollIntoView({behavior: 'smooth'}); // Reset form after delay setTimeout(function () { From 707b6d4a7815ed101fd23aef73a08c5607e0b54f Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Tue, 9 Dec 2025 06:09:43 -0600 Subject: [PATCH 5/8] DP-43340: Remove geolocation support from feedback form handling. Signed-off-by: Dmytro Storozhuk --- .../js/mass-feedback-form.behaviors.js | 87 ++----------------- 1 file changed, 5 insertions(+), 82 deletions(-) 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 5fa67902d7..05e6af2ad3 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 @@ -8,35 +8,11 @@ 'use strict'; /** - * Support feedback form submission to Lambda API with geolocation support. + * Support feedback form submission to Lambda API. */ Drupal.behaviors.massFeedbackForm = { attach: function (context) { - // Cache for geolocation promise (to avoid multiple requests) - let geoLocationPromise = null; - let geoLocationPromiseStarted = false; - - /** - * Get or create the geolocation promise. - * Only requests geolocation once, subsequent calls return the same promise. - * - * @return {Promise} Promise resolving with geolocation data or rejection. - */ - function getGeolocationPromise() { - if (!geoLocationPromiseStarted) { - geoLocationPromiseStarted = true; - geoLocationPromise = getMassgovGeolocation(); - // Ensure any promise rejection is handled to prevent unhandled rejection errors - geoLocationPromise.catch(function (error) { - console.warn('Geolocation request failed:', error.message || error); - // Don't rethrow - let consumers handle it - return {}; - }); - } - return geoLocationPromise; - } - // Process feedback forms using Drupal's once() function once('massFeedbackForm', '.ma__mass-feedback-form', context).forEach(function (element) { const $self = $(element); @@ -71,73 +47,26 @@ isSubmitting = true; $submitBtn.prop('disabled', true); - // Wait for geolocation to complete (or fail), then submit - var geoPromise = getGeolocationPromise(); - geoPromise.then(function (geoData) { - submitFeedback($form, formAction, geoData, $success, feedback, $submitBtn, function () { - isSubmitting = false; - }); - }).catch(function (error) { - console.warn('Geolocation error, submitting without coordinates:', error); - submitFeedback($form, formAction, {}, $success, feedback, $submitBtn, function () { - isSubmitting = false; - }); + // Submit feedback. + submitFeedback($form, formAction, $success, feedback, $submitBtn, function () { + isSubmitting = false; }); return false; }); }); - /** - * Get user's geolocation if available. - * Returns a promise that resolves with {latitude, longitude} or rejects. - * - * @return {Promise} Promise resolving with {latitude, longitude} object or rejection. - */ - function getMassgovGeolocation() { - return new Promise(function (resolve, reject) { - if (!navigator.geolocation) { - reject(new Error('Geolocation not supported')); - return; - } - - var timeoutId = setTimeout(function () { - reject(new Error('Geolocation timeout')); - }, 10000); // 10 second timeout - - navigator.geolocation.getCurrentPosition( - function (position) { - clearTimeout(timeoutId); - resolve({ - latitude: position.coords.latitude.toString(), - longitude: position.coords.longitude.toString() - }); - }, - function (error) { - clearTimeout(timeoutId); - reject(error); - }, - { - enableHighAccuracy: false, - timeout: 10000, - maximumAge: 300000 // 5 minutes cache - } - ); - }); - } - /** * Submit feedback to Lambda API. * * @param {jQuery} $form The form element. * @param {string} formAction The API endpoint URL. - * @param {Object} geoData The geolocation data {latitude, longitude}. * @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, geoData, $success, feedback, $submitBtn, onComplete) { + 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 @@ -161,12 +90,6 @@ timestamp: new Date().toISOString() }; - // Add geolocation if available - if (geoData && geoData.latitude && geoData.longitude) { - payload.latitude = geoData.latitude; - payload.longitude = geoData.longitude; - } - fetch(formAction, { method: 'POST', headers: { From feefbca8ec9055b7dc74a4848fef123b91d706b0 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 9 Dec 2025 06:14:54 -0600 Subject: [PATCH 6/8] Fix typos Co-authored-by: Thomas Fleming --- .ddev/.env.example | 2 +- changelogs/DP-43340.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ddev/.env.example b/.ddev/.env.example index 01f692c6ca..910941075a 100644 --- a/.ddev/.env.example +++ b/.ddev/.env.example @@ -84,5 +84,5 @@ 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 labda from ETL repo (this can be outdated). +## 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 index 143202fc1b..ed9b8157ff 100644 --- a/changelogs/DP-43340.yml +++ b/changelogs/DP-43340.yml @@ -37,5 +37,5 @@ # issue: DP-19843 # Changed: - - description: Switch to custom feedback backend, remove useless code, improve feedback form for nodes withour organization. + - description: Switch to custom feedback backend, remove useless code, improve feedback form for nodes without organization. issue: DP-43340 From bc4b0a84afd9086ee36421243e5bd54f156febc5 Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Tue, 9 Dec 2025 10:00:24 -0600 Subject: [PATCH 7/8] DP-43340: Remove geolocation fields and disable form reset behavior in feedback submission Signed-off-by: Dmytro Storozhuk --- .../js/mass-feedback-form.behaviors.js | 10 +--------- ...mass-feedback-form-without-organization.html.twig | 12 ------------ .../content/node--org-page--feedback.html.twig | 12 ------------ 3 files changed, 1 insertion(+), 33 deletions(-) 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 05e6af2ad3..7845ebe15f 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 @@ -113,18 +113,10 @@ return response.json(); }) .then(function (data) { - // Show success screen + // Show success screen and keep it visible $form.addClass('hidden'); $success.removeClass('hidden'); feedback.scrollIntoView({behavior: 'smooth'}); - - // Reset form after delay - setTimeout(function () { - $form.removeClass('hidden'); - $success.addClass('hidden'); - $form[0].reset(); - $submitBtn.prop('disabled', false); - }, 5000); }) .catch(function (error) { console.error('Feedback submission failed:', error); diff --git a/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig b/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig index 443755a62c..9534ec0c1d 100644 --- a/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig +++ b/docroot/themes/custom/mass_theme/templates/content/node--mass-feedback-form-without-organization.html.twig @@ -8,18 +8,6 @@ "name": "node_id", "value": node.id, "class": "ma-feedback-hidden-field" - }, - { - "id": "latitude-hidden", - "name": "latitude", - "value": "", - "class": "ma-feedback-hidden-field ma-feedback-latitude" - }, - { - "id": "longitude-hidden", - "name": "longitude", - "value": "", - "class": "ma-feedback-hidden-field ma-feedback-longitude" } ] %} 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 261b12c993..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 @@ -17,18 +17,6 @@ "name": "node_id", "value": node.id, "class": "ma-feedback-hidden-field" - }, - { - "id": "latitude-hidden", - "name": "latitude", - "value": "", - "class": "ma-feedback-hidden-field ma-feedback-latitude" - }, - { - "id": "longitude-hidden", - "name": "longitude", - "value": "", - "class": "ma-feedback-hidden-field ma-feedback-longitude" } ] %} From 435f57ae5bcf63770f76b5af810967d01152a469 Mon Sep 17 00:00:00 2001 From: Dmytro Storozhuk Date: Tue, 9 Dec 2025 13:49:01 -0600 Subject: [PATCH 8/8] DP-43340: Add unique device ID to feedback payload for enhanced spam detection Signed-off-by: Dmytro Storozhuk --- .../mass_feedback_form/js/mass-feedback-form.behaviors.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 7845ebe15f..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 @@ -82,12 +82,17 @@ } }); + // 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() + timestamp: new Date().toISOString(), + mg_feedback_id: mgFeedbackId }; fetch(formAction, {