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