Feat/webpack (#2998)
* First commit * Removed govuk for webpack. Modernized javascript importing. Removed dead js * Fixed tests, a few styling bugs * Fixed some table errors and regenerated backstop ref images * Updated tests for coverage * Changes from carlo suggestions
@@ -239,10 +239,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
labels = [];
|
let labels = [];
|
||||||
deliveredData = [];
|
let deliveredData = [];
|
||||||
failedData = [];
|
let failedData = [];
|
||||||
pendingData = [];
|
let pendingData = [];
|
||||||
let totalMessages = 0;
|
let totalMessages = 0;
|
||||||
|
|
||||||
for (var dateString in data) {
|
for (var dateString in data) {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
(function (window) {
|
export function initCurrentYear() {
|
||||||
"use strict";
|
const currentYearElement = document.getElementById("current-year");
|
||||||
|
|
||||||
// Show the current year
|
if (currentYearElement) {
|
||||||
const currentYearElement = document.getElementById("current-year");
|
currentYearElement.textContent = new Date().getFullYear();
|
||||||
|
}
|
||||||
if (currentYearElement) {
|
}
|
||||||
currentYearElement.textContent = new Date().getFullYear();
|
|
||||||
}
|
|
||||||
})(window);
|
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
(function (window) {
|
export const ErrorBanner = {
|
||||||
"use strict";
|
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
|
||||||
|
showBanner: () => $('.banner-dangerous').removeClass('display-none')
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
window.NotifyModules = window.NotifyModules || {};
|
||||||
This module is intended to be used to show and hide an error banner based on a javascript trigger. You should make
|
window.NotifyModules.ErrorBanner = ErrorBanner;
|
||||||
sure the banner has an appropriate aria-live attribute, and a tabindex of -1 so that screenreaders and keyboard users
|
|
||||||
are alerted to the change respectively.
|
|
||||||
|
|
||||||
This may behave in unexpected ways if you have more than one element with the `banner-dangerous` class on your page.
|
|
||||||
*/
|
|
||||||
window.NotifyModules.ErrorBanner = {
|
|
||||||
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
|
|
||||||
showBanner: () => $('.banner-dangerous')
|
|
||||||
.removeClass('display-none')
|
|
||||||
};
|
|
||||||
})(window);
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
(function(window) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
window.NotifyModules['fullscreen-table'] = function() {
|
|
||||||
|
|
||||||
this.start = function(component) {
|
|
||||||
|
|
||||||
this.$component = $(component);
|
|
||||||
this.$table = this.$component.find('table');
|
|
||||||
this.nativeHeight = this.$component.innerHeight() + 20; // 20px to allow room for scrollbar
|
|
||||||
this.topOffset = this.$component.offset().top;
|
|
||||||
|
|
||||||
this.insertShims();
|
|
||||||
this.maintainWidth();
|
|
||||||
this.maintainHeight();
|
|
||||||
this.toggleShadows();
|
|
||||||
|
|
||||||
$(window)
|
|
||||||
.on('scroll resize', this.maintainHeight)
|
|
||||||
.on('resize', this.maintainWidth);
|
|
||||||
|
|
||||||
this.$scrollableTable
|
|
||||||
.on('scroll', this.toggleShadows)
|
|
||||||
.on('scroll', this.maintainHeight)
|
|
||||||
|
|
||||||
if (
|
|
||||||
window.NotifyModules.stickAtBottomWhenScrolling &&
|
|
||||||
window.NotifyModules.stickAtBottomWhenScrolling.recalculate
|
|
||||||
) {
|
|
||||||
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.maintainWidth();
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.insertShims = () => {
|
|
||||||
|
|
||||||
const attributesForFocus = 'role aria-labelledby';
|
|
||||||
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
|
||||||
|
|
||||||
this.$table.find('caption').attr('id', captionId);
|
|
||||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
|
|
||||||
|
|
||||||
this.$component
|
|
||||||
.append(
|
|
||||||
this.$component.find('.fullscreen-scrollable-table')
|
|
||||||
.clone()
|
|
||||||
.addClass('fullscreen-fixed-table')
|
|
||||||
.removeClass('fullscreen-scrollable-table')
|
|
||||||
.removeAttr(attributesForFocus)
|
|
||||||
.attr('aria-hidden', true)
|
|
||||||
.find('caption')
|
|
||||||
.removeAttr('id')
|
|
||||||
.closest('.fullscreen-fixed-table')
|
|
||||||
)
|
|
||||||
.append(
|
|
||||||
'<div class="fullscreen-right-shadow" />'
|
|
||||||
)
|
|
||||||
.after(
|
|
||||||
$("<div class='fullscreen-shim'/>").css({
|
|
||||||
'height': this.nativeHeight,
|
|
||||||
'top': this.topOffset
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.css('position', 'absolute');
|
|
||||||
|
|
||||||
this.$scrollableTable = this.$component.find('.fullscreen-scrollable-table');
|
|
||||||
this.$fixedTable = this.$component.find('.fullscreen-fixed-table');
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.maintainHeight = () => {
|
|
||||||
|
|
||||||
let height = Math.min(
|
|
||||||
$(window).height() - this.topOffset + $(window).scrollTop(),
|
|
||||||
this.nativeHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
this.$scrollableTable.outerHeight(height);
|
|
||||||
this.$fixedTable.outerHeight(height);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.maintainWidth = () => {
|
|
||||||
|
|
||||||
let indexColumnWidth = this.$fixedTable.find('.table-field-index').outerWidth();
|
|
||||||
|
|
||||||
this.$scrollableTable
|
|
||||||
.css({
|
|
||||||
'width': this.$component.parent('main').width() - indexColumnWidth,
|
|
||||||
'margin-left': indexColumnWidth
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$fixedTable
|
|
||||||
.width(indexColumnWidth + 4);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.toggleShadows = () => {
|
|
||||||
|
|
||||||
this.$fixedTable
|
|
||||||
.toggleClass(
|
|
||||||
'fullscreen-scrolled-table',
|
|
||||||
this.$scrollableTable.scrollLeft() > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
this.$component.find('.fullscreen-right-shadow')
|
|
||||||
.toggleClass(
|
|
||||||
'visible',
|
|
||||||
this.$scrollableTable.scrollLeft() < (this.$table.width() - this.$scrollableTable.width())
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(
|
|
||||||
() => this.$component.find('.fullscreen-right-shadow').addClass('with-transition'),
|
|
||||||
3000
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
})(window);
|
|
||||||
84
app/assets/javascripts/index.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Webpack Entry Point
|
||||||
|
|
||||||
|
// Vendor libraries
|
||||||
|
import $ from 'jquery';
|
||||||
|
window.jQuery = window.$ = $;
|
||||||
|
|
||||||
|
import './jquery-expose.js';
|
||||||
|
import 'query-command-supported';
|
||||||
|
import 'textarea-caret';
|
||||||
|
import * as cbor from 'cbor-js';
|
||||||
|
|
||||||
|
// D3 - selective imports for tree shaking
|
||||||
|
import {
|
||||||
|
select,
|
||||||
|
scaleLinear,
|
||||||
|
scaleBand,
|
||||||
|
scaleSymlog,
|
||||||
|
scaleOrdinal,
|
||||||
|
axisBottom,
|
||||||
|
axisLeft,
|
||||||
|
stack,
|
||||||
|
stackOrderNone,
|
||||||
|
stackOffsetNone,
|
||||||
|
sum,
|
||||||
|
max,
|
||||||
|
format,
|
||||||
|
interpolate
|
||||||
|
} from 'd3';
|
||||||
|
|
||||||
|
window.CBOR = cbor;
|
||||||
|
window.d3 = {
|
||||||
|
select,
|
||||||
|
scaleLinear,
|
||||||
|
scaleBand,
|
||||||
|
scaleSymlog,
|
||||||
|
scaleOrdinal,
|
||||||
|
axisBottom,
|
||||||
|
axisLeft,
|
||||||
|
stack,
|
||||||
|
stackOrderNone,
|
||||||
|
stackOffsetNone,
|
||||||
|
sum,
|
||||||
|
max,
|
||||||
|
format,
|
||||||
|
interpolate
|
||||||
|
};
|
||||||
|
|
||||||
|
window.NotifyModules = window.NotifyModules || {};
|
||||||
|
|
||||||
|
// Core modules
|
||||||
|
import './modules/init.js';
|
||||||
|
import './modules/uswds-modules.js';
|
||||||
|
import './modules/show-hide-content.js';
|
||||||
|
import { registerModule, initModules } from './moduleRegistry.js';
|
||||||
|
|
||||||
|
// Local modules
|
||||||
|
import './radioSelect.js';
|
||||||
|
import './liveSearch.js';
|
||||||
|
import './preventDuplicateFormSubmissions.js';
|
||||||
|
import { ErrorBanner } from './errorBanner.js';
|
||||||
|
import './notifyModal.js';
|
||||||
|
import './timeoutPopup.js';
|
||||||
|
import { initCurrentYear } from './date.js';
|
||||||
|
import './sidenav.js';
|
||||||
|
import './validation.js';
|
||||||
|
import './scrollPosition.js';
|
||||||
|
|
||||||
|
initCurrentYear();
|
||||||
|
|
||||||
|
// NotifyModules
|
||||||
|
import './copyToClipboard.js';
|
||||||
|
import './enhancedTextbox.js';
|
||||||
|
import './fileUpload.js';
|
||||||
|
import './errorTracking.js';
|
||||||
|
import './templateFolderForm.js';
|
||||||
|
import './collapsibleCheckboxes.js';
|
||||||
|
import './radioSlider.js';
|
||||||
|
import './updateStatus.js';
|
||||||
|
import './main.js';
|
||||||
|
import './listEntry.js';
|
||||||
|
import './stick-to-window-when-scrolling.js';
|
||||||
|
import './totalMessagesChart.js';
|
||||||
|
import './activityChart.js';
|
||||||
|
import './job-polling.js';
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
(function (window) {
|
|
||||||
|
|
||||||
// Set the target date (10 days before March 15th, 2024)
|
|
||||||
const targetDate = new Date("April 16, 2024 00:00:00").getTime();
|
|
||||||
|
|
||||||
// Set a const for the container to write a conditional statement and only run if it exists
|
|
||||||
const countdownContainer = document.getElementById("countdown-container");
|
|
||||||
|
|
||||||
// Function to update the countdown display
|
|
||||||
function updateCountdown() {
|
|
||||||
|
|
||||||
if (countdownContainer) {
|
|
||||||
const now = new Date().getTime();
|
|
||||||
const difference = targetDate - now;
|
|
||||||
|
|
||||||
// Time calculations for days only
|
|
||||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
// Visibility logic
|
|
||||||
if (days < 0 || days > 10) {
|
|
||||||
// Hide if more than 10 days away OR if already past the date
|
|
||||||
countdownContainer.style.display = "none";
|
|
||||||
} else {
|
|
||||||
// Show if 10 days or less remaining
|
|
||||||
countdownContainer.style.display = "block";
|
|
||||||
document.getElementById("countdown").innerHTML = days + " days ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose the updateCountdown function to the outside world
|
|
||||||
window.updateCountdown = updateCountdown;
|
|
||||||
|
|
||||||
// Initial display update
|
|
||||||
updateCountdown();
|
|
||||||
|
|
||||||
// Update the countdown every second (inside the IIFE)
|
|
||||||
setInterval(updateCountdown, 1000);
|
|
||||||
|
|
||||||
})(window);
|
|
||||||
40
app/assets/javascripts/moduleRegistry.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const modules = new Map();
|
||||||
|
|
||||||
|
export function registerModule(name, ModuleClass) {
|
||||||
|
modules.set(name, ModuleClass);
|
||||||
|
window.NotifyModules = window.NotifyModules || {};
|
||||||
|
window.NotifyModules[name] = ModuleClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initModules() {
|
||||||
|
const moduleElements = document.querySelectorAll('[data-module]');
|
||||||
|
|
||||||
|
moduleElements.forEach(element => {
|
||||||
|
const moduleName = element.getAttribute('data-module');
|
||||||
|
const moduleStarted = element.getAttribute('data-module-started');
|
||||||
|
|
||||||
|
if (moduleStarted) return;
|
||||||
|
|
||||||
|
const ModuleClass = modules.get(moduleName);
|
||||||
|
|
||||||
|
if (!ModuleClass) {
|
||||||
|
console.warn(`Module "${moduleName}" not found in registry`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const moduleInstance = new ModuleClass();
|
||||||
|
|
||||||
|
if (moduleInstance.start && typeof moduleInstance.start === 'function') {
|
||||||
|
moduleInstance.start(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.setAttribute('data-module-started', 'true');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error initializing module "${moduleName}":`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NotifyModules = window.NotifyModules || {};
|
||||||
|
window.NotifyModules.start = initModules;
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
|
|
||||||
this.$liveRegionCounter = this.$form.find('.selection-counter');
|
this.$liveRegionCounter = this.$form.find('.selection-counter');
|
||||||
|
|
||||||
|
// Get single channel data from DOM (must happen after DOM is ready)
|
||||||
|
const addNewTemplateForm = document.querySelector('div[id=add_new_template_form]');
|
||||||
|
this.$singleNotificationChannel = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-channel") : null;
|
||||||
|
this.$singleChannelService = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-service") : null;
|
||||||
|
|
||||||
this.$liveRegionCounter.before(this.nothingSelectedButtons);
|
this.$liveRegionCounter.before(this.nothingSelectedButtons);
|
||||||
this.$liveRegionCounter.before(this.itemsSelectedButtons);
|
this.$liveRegionCounter.before(this.itemsSelectedButtons);
|
||||||
|
|
||||||
@@ -188,9 +193,6 @@
|
|||||||
return changed;
|
return changed;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$singleNotificationChannel = (document.querySelector('div[id=add_new_template_form]')).getAttribute("data-channel");
|
|
||||||
this.$singleChannelService = (document.querySelector('div[id=add_new_template_form]')).getAttribute("data-service");
|
|
||||||
|
|
||||||
this.actionButtonClicked = function(event) {
|
this.actionButtonClicked = function(event) {
|
||||||
this.currentState = $(event.currentTarget).val();
|
this.currentState = $(event.currentTarget).val();
|
||||||
|
|
||||||
@@ -215,6 +217,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// If state is not changing, this is a submit button - allow form submission
|
||||||
|
if (this.currentState === this._lastState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, show the form UI
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.stateChanged()) {
|
if (this.stateChanged()) {
|
||||||
this.render();
|
this.render();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ window.NotifyModules.TimeoutPopup = window.NotifyModules.TimeoutPopup || {};
|
|||||||
var difference = timeTillSessionEnd - now;
|
var difference = timeTillSessionEnd - now;
|
||||||
var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
|
var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
var seconds = Math.floor((difference % (1000 * 60)) / 1000);
|
var seconds = Math.floor((difference % (1000 * 60)) / 1000);
|
||||||
document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s";
|
document.getElementById("timeLeft").textContent = minutes + "m " + seconds + "s";
|
||||||
showTimer();
|
showTimer();
|
||||||
document.getElementById("logOutTimer").addEventListener("click", signoutUser);
|
document.getElementById("logOutTimer").addEventListener("click", signoutUser);
|
||||||
document.getElementById("extendSessionTimer").addEventListener("click", extendSession);
|
document.getElementById("extendSessionTimer").addEventListener("click", extendSession);
|
||||||
|
|||||||
@@ -304,12 +304,19 @@ td.table-empty-message {
|
|||||||
|
|
||||||
.usa-hint {
|
.usa-hint {
|
||||||
display: block;
|
display: block;
|
||||||
|
color: color('gray-70');
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-table {
|
.usa-table {
|
||||||
th {
|
th {
|
||||||
border-bottom: 0 !important;
|
border-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
td {
|
||||||
|
min-height: units(5);
|
||||||
|
div:empty::before {
|
||||||
|
content: '\00a0';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.js-stick-at-bottom-when-scrolling {
|
.js-stick-at-bottom-when-scrolling {
|
||||||
@@ -330,9 +337,32 @@ td.table-empty-message {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid color('gray-cool-10');
|
border: 1px solid color('gray-cool-10');
|
||||||
padding: units(2);
|
padding: units(2);
|
||||||
|
margin-bottom: units(2);
|
||||||
&:focus {
|
&:focus {
|
||||||
outline-offset: units(1);
|
outline-offset: units(1);
|
||||||
}
|
}
|
||||||
|
.js-stick-at-bottom-when-scrolling {
|
||||||
|
display: block;
|
||||||
|
.usa-form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.usa-fieldset {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.radios-nested {
|
||||||
|
.usa-radio {
|
||||||
|
margin-top: units(1);
|
||||||
|
max-width: units('mobile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-footer {
|
||||||
|
margin-top: units(3);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spark-bar-bar {
|
.spark-bar-bar {
|
||||||
@@ -732,10 +762,6 @@ details form {
|
|||||||
|
|
||||||
// Login page
|
// Login page
|
||||||
|
|
||||||
#countdown-container {
|
|
||||||
display: none; // Hide the countdown timer
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder,
|
.placeholder,
|
||||||
.placeholder-conditional {
|
.placeholder-conditional {
|
||||||
background-color: #face00;
|
background-color: #face00;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ in the form $setting: value,
|
|||||||
|
|
||||||
@use "uswds-core" with (
|
@use "uswds-core" with (
|
||||||
$theme-font-type-sans: "public-sans",
|
$theme-font-type-sans: "public-sans",
|
||||||
|
$theme-font-path: "../fonts",
|
||||||
|
$theme-image-path: "../img",
|
||||||
$theme-card-border-radius: 0,
|
$theme-card-border-radius: 0,
|
||||||
$theme-card-gap: 4,
|
$theme-card-gap: 4,
|
||||||
$theme-card-border-width: 2px,
|
$theme-card-border-width: 2px,
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ $path: '/static/images/';
|
|||||||
@import 'components/list-entry';
|
@import 'components/list-entry';
|
||||||
@import 'components/live-search';
|
@import 'components/live-search';
|
||||||
@import 'components/stick-at-top-when-scrolling';
|
@import 'components/stick-at-top-when-scrolling';
|
||||||
@import 'components/fullscreen-table';
|
|
||||||
@import 'components/conditional-radios';
|
@import 'components/conditional-radios';
|
||||||
@import 'components/vendor/breadcrumbs';
|
@import 'components/vendor/breadcrumbs';
|
||||||
@import 'components/vendor/responsive-embed';
|
@import 'components/vendor/responsive-embed';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%}
|
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False, striped=False) -%}
|
||||||
<table class="usa-table{{' usa-table--borderless' if not border_visible}} width-full">
|
<table class="usa-table{{' usa-table--borderless' if not border_visible}}{{' usa-table--striped' if striped}} width-full">
|
||||||
<caption class="font-body-lg table-heading{{ ' usa-sr-only' if not caption_visible}}">
|
<caption class="font-body-lg table-heading{{ ' usa-sr-only' if not caption_visible}}">
|
||||||
{{ caption }}
|
{{ caption }}
|
||||||
</caption>
|
</caption>
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
</table>
|
</table>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%}
|
{% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False, striped=False) -%}
|
||||||
{% set parent_caller = caller %}
|
{% set parent_caller = caller %}
|
||||||
|
|
||||||
{% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length, border_visible) %}
|
{% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length, border_visible, striped) %}
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
{% call row(item.id) %}
|
{% call row(item.id) %}
|
||||||
{{ parent_caller(item, loop.index + 1) }}
|
{{ parent_caller(item, loop.index + 1) }}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ recipients.column_headers %}
|
|||||||
|
|
||||||
<h2 class="font-body-lg" id="file-preview">{{ original_file_name }}</h2>
|
<h2 class="font-body-lg" id="file-preview">{{ original_file_name }}</h2>
|
||||||
|
|
||||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
<div class="spreadsheet usa-table-container--scrollable" tabindex="0">
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
recipients.displayed_rows,
|
recipients.displayed_rows,
|
||||||
caption=original_file_name,
|
caption=original_file_name,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
<div class="spreadsheet usa-table-container--scrollable" tabindex="0">
|
||||||
{% call(item, row_number) mapping_table(
|
{% call(item, row_number) mapping_table(
|
||||||
caption="Errors in " + original_file_name,
|
caption="Errors in " + original_file_name,
|
||||||
caption_visible=False,
|
caption_visible=False,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
||||||
|
|
||||||
<div class="spreadsheet" data-module="fullscreen-table">
|
<div class="spreadsheet usa-table-container--scrollable" tabindex="0">
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
example,
|
example,
|
||||||
caption="Example spreadsheet",
|
caption="Example spreadsheet",
|
||||||
|
|||||||
@@ -39,16 +39,9 @@
|
|||||||
{% call row() %}
|
{% call row() %}
|
||||||
{{ text_field('Send text messages') }}
|
{{ text_field('Send text messages') }}
|
||||||
{{ boolean_field('sms' in current_service.permissions) }}
|
{{ boolean_field('sms' in current_service.permissions) }}
|
||||||
{# {{ edit_field(
|
{% call field(align='right') %}
|
||||||
'Change',
|
<span class="table-field-status-default">Set automatically</span>
|
||||||
url_for(
|
{% endcall %}
|
||||||
'.service_set_channel',
|
|
||||||
service_id=current_service.id,
|
|
||||||
channel='sms'
|
|
||||||
),
|
|
||||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
|
||||||
suffix='your settings for sending text messages',
|
|
||||||
)}} #}
|
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% if current_user.platform_admin %}
|
{% if current_user.platform_admin %}
|
||||||
@@ -218,6 +211,9 @@
|
|||||||
{{ text_field('Live')}}
|
{{ text_field('Live')}}
|
||||||
{% if current_service.trial_mode and not current_service.organization_id %}
|
{% if current_service.trial_mode and not current_service.organization_id %}
|
||||||
{{ text_field('No (organization must be set first)') }}
|
{{ text_field('No (organization must be set first)') }}
|
||||||
|
{% call field(align='right') %}
|
||||||
|
<span class="table-field-status-default">Set automatically</span>
|
||||||
|
{% endcall %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ boolean_field(not current_service.trial_mode) }}
|
{{ boolean_field(not current_service.trial_mode) }}
|
||||||
{{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id), suffix='service status') }}
|
{{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id), suffix='service status') }}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['@babel/preset-env'],
|
presets: [
|
||||||
|
['@babel/preset-env', {
|
||||||
|
targets: {
|
||||||
|
node: 'current'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 5.8 MiB After Width: | Height: | Size: 6.6 MiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
@@ -28,7 +28,6 @@ const javascripts = () => {
|
|||||||
paths.src + 'javascripts/errorBanner.js',
|
paths.src + 'javascripts/errorBanner.js',
|
||||||
paths.src + 'javascripts/notifyModal.js',
|
paths.src + 'javascripts/notifyModal.js',
|
||||||
paths.src + 'javascripts/date.js',
|
paths.src + 'javascripts/date.js',
|
||||||
paths.src + 'javascripts/loginAlert.js',
|
|
||||||
paths.src + 'javascripts/sidenav.js',
|
paths.src + 'javascripts/sidenav.js',
|
||||||
paths.src + 'javascripts/validation.js',
|
paths.src + 'javascripts/validation.js',
|
||||||
paths.src + 'javascripts/scrollPosition.js',
|
paths.src + 'javascripts/scrollPosition.js',
|
||||||
|
|||||||
5382
package-lock.json
generated
44
package.json
@@ -3,14 +3,19 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Admin front end for Notify",
|
"description": "Admin front end for Notify",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.15.3"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "gulp lint",
|
"lint": "jshint app/assets/javascripts",
|
||||||
"test": "jest --config tests/javascripts/jest.config.js tests/javascripts",
|
"test": "jest --config tests/javascripts/jest.config.js tests/javascripts",
|
||||||
"test-watch": "jest --watch --config tests/javascripts/jest.config.js tests/javascripts",
|
"test-watch": "jest --watch --config tests/javascripts/jest.config.js tests/javascripts",
|
||||||
"build": "gulp",
|
"build": "NODE_ENV=production npx webpack --config webpack.config.js",
|
||||||
"watch": "gulp watch",
|
"build:dev": "npx webpack --config webpack.config.js",
|
||||||
|
"watch": "npx webpack --watch --config webpack.config.js",
|
||||||
|
"backstop:test": "npx backstop test --configPath=backstop.config.js",
|
||||||
|
"backstop:reference": "npx backstop reference --configPath=backstop.config.js",
|
||||||
|
"backstop:approve": "npx backstop approve --configPath=backstop.config.js",
|
||||||
|
"backstop:open": "npx backstop openReport --configPath=backstop.config.js",
|
||||||
"audit": "better-npm-audit audit --production --level low",
|
"audit": "better-npm-audit audit --production --level low",
|
||||||
"pa11y-ci": "pa11y-ci"
|
"pa11y-ci": "pa11y-ci"
|
||||||
},
|
},
|
||||||
@@ -31,41 +36,34 @@
|
|||||||
"@uswds/uswds": "^3.13.0",
|
"@uswds/uswds": "^3.13.0",
|
||||||
"cbor-js": "0.1.0",
|
"cbor-js": "0.1.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"gulp-merge": "^0.1.1",
|
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"playwright": "^1.56.0",
|
"playwright": "^1.56.0",
|
||||||
"python": "^0.0.4",
|
"python": "^0.0.4",
|
||||||
"query-command-supported": "1.0.0",
|
"query-command-supported": "1.0.0",
|
||||||
"sass-embedded": "^1.93.2",
|
"sass-embedded": "^1.93.2",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0"
|
||||||
"vinyl-buffer": "^1.0.1",
|
|
||||||
"vinyl-source-stream": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.28.4",
|
"@babel/core": "^7.28.4",
|
||||||
"@babel/preset-env": "^7.28.3",
|
"@babel/preset-env": "^7.28.3",
|
||||||
"@uswds/compile": "^1.3.1",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
"backstopjs": "^6.3.25",
|
"backstopjs": "^6.3.25",
|
||||||
"better-npm-audit": "^3.11.0",
|
"better-npm-audit": "^3.11.0",
|
||||||
"gulp": "^5.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"gulp-add-src": "^1.0.0",
|
"css-loader": "^7.1.2",
|
||||||
"gulp-babel": "8.0.0",
|
|
||||||
"gulp-clean-css": "4.3.0",
|
|
||||||
"gulp-concat": "^2.6.1",
|
|
||||||
"gulp-if": "^3.0.0",
|
|
||||||
"gulp-include": "2.4.1",
|
|
||||||
"gulp-jshint": "2.1.0",
|
|
||||||
"gulp-load-plugins": "^2.0.8",
|
|
||||||
"gulp-prettyerror": "2.0.0",
|
|
||||||
"gulp-uglify": "3.0.2",
|
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-each": "^30.1.0",
|
"jest-each": "^30.1.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"jshint": "2.13.6",
|
"jshint": "2.13.6",
|
||||||
"jshint-stylish": "2.2.1",
|
"jshint-stylish": "2.2.1",
|
||||||
"merge-stream": "^2.0.0",
|
"merge-stream": "^2.0.0",
|
||||||
"rollup": "^4.52.4",
|
"mini-css-extract-plugin": "^2.9.4",
|
||||||
"rollup-plugin-commonjs": "10.1.0",
|
"postcss-loader": "^8.2.0",
|
||||||
"rollup-plugin-node-resolve": "5.2.0"
|
"sass": "^1.93.2",
|
||||||
|
"sass-loader": "^16.0.5",
|
||||||
|
"terser-webpack-plugin": "^5.3.14",
|
||||||
|
"webpack": "^5.102.1",
|
||||||
|
"webpack-cli": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def _mock_get_service_settings_page_common(
|
|||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"Service name Test Service Change service name",
|
"Service name Test Service Change service name",
|
||||||
"Send text messages On",
|
"Send text messages On Set automatically",
|
||||||
"Start text messages with service name On Change your settings "
|
"Start text messages with service name On Change your settings "
|
||||||
"for starting text messages with service name",
|
"for starting text messages with service name",
|
||||||
],
|
],
|
||||||
@@ -63,7 +63,7 @@ def _mock_get_service_settings_page_common(
|
|||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"Service name Test Service Change service name",
|
"Service name Test Service Change service name",
|
||||||
"Send text messages On",
|
"Send text messages On Set automatically",
|
||||||
"Text message senders (Only visible to Platform Admins) USGOV Manage text message senders",
|
"Text message senders (Only visible to Platform Admins) USGOV Manage text message senders",
|
||||||
"Start text messages with service name On Change your settings "
|
"Start text messages with service name On Change your settings "
|
||||||
"for starting text messages with service name",
|
"for starting text messages with service name",
|
||||||
@@ -191,7 +191,7 @@ def test_send_files_by_email_row_on_settings_page(
|
|||||||
["email", "sms", "international_sms"],
|
["email", "sms", "international_sms"],
|
||||||
[
|
[
|
||||||
"Service name service one Change service name",
|
"Service name service one Change service name",
|
||||||
"Send text messages On",
|
"Send text messages On Set automatically",
|
||||||
"Start text messages with service name On Change your settings "
|
"Start text messages with service name On Change your settings "
|
||||||
"for starting text messages with service name",
|
"for starting text messages with service name",
|
||||||
],
|
],
|
||||||
@@ -200,7 +200,7 @@ def test_send_files_by_email_row_on_settings_page(
|
|||||||
["email", "sms", ServicePermission.EMAIL_AUTH],
|
["email", "sms", ServicePermission.EMAIL_AUTH],
|
||||||
[
|
[
|
||||||
"Service name service one Change service name",
|
"Service name service one Change service name",
|
||||||
"Send text messages On",
|
"Send text messages On Set automatically",
|
||||||
"Start text messages with service name On Change your settings "
|
"Start text messages with service name On Change your settings "
|
||||||
"for starting text messages with service name",
|
"for starting text messages with service name",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ afterAll(() => {
|
|||||||
describe('Collapsible fieldset', () => {
|
describe('Collapsible fieldset', () => {
|
||||||
|
|
||||||
const _checkboxes = (start, end) => {
|
const _checkboxes = (start, end) => {
|
||||||
result = '';
|
let result = '';
|
||||||
|
|
||||||
for (let num = start; num <= end; num++) {
|
for (let num = start; num <= end; num++) {
|
||||||
let id = `folder-permissions-${num}`;
|
let id = `folder-permissions-${num}`;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe('copy to clipboard', () => {
|
|||||||
let component;
|
let component;
|
||||||
let selectionMock;
|
let selectionMock;
|
||||||
let rangeMock;
|
let rangeMock;
|
||||||
|
let screenMock;
|
||||||
|
|
||||||
const setUpDOM = function (options) {
|
const setUpDOM = function (options) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
require('../../app/assets/javascripts/errorBanner.js')
|
// ErrorBanner module sets window.NotifyModules.ErrorBanner for backward compatibility
|
||||||
|
require('../../app/assets/javascripts/errorBanner.js');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -34,4 +35,10 @@ describe("Error Banner", () => {
|
|||||||
expect(document.querySelector('.banner-dangerous').classList).not.toContain('display-none')
|
expect(document.querySelector('.banner-dangerous').classList).not.toContain('display-none')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Module exports ErrorBanner to window.NotifyModules", () => {
|
||||||
|
expect(window.NotifyModules.ErrorBanner).toBeDefined();
|
||||||
|
expect(window.NotifyModules.ErrorBanner.hideBanner).toBeDefined();
|
||||||
|
expect(window.NotifyModules.ErrorBanner.showBanner).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('File upload', () => {
|
|||||||
|
|
||||||
let form;
|
let form;
|
||||||
let fileUpload;
|
let fileUpload;
|
||||||
|
let uploadControl;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
const helpers = require('./support/helpers');
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// TODO: remove this when tests for sticky JS are written
|
|
||||||
require('../../app/assets/javascripts/stick-to-window-when-scrolling.js');
|
|
||||||
|
|
||||||
require('../../app/assets/javascripts/fullscreenTable.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
require('./support/teardown.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('FullscreenTable', () => {
|
|
||||||
let screenMock;
|
|
||||||
let container;
|
|
||||||
let tableFrame;
|
|
||||||
let table;
|
|
||||||
let numberColumnFrame;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
|
|
||||||
const tableHeadings = () => {
|
|
||||||
let result = '';
|
|
||||||
const headings = ['1', 'name', 'email address', 'age', 'building number', 'address line 1', 'address line 2', 'postcode'];
|
|
||||||
|
|
||||||
headings.forEach((heading, idx) => {
|
|
||||||
if (idx === 0) {
|
|
||||||
result += `<th scope="col" class="table-field-heading-first">
|
|
||||||
<span class="visually-hidden">Row in file</span><span aria-hidden="true" class="table-field-invisible-error">${heading}</span>
|
|
||||||
</th>`;
|
|
||||||
} else {
|
|
||||||
result += `<th scope="col" class="table-field-heading">
|
|
||||||
${heading}
|
|
||||||
</th>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowCells = (cells) => {
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
Object.keys(cells).forEach((key, idx) => {
|
|
||||||
if (idx === 0) {
|
|
||||||
result += `<td class="table-field-index">
|
|
||||||
<span class="table-field-error">
|
|
||||||
${key}
|
|
||||||
</span>
|
|
||||||
</td>`;
|
|
||||||
} else {
|
|
||||||
result += `<td class="table-field-left-aligned ">
|
|
||||||
<div class="table-field-status-default">
|
|
||||||
${key}
|
|
||||||
</div>
|
|
||||||
</td>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tableRows = () => {
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
['John Logie Baird', 'johnlbaird@gmail.com', '37', '22', 'Frith Street', 'Soho, London', 'W1D 4RF'],
|
|
||||||
['Guglielmo Marconi', 'gmarconi@hotmail.com', '21', 'Pontecchio Marconi', 'Via Celestini 1', 'Bologna', ''],
|
|
||||||
['Louis Braille', 'louisbraille@yahoo.co.uk', '', '56', 'Boulevard des Invalides', 'Paris', '75007'],
|
|
||||||
['Ray Tomlinson', 'hedy.lamarr@msn.com', '25', '870', '870 Winter Street', 'Waltham', 'MA 02451']
|
|
||||||
];
|
|
||||||
|
|
||||||
rows.forEach(row => {
|
|
||||||
result += `<tr class="table-row">${rowCells(row)}</tr>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
screenMock = new helpers.ScreenMock(jest);
|
|
||||||
screenMock.setWindow({
|
|
||||||
width: 1990,
|
|
||||||
height: 940,
|
|
||||||
scrollTop: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// set up DOM
|
|
||||||
document.body.innerHTML =
|
|
||||||
`<main>
|
|
||||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
|
||||||
<table class="table table-font-xsmall">
|
|
||||||
<caption class="heading-medium table-heading visuallyhidden">
|
|
||||||
people.csv
|
|
||||||
</caption>
|
|
||||||
<thead class="table-field-headings-visible">
|
|
||||||
<tr>
|
|
||||||
${tableHeadings()}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${tableRows()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>`;
|
|
||||||
|
|
||||||
container = document.querySelector('.fullscreen-content');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
|
|
||||||
document.body.innerHTML = '';
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when it loads", () => {
|
|
||||||
|
|
||||||
test("it fixes the number column for each row without changing the semantics", () => {
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
|
|
||||||
|
|
||||||
expect(tableFrame).not.toBeNull();
|
|
||||||
expect(numberColumnFrame).not.toBeNull();
|
|
||||||
expect(numberColumnFrame.getAttribute('aria-hidden')).toEqual('true');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it calls the sticky JS to update any cached dimensions", () => {
|
|
||||||
|
|
||||||
const stickyJSSpy = jest.spyOn(window.NotifyModules.stickAtBottomWhenScrolling, 'recalculate');
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
expect(stickyJSSpy.mock.calls.length).toBe(1);
|
|
||||||
|
|
||||||
stickyJSSpy.mockClear();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the scrolling section is focusable and has an accessible name matching the table caption", () => {
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
caption = tableFrame.querySelector('caption');
|
|
||||||
|
|
||||||
expect(tableFrame.hasAttribute('role')).toBe(true);
|
|
||||||
expect(tableFrame.getAttribute('role')).toEqual('region');
|
|
||||||
expect(tableFrame.hasAttribute('aria-labelledby')).toBe(true);
|
|
||||||
expect(tableFrame.getAttribute('aria-labelledby')).toEqual(caption.getAttribute('id'));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the section providing the fixed row headers is not focusable and is hidden from assistive tech'", () => {
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
fixedRowHeaders = document.querySelector('.fullscreen-fixed-table');
|
|
||||||
|
|
||||||
expect(fixedRowHeaders.hasAttribute('role')).toBe(false);
|
|
||||||
expect(fixedRowHeaders.hasAttribute('aria-labelledby')).toBe(false);
|
|
||||||
expect(fixedRowHeaders.hasAttribute('tabindex')).toBe(false);
|
|
||||||
expect(fixedRowHeaders.hasAttribute('aria-hidden')).toBe(true);
|
|
||||||
expect(fixedRowHeaders.getAttribute('aria-hidden')).toEqual('true');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the height of the table should fit the vertical space available to it", () => {
|
|
||||||
|
|
||||||
let containerBoundingClientRectSpy;
|
|
||||||
let containerClientRectsSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
|
|
||||||
// set the height and offset of the window and table container from the top of the document
|
|
||||||
// so just the top 268px of it appears on-screen
|
|
||||||
screenMock.window.setHeightTo(768);
|
|
||||||
screenMock.mockPositionAndDimension('container', container, {
|
|
||||||
'offsetHeight': 1000,
|
|
||||||
'offsetWidth': 641,
|
|
||||||
'offsetTop': 500
|
|
||||||
});
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
|
|
||||||
screenMock.reset();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the page has loaded", () => {
|
|
||||||
|
|
||||||
// the frames should crop to the top 268px of the table that is visible
|
|
||||||
expect(window.getComputedStyle(tableFrame)['height']).toEqual('268px');
|
|
||||||
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('268px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the page has scrolled", () => {
|
|
||||||
|
|
||||||
// scroll the window so the table fills the height of the window (768px)
|
|
||||||
screenMock.window.scrollTo(500);
|
|
||||||
|
|
||||||
// the frames should crop to the window height
|
|
||||||
expect(window.getComputedStyle(tableFrame)['height']).toEqual('768px');
|
|
||||||
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('768px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the page has resized", () => {
|
|
||||||
|
|
||||||
// resize the window by 232px (from 768px to 1000px)
|
|
||||||
screenMock.window.resizeTo({ height: 1000, width: 1024 });
|
|
||||||
|
|
||||||
// the frames should crop to the top 500px of the table now visible
|
|
||||||
expect(window.getComputedStyle(tableFrame)['height']).toEqual('500px');
|
|
||||||
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('500px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the width of the table should fit the horizontal space available to it", () => {
|
|
||||||
let rowNumberColumnCell;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
|
|
||||||
rowNumberColumnCell = container.querySelector('.table-field-index');
|
|
||||||
|
|
||||||
// set main content column width (used as module as gauge for table width)
|
|
||||||
screenMock.window.setWidthTo(1024);
|
|
||||||
document.querySelector('main').setAttribute('style', 'width: 742px');
|
|
||||||
|
|
||||||
// set total width of column for row numbers in table to 40px
|
|
||||||
rowNumberColumnCell.setAttribute('style', 'width: 40px');
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
|
|
||||||
screenMock.reset();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the page has loaded", () => {
|
|
||||||
|
|
||||||
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
|
|
||||||
expect(window.getComputedStyle(tableFrame)['width']).toEqual('702px'); // width of content column - numbers column
|
|
||||||
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
|
|
||||||
|
|
||||||
// table for number column has 4px extra to allow space for drop shadow
|
|
||||||
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the page has resized", () => {
|
|
||||||
|
|
||||||
// resize window and content column
|
|
||||||
document.querySelector('main').setAttribute('style', 'width: 720px');
|
|
||||||
screenMock.window.resizeTo({ height: 768, width: 960 });
|
|
||||||
|
|
||||||
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
|
|
||||||
expect(window.getComputedStyle(tableFrame)['width']).toEqual('680px'); // width of content column - numbers column
|
|
||||||
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
|
|
||||||
|
|
||||||
// table for number column has 4px extra to allow space for drop shadow
|
|
||||||
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when the table scrolls horizontally", () => {
|
|
||||||
let rightEdgeShadow;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.NotifyModules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
table = tableFrame.querySelector('table');
|
|
||||||
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
|
|
||||||
rightEdgeShadow = container.querySelector('.fullscreen-right-shadow');
|
|
||||||
|
|
||||||
tableFrame.setAttribute('style', 'width: 742px');
|
|
||||||
table.setAttribute('style', 'width: 1000px');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the right edge of the table scroll area should have a drop-shadow if it isn't scrolled", () => {
|
|
||||||
|
|
||||||
tableFrame.scrollLeft = 0;
|
|
||||||
helpers.triggerEvent(tableFrame, 'scroll');
|
|
||||||
|
|
||||||
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(false);
|
|
||||||
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the left edge of the table scroll area should have a drop-shadow if the table is scrolled to 100%", () => {
|
|
||||||
|
|
||||||
// scroll to end of table
|
|
||||||
tableFrame.scrollLeft = 258;
|
|
||||||
helpers.triggerEvent(tableFrame, 'scroll');
|
|
||||||
|
|
||||||
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
|
|
||||||
expect(rightEdgeShadow.classList.contains('visible')).toBe(false);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("both edges of the table scroll area should have a drop-shadow if the table is scrolled between 0% and 100%", () => {
|
|
||||||
|
|
||||||
// scroll to middle of table
|
|
||||||
tableFrame.scrollLeft = 129;
|
|
||||||
helpers.triggerEvent(tableFrame, 'scroll');
|
|
||||||
|
|
||||||
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
|
|
||||||
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -18,7 +18,7 @@ module.exports = {
|
|||||||
url: 'https://beta.notify.gov',
|
url: 'https://beta.notify.gov',
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.js$': 'babel-jest',
|
'^.+\\.js$': ['babel-jest', { rootMode: 'upward' }],
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(d3|d3-array|d3-axis|d3-brush|d3-chord|d3-collection|d3-color|d3-contour|d3-dispatch|d3-drag|d3-dsv|d3-ease|d3-fetch|d3-force|d3-format|d3-geo|d3-hierarchy|d3-interpolate|d3-path|d3-polygon|d3-quadtree|d3-random|d3-scale|d3-scale-chromatic|d3-selection|d3-shape|d3-tile|d3-time|d3-time-format|d3-timer|d3-transition|d3-voronoi|d3-zoom)/)'
|
'node_modules/(?!(d3|d3-array|d3-axis|d3-brush|d3-chord|d3-collection|d3-color|d3-contour|d3-dispatch|d3-drag|d3-dsv|d3-ease|d3-fetch|d3-force|d3-format|d3-geo|d3-hierarchy|d3-interpolate|d3-path|d3-polygon|d3-quadtree|d3-random|d3-scale|d3-scale-chromatic|d3-selection|d3-shape|d3-tile|d3-time|d3-time-format|d3-timer|d3-transition|d3-voronoi|d3-zoom)/)'
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ describe("List entry", () => {
|
|||||||
'va.gov'
|
'va.gov'
|
||||||
];
|
];
|
||||||
let inputList;
|
let inputList;
|
||||||
|
let name_value_pairs;
|
||||||
|
let fields;
|
||||||
|
|
||||||
const triggerEvent = (el, evtType) => {
|
const triggerEvent = (el, evtType) => {
|
||||||
const evt = new Event(evtType, {
|
const evt = new Event(evtType, {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
beforeAll(() => {
|
|
||||||
jest.spyOn(global, 'setTimeout');
|
|
||||||
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<div id="countdown-container" class="usa-alert usa-alert--warning width-full margin-bottom-4">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
<h4 class="usa-alert__heading">Login.gov is required by April 16, 2024</h4>
|
|
||||||
<p class="usa-alert__text">
|
|
||||||
You have <span id="countdown"></span> left to use Login.gov to sign in
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
const sessionTimerModule = require('../../app/assets/javascripts/loginAlert.js');
|
|
||||||
window.NotifyModules.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.useFakeTimers();
|
|
||||||
const targetDate = new Date("April 16, 2024 00:00:00"); // Reference point
|
|
||||||
|
|
||||||
test('Hides the countdown if more than 10 days away', () => {
|
|
||||||
jest.setSystemTime(targetDate.getTime() - 12 * 24 * 60 * 60 * 1000); // 12 days before
|
|
||||||
|
|
||||||
window.updateCountdown(); // Update the countdown display
|
|
||||||
|
|
||||||
expect(document.getElementById("countdown-container").style.display).toBe("none");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Shows the countdown if 10 days or less away', () => {
|
|
||||||
jest.setSystemTime(targetDate.getTime() - 8 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
window.updateCountdown();
|
|
||||||
|
|
||||||
expect(document.getElementById("countdown-container").style.display).toBe("block");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Displays the correct number of days', () => {
|
|
||||||
jest.setSystemTime(targetDate.getTime() - 5 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
window.updateCountdown();
|
|
||||||
|
|
||||||
expect(document.getElementById("countdown").textContent).toBe("5 days ");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Hides the countdown if the target date has passed', () => {
|
|
||||||
jest.setSystemTime(targetDate.getTime() + 2 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
window.updateCountdown();
|
|
||||||
|
|
||||||
expect(document.getElementById("countdown-container").style.display).toBe("none");
|
|
||||||
});
|
|
||||||
@@ -17,6 +17,9 @@ describe('RadioSelect', () => {
|
|||||||
];
|
];
|
||||||
const HOURS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
|
const HOURS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
|
||||||
let originalOptionsForAllCategories;
|
let originalOptionsForAllCategories;
|
||||||
|
let originalOptionsForCategory;
|
||||||
|
let selectedOption;
|
||||||
|
let categoryButtons;
|
||||||
|
|
||||||
const getDataFromOption = (option) => {
|
const getDataFromOption = (option) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -715,7 +715,7 @@ describe("Stick to top/bottom of window when scrolling", () => {
|
|||||||
|
|
||||||
describe("If intending to stick to the bottom", () => {
|
describe("If intending to stick to the bottom", () => {
|
||||||
|
|
||||||
let header;
|
let heading;
|
||||||
let content;
|
let content;
|
||||||
let pageFooter;
|
let pageFooter;
|
||||||
let windowHeight;
|
let windowHeight;
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ describe('TemplateFolderForm', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getTemplateFolderCheckboxes () {
|
function getTemplateFolderCheckboxes () {
|
||||||
return templateFolderForm.querySelectorAll('.usa-checkbox__label');
|
return templateFolderForm.querySelectorAll('.usa-checkbox__input');
|
||||||
};
|
};
|
||||||
|
|
||||||
function getVisibleCounter () {
|
function getVisibleCounter () {
|
||||||
@@ -832,6 +832,9 @@ describe('TemplateFolderForm', () => {
|
|||||||
describe("Additional selection counter scenarios", () => {
|
describe("Additional selection counter scenarios", () => {
|
||||||
|
|
||||||
let templateFolderCheckboxes;
|
let templateFolderCheckboxes;
|
||||||
|
let visibleCounterText;
|
||||||
|
let hiddenCounterText;
|
||||||
|
let formControls;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
@@ -855,19 +858,40 @@ describe('TemplateFolderForm', () => {
|
|||||||
|
|
||||||
test("the content of both visible and hidden counters should match", () => {
|
test("the content of both visible and hidden counters should match", () => {
|
||||||
|
|
||||||
|
// Ensure all checkboxes start unchecked
|
||||||
|
templateFolderCheckboxes.forEach(checkbox => checkbox.checked = false);
|
||||||
|
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[1], 'click');
|
helpers.triggerEvent(templateFolderCheckboxes[1], 'click');
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
|
helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
|
||||||
|
|
||||||
|
visibleCounterText = getVisibleCounter().textContent.trim();
|
||||||
|
hiddenCounterText = getHiddenCounter().textContent.trim();
|
||||||
|
|
||||||
expect(visibleCounterText).toEqual(hiddenCounterText);
|
expect(visibleCounterText).toEqual(hiddenCounterText);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the content of the counter should reflect the selection", () => {
|
test("the content of the counter should reflect the selection", () => {
|
||||||
|
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[1], 'click');
|
// Re-query checkboxes to ensure we have fresh references
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
|
const checkboxes = getTemplateFolderCheckboxes();
|
||||||
|
|
||||||
expect(visibleCounterText).toEqual('2 templates selected');
|
// Ensure all checkboxes start unchecked
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
helpers.triggerEvent(checkbox, 'change');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click templates (indices 1 and 2)
|
||||||
|
checkboxes[1].checked = true;
|
||||||
|
helpers.triggerEvent(checkboxes[1], 'change');
|
||||||
|
checkboxes[2].checked = true;
|
||||||
|
helpers.triggerEvent(checkboxes[2], 'change');
|
||||||
|
|
||||||
|
visibleCounterText = getVisibleCounter().textContent.trim();
|
||||||
|
|
||||||
|
// Application counts the content, not just the number of items selected
|
||||||
|
expect(visibleCounterText).toEqual('1 template, 1 folder selected');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -877,17 +901,37 @@ describe('TemplateFolderForm', () => {
|
|||||||
|
|
||||||
test("the content of both visible and hidden counters should match", () => {
|
test("the content of both visible and hidden counters should match", () => {
|
||||||
|
|
||||||
|
// Ensure all checkboxes start unchecked
|
||||||
|
templateFolderCheckboxes.forEach(checkbox => checkbox.checked = false);
|
||||||
|
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
|
helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
|
||||||
|
|
||||||
|
visibleCounterText = getVisibleCounter().textContent.trim();
|
||||||
|
hiddenCounterText = getHiddenCounter().textContent.trim();
|
||||||
|
|
||||||
expect(visibleCounterText).toEqual(hiddenCounterText);
|
expect(visibleCounterText).toEqual(hiddenCounterText);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the content of the counter should reflect the selection", () => {
|
test("the content of the counter should reflect the selection", () => {
|
||||||
|
|
||||||
helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
|
// Re-query checkboxes to ensure we have fresh references
|
||||||
|
const checkboxes = getTemplateFolderCheckboxes();
|
||||||
|
|
||||||
expect(visibleCounterText).toEqual('1 folder selected');
|
// Ensure all checkboxes start unchecked
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
helpers.triggerEvent(checkbox, 'change');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click folder (index 0)
|
||||||
|
checkboxes[0].checked = true;
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'change');
|
||||||
|
|
||||||
|
visibleCounterText = getVisibleCounter().textContent.trim();
|
||||||
|
|
||||||
|
// Application counts the content within the folder
|
||||||
|
expect(visibleCounterText).toEqual('1 template, 1 folder selected');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -110,4 +110,22 @@ describe("Form Validation", () => {
|
|||||||
expect(errorElement.style.display).toBe("none");
|
expect(errorElement.style.display).toBe("none");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Uses 'This field' as label when no label element exists", async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<form data-force-focus="True">
|
||||||
|
<input id="unlabeled-input" name="unlabeled" type="text" />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
form = document.querySelector('form[data-force-focus="True"]');
|
||||||
|
attachValidation();
|
||||||
|
|
||||||
|
form.dispatchEvent(new Event("submit", { bubbles: true }));
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||||||
|
|
||||||
|
const errorMessage = document.getElementById("unlabeled-input-error");
|
||||||
|
expect(errorMessage.textContent).toBe("Error: This field is required.");
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
162
webpack.config.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: isProduction ? 'production' : 'development',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
all: path.resolve(__dirname, 'app/assets/javascripts/index.js'),
|
||||||
|
styles: path.resolve(__dirname, 'app/assets/sass/uswds/styles.scss')
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'app/static/javascripts'),
|
||||||
|
filename: '[name].js',
|
||||||
|
clean: true
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
url: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [require('autoprefixer')]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sassOptions: {
|
||||||
|
loadPaths: [
|
||||||
|
path.resolve(__dirname, 'app/assets/sass/uswds'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@uswds'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@uswds/uswds/packages'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/scss/stylesheets/packages')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js'],
|
||||||
|
alias: {
|
||||||
|
'@javascripts': path.resolve(__dirname, 'app/assets/javascripts'),
|
||||||
|
'@modules': path.resolve(__dirname, 'app/assets/javascripts/modules')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
$: 'jquery',
|
||||||
|
jQuery: 'jquery',
|
||||||
|
'window.jQuery': 'jquery'
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '../css/styles.css'
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'app/assets/images'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/images'),
|
||||||
|
noErrorOnMissing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'app/assets/pdf'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/pdf'),
|
||||||
|
noErrorOnMissing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'app/assets/js/gtm_head.js'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/js/gtm_head.js'),
|
||||||
|
noErrorOnMissing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'app/assets/js/setTimezone.js'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/js/setTimezone.js'),
|
||||||
|
noErrorOnMissing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/js/uswds.min.js'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/js/uswds.min.js')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/fonts'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/fonts')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/img'),
|
||||||
|
to: path.resolve(__dirname, 'app/static/img')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimize: isProduction,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: isProduction,
|
||||||
|
drop_debugger: true,
|
||||||
|
pure_funcs: isProduction ? ['console.info', 'console.debug', 'console.warn', 'console.error'] : [],
|
||||||
|
},
|
||||||
|
mangle: true,
|
||||||
|
format: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extractComments: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
splitChunks: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
devtool: isProduction ? false : 'source-map',
|
||||||
|
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false
|
||||||
|
},
|
||||||
|
|
||||||
|
performance: {
|
||||||
|
hints: isProduction ? 'warning' : false,
|
||||||
|
maxEntrypointSize: 512000,
|
||||||
|
maxAssetSize: 512000,
|
||||||
|
}
|
||||||
|
};
|
||||||