Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .ddev/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=''
41 changes: 41 additions & 0 deletions changelogs/DP-43340.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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('<div class="messages" style="font-weight: bold; color: red"/>');
$messages = $('.messages', $form);
$form.prepend('<div class="messages" style="font-weight: bold; color: #d73d32; margin-bottom: 20px;"/>');
$messages = $form.find('.messages');
}
return $messages;
}
$messages.html(message).show();

// Auto-hide after 5 seconds
setTimeout(function () {
$messages.fadeOut();
}, 5000);
}
}
};
})(jQuery);
})(jQuery, once);
Original file line number Diff line number Diff line change
@@ -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
23 changes: 13 additions & 10 deletions docroot/modules/custom/mass_feedback_form/mass_feedback_form.module
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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()
*/
Expand Down Expand Up @@ -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)) {
Expand Down
Loading