From 754638c664fce43e18caef59799eed65cc45b9cf Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Mon, 18 Mar 2024 17:46:47 +0000 Subject: [PATCH 01/10] addition of react notification banner --- .../src/main/resources/application.conf | 59 ++++ kahuna/app/controllers/KahunaController.scala | 7 + kahuna/app/lib/KahunaConfig.scala | 2 + kahuna/app/views/main.scala.html | 3 + kahuna/conf/routes | 1 + .../gr-notifications-banner.css | 83 ++++++ .../gr-notifications-banner.tsx | 270 ++++++++++++++++++ kahuna/public/js/main.js | 3 + .../js/notifications/notifications.html | 3 + .../public/js/notifications/notifications.js | 43 +++ kahuna/public/js/search/index.js | 1 + kahuna/public/js/window.ts | 5 +- kahuna/public/stylesheets/main.css | 13 + 13 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css create mode 100644 kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx create mode 100644 kahuna/public/js/notifications/notifications.html create mode 100644 kahuna/public/js/notifications/notifications.js diff --git a/common-lib/src/main/resources/application.conf b/common-lib/src/main/resources/application.conf index a94c989b9e..89be25aaac 100644 --- a/common-lib/src/main/resources/application.conf +++ b/common-lib/src/main/resources/application.conf @@ -104,6 +104,65 @@ usageRightsConfigProvider = { } } +# ------------------------------------------------------------- +# Announcements - notifications to be seen by users +# Format: +# [ (array) +# { (json object) +# announceId: (string) the unique id of the announcement - should be unique among all active announcements +# description: (string) the main text to display in the notification notification_banner +# endDate: (string, optional, format="yyyy-mm-dd") the date beyond which the announcement should not be seen, if not present set as today + 1 year +# url: (string, optional) a link to a page/document providing further details regarding announcement +# urlText: (string, optional) text to be included in a-tag hyperlink (will revert to default if not present) +# category: (string) the type of announcement - will control styling and display, Enum=announcement, information, warning, error, success +# lifespan: (string) the lifecycle behaviour Enum=transient (message disappears on any click etc), +# session (message must be acknowledged but action NOT stored in client cookie - used for current session messages) +# persistent (message must be acknowledged and action stored in client cookie - used for long running announcements) +# }, +# ... +# ] +# ----------------------------------------------------------------- +announcements = [ + { + announceId: "notification_banner", + description: "A New Feature Notification Banner", + endDate: "2025-03-31", + url: "https://runacharainn.com", + urlText: "Runach Arainn Glamping", + category: "announcement", + lifespan: "persistent" + }, + { + announceId: "sort_control", + description: "Sort Control", + endDate: "2025-04-01", + url: "https://www.bbc.co.uk", + category: "warning", + lifespan: "transient" + }, + { + announceId: "success_ex", + description: "This is an example of a success message", + endDate: "2025-04-01", + category: "success", + lifespan: "transient" + }, + { + announceId: "error_msg", + description: "This is an example of a error message. Please click to acknowledge.", + endDate: "2025-04-01", + category: "error", + lifespan: "session" + }, + { + announceId: "old_message", + description: "This is an old message", + endDate: "2024-01-01", + category: "success", + lifespan: "persistent" + } +] + domainMetadata.specifications = [] metadata.templates = [] diff --git a/kahuna/app/controllers/KahunaController.scala b/kahuna/app/controllers/KahunaController.scala index cc57a69d80..a9d447ae16 100644 --- a/kahuna/app/controllers/KahunaController.scala +++ b/kahuna/app/controllers/KahunaController.scala @@ -53,6 +53,7 @@ class KahunaController( val domainMetadataSpecs: String = Json.toJson(config.domainMetadataSpecs).toString() val fieldAliases: String = Json.toJson(config.fieldAliasConfigs).toString() val metadataTemplates: String = Json.toJson(config.metadataTemplates).toString() + val announcements: String = Json.toJson(config.announcements).toString() val returnUri = config.rootUri + okPath val costFilterLabel = config.costFilterLabel.getOrElse("Free to use only") val costFilterChargeable = config.costFilterChargeable.getOrElse(false) @@ -68,6 +69,7 @@ class KahunaController( scriptsToLoad, domainMetadataSpecs, metadataTemplates, + announcements, additionalNavigationLinks, costFilterLabel, costFilterChargeable, @@ -81,6 +83,11 @@ class KahunaController( Ok(views.html.quotas(config.mediaApiUri)) } + def notifications = authentication { req => + val announcements: String = Json.toJson(config.announcements).toString() + Ok(announcements) + } + def ok = Action { implicit request => Ok("ok") } diff --git a/kahuna/app/lib/KahunaConfig.scala b/kahuna/app/lib/KahunaConfig.scala index a9006ef139..4e127332b7 100644 --- a/kahuna/app/lib/KahunaConfig.scala +++ b/kahuna/app/lib/KahunaConfig.scala @@ -59,6 +59,8 @@ class KahunaConfig(resources: GridConfigResources) extends CommonConfig(resource val metadataTemplates: Seq[MetadataTemplate] = configuration.get[Seq[MetadataTemplate]]("metadata.templates") + val announcements: Seq[Announcement] = configuration.getOptional[Seq[Announcement]]("announcements").getOrElse(Seq.empty) + //BBC custom warning text val warningTextHeader: String = configuration.getOptional[String]("warningText.header") .getOrElse("This image can be used, but has warnings:") diff --git a/kahuna/app/views/main.scala.html b/kahuna/app/views/main.scala.html index 4a091f9ea8..dfcfec13a9 100644 --- a/kahuna/app/views/main.scala.html +++ b/kahuna/app/views/main.scala.html @@ -7,6 +7,7 @@ scriptsToLoad: List[ScriptToLoad], domainMetadataSpecs: String, metadataTemplates: String, + announcements: String, additionalNavigationLinks: String, costFilterLabel: String, costFilterChargeable: Boolean, @@ -77,6 +78,7 @@ telemetryUri: '@kahunaConfig.telemetryUri.getOrElse("")', defaultShouldBlurGraphicImages: @kahunaConfig.defaultShouldBlurGraphicImages, shouldUploadStraightToBucket: @kahunaConfig.shouldUploadStraightToBucket, + announcements: @Html(announcements), } @@ -86,6 +88,7 @@
+ diff --git a/kahuna/conf/routes b/kahuna/conf/routes index 3de2853014..b8ad420d3c 100644 --- a/kahuna/conf/routes +++ b/kahuna/conf/routes @@ -8,6 +8,7 @@ GET /images/$id<[0-9a-z]+>/crop controllers.KahunaControll GET /search controllers.KahunaController.index(ignored="") GET /upload controllers.KahunaController.index(ignored="") GET /quotas controllers.KahunaController.quotas +GET /notifications controllers.KahunaController.notifications # Empty page to return to the same domain as the app GET /ok controllers.KahunaController.ok diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css new file mode 100644 index 0000000000..33468fda46 --- /dev/null +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css @@ -0,0 +1,83 @@ +.outer-notifications { + width: 100%; +} + +.notification-container { + width: 100%; + display: flex; + border-bottom: 1px solid black; +} + +.notification-start { + flex: 0 0 initial; + display: flex; + align-items: center; + margin-top: 6px; + padding: 8px 16px 8px 24px; + justify-content: center; +} + +.notification-start-icon { + text-align: center; +} + +.notification { + flex: 1; + display: flex; + align-items: center; + padding: 8px 0px 8px 0px; + min-height: 28px; +} + +.notification-end { + flex: 0 0 initial; + display: flex; + align-items: center; + padding: 8px 24px 8px 24px; +} + +.notification-url { + color: black; + text-decoration: underline; +} + +.notification-button { + width: 80px; + padding: 8px 0 0 0; + text-align: center; +} + +.notification-button:hover { + background: grey; + cursor: pointer; +} + +.notification-announcement { + background: #A8CFFF; + color: black; + stroke: #A8CFFF; +} + +.notification-information { + background: #A8CFFF; + color: black; + stroke: #A8CFFF; +} + +.notification-warning { + background: #FEDB8B; + color: black; + stroke: #FEDB8B; +} + +.notification-error { + background: #EEBFBE; + color: black; + stroke: #EEBFBE; +} + +.notification-success { + background: #6FE290; + color: black; + stroke: black; +} diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx new file mode 100644 index 0000000000..773bed9380 --- /dev/null +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -0,0 +1,270 @@ +import * as React from "react"; +import * as angular from "angular"; +import {react2angular} from "react2angular"; +import {useEffect, useState} from "react"; + +import "./gr-notifications-banner.css"; + +const NOTIFICATION_LABEL = "Acknowledge and Close Notification"; +const DEFAULT_URL_TEXT = "Click here for further information"; +const PERSISTENT = "persistent"; +const TRANSIENT = "transient"; +const NOTIFICATION_COOKIE = "notification_cookie"; +const cookie_age = 31536000; +const checkNotificationsUri = window._clientConfig.rootUri + "/notifications"; +const checkNotificationsInterval = 60000; // in ms + +const tickIcon = () => + + + ; + +const emptyIcon = () => + + + ; + +const triangleIcon = () => + + + + + + + + + + +const crossIcon = () => + + + + + + +const circleIcon = () => + + + + + + + + + +export interface Notification { + announceId: string, + description: string, + endDate: string, + url: string, + urlText: string, + category: string, + lifespan: string +} + +export interface NotificationBannerProps { + heading: string; +} + +const NotificationsBanner: React.FC = ({heading}) => { + const bannerHeading = heading; + const initNotifications: Notification[] = []; + const [notifications, setNotifications] = useState(initNotifications); + let checkNotificationsRef: any; + + const todayStr = (): string => { + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth() + 1; + const day = today.getDate(); + const mthStr = (month < 10) ? `0${month}` : `${month}`; + const dayStr = (day < 10) ? `0${day}` : `${day}`; + return (`${year}-${mthStr}-${dayStr}`); + } + + const getCookie = (cookieName: string): string => { + const name = cookieName + "="; + const decodedCookie = decodeURIComponent(document.cookie); + const cookieArray = decodedCookie.split(';'); + + for (let i = 0; i < cookieArray.length; i++) { + let cookie = cookieArray[i]; + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1); + } + if (cookie.indexOf(name) === 0) { + return cookie.substring(name.length, cookie.length); + } + } + + return null; // Return null if the cookie is not found + } + + const mergeArraysByKey = (array1: Notification[], array2: Notification[], key: keyof Notification): Notification[] => { + const merged = new Map(); + const addOrUpdate = (item: Notification) => { + merged.set(item[key], item); + }; + + array1.forEach(addOrUpdate); + array2.forEach(addOrUpdate); + return Array.from(merged.values()) + } + + const autoHideListener = (event: any) => { + if (event.type === "keydown" && event.key === "Escape") { + setNotifications(prevNotifs => prevNotifs.filter(n => n.lifespan !== TRANSIENT)); + } else if (event.type !== "keydown") { + if (event.target.className !== "notification-url") { + setNotifications(prevNotifs => prevNotifs.filter(n => n.lifespan !== TRANSIENT)); + } + } + } + + const checkNotifications = () => { + fetch(checkNotificationsUri) + .then(response => { + if (!response.ok) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(data => { + const announce: Notification[] = data; + const tdy = todayStr(); + let notif_cookie = getCookie(NOTIFICATION_COOKIE); + if (!notif_cookie) { + notif_cookie = ""; + } + const current_notifs = announce.filter(ann => ann.endDate > tdy) + .filter(ann => !notif_cookie.includes(ann.announceId)); + + setNotifications(prev_notifs => mergeArraysByKey(prev_notifs, current_notifs, 'announceId')); + }) + .catch(error => { + console.error('There was a problem checking for Notifications:', error); + }); + } + + useEffect(() => { + const announce = window._clientConfig.announcements; + const checkUrl = window._clientConfig.telemetryUri; + const tdy = todayStr(); + let notif_cookie = getCookie(NOTIFICATION_COOKIE); + if (!notif_cookie) { + notif_cookie = ""; + } + const current_notifs = announce.filter(ann => ann.endDate > tdy) + .filter(ann => !notif_cookie.includes(ann.announceId)); + + setNotifications(current_notifs); + + // trigger server call to check notifications + checkNotificationsRef = setInterval(checkNotifications, checkNotificationsInterval); + + document.addEventListener("mouseup", autoHideListener); + document.addEventListener("scroll", autoHideListener); + document.addEventListener("keydown", autoHideListener); + + // clean up cookie + if (notif_cookie) { + const current_notif_ids = announce.map(ann => ann.announceId).join(","); + const notif_ids = notif_cookie.split(','); + const new_notif_ids = notif_ids.filter(n_id => current_notif_ids.includes(n_id)).join(","); + document.cookie = (`${NOTIFICATION_COOKIE}=${new_notif_ids}; max-age=${cookie_age}`); + } + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("mouseup", autoHideListener); + window.removeEventListener("scroll", autoHideListener); + window.removeEventListener("keydown", autoHideListener); + clearInterval(checkNotificationsRef); + }; + + }, []); + + const getIcon = (notification: Notification): JSX.Element => { + switch (notification.category) { + case "success": + return tickIcon(); + case "error": + case "warning": + case "information": + return triangleIcon(); + case "announcement": + return circleIcon(); + default: + return emptyIcon(); + } + } + + const handleNotificationClick = (notif: Notification) => { + const ns = notifications.filter(n => n.announceId !== notif.announceId); + + // persistent management + if (notif.lifespan == PERSISTENT) { + const current_cookie = getCookie(NOTIFICATION_COOKIE); + let new_cookie = notif.announceId; + if (current_cookie) { + new_cookie = current_cookie + "," + notif.announceId; + } + document.cookie = `${NOTIFICATION_COOKIE}=${new_cookie}; max-age=${cookie_age}`; + } + + setNotifications(ns); + } + + return ( +
+ {notifications.map((notification) => ( +
+
+
+ {getIcon(notification)} +
+
+
+
+ + {notification.description}  + + {(notification.url != "") && + +   + + {notification.urlText ? notification.urlText : DEFAULT_URL_TEXT} + + + } +
+
+
+
handleNotificationClick(notification)}> + {crossIcon()} +
+
+
+ ))} +
+ ); +}; + +export const notificationsBanner = angular.module('gr.notificationsBanner', []) + .component('notificationsBanner', react2angular(NotificationsBanner, ["heading"])); diff --git a/kahuna/public/js/main.js b/kahuna/public/js/main.js index d6765027a4..f64adee360 100644 --- a/kahuna/public/js/main.js +++ b/kahuna/public/js/main.js @@ -27,6 +27,8 @@ import {userActions} from './common/user-actions'; import {httpErrors} from './errors/http'; import {globalErrors} from './errors/global'; +import {notifications} from './notifications/notifications'; + import {icon} from './components/gr-icon/gr-icon'; import {tooltip} from './components/gr-tooltip/gr-tooltip'; @@ -80,6 +82,7 @@ var kahuna = angular.module('kahuna', [ userActions.name, httpErrors.name, globalErrors.name, + notifications.name, // directives used throughout imageFade.name, diff --git a/kahuna/public/js/notifications/notifications.html b/kahuna/public/js/notifications/notifications.html new file mode 100644 index 0000000000..c8dc15148b --- /dev/null +++ b/kahuna/public/js/notifications/notifications.html @@ -0,0 +1,3 @@ + + + diff --git a/kahuna/public/js/notifications/notifications.js b/kahuna/public/js/notifications/notifications.js new file mode 100644 index 0000000000..7a78661f71 --- /dev/null +++ b/kahuna/public/js/notifications/notifications.js @@ -0,0 +1,43 @@ +import angular from 'angular'; +import template from './notifications.html'; +import '../components/gr-notifications-banner/gr-notifications-banner'; + +import 'angular-messages'; +import 'pandular'; +import '../sentry/sentry'; + +export var notifications = angular.module( + 'kahuna.notifications', + ['ngMessages', 'pandular.session', 'gr.notificationsBanner'] +); + +notifications.controller('NotificationsCtrl', + ['$location', '$scope', + function ($location, $scope) { + + var ctrl = this; + + ctrl.$onInit = () => { + ctrl.notifications = window._clientConfig.announcements; + ctrl.hasNotifications = ctrl.notifications.length > 0; + + // handy as these can happen anywhere + ctrl.getCurrentLocation = () => $location.url(); + + ctrl.dismiss = (notification) => { + // remove notification + } + + }; + } +]); + +notifications.directive('uiNotifications', [function() { + return { + restrict: 'E', + controller: 'NotificationsCtrl', + controllerAs: 'ctrl', + bindToController: true, + template: template + }; +}]); diff --git a/kahuna/public/js/search/index.js b/kahuna/public/js/search/index.js index 4dcbb2484a..0a6782c0ef 100644 --- a/kahuna/public/js/search/index.js +++ b/kahuna/public/js/search/index.js @@ -85,6 +85,7 @@ search.config(['$stateProvider', '$urlMatcherFactoryProvider', const ctrl = this; ctrl.canUpload = false; + ctrl.hasNotifications = window._clientConfig.announcements.length > 0; mediaApi.canUserUpload().then(canUpload => { ctrl.canUpload = canUpload; diff --git a/kahuna/public/js/window.ts b/kahuna/public/js/window.ts index c0a40de958..f015022300 100644 --- a/kahuna/public/js/window.ts +++ b/kahuna/public/js/window.ts @@ -1,11 +1,14 @@ import { FeatureSwitchData } from "./components/gr-feature-switch-panel/gr-feature-switch-panel"; +import { Notification } from "./components/gr-notifications-banner/gr-notifications-banner"; declare global { interface Window { _clientConfig: { + rootUri: string; telemetryUri: string; - featureSwitches: Array + featureSwitches: Array; maybeOrgOwnedValue: string | undefined; + announcements: Array; } } } diff --git a/kahuna/public/stylesheets/main.css b/kahuna/public/stylesheets/main.css index da1cd0c2d5..836f7a9902 100644 --- a/kahuna/public/stylesheets/main.css +++ b/kahuna/public/stylesheets/main.css @@ -804,6 +804,19 @@ textarea.ng-invalid { padding-top: 10px; } +/* ================================ + Notifications + ================================ */ +.global-notifications { + position: fixed; + margin: 0 auto; + left: 0; + right: 0; + top: 88px; + z-index: 40; + text-align: left; + max-width: 100%; +} /* ========================================================================== Errors / status From 557a05d6be3b5c1784699d053b51e2d592198dd1 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Mon, 18 Mar 2024 17:57:55 +0000 Subject: [PATCH 02/10] adding announcement config class --- kahuna/app/lib/AnnouncementsConfig.scala | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 kahuna/app/lib/AnnouncementsConfig.scala diff --git a/kahuna/app/lib/AnnouncementsConfig.scala b/kahuna/app/lib/AnnouncementsConfig.scala new file mode 100644 index 0000000000..7c170bc0dd --- /dev/null +++ b/kahuna/app/lib/AnnouncementsConfig.scala @@ -0,0 +1,51 @@ +package lib + +import play.api.ConfigLoader +import play.api.libs.json._ +import scala.collection.JavaConverters._ +import java.time.{LocalDate, Period} + +case class Announcement( + announceId: String, + description: String, + endDate: LocalDate, + url: String, + urlText: String, + category: String, + lifespan: String +) + +object Announcement { + + implicit val writes: Writes[Announcement] = Json.writes[Announcement] + + implicit val configLoader: ConfigLoader[Seq[Announcement]] = { + ConfigLoader(_.getConfigList).map( + _.asScala.map(config => { + + val endDate = if (config.hasPath("endDate")) { + LocalDate.parse(config.getString("endDate")) + } else { + LocalDate.now().plus(Period.ofYears(1)) + } + + val announceUrl = if (config.hasPath("url")) { + config.getString("url") + } else "" + + val urlText = if (config.hasPath("urlText")) { + config.getString("urlText") + } else "" + + Announcement(config.getString("announceId"), + config.getString("description"), + endDate, + announceUrl, + urlText, + config.getString("category"), + config.getString("lifespan") + ) + })) + } + +} From 08b30eb8b205d71224c22eb2a18bb8471bdac857 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 19 Mar 2024 09:58:42 +0000 Subject: [PATCH 03/10] correcting js errors --- .../gr-notifications-banner.tsx | 26 +++++++++---------- .../public/js/notifications/notifications.js | 15 +++-------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx index 773bed9380..7a82a088f5 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -35,7 +35,7 @@ const triangleIcon = () => - + ; const crossIcon = () => @@ -44,7 +44,7 @@ const crossIcon = () => - + ; const circleIcon = () => @@ -56,7 +56,7 @@ const circleIcon = () => - + ; export interface Notification { announceId: string, @@ -76,7 +76,7 @@ const NotificationsBanner: React.FC = ({heading}) => { const bannerHeading = heading; const initNotifications: Notification[] = []; const [notifications, setNotifications] = useState(initNotifications); - let checkNotificationsRef: any; + let checkNotificationsRef: NodeJS.Timer; const todayStr = (): string => { const today = new Date(); @@ -86,7 +86,7 @@ const NotificationsBanner: React.FC = ({heading}) => { const mthStr = (month < 10) ? `0${month}` : `${month}`; const dayStr = (day < 10) ? `0${day}` : `${day}`; return (`${year}-${mthStr}-${dayStr}`); - } + }; const getCookie = (cookieName: string): string => { const name = cookieName + "="; @@ -104,7 +104,7 @@ const NotificationsBanner: React.FC = ({heading}) => { } return null; // Return null if the cookie is not found - } + }; const mergeArraysByKey = (array1: Notification[], array2: Notification[], key: keyof Notification): Notification[] => { const merged = new Map(); @@ -114,8 +114,8 @@ const NotificationsBanner: React.FC = ({heading}) => { array1.forEach(addOrUpdate); array2.forEach(addOrUpdate); - return Array.from(merged.values()) - } + return Array.from(merged.values()); + }; const autoHideListener = (event: any) => { if (event.type === "keydown" && event.key === "Escape") { @@ -125,7 +125,7 @@ const NotificationsBanner: React.FC = ({heading}) => { setNotifications(prevNotifs => prevNotifs.filter(n => n.lifespan !== TRANSIENT)); } } - } + }; const checkNotifications = () => { fetch(checkNotificationsUri) @@ -150,7 +150,7 @@ const NotificationsBanner: React.FC = ({heading}) => { .catch(error => { console.error('There was a problem checking for Notifications:', error); }); - } + }; useEffect(() => { const announce = window._clientConfig.announcements; @@ -203,7 +203,7 @@ const NotificationsBanner: React.FC = ({heading}) => { default: return emptyIcon(); } - } + }; const handleNotificationClick = (notif: Notification) => { const ns = notifications.filter(n => n.announceId !== notif.announceId); @@ -219,7 +219,7 @@ const NotificationsBanner: React.FC = ({heading}) => { } setNotifications(ns); - } + }; return (
@@ -243,7 +243,7 @@ const NotificationsBanner: React.FC = ({heading}) => { role="link" aria-label={DEFAULT_URL_TEXT}>   - + {notification.urlText ? notification.urlText : DEFAULT_URL_TEXT} diff --git a/kahuna/public/js/notifications/notifications.js b/kahuna/public/js/notifications/notifications.js index 7a78661f71..a61f0d1a44 100644 --- a/kahuna/public/js/notifications/notifications.js +++ b/kahuna/public/js/notifications/notifications.js @@ -12,22 +12,13 @@ export var notifications = angular.module( ); notifications.controller('NotificationsCtrl', - ['$location', '$scope', - function ($location, $scope) { - - var ctrl = this; + ['$location', + function ($location) { + const ctrl = this; ctrl.$onInit = () => { ctrl.notifications = window._clientConfig.announcements; ctrl.hasNotifications = ctrl.notifications.length > 0; - - // handy as these can happen anywhere - ctrl.getCurrentLocation = () => $location.url(); - - ctrl.dismiss = (notification) => { - // remove notification - } - }; } ]); From ecd9144926adf313464bfa9f72a867ce135c1575 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 19 Mar 2024 10:04:08 +0000 Subject: [PATCH 04/10] correcting js errors --- kahuna/public/js/notifications/notifications.js | 1 + 1 file changed, 1 insertion(+) diff --git a/kahuna/public/js/notifications/notifications.js b/kahuna/public/js/notifications/notifications.js index a61f0d1a44..4d607d52cf 100644 --- a/kahuna/public/js/notifications/notifications.js +++ b/kahuna/public/js/notifications/notifications.js @@ -17,6 +17,7 @@ notifications.controller('NotificationsCtrl', const ctrl = this; ctrl.$onInit = () => { + ctrl.getCurrentLocation = () => $location.url(); ctrl.notifications = window._clientConfig.announcements; ctrl.hasNotifications = ctrl.notifications.length > 0; }; From 8816fd3ea83eebcbfe0c0198a44ba64d0cce50e4 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 19 Mar 2024 10:09:04 +0000 Subject: [PATCH 05/10] correcting html errors --- kahuna/public/js/notifications/notifications.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kahuna/public/js/notifications/notifications.html b/kahuna/public/js/notifications/notifications.html index c8dc15148b..f98b76a83f 100644 --- a/kahuna/public/js/notifications/notifications.html +++ b/kahuna/public/js/notifications/notifications.html @@ -1,3 +1,3 @@ - - + + From 62a7ce07333930a41975e86a59bcbe9afcd1b1b6 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 19 Mar 2024 10:11:10 +0000 Subject: [PATCH 06/10] removing test announcements --- .../src/main/resources/application.conf | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/common-lib/src/main/resources/application.conf b/common-lib/src/main/resources/application.conf index 89be25aaac..31be9e1401 100644 --- a/common-lib/src/main/resources/application.conf +++ b/common-lib/src/main/resources/application.conf @@ -122,46 +122,7 @@ usageRightsConfigProvider = { # ... # ] # ----------------------------------------------------------------- -announcements = [ - { - announceId: "notification_banner", - description: "A New Feature Notification Banner", - endDate: "2025-03-31", - url: "https://runacharainn.com", - urlText: "Runach Arainn Glamping", - category: "announcement", - lifespan: "persistent" - }, - { - announceId: "sort_control", - description: "Sort Control", - endDate: "2025-04-01", - url: "https://www.bbc.co.uk", - category: "warning", - lifespan: "transient" - }, - { - announceId: "success_ex", - description: "This is an example of a success message", - endDate: "2025-04-01", - category: "success", - lifespan: "transient" - }, - { - announceId: "error_msg", - description: "This is an example of a error message. Please click to acknowledge.", - endDate: "2025-04-01", - category: "error", - lifespan: "session" - }, - { - announceId: "old_message", - description: "This is an old message", - endDate: "2024-01-01", - category: "success", - lifespan: "persistent" - } -] +announcements = [] domainMetadata.specifications = [] From 4655d241fc3ca982c4685315df0cf94d7511b1d0 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 19 Mar 2024 17:21:47 +0000 Subject: [PATCH 07/10] adding in limited support for medium weight fonts and other minor css changes --- .../gr-notifications-banner.css | 16 ++++++++++--- .../gr-notifications-banner.tsx | 5 ++--- .../fonts/opensans-semibold-webfont.woff2 | Bin 0 -> 18632 bytes .../opensans-semibolditalic-webfont.woff2 | Bin 0 -> 20460 bytes kahuna/public/stylesheets/main.css | 21 +++++++++++++++++- 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 kahuna/public/stylesheets/fonts/opensans-semibold-webfont.woff2 create mode 100644 kahuna/public/stylesheets/fonts/opensans-semibolditalic-webfont.woff2 diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css index 33468fda46..2ab9a364a2 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.css @@ -8,6 +8,11 @@ border-bottom: 1px solid black; } +.notification-container-last { + width: 100%; + display: flex; +} + .notification-start { flex: 0 0 initial; display: flex; @@ -38,7 +43,13 @@ .notification-url { color: black; - text-decoration: underline; + border-bottom: 1px solid black; + font-weight: 500; +} + +.notification-url:hover { + color: black; + font-weight: 500; } .notification-button { @@ -47,8 +58,7 @@ text-align: center; } -.notification-button:hover { - background: grey; +.notification-button:hover { cursor: pointer; } diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx index 7a82a088f5..c98de3c453 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -154,7 +154,6 @@ const NotificationsBanner: React.FC = ({heading}) => { useEffect(() => { const announce = window._clientConfig.announcements; - const checkUrl = window._clientConfig.telemetryUri; const tdy = todayStr(); let notif_cookie = getCookie(NOTIFICATION_COOKIE); if (!notif_cookie) { @@ -223,8 +222,8 @@ const NotificationsBanner: React.FC = ({heading}) => { return (
- {notifications.map((notification) => ( -
( +
diff --git a/kahuna/public/stylesheets/fonts/opensans-semibold-webfont.woff2 b/kahuna/public/stylesheets/fonts/opensans-semibold-webfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..85c2c419ee78355cc45c235c5bb810841ecca284 GIT binary patch literal 18632 zcmV)AK*YayPew8T0RR9107%FH5&!@I0I{q907zZ{0RR9100000000000000000000 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 Date: Tue, 19 Mar 2024 18:13:38 +0000 Subject: [PATCH 08/10] correct class assigned to timeout ref --- .../gr-notifications-banner/gr-notifications-banner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx index c98de3c453..9f1eb19b83 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -76,7 +76,7 @@ const NotificationsBanner: React.FC = ({heading}) => { const bannerHeading = heading; const initNotifications: Notification[] = []; const [notifications, setNotifications] = useState(initNotifications); - let checkNotificationsRef: NodeJS.Timer; + let checkNotificationsRef: NodeJS.Timeout; const todayStr = (): string => { const today = new Date(); From d8625edbcfc7ff62e6bbe1bf4fd1cd2e250b41f8 Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 16 Apr 2024 14:42:59 +0100 Subject: [PATCH 09/10] modifications to address issues identified by the Guardian --- kahuna/app/lib/AnnouncementsConfig.scala | 22 +++- .../gr-notifications-banner.tsx | 109 ++++++++---------- .../js/notifications/notifications.html | 2 +- 3 files changed, 65 insertions(+), 68 deletions(-) diff --git a/kahuna/app/lib/AnnouncementsConfig.scala b/kahuna/app/lib/AnnouncementsConfig.scala index 7c170bc0dd..01aacc82b9 100644 --- a/kahuna/app/lib/AnnouncementsConfig.scala +++ b/kahuna/app/lib/AnnouncementsConfig.scala @@ -3,6 +3,7 @@ package lib import play.api.ConfigLoader import play.api.libs.json._ import scala.collection.JavaConverters._ +import scala.util.{Try, Success, Failure} import java.time.{LocalDate, Period} case class Announcement( @@ -17,6 +18,9 @@ case class Announcement( object Announcement { + val announceCategory = Set("announcement", "information", "warning", "error", "success") + val announceLifespan = Set("transient", "session", "persistent") + implicit val writes: Writes[Announcement] = Json.writes[Announcement] implicit val configLoader: ConfigLoader[Seq[Announcement]] = { @@ -24,7 +28,11 @@ object Announcement { _.asScala.map(config => { val endDate = if (config.hasPath("endDate")) { - LocalDate.parse(config.getString("endDate")) + val dte = Try(LocalDate.parse(config.getString("endDate"))) + dte match { + case Success(value) => value + case Failure(_) => LocalDate.now().plus(Period.ofYears(1)) + } } else { LocalDate.now().plus(Period.ofYears(1)) } @@ -37,13 +45,21 @@ object Announcement { config.getString("urlText") } else "" + val category = if (announceCategory.contains(config.getString("category"))) { + config.getString("category") + } else "announcement" // the expected category applicationConf announcements + + val lifespan = if (announceLifespan.contains(config.getString("lifespan"))) { + config.getString("lifespan") + } else "persistent" // the expected lifespan for applicationConf announcements + Announcement(config.getString("announceId"), config.getString("description"), endDate, announceUrl, urlText, - config.getString("category"), - config.getString("lifespan") + category, + lifespan ) })) } diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx index 9f1eb19b83..db2de2eed4 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as angular from "angular"; import {react2angular} from "react2angular"; -import {useEffect, useState} from "react"; +import {useEffect, useState, useRef} from "react"; import "./gr-notifications-banner.css"; @@ -37,7 +37,6 @@ const triangleIcon = () => ; - const crossIcon = () => @@ -68,54 +67,51 @@ export interface Notification { lifespan: string } -export interface NotificationBannerProps { - heading: string; -} +const todayStr = (): string => { + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth() + 1; + const day = today.getDate(); + const mthStr = (month < 10) ? `0${month}` : `${month}`; + const dayStr = (day < 10) ? `0${day}` : `${day}`; + return (`${year}-${mthStr}-${dayStr}`); +}; -const NotificationsBanner: React.FC = ({heading}) => { - const bannerHeading = heading; - const initNotifications: Notification[] = []; - const [notifications, setNotifications] = useState(initNotifications); - let checkNotificationsRef: NodeJS.Timeout; +const getCookie = (cookieName: string): string => { + const name = cookieName + "="; + const decodedCookie = decodeURIComponent(document.cookie); + const cookieArray = decodedCookie.split(';'); + return cookieArray.find((cookie) => cookie.trim().startsWith(cookieName)); +}; - const todayStr = (): string => { - const today = new Date(); - const year = today.getFullYear(); - const month = today.getMonth() + 1; - const day = today.getDate(); - const mthStr = (month < 10) ? `0${month}` : `${month}`; - const dayStr = (day < 10) ? `0${day}` : `${day}`; - return (`${year}-${mthStr}-${dayStr}`); +const mergeArraysByKey = (array1: Notification[], array2: Notification[], key: keyof Notification): Notification[] => { + const merged = new Map(); + const addOrUpdate = (item: Notification) => { + merged.set(item[key], item); }; - const getCookie = (cookieName: string): string => { - const name = cookieName + "="; - const decodedCookie = decodeURIComponent(document.cookie); - const cookieArray = decodedCookie.split(';'); - - for (let i = 0; i < cookieArray.length; i++) { - let cookie = cookieArray[i]; - while (cookie.charAt(0) === ' ') { - cookie = cookie.substring(1); - } - if (cookie.indexOf(name) === 0) { - return cookie.substring(name.length, cookie.length); - } - } - - return null; // Return null if the cookie is not found - }; + array1.forEach(addOrUpdate); + array2.forEach(addOrUpdate); + return Array.from(merged.values()); +}; - const mergeArraysByKey = (array1: Notification[], array2: Notification[], key: keyof Notification): Notification[] => { - const merged = new Map(); - const addOrUpdate = (item: Notification) => { - merged.set(item[key], item); - }; +const getIcon = (notification: Notification): JSX.Element => { + switch (notification.category) { + case "success": + return tickIcon(); + case "error": + case "warning": + case "information": + return triangleIcon(); + case "announcement": + return circleIcon(); + default: + return emptyIcon(); + } +}; - array1.forEach(addOrUpdate); - array2.forEach(addOrUpdate); - return Array.from(merged.values()); - }; +const NotificationsBanner: React.FC = () => { + const [notifications, setNotifications] = useState([]); const autoHideListener = (event: any) => { if (event.type === "keydown" && event.key === "Escape") { @@ -165,7 +161,7 @@ const NotificationsBanner: React.FC = ({heading}) => { setNotifications(current_notifs); // trigger server call to check notifications - checkNotificationsRef = setInterval(checkNotifications, checkNotificationsInterval); + const checkNotificationsRef:NodeJS.Timeout = setInterval(checkNotifications, checkNotificationsInterval); document.addEventListener("mouseup", autoHideListener); document.addEventListener("scroll", autoHideListener); @@ -181,29 +177,14 @@ const NotificationsBanner: React.FC = ({heading}) => { // Clean up the event listener when the component unmounts return () => { - window.removeEventListener("mouseup", autoHideListener); - window.removeEventListener("scroll", autoHideListener); - window.removeEventListener("keydown", autoHideListener); + document.removeEventListener("mouseup", autoHideListener); + document.removeEventListener("scroll", autoHideListener); + document.removeEventListener("keydown", autoHideListener); clearInterval(checkNotificationsRef); }; }, []); - const getIcon = (notification: Notification): JSX.Element => { - switch (notification.category) { - case "success": - return tickIcon(); - case "error": - case "warning": - case "information": - return triangleIcon(); - case "announcement": - return circleIcon(); - default: - return emptyIcon(); - } - }; - const handleNotificationClick = (notif: Notification) => { const ns = notifications.filter(n => n.announceId !== notif.announceId); @@ -266,4 +247,4 @@ const NotificationsBanner: React.FC = ({heading}) => { }; export const notificationsBanner = angular.module('gr.notificationsBanner', []) - .component('notificationsBanner', react2angular(NotificationsBanner, ["heading"])); + .component('notificationsBanner', react2angular(NotificationsBanner)); diff --git a/kahuna/public/js/notifications/notifications.html b/kahuna/public/js/notifications/notifications.html index f98b76a83f..a76de08d96 100644 --- a/kahuna/public/js/notifications/notifications.html +++ b/kahuna/public/js/notifications/notifications.html @@ -1,3 +1,3 @@ - + From a400cccd4ad7107aea7c9e483fbbbffe748d3d5b Mon Sep 17 00:00:00 2001 From: AndyKilmory Date: Tue, 16 Apr 2024 14:55:01 +0100 Subject: [PATCH 10/10] minor ts tidy up following github checks --- .../gr-notifications-banner/gr-notifications-banner.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx index db2de2eed4..e009744a85 100644 --- a/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx +++ b/kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as angular from "angular"; import {react2angular} from "react2angular"; -import {useEffect, useState, useRef} from "react"; +import {useEffect, useState} from "react"; import "./gr-notifications-banner.css"; @@ -78,7 +78,6 @@ const todayStr = (): string => { }; const getCookie = (cookieName: string): string => { - const name = cookieName + "="; const decodedCookie = decodeURIComponent(document.cookie); const cookieArray = decodedCookie.split(';'); return cookieArray.find((cookie) => cookie.trim().startsWith(cookieName));