diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml
index 2f9458acc..9a971a597 100644
--- a/core/datamodel.core.xml
+++ b/core/datamodel.core.xml
@@ -390,7 +390,18 @@
false
-
+
+
+
+ -
+ 10
+
+ -
+ 20
+
+
+
+
diff --git a/css/backoffice/_shame.scss b/css/backoffice/_shame.scss
index 293b4fcf5..71b25c5af 100644
--- a/css/backoffice/_shame.scss
+++ b/css/backoffice/_shame.scss
@@ -49,3 +49,63 @@
.ibo-navigation-menu.ibo-is-active .ibo-navigation-menu--drawer{
transform: translate3d(0,0,0);
}
+
+// Toggler legacy CSS that has somehow been added to iTop 3.0 and that is now used by some extensions
+// Round Toggle
+/* The switch - the box around the slider */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 36px;
+ height: 20px;
+ vertical-align: baseline;
+}
+
+/* Hide default HTML checkbox */
+.switch input {
+ display: none;
+}
+
+/* The slider */
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: $ibo-color-secondary-600;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 15px;
+ width: 15px;
+ left: 3px;
+ bottom: 3px;
+ background-color: $ibo-color-secondary-300;
+ transition: .4s;
+}
+
+input:checked + .slider {
+ background-color: $ibo-color-primary-600;
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 1px $ibo-color-primary-600;
+}
+
+input:checked + .slider:before {
+ transform: translateX(14.5px);
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: 20px;
+}
+
+.slider.round:before {
+ border-radius: 7px;
+}
diff --git a/css/backoffice/components/_panel.scss b/css/backoffice/components/_panel.scss
index 2e5b5e50f..ff011538b 100644
--- a/css/backoffice/components/_panel.scss
+++ b/css/backoffice/components/_panel.scss
@@ -85,6 +85,9 @@ $ibo-panel--collapsible-toggler--margin-right: $ibo-spacing-300 !default;
$ibo-panel--collapsible-toggler--font-size: $ibo-font-size-250 !default;
$ibo-panel--collapsible-toggler--color: $ibo-color-grey-700 !default;
+$ibo-panel--is-selectable--body--after--z-index: $ibo-panel--header--z-index + 1 !default;
+$ibo-panel--is-selectable--body--after--font-size: $ibo-font-size-700 !default;
+
/* Rules */
.ibo-panel {
--ibo-main-color: #{map-get($ibo-panel-colors, 'neutral')}; /* --ibo-main-color is to allow overload from custom dynamic value from the DM. The overload will be done through an additional CSS class of a particular DM class or DM attribute */
@@ -128,6 +131,30 @@ $ibo-panel--collapsible-toggler--color: $ibo-color-grey-700 !default;
}
}
}
+ &.ibo-is-selectable .ibo-panel--body::after {
+ @include ibo-selectable;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ height: 100%;
+ width: 100%;
+ z-index: $ibo-panel--is-selectable--body--after--z-index;
+ font-size: $ibo-panel--is-selectable--body--after--font-size;
+ }
+ &.ibo-is-selectable:hover .ibo-panel--body::after {
+ @include ibo-selectable-hover;
+ display: flex;
+ }
+ &.ibo-is-selected .ibo-panel--body::after {
+ @include ibo-selected;
+ display: flex;
+ }
+ &.ibo-is-selected:hover .ibo-panel--body::after {
+ @include ibo-selected-hover;
+ display: flex;
+ }
}
.ibo-panel--header {
diff --git a/css/backoffice/components/input/_all.scss b/css/backoffice/components/input/_all.scss
index bed09f767..f4b720fd1 100644
--- a/css/backoffice/components/input/_all.scss
+++ b/css/backoffice/components/input/_all.scss
@@ -16,3 +16,4 @@
@import "input-one-way-password";
@import "input-set";
@import "input-text";
+@import "input-toggler";
diff --git a/css/backoffice/components/input/_input-toggler.scss b/css/backoffice/components/input/_input-toggler.scss
new file mode 100644
index 000000000..f16adc46d
--- /dev/null
+++ b/css/backoffice/components/input/_input-toggler.scss
@@ -0,0 +1,72 @@
+/*
+ * @copyright Copyright (C) 2010-2024 Combodo SARL
+ * @license http://opensource.org/licenses/AGPL-3.0
+ */
+
+$ibo-toggler--wrapper--width: 36px !default;
+$ibo-toggler--wrapper--height: 20px !default;
+
+$ibo-toggler--slider--border-radius: $ibo-border-radius-900 !default;
+$ibo-toggler--slider--background-color: $ibo-color-secondary-600 !default;
+
+$ibo-toggler--slider--before--height: 15px !default;
+$ibo-toggler--slider--before--width: 15px !default;
+$ibo-toggler--slider--before--border-radius: $ibo-border-radius-full !default;
+$ibo-toggler--slider--before--background-color: $ibo-color-grey-100 !default;
+
+$ibo-toggler--slider--checked--background-color: $ibo-color-primary-600 !default;
+$ibo-toggler--slider--focus--box-shadow: 0 0 1px $ibo-color-primary-600 !default;
+
+$ibo-toggler--label--margin-left: 4px !default;
+
+
+.ibo-toggler--wrapper {
+ position: relative;
+ display: inline-block;
+ width: $ibo-toggler--wrapper--width;
+ height: $ibo-toggler--wrapper--height;
+ vertical-align: baseline;
+ .ibo-toggler {
+ display: none;
+ }
+}
+
+.ibo-toggler--slider{
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: $ibo-toggler--slider--border-radius;
+ background-color: $ibo-toggler--slider--background-color;
+ transition: .4s;
+}
+
+.ibo-toggler--slider:before {
+ content: "";
+ position: absolute;
+ left: 3px;
+ bottom: 3px;
+ height: $ibo-toggler--slider--before--height;
+ width: $ibo-toggler--slider--before--width;
+ border-radius: $ibo-toggler--slider--before--border-radius;
+ background-color: $ibo-toggler--slider--before--background-color;
+ transition: .4s;
+}
+
+.ibo-toggler--wrapper input:checked + .ibo-toggler--slider {
+ background-color: $ibo-toggler--slider--checked--background-color;
+}
+
+input:focus + .ibo-toggler--slider {
+ box-shadow: $ibo-toggler--slider--focus--box-shadow;
+}
+
+input:checked + .ibo-toggler--slider:before {
+ transform: translateX(14.5px);
+}
+
+label ~ .ibo-toggler--wrapper {
+ margin-left: $ibo-toggler--label--margin-left;
+}
\ No newline at end of file
diff --git a/css/backoffice/layout/_top-bar.scss b/css/backoffice/layout/_top-bar.scss
index e326575df..007b54c84 100644
--- a/css/backoffice/layout/_top-bar.scss
+++ b/css/backoffice/layout/_top-bar.scss
@@ -62,63 +62,4 @@ $ibo-top-bar--toolbar-dashboard-title--max-width: 350px !default;
@extend %ibo-full-height-content;
display: flex;
align-items: center;
-}
-
-// Round Toggle
-/* The switch - the box around the slider */
-.switch {
- position: relative;
- display: inline-block;
- width: 36px;
- height: 20px;
- vertical-align: baseline;
-}
-
-/* Hide default HTML checkbox */
-.switch input {
- display: none;
-}
-
-/* The slider */
-.slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: $ibo-color-secondary-600;
- transition: .4s;
-}
-
-.slider:before {
- position: absolute;
- content: "";
- height: 15px;
- width: 15px;
- left: 3px;
- bottom: 3px;
- background-color: $ibo-color-secondary-300;
- transition: .4s;
-}
-
-input:checked + .slider {
- background-color: $ibo-color-primary-600;
-}
-
-input:focus + .slider {
- box-shadow: 0 0 1px $ibo-color-primary-600;
-}
-
-input:checked + .slider:before {
- transform: translateX(14.5px);
-}
-
-/* Rounded sliders */
-.slider.round {
- border-radius: 20px;
-}
-
-.slider.round:before {
- border-radius: 7px;
-}
+}
\ No newline at end of file
diff --git a/css/backoffice/layout/object/_object-summary.scss b/css/backoffice/layout/object/_object-summary.scss
index a72706c85..47e09bce8 100644
--- a/css/backoffice/layout/object/_object-summary.scss
+++ b/css/backoffice/layout/object/_object-summary.scss
@@ -3,7 +3,8 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
-$ibo-object-summary--header--margin-y: $ibo-panel--highlight--height!default;
+$ibo-object-summary--header--margin-top: $ibo-panel--highlight--height!default;
+$ibo-object-summary--header--margin-bottom: $ibo-spacing-0!default;
$ibo-object-summary--header--margin-x: $ibo-spacing-0 !default;
$ibo-object-summary--header--padding-y: $ibo-spacing-300 !default;
@@ -51,7 +52,7 @@ $ibo-object-summary--content--attributes--code--padding-right: $ibo-spacing-500
}
.ibo-object-summary--header{
- margin: $ibo-object-summary--header--margin-y $ibo-object-summary--header--margin-x;
+ margin: $ibo-object-summary--header--margin-top $ibo-object-summary--header--margin-x $ibo-object-summary--header--margin-bottom $ibo-object-summary--header--margin-x;
padding: $ibo-object-summary--header--padding-y $ibo-object-summary--header--padding-x;
background-color: $ibo-object-summary--header--background-color;
border-bottom: $ibo-object-summary--header--border;
diff --git a/css/backoffice/pages/_all.scss b/css/backoffice/pages/_all.scss
index c6bb9b859..e766b115d 100644
--- a/css/backoffice/pages/_all.scss
+++ b/css/backoffice/pages/_all.scss
@@ -16,4 +16,5 @@
@import "run-query";
@import "welcome-popup";
@import "oauth.wizard";
+@import "notifications";
@import "notifications-center";
\ No newline at end of file
diff --git a/css/backoffice/pages/_notifications.scss b/css/backoffice/pages/_notifications.scss
new file mode 100644
index 000000000..56a2f5ee3
--- /dev/null
+++ b/css/backoffice/pages/_notifications.scss
@@ -0,0 +1,66 @@
+/*
+ * @copyright Copyright (C) 2010-2024 Combodo SARL
+ * @license http://opensource.org/licenses/AGPL-3.0
+ */
+
+$ibo-notifications--view-all--container--grid-gap: $ibo-spacing-600 !default;
+$ibo-notifications--view-all--container--object-summary--panel--body--max-height: unset !default;
+
+$ibo-notifications--view-all--item--unread--highlight--background-color: $ibo-color-red-600 !default;
+$ibo-notifications--view-all--item--read--highlight--background-color: $ibo-color-grey-200 !default;
+
+$ibo-notifications--view-all--container--large--grid-template-columns: repeat(3, 1fr) !default;
+$ibo-notifications--view-all--container--medium--grid-template-columns: repeat(2, 1fr) !default;
+$ibo-notifications--view-all--container--small--grid-template-columns: repeat(1, 1fr) !default;
+
+$ibo-notifications--view-all--empty--margin-top: $ibo-spacing-950 !default;
+$ibo-notifications--view-all--empty--svg--max-width: 30% !default;
+
+.ibo-notifications--view-all--container{
+ display: grid;
+ grid-gap: $ibo-notifications--view-all--container--grid-gap;
+ .ibo-object-summary .ibo-panel--title{
+ font-size: $ibo-font-size-250;
+ }
+ .ibo-object-summary > .ibo-panel--body{
+ box-shadow: none;
+ max-height: $ibo-notifications--view-all--container--object-summary--panel--body--max-height;
+ }
+ .ibo-object-summary + .ibo-object-summary{
+ margin-top: 0;
+ }
+
+ @include mobile {
+ grid-template-columns: $ibo-notifications--view-all--container--small--grid-template-columns;
+ }
+ @include desktop {
+ grid-template-columns: $ibo-notifications--view-all--container--medium--grid-template-columns;
+ }
+ @include fullhd {
+ grid-template-columns: $ibo-notifications--view-all--container--large--grid-template-columns; }
+}
+.ibo-notifications--view-all--toolbar {
+ justify-content: space-between;
+}
+.ibo-notifications--view-all--toggler {
+ display: flex;
+ align-content: center;
+}
+
+.ibo-notifications--view-all--item--read .ibo-panel--body::before{
+ background-color: $ibo-notifications--view-all--item--read--highlight--background-color;
+}
+.ibo-notifications--view-all--item--unread .ibo-panel--body::before{
+ background-color: $ibo-notifications--view-all--item--unread--highlight--background-color;
+}
+
+.ibo-notifications--view-all--empty {
+ @extend %ibo-fully-centered-content;
+ flex-direction: column;
+ margin-top: $ibo-notifications--view-all--empty--margin-top;
+
+ svg {
+ max-width: $ibo-notifications--view-all--empty--svg--max-width;
+ height: auto;
+ }
+}
\ No newline at end of file
diff --git a/css/backoffice/utils/mixins/_all.scss b/css/backoffice/utils/mixins/_all.scss
index bb4c6e405..f69ac4ca4 100644
--- a/css/backoffice/utils/mixins/_all.scss
+++ b/css/backoffice/utils/mixins/_all.scss
@@ -3,4 +3,5 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
-@import "highlight";
\ No newline at end of file
+@import "highlight";
+@import "selectable";
\ No newline at end of file
diff --git a/css/backoffice/utils/mixins/_selectable.scss b/css/backoffice/utils/mixins/_selectable.scss
new file mode 100644
index 000000000..533e688e4
--- /dev/null
+++ b/css/backoffice/utils/mixins/_selectable.scss
@@ -0,0 +1,40 @@
+/*
+ * @copyright Copyright (C) 2010-2024 Combodo SARL
+ * @license http://opensource.org/licenses/AGPL-3.0
+ */
+
+$ibo-selectable--background-color: transparent !default;
+
+$ibo-selectable--hover--color: $ibo-color-grey-100 !default;
+$ibo-selectable--hover--background-color: $ibo-color-grey-600 !default;
+$ibo-selectable--hover--background-opacity: 0.6 !default;
+
+$ibo-selected--color: $ibo-color-grey-100 !default;
+$ibo-selected--background-color: $ibo-color-grey-900 !default;
+$ibo-selected--background-opacity: 0.5 !default;
+
+$ibo-selected--hover--background-color: $ibo-color-grey-700 !default;
+$ibo-selected--hover--background-opacity: 0.5 !default;
+@mixin ibo-selectable {
+ content: ' ';
+ @extend %fa-solid-base;
+ background-color: $ibo-selectable--background-color;
+ cursor: pointer;
+}
+@mixin ibo-selectable-hover {
+ @extend %fa-regular-base;
+ content: '\f058';
+ color: $ibo-selectable--hover--color;
+ background-color: transparentize($ibo-selectable--hover--background-color, $ibo-selectable--hover--background-opacity);
+}
+
+@mixin ibo-selected {
+ @extend %fa-solid-base;
+ content: '\f058';
+ color: $ibo-selected--color;
+ background-color: transparentize($ibo-selected--background-color, $ibo-selected--background-opacity);
+}
+
+@mixin ibo-selected-hover {
+ background-color: transparentize($ibo-selected--hover--background-color, $ibo-selected--hover--background-opacity);
+}
\ No newline at end of file
diff --git a/css/backoffice/utils/variables/_spacing.scss b/css/backoffice/utils/variables/_spacing.scss
index 04fc1b977..2f64e4c64 100644
--- a/css/backoffice/utils/variables/_spacing.scss
+++ b/css/backoffice/utils/variables/_spacing.scss
@@ -13,6 +13,7 @@ $ibo-spacing-600: $ibo-size-300 !default;
$ibo-spacing-700: $ibo-size-350 !default;
$ibo-spacing-800: $ibo-size-400 !default;
$ibo-spacing-900: $ibo-size-450 !default;
+$ibo-spacing-950: $ibo-size-500 !default;
:root{
--ibo-spacing-0: #{$ibo-size-0};
diff --git a/css/backoffice/utils/variables/_typography.scss b/css/backoffice/utils/variables/_typography.scss
index 7b5017e16..3388a9476 100644
--- a/css/backoffice/utils/variables/_typography.scss
+++ b/css/backoffice/utils/variables/_typography.scss
@@ -15,6 +15,9 @@ $ibo-font-size-400: 2rem !default; /* 24px */
$ibo-font-size-450: 2.5rem !default; /* 30px */
$ibo-font-size-500: 3rem !default; /* 36px */
$ibo-font-size-550: 4rem !default; /* 48px */
+$ibo-font-size-600: 5rem !default; /* 60px */
+$ibo-font-size-650: 6rem !default; /* 72px */
+$ibo-font-size-700: 7rem !default; /* 84px */
/* Value Common weight name (https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight) */
$ibo-font-weight-100: 100 !default; /* 100 Thin (Harline) */
diff --git a/dictionaries/ui/application/newsroom/en.dictionary.itop.newsroom.php b/dictionaries/ui/application/newsroom/en.dictionary.itop.newsroom.php
index 80cca5d96..8b14f146e 100644
--- a/dictionaries/ui/application/newsroom/en.dictionary.itop.newsroom.php
+++ b/dictionaries/ui/application/newsroom/en.dictionary.itop.newsroom.php
@@ -1,6 +1,6 @@
ITOP_APPLICATION_SHORT,
- 'UI:Newsroom:iTopNotification:ViewAllPage:Title' => ITOP_APPLICATION_SHORT.' notifications',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Title' => 'Your ' . ITOP_APPLICATION_SHORT.' notifications',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Read:Label' => 'Read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Unread:Label' => 'Unread',
+ 'UI:Newsroom:iTopNotification:SelectMode:Label' => 'Select mode',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsRead:Label' => 'Mark all as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsUnread:Label' => 'Mark all as unread',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Label' => 'Delete all',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Success:Message' => 'All %1$s notifications have been deleted',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Confirmation:Title' => 'Delete all notifications',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Confirmation:Message' => 'Are you sure you want to delete all notifications?',
+
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Empty:Title' => 'No notification, you are up to date!',
+
+ // Actions
+ // - Unitary buttons
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Label' => 'Delete this notification',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:ViewObject:Label' => 'Go to the notification url',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Label' => 'Mark as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:Label' => 'Mark as unread',
+ // - Bulk buttons
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsRead:Label' => 'Mark selected as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsUnread:Label' => 'Mark selected as unread',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Label' => 'Delete selected',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Confirmation:Title' => 'Delete selected notifications',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Confirmation:Message' => 'Are you sure you want to delete selected notifications?',
+
+ // Feedback messages
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:InvalidAction:Message' => 'Invalid action: "%1$s"',
+ // - Mark as read
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:NoEvent:Message' => 'No notification to mark as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Success:Message' => 'The notification has been marked as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsReadMultiple:Success:Message' => '%1$s notifications have been marked as read',
+ // - Mark as unread
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:NoEvent:Message' => 'No notification to mark as read',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:Success:Message' => 'The notification has been marked as unread',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnreadMultiple:Success:Message' => '%1$s notifications have been marked as unread',
+ // Delete
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:NoEvent:Message' => 'No notification to delete',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Success:Message' => 'The notification has been deleted',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteMultiple:Success:Message' => '%1$s notifications have been deleted',
));
\ No newline at end of file
diff --git a/dictionaries/ui/application/newsroom/fr.dictionary.itop.newsroom.php b/dictionaries/ui/application/newsroom/fr.dictionary.itop.newsroom.php
new file mode 100644
index 000000000..11c624acc
--- /dev/null
+++ b/dictionaries/ui/application/newsroom/fr.dictionary.itop.newsroom.php
@@ -0,0 +1,62 @@
+ ITOP_APPLICATION_SHORT,
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Title' => 'Vos notifications ' . ITOP_APPLICATION_SHORT,
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Read:Label' => 'Lue',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Unread:Label' => 'Non lue',
+ 'UI:Newsroom:iTopNotification:SelectMode:Label' => 'Sélection multiple',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsRead:Label' => 'Marquer tout comme lu',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsUnread:Label' => 'Marquer tout comme non lu',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Label' => 'Supprimer tout',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Success:Message' => '%1$s notifications ont été supprimées',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Confirmation:Title' => 'Supprimer toutes les notifications',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteAll:Confirmation:Message' => 'Êtes-vous sûr de vouloir supprimer toutes les notifications ?',
+
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Empty:Title' => 'Aucune notification, vous êtes à jour !',
+
+ // Actions
+ // - Unitary buttons
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Label' => 'Supprimer cette notification',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:ViewObject:Label' => 'Aller à l\'url de la notification',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Label' => 'Marquer comme lu',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:Label' => 'Marquer comme non lu',
+ // - Bulk buttons
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsRead:Label' => 'Marquer sélectionnée(s) comme lu',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsUnread:Label' => 'Marquer sélectionnée(s) comme non lu',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Label' => 'Supprimer sélectionnée(s)',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Confirmation:Title' => 'Supprimer les notifications sélectionnées',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteSelected:Confirmation:Message' => 'Êtes-vous sûr de vouloir supprimer les notifications sélectionnées ?',
+
+ // Feedback messages
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:InvalidAction:Message' => 'Action invalide : "%1$s"',
+ // - Mark as read
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:NoEvent:Message' => 'Aucune notification à marquer comme lue',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Success:Message' => 'La notification a été marquée comme lue',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsReadMultiple:Success:Message' => '%1$s notifications ont été marquées comme lues',
+ // - Mark as unread
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:NoEvent:Message' => 'Aucune notification à marquer comme non lue',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnread:Success:Message' => 'La notification a été marquée comme non lue',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsUnreadMultiple:Success:Message' => '%1$s notifications ont été marquées comme non lues',
+ // Delete
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:NoEvent:Message' => 'Aucune notification à supprimer',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Success:Message' => 'La notification a été supprimée',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:DeleteMultiple:Success:Message' => '%1$s notifications ont été supprimées',
+));
\ No newline at end of file
diff --git a/images/illustrations/undraw_social_serenity.svg b/images/illustrations/undraw_social_serenity.svg
new file mode 100644
index 000000000..34177a07a
--- /dev/null
+++ b/images/illustrations/undraw_social_serenity.svg
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/js/pages/backoffice/itop-newsroom.view-all.js b/js/pages/backoffice/itop-newsroom.view-all.js
new file mode 100644
index 000000000..210b4e7cc
--- /dev/null
+++ b/js/pages/backoffice/itop-newsroom.view-all.js
@@ -0,0 +1,35 @@
+$('body').on('change', '.ibo-toggler', function() {
+ $('.ibo-notifications--view-all--bulk-buttons').toggleClass('ibo-is-hidden');
+ $('.ibo-object-summary').toggleClass('ibo-is-selectable').removeClass('ibo-is-selected');
+});
+
+$('body').on('click', '.ibo-object-summary.ibo-is-selectable', function() {
+ $(this).toggleClass('ibo-is-selected');
+});
+
+$('body').on('itop.notification.deleted', '.ibo-notifications--view-all--container', function() {
+ if($(this).find('.ibo-object-summary').length === 0) {
+ $('.ibo-notifications--view-all--empty').removeClass('ibo-is-hidden');
+ $('.ibo-notifications--view-all--container').addClass('ibo-is-hidden');
+ $('.ibo-notifications--view-all--read-action').attr('disabled', 'disabled');
+ $('.ibo-notifications--view-all--unread-action').attr('disabled', 'disabled');
+ $('.ibo-notifications--view-all--delete-action').attr('disabled', 'disabled');
+ }
+});
+
+let fReadUnreadDisabled = function() {
+ if($('.ibo-object-summary.ibo-notifications--view-all--item--unread').length === 0) {
+ $('.ibo-notifications--view-all--read-action').attr('disabled', 'disabled');
+ $('.ibo-notifications--view-all--unread-action').removeAttr('disabled');
+ } else if ($('.ibo-object-summary.ibo-notifications--view-all--item--read').length === 0) {
+ $('.ibo-notifications--view-all--read-action').removeAttr('disabled');
+ $('.ibo-notifications--view-all--unread-action').attr('disabled', 'disabled');
+ } else {
+ $('.ibo-notifications--view-all--read-action').removeAttr('disabled');
+ $('.ibo-notifications--view-all--unread-action').removeAttr('disabled');
+ }
+}
+
+$('body').on('itop.notification.read itop.notification.unread', '.ibo-notifications--view-all--container', fReadUnreadDisabled);
+
+$('body').on('itop.notification.unread', '.ibo-notifications--view-all--container', fReadUnreadDisabled);
\ No newline at end of file
diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php
index 301857879..4219ad8b7 100644
--- a/lib/composer/autoload_classmap.php
+++ b/lib/composer/autoload_classmap.php
@@ -265,6 +265,7 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\Set' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/Set.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\SetUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\TextArea' => $baseDir . '/sources/Application/UI/Base/Component/Input/TextArea.php',
+ 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Toggler' => $baseDir . '/sources/Application/UI/Base/Component/Input/Toggler.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => $baseDir . '/sources/Application/UI/Base/Component/Input/tInputLabel.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => $baseDir . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Modal\\DoNotShowAgainOptionBlock' => $baseDir . '/sources/Application/UI/Base/Component/Modal/DoNotShowAgainOptionBlock.php',
diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php
index 33080132f..79e7127ad 100644
--- a/lib/composer/autoload_static.php
+++ b/lib/composer/autoload_static.php
@@ -640,6 +640,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\Set' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/Set.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\SetUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\TextArea' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/TextArea.php',
+ 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Toggler' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Toggler.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/tInputLabel.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Modal\\DoNotShowAgainOptionBlock' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Modal/DoNotShowAgainOptionBlock.php',
diff --git a/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php b/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php
index 0cfc89b3c..ec0c8ac8e 100644
--- a/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php
+++ b/sources/Application/UI/Base/Component/ButtonGroup/ButtonGroup.php
@@ -38,14 +38,8 @@ class ButtonGroup extends UIBlock
/**
* Button constructor.
*
- * @param string $sLabel
+ * @param array $aButtons
* @param string|null $sId
- * @param string $sTooltip
- * @param string $sIconClass
- * @param string $sActionType
- * @param string $sColor
- * @param string $sJsCode
- * @param string $sOnClickJsCode
*/
public function __construct(array $aButtons = [], ?string $sId = null)
{
diff --git a/sources/Application/UI/Base/Component/Input/InputUIBlockFactory.php b/sources/Application/UI/Base/Component/Input/InputUIBlockFactory.php
index a9cf6da1e..602656e62 100644
--- a/sources/Application/UI/Base/Component/Input/InputUIBlockFactory.php
+++ b/sources/Application/UI/Base/Component/Input/InputUIBlockFactory.php
@@ -95,10 +95,11 @@ class InputUIBlockFactory extends AbstractUIBlockFactory
* @param string $sLabel
* @param \Combodo\iTop\Application\UI\Base\Component\Input\Input $oInput
* @param string|null $sId
+ * @since 3.2.0 method is now public
*
* @return \Combodo\iTop\Application\UI\Base\Component\Input\InputWithLabel
*/
- private static function MakeInputWithLabel(string $sName, string $sLabel, Input $oInput, ?string $sId = null)
+ public static function MakeInputWithLabel(string $sName, string $sLabel, Input $oInput, ?string $sId = null)
{
$oInput->SetName($sName);
diff --git a/sources/Application/UI/Base/Component/Input/InputWithLabel.php b/sources/Application/UI/Base/Component/Input/InputWithLabel.php
index 262a45709..d047aa862 100644
--- a/sources/Application/UI/Base/Component/Input/InputWithLabel.php
+++ b/sources/Application/UI/Base/Component/Input/InputWithLabel.php
@@ -138,4 +138,8 @@ class InputWithLabel extends UIBlock
return utils::IsNotNullOrEmptyString($this->sDescription);
}
+ public function GetSubBlocks(): array
+ {
+ return [$this->oInput->GetId() => $this->oInput];
+ }
}
\ No newline at end of file
diff --git a/sources/Application/UI/Base/Component/Input/Toggler.php b/sources/Application/UI/Base/Component/Input/Toggler.php
new file mode 100644
index 000000000..2b25ba8dd
--- /dev/null
+++ b/sources/Application/UI/Base/Component/Input/Toggler.php
@@ -0,0 +1,37 @@
+SetType('checkbox');
+ }
+
+ public function SetIsToggled(bool $bIsToggled): static
+ {
+ return $this->SetIsChecked($bIsToggled);
+ }
+
+ public function IsToggled(): bool
+ {
+ return $this->IsChecked();
+ }
+}
\ No newline at end of file
diff --git a/sources/Application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php b/sources/Application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php
index a363073f2..12a8d60b6 100644
--- a/sources/Application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php
+++ b/sources/Application/UI/Base/Component/PopoverMenu/NewsroomMenu/NewsroomMenuFactory.php
@@ -87,7 +87,7 @@ class NewsroomMenuFactory
$sPlaceholderImageUrl= 'far fa-envelope';
$aParams = array(
'image_icon' => $sImageUrl,
- 'no_message_icon' => file_get_contents(APPROOT.'images/illustrations/undraw_empty.svg'),
+ 'no_message_icon' => file_get_contents(APPROOT.'images/illustrations/undraw_social_serenity.svg'),
'placeholder_image_icon' => $sPlaceholderImageUrl,
'cache_uuid' => 'itop-newsroom-'.UserRights::GetUserId().'-'.md5(APPROOT),
'providers' => $aProviderParams,
diff --git a/sources/Application/UI/Base/Layout/Object/ObjectDetails.php b/sources/Application/UI/Base/Layout/Object/ObjectDetails.php
index d2571afd6..047478e70 100644
--- a/sources/Application/UI/Base/Layout/Object/ObjectDetails.php
+++ b/sources/Application/UI/Base/Layout/Object/ObjectDetails.php
@@ -103,6 +103,17 @@ class ObjectDetails extends Panel implements iKeyboardShortcut
return $this->sClassName;
}
+ /**
+ * @see self::$sClassLabel
+ * @return $this
+ */
+ public function SetClassLabel($sClassLabel)
+ {
+ $this->sClassLabel = $sClassLabel;
+
+ return $this;
+ }
+
/**
* @see self::$sClassLabel
* @return string
diff --git a/sources/Application/UI/Base/Layout/Object/ObjectSummary.php b/sources/Application/UI/Base/Layout/Object/ObjectSummary.php
index 7925ca46d..d692e53be 100644
--- a/sources/Application/UI/Base/Layout/Object/ObjectSummary.php
+++ b/sources/Application/UI/Base/Layout/Object/ObjectSummary.php
@@ -102,8 +102,8 @@ class ObjectSummary extends ObjectDetails
{
$oRouter = Router::GetInstance();
$oDetailsButton = null;
- if(UserRights::IsActionAllowed($this->sClassName, UR_ACTION_MODIFY)) {
- $sRootUrl = utils::GetAbsoluteUrlAppRoot();
+ // We can pass a DBObject to the UIBlock, so we check for the DisplayModifyForm method
+ if(method_exists($this->oObject, 'DisplayModifyForm') && UserRights::IsActionAllowed($this->sClassName, UR_ACTION_MODIFY)) {
$oPopoverMenu = new PopoverMenu();
$oDetailsAction = new URLPopupMenuItem(
diff --git a/sources/Controller/Newsroom/iTopNewsroomController.php b/sources/Controller/Newsroom/iTopNewsroomController.php
index b32a85c24..15515c127 100644
--- a/sources/Controller/Newsroom/iTopNewsroomController.php
+++ b/sources/Controller/Newsroom/iTopNewsroomController.php
@@ -5,15 +5,33 @@ namespace Combodo\iTop\Controller\Newsroom;
use ArchivedObjectException;
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\TwigBase\Controller\Controller;
+use Combodo\iTop\Application\UI\Base\Component\Button\Button;
+use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroupUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Component\Html\Html;
+use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Component\Input\Toggler;
+use Combodo\iTop\Application\UI\Base\Component\Panel\Panel;
+use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
+use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
+use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
+use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary;
+use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Application\WebPage\iTopWebPage;
+use Combodo\iTop\Application\WebPage\JsonPage;
use Combodo\iTop\Application\WebPage\JsonPPage;
+use Combodo\iTop\Service\Notification\NotificationsRepository;
use Combodo\iTop\Service\Router\Router;
use CoreException;
use DBObjectSearch;
use DBObjectSet;
use Dict;
-use DisplayBlock;
+use JSPopupMenuItem;
use MetaModel;
+use SecurityException;
+use URLPopupMenuItem;
use UserRights;
use utils;
@@ -38,12 +56,476 @@ class iTopNewsroomController extends Controller
public function OperationViewAll()
{
$oPage = new iTopWebPage(Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Title'));
- $oSearch = DBObjectSearch::FromOQL('SELECT EventiTopNotification WHERE read = "no"');
- $oSearch->AddCondition('contact_id', UserRights::GetContactId(), '=');
- $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, []);
- $oBlock->Display($oPage, 0);
- $oPage->add("
");
+ $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/pages/backoffice/itop-newsroom.view-all.js');
+ // Add title block
+ // Make bulk actions block
+ $oBulkActionsBlock = PanelUIBlockFactory::MakeForInformation(Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Title'));
+ $oToolbar = ToolbarUIBlockFactory::MakeStandard();
+ $oToolbar->AddCSSClass('ibo-notifications--view-all--toolbar');
+ $oAllModeButtonsContainer = new UIContentBlock('ibo-notifications--view-all--all-mode-buttons', ['ibo-notifications--view-all--bulk-buttons', 'ibo-notifications--view-all--all-mode-buttons']);
+ // Create CSRF token we'll use in this page
+ $sCSRFToken = utils::GetNewTransactionId();
+ // Make button to mark all as read
+ $sMarkMultipleAsReadUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.mark_multiple_as_read', ['token' => $sCSRFToken]);
+ $sMarkMultipleAsUnreadUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.mark_multiple_as_unread', ['token' => $sCSRFToken]);
+ $sDeleteMultipleUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.delete_multiple', ['token' => $sCSRFToken]);
+ $oMarkAllAsReadButton = ButtonUIBlockFactory::MakeForSecondaryAction(
+ Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsRead:Label'),
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsRead:Label',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAllAsRead:Label'
+ );
+ $oMarkAllAsReadButton->SetIconClass('far fa-envelope-open')
+ ->AddCSSClass('ibo-notifications--view-all--read-action')
+ ->SetOnClickJsCode(
+ <<SetIconClass('far fa-envelope')
+ ->AddCSSClass('ibo-notifications--view-all--unread-action')
+ ->SetOnClickJsCode(
+ <<SetActionType(Button::ENUM_ACTION_TYPE_ALTERNATIVE);
+ $oDeleteAllButton->SetIconClass('fas fa-trash-alt')
+ ->AddCSSClass('ibo-notifications--view-all--delete-action')
+ ->SetOnClickJsCode(
+ <<AddSubBlock($oMarkAllAsReadButton);
+ $oAllModeButtonsContainer->AddSubBlock($oMarkAllAsUnreadButton);
+ $oAllModeButtonsContainer->AddSubBlock($oDeleteAllButton);
+ $oToolbar->AddSubBlock($oAllModeButtonsContainer);
+
+ $oSelectedModelButtonsContainer = new UIContentBlock('ibo-notifications--view-all--selected-mode-buttons', ['ibo-is-hidden', 'ibo-notifications--view-all--bulk-buttons', 'ibo-notifications--view-all--selected-mode-buttons']);
+ // Make button mark all selected as read
+ $oMarkSelectedAsReadButton = ButtonUIBlockFactory::MakeForSecondaryAction(
+ Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsRead:Label'),
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsRead:Label',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkSelectedAsRead:Label'
+ );
+ $oMarkSelectedAsReadButton->SetIconClass('far fa-envelope-open')
+ ->AddCSSClass('ibo-notifications--view-all--read-action')
+ ->SetOnClickJsCode(
+ <<SetIconClass('far fa-envelope')
+ ->AddCSSClass('ibo-notifications--view-all--unread-action')
+ ->SetOnClickJsCode(
+ <<SetActionType(Button::ENUM_ACTION_TYPE_ALTERNATIVE);
+ $oDeleteSelectedButton->SetIconClass('fas fa-trash-alt')
+ ->AddCSSClass('ibo-notifications--view-all--delete-action')
+ ->SetOnClickJsCode(
+ <<AddSubBlock($oMarkSelectedAsReadButton);
+ $oSelectedModelButtonsContainer->AddSubBlock($oMarkSelectedAsUnreadButton);
+ $oSelectedModelButtonsContainer->AddSubBlock($oDeleteSelectedButton);
+
+ $oToolbar->AddSubBlock($oSelectedModelButtonsContainer);
+
+ // Make toggler to switch between "all" and "selected" mode
+ $oTogglerContentBlock = new UIContentBlock('ibo-notifications--view-all--toggler', ['ibo-notifications--view-all--toggler']);
+ $oToggler = new Toggler();
+ $oInputWithLabel = InputUIBlockFactory::MakeInputWithLabel('slider', Dict::S('UI:Newsroom:iTopNotification:SelectMode:Label'), $oToggler);
+ $oTogglerContentBlock->AddSubBlock($oInputWithLabel);
+ $oToolbar->AddSubBlock($oTogglerContentBlock);
+
+ $oBulkActionsBlock->AddSubBlock($oToolbar);
+ $oPage->AddUiBlock($oBulkActionsBlock);
+
+ // Search for all notifications for the current user
+ $oSearch = DBObjectSearch::FromOQL('SELECT EventiTopNotification');
+ $oSearch->AddCondition('contact_id', UserRights::GetContactId(), '=');
+ $oSet = new DBObjectSet($oSearch, array('read' => true, 'date' => true), array());
+
+ // Add main content block
+ $oMainContentBlock = new UIContentBlock(null, ['ibo-notifications--view-all--container']);
+ $oPage->AddUiBlock($oMainContentBlock);
+
+ while ($oEvent = $oSet->Fetch()) {
+ $iEventId = $oEvent->GetKey();
+ // Prepare object summary block
+ $sReadColor = $oEvent->Get('read') === 'no' ? 'ibo-notifications--view-all--item--unread' : 'ibo-notifications--view-all--item--read';
+ $sReadLabel = $oEvent->Get('read') === 'no' ? Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Unread:Label') : Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Read:Label');
+ $oEventBlock = new ObjectSummary($oEvent);
+ $oEventBlock->SetCSSColorClass($sReadColor);
+ $oEventBlock->SetSubTitle($sReadLabel);
+ $oEventBlock->SetClassLabel('');
+ $oImage = $oEvent->Get('icon');
+ if (!$oImage->IsEmpty()) {
+ $sIconUrl = $oImage->GetDisplayURL(get_class($oEvent), $iEventId, 'icon');
+ $oEventBlock->SetIcon($sIconUrl, Panel::ENUM_ICON_COVER_METHOD_COVER,true);
+ }
+
+ // Prepare Event actions
+ $oMarkAsReadPopoverMenu = new PopoverMenu();
+ $oMarkAsUnreadPopoverMenu = new PopoverMenu();
+
+ // Common actions
+ $sDeleteUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.delete_event', ['notification_id' => $oEvent->GetKey(), 'token' => $sCSRFToken]);
+ $oDeleteButton = new JSPopupMenuItem(
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Label',
+ Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Action:Delete:Label'),
+ <<GenerateUrl(self::ROUTE_NAMESPACE.'.view_event', ['event_id' => $oEvent->GetKey()]),
+ '_blank'
+ );
+
+ // Mark as read action
+ $oMarkAsReadButton = ButtonUIBlockFactory::MakeForAlternativeSecondaryAction(
+ Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Label'),
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Label',
+ 'UI:Newsroom:iTopNotification:ViewAllPage:Action:MarkAsRead:Label',
+ );
+
+ // Mark as read action
+ $oMarkAsReadPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oViewButton))->SetContainer(PopoverMenu::ENUM_CONTAINER_PARENT);
+ $oMarkAsReadPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oDeleteButton))->SetContainer(PopoverMenu::ENUM_CONTAINER_PARENT);
+
+ // Mark as unread action
+ $oMarkAsUnreadPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oViewButton))->SetContainer(PopoverMenu::ENUM_CONTAINER_PARENT);
+ $oMarkAsUnreadPopoverMenu->AddItem('more-actions', PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oDeleteButton))->SetContainer(PopoverMenu::ENUM_CONTAINER_PARENT);
+
+
+ // Mark as unread action
+ $sMarkAsReadUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.mark_as_read', ['notification_id' => $oEvent->GetKey(), 'token' => $sCSRFToken]);
+ $oMarkAsReadButton->SetOnClickJsCode(
+ <<GenerateUrl(self::ROUTE_NAMESPACE.'.mark_as_unread', ['notification_id' => $oEvent->GetKey(), 'token' => $sCSRFToken]);
+ $oMarkAsUnreadButton->SetOnClickJsCode(
+ <<GetActions()->GetId();
+ $oEventBlock->RemoveSubBlock($oOldButtonId);
+ $oEventBlock->SetToolBlocks([$oMarkAsReadButtonGroup, $oMarkAsUnreadButtonGroup]);
+ $oActionsBlock = new UIContentBlock();
+ $oActionsBlock->AddSubBlock($oMarkAsReadButtonGroup);
+ $oActionsBlock->AddSubBlock($oMarkAsUnreadButtonGroup);
+ $oEventBlock->SetActions($oActionsBlock);
+
+ // Display the right button depending on the read status
+ if($oEvent->Get('read') === 'no'){
+ $oMarkAsUnreadButtonGroup->SetCSSClasses(['ibo-is-hidden']);
+ }
+ else{
+ $oMarkAsReadButtonGroup->SetCSSClasses(['ibo-is-hidden']);
+ }
+
+ $oMainContentBlock->AddSubBlock($oEventBlock);
+ }
+
+ // Add empty content block
+ $oEmptyContentBlock = new UIContentBlock('ibo-notifications--view-all--empty', ['ibo-notifications--view-all--empty', 'ibo-svg-illustration--container']);
+ $oEmptyContentBlock->AddSubBlock(new Html(file_get_contents(APPROOT.'/images/illustrations/undraw_social_serenity.svg')));
+ $oEmptyContentBlock->AddSubBlock(TitleUIBlockFactory::MakeNeutral(Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Empty:Title')));
+ $oPage->AddUiBlock($oEmptyContentBlock);
+
+ // Hide empty content block if there are notifications
+ if($oSet->Count() === 0){
+ $oMainContentBlock->AddCSSClass('ibo-is-hidden');
+ }
+ else {
+ $oEmptyContentBlock->AddCSSClass('ibo-is-hidden');
+ }
+
return $oPage;
}
@@ -159,4 +641,163 @@ HTML;
}
}
}
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationMarkAsUnread(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnSingleNotification('mark_as_unread'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationMarkAsRead(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnSingleNotification('mark_as_read'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationDeleteEvent(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnSingleNotification('delete'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationMarkMultipleAsRead(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnMultipleNotifications('mark_as_read'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationMarkMultipleAsUnread(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnMultipleNotifications('mark_as_unread'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @return \Combodo\iTop\Application\WebPage\JsonPage
+ */
+ public function OperationDeleteMultiple(): JsonPage
+ {
+ $oPage = new JsonPage();
+ $oPage->SetData($this->PerformActionOnMultipleNotifications('delete'));
+ $oPage->SetOutputDataOnly(true);
+ return $oPage;
+ }
+
+ /**
+ * @param string $sAction
+ *
+ * @return string[]
+ * @throws \SecurityException
+ */
+ protected function PerformActionOnSingleNotification(string $sAction): array
+ {
+ $iNotificationId = utils::ReadParam('notification_id', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
+ return $this->PerformAction($sAction, [$iNotificationId]);
+ }
+
+ /**
+ * @param string $sAction
+ *
+ * @return string[]
+ * @throws \SecurityException
+ */
+ protected function PerformActionOnMultipleNotifications(string $sAction): array
+ {
+ $aNotificationIds = utils::ReadParam('notification_ids', []);
+ return $this->PerformAction($sAction, $aNotificationIds);
+ }
+
+ /**
+ * @param string $sAction
+ * @param array $aNotificationIds
+ *
+ * @return string[]
+ * @throws \SecurityException
+ */
+ protected function PerformAction(string $sAction, array $aNotificationIds): array
+ {
+ $sCSRFToken = utils::ReadParam('token', '', false, 'raw_data');
+ if(utils::IsTransactionValid($sCSRFToken, false) === false){
+ throw new SecurityException('Invalid CSRF token');
+ }
+
+ $sActionAsCamelCase = utils::ToCamelCase($sAction);
+ $aReturnData = [
+ 'status' => 'error',
+ 'message' => 'Invalid notification(s)'
+ ];
+
+ // Check action type
+ if (false === in_array($sAction, ['mark_as_read', 'mark_as_unread', 'delete'])) {
+ $aReturnData['message'] = Dict::S("UI:Newsroom:iTopNotification:ViewAllPage:Action:InvalidAction:Message");
+ return $aReturnData;
+ }
+
+ // No ID passed to the API
+ if (count($aNotificationIds) === 0) {
+ $aReturnData['message'] = Dict::S("UI:Newsroom:iTopNotification:ViewAllPage:Action:$sActionAsCamelCase:NoEvent:Message");
+ return $aReturnData;
+ }
+
+ try {
+ $sRepositoryMethodName = "SearchNotificationsTo{$sActionAsCamelCase}ByContact";
+ $oSet = NotificationsRepository::GetInstance()->$sRepositoryMethodName(UserRights::GetContactId(), $aNotificationIds);
+
+ // No notification found
+ $iCount = $oSet->Count();
+ if($iCount === 0) {
+ $aReturnData['message'] = Dict::S("UI:Newsroom:iTopNotification:ViewAllPage:Action:$sActionAsCamelCase:NoEvent:Message");
+ return $aReturnData;
+ }
+
+ while ($oEvent = $oSet->Fetch()) {
+ if ($sAction === 'mark_as_read') {
+ $oEvent->Set('read', 'yes');
+ $oEvent->SetCurrentDate('read_date');
+ $oEvent->DBWrite();
+ } elseif ($sAction === 'mark_as_unread') {
+ $oEvent->Set('read', 'no');
+ $oEvent->DBWrite();
+ } elseif ($sAction === 'delete') {
+ $oEvent->DBDelete();
+ }
+ }
+
+ $aReturnData['status'] = 'success';
+ if ($iCount === 1) {
+ $aReturnData['message'] = Dict::S("UI:Newsroom:iTopNotification:ViewAllPage:Action:{$sActionAsCamelCase}:Success:Message");
+ } else {
+ $aReturnData['message'] = Dict::Format("UI:Newsroom:iTopNotification:ViewAllPage:Action:{$sActionAsCamelCase}Multiple:Success:Message", $iCount);
+ }
+ } catch (Exception $oException) {
+ $aReturnData['message'] = $oException->getMessage();
+ }
+
+ return $aReturnData;
+ }
}
\ No newline at end of file
diff --git a/sources/Service/Notification/NotificationsRepository.php b/sources/Service/Notification/NotificationsRepository.php
index fed5a8eee..968f9f833 100644
--- a/sources/Service/Notification/NotificationsRepository.php
+++ b/sources/Service/Notification/NotificationsRepository.php
@@ -2,8 +2,12 @@
namespace Combodo\iTop\Service\Notification;
+use BinaryExpression;
use DBObjectSearch;
use DBObjectSet;
+use Expression;
+use FieldExpression;
+use VariableExpression;
/**
* Class NotificationsRepository
@@ -44,6 +48,74 @@ class NotificationsRepository
// Don't do anything, we don't want to be initialized
}
+ /**
+ * @param int $iContactId ID of the contact to retrieve notifications for
+ * @param array $aNotificationIds Optional IDs of the notifications to retrieve, if omitted all notifications will be retrieved
+ *
+ * @return \DBObjectSet Set of notifications for $iContactId, optionally limited to $aNotificationIds
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function SearchNotificationsByContact(int $iContactId, array $aNotificationIds = []): DBObjectSet
+ {
+ $oSearch = DBObjectSearch::FromOQL("SELECT EventiTopNotification WHERE contact_id = :contact_id");
+ $aParams = [
+ "contact_id" => $iContactId,
+ ];
+
+ if (count($aNotificationIds) > 0) {
+ $oSearch->AddConditionExpression(Expression::FromOQL("{$oSearch->GetClassAlias()}.id IN (:notification_ids)"));
+ $aParams["notification_ids"] = $aNotificationIds;
+ }
+
+ return new DBObjectSet($oSearch, [], $aParams);
+ }
+
+ /**
+ * @param int $iContactId ID of the contact to retrieve unread notifications for
+ * @param array $aNotificationIds Optional IDs of the unread notifications to retrieve, if omitted all unread notifications will be retrieved
+ *
+ * @return \DBObjectSet Set of unread notifications for $iContactId, optionally limited to $aNotificationIds
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function SearchNotificationsToMarkAsReadByContact(int $iContactId, array $aNotificationIds = []): DBObjectSet
+ {
+ $oSet = $this->SearchNotificationsByContact($iContactId, $aNotificationIds);
+ $oSet->GetFilter()->AddCondition("read", "=", "no");
+
+ return $oSet;
+ }
+
+ /**
+ * @param int $iContactId ID of the contact to retrieve read notifications for
+ * @param array $aNotificationIds Optional IDs of the read notifications to retrieve, if omitted all read notifications will be retrieved
+ *
+ * @return \DBObjectSet Set of read notifications for $iContactId, optionally limited to $aNotificationIds
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function SearchNotificationsToMarkAsUnreadByContact(int $iContactId, array $aNotificationIds = []): DBObjectSet
+ {
+ $oSet = $this->SearchNotificationsByContact($iContactId, $aNotificationIds);
+ $oSet->GetFilter()->AddCondition("read", "=", "yes");
+
+ return $oSet;
+ }
+
+ /**
+ * @param int $iContactId ID of the contact to retrieve read notifications for
+ * @param array $aNotificationIds Optional IDs of the notifications to retrieve, if omitted all notifications will be retrieved
+ *
+ * @return \DBObjectSet Set of notifications for $iContactId, optionally limited to $aNotificationIds
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function SearchNotificationsToDeleteByContact(int $iContactId, array $aNotificationIds = []): DBObjectSet
+ {
+ return $this->SearchNotificationsByContact($iContactId, $aNotificationIds);
+ }
+
/**
* Search for subscriptions by contact ID.
*
diff --git a/templates/base/components/input/input-toggler.html.twig b/templates/base/components/input/input-toggler.html.twig
new file mode 100644
index 000000000..f9bd74c6b
--- /dev/null
+++ b/templates/base/components/input/input-toggler.html.twig
@@ -0,0 +1,7 @@
+{% extends "base/components/input/layout.html.twig" %}
+{% block iboInput %}
+
+ {{ parent() }}
+
+
+{% endblock %}
diff --git a/templates/base/components/input/input-toggler.ready.js.twig b/templates/base/components/input/input-toggler.ready.js.twig
new file mode 100644
index 000000000..d3fdecc0f
--- /dev/null
+++ b/templates/base/components/input/input-toggler.ready.js.twig
@@ -0,0 +1,5 @@
+$('#{{ oUIBlock.GetId() }}').parent().on('click', function() {
+ let oInput = $(this).find('.ibo-toggler');
+ oInput.prop('checked', !oInput.prop('checked'));
+ oInput.trigger('change');
+});
\ No newline at end of file