From 26e02e32841671fa162cf11cb0fa925aa4452da5 Mon Sep 17 00:00:00 2001 From: Thomas Hackl Date: Tue, 18 Jun 2024 12:18:17 +0000 Subject: =?UTF-8?q?Resolve=20"Neues=20Benachrichtungssystem=20f=C3=BCr=20S?= =?UTF-8?q?tud.IP"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #660 Merge request studip/studip!2557 --- app/controllers/course/basicdata.php | 8 + app/controllers/settings/general.php | 2 + app/views/course/basicdata/view.php | 6 - app/views/settings/general.php | 15 ++ db/migrations/6.0.9_notification_positioning.php | 35 +++++ lib/classes/MessageBox.class.php | 9 ++ lib/classes/PageLayout.php | 12 +- lib/phplib/Seminar_Auth.class.php | 4 +- .../javascripts/bootstrap/system-notifications.js | 11 ++ resources/assets/javascripts/entry-base.js | 2 + resources/assets/stylesheets/scss/responsive.scss | 20 +++ .../stylesheets/scss/system-notifications.scss | 174 +++++++++++++++++++++ resources/assets/stylesheets/studip.scss | 1 + resources/vue/components/SystemNotification.vue | 156 ++++++++++++++++++ .../vue/components/SystemNotificationManager.vue | 52 ++++++ .../courseware/layouts/CoursewareCompanionBox.vue | 46 ++++-- .../layouts/CoursewareCompanionOverlay.vue | 55 ++++--- .../store/courseware/courseware-shelf.module.js | 24 +-- .../vue/store/courseware/courseware.module.js | 20 +-- templates/layouts/base.php | 9 +- templates/loginform.php | 19 +-- 21 files changed, 592 insertions(+), 88 deletions(-) create mode 100644 db/migrations/6.0.9_notification_positioning.php create mode 100644 resources/assets/javascripts/bootstrap/system-notifications.js create mode 100644 resources/assets/stylesheets/scss/system-notifications.scss create mode 100644 resources/vue/components/SystemNotification.vue create mode 100644 resources/vue/components/SystemNotificationManager.vue diff --git a/app/controllers/course/basicdata.php b/app/controllers/course/basicdata.php index f4f50fe..97ec053 100644 --- a/app/controllers/course/basicdata.php +++ b/app/controllers/course/basicdata.php @@ -445,6 +445,14 @@ class Course_BasicdataController extends AuthenticatedController $widget = new CourseManagementSelectWidget(); $sidebar->addWidget($widget); } + + foreach ($this->flash['msg'] ?? [] as $msg) { + match ($msg[0]) { + 'msg' => PageLayout::postSuccess($msg[1]), + 'error' => PageLayout::postError($msg[1]), + 'info' => PageLayout::postInfo($msg[1]), + }; + } } /** diff --git a/app/controllers/settings/general.php b/app/controllers/settings/general.php index 734cca1..0e8ec70 100644 --- a/app/controllers/settings/general.php +++ b/app/controllers/settings/general.php @@ -44,6 +44,7 @@ class Settings_GeneralController extends Settings_SettingsController public function index_action() { $this->user_language = getUserLanguage($this->user->id); + $this->notifications_placement = User::findCurrent()->getConfiguration()->SYSTEM_NOTIFICATIONS_PLACEMENT; } /** @@ -80,6 +81,7 @@ class Settings_GeneralController extends Settings_SettingsController } else { PersonalNotifications::deactivateAudioFeedback($this->user->id); } + $this->config->store('SYSTEM_NOTIFICATIONS_PLACEMENT', Request::get('system_notifications_placement')); PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); $this->redirect('settings/general'); diff --git a/app/views/course/basicdata/view.php b/app/views/course/basicdata/view.php index 380fdb4..9438aeb 100644 --- a/app/views/course/basicdata/view.php +++ b/app/views/course/basicdata/view.php @@ -12,14 +12,8 @@ use Studip\Button, Studip\LinkButton; */ $dialog_attr = Request::isXhr() ? ' data-dialog="size=50%"' : ''; - -$message_types = ['msg' => "success", 'error' => "error", 'info' => "info"]; ?> - - - -
class="default collapsable"> diff --git a/app/views/settings/general.php b/app/views/settings/general.php index 4bd5433..32b1a6f 100644 --- a/app/views/settings/general.php +++ b/app/views/settings/general.php @@ -93,6 +93,21 @@ $start_pages = [ '- auch wenn Sie gerade einen anderen Browsertab anschauen. Der Plopp ist ' . 'nur zu hören, wenn Sie die Benachrichtigungen über Javascript aktiviert haben.')) ?> + +
diff --git a/db/migrations/6.0.9_notification_positioning.php b/db/migrations/6.0.9_notification_positioning.php new file mode 100644 index 0000000..d6693f3 --- /dev/null +++ b/db/migrations/6.0.9_notification_positioning.php @@ -0,0 +1,35 @@ +execute( + "INSERT IGNORE INTO `config` VALUES (:field, :value, :type, :range, :section, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :desc)", + [ + 'field' => 'SYSTEM_NOTIFICATIONS_PLACEMENT', + 'value' => 'topcenter', + 'type' => 'string', + 'range' => 'user', + 'section' => '', + 'desc' => 'Wo sollen Systembenachrichtigungen im Fenster angezeigt werden? Gültige Werte sind "topcenter" und "bottomright"' + ] + ); + } + + protected function down() + { + DBManager::get()->execute( + "DELETE FROM `config_values` WHERE `field` = :field", + ['field' => 'SYSTEM_NOTIFICATIONS_PLACEMENT'] + ); + DBManager::get()->execute( + "DELETE FROM `config` WHERE `field` = :field", + ['field' => 'SYSTEM_NOTIFICATIONS_PLACEMENT'] + ); + } +}; diff --git a/lib/classes/MessageBox.class.php b/lib/classes/MessageBox.class.php index c0d94b2..8936ebe 100644 --- a/lib/classes/MessageBox.class.php +++ b/lib/classes/MessageBox.class.php @@ -142,6 +142,15 @@ class MessageBox implements LayoutMessage } /** + * Return whether this messagebox can be closed or not. + * @return bool + */ + public function isCloseable(): bool + { + return $this->hide_close; + } + + /** * This method renders a MessageBox object to a string. * * @return string html output of the message box diff --git a/lib/classes/PageLayout.php b/lib/classes/PageLayout.php index d6b28ec..7f95b7a 100644 --- a/lib/classes/PageLayout.php +++ b/lib/classes/PageLayout.php @@ -599,10 +599,18 @@ class PageLayout if (!isset($_SESSION['messages'])) { $_SESSION['messages'] = []; } + + $structure = [ + 'type' => $message->class, + 'message' => $message->message, + 'details' => $message->details, + 'closeable' => $message->isCloseable() + ]; + if ($id === null ) { - $_SESSION['messages'][] = $message; + $_SESSION['messages'][] = $structure; } else { - $_SESSION['messages'][$id] = $message; + $_SESSION['messages'][$id] = $structure; } } diff --git a/lib/phplib/Seminar_Auth.class.php b/lib/phplib/Seminar_Auth.class.php index 4849ece..2c1c60c 100644 --- a/lib/phplib/Seminar_Auth.class.php +++ b/lib/phplib/Seminar_Auth.class.php @@ -336,7 +336,9 @@ class Seminar_Auth } else { unset($_SESSION['semi_logged_in']); // used by email activation $login_template = $GLOBALS['template_factory']->open('loginform'); - $login_template->set_attribute('loginerror', (isset($this->auth["uname"]) && $this->error_msg)); + if (isset($this->auth['uname']) && $this->error_msg) { + PageLayout::postException(_('Bei der Anmeldung trat ein Fehler auf!'), $this->error_msg); + } $login_template->set_attribute('error_msg', $this->error_msg); $login_template->set_attribute('uname', (isset($this->auth["uname"]) ? $this->auth["uname"] : Request::username('loginname'))); $login_template->set_attribute('self_registration_activated', Config::get()->ENABLE_SELF_REGISTRATION); diff --git a/resources/assets/javascripts/bootstrap/system-notifications.js b/resources/assets/javascripts/bootstrap/system-notifications.js new file mode 100644 index 0000000..7a85fcd --- /dev/null +++ b/resources/assets/javascripts/bootstrap/system-notifications.js @@ -0,0 +1,11 @@ +import SystemNotificationManager from '../../../vue/components/SystemNotificationManager.vue'; + +STUDIP.domReady(() => { + document.getElementById('system-notifications')?.classList.add('vueified'); + STUDIP.Vue.load().then(({ createApp }) => { + createApp({ + el: '#system-notifications', + components: { SystemNotificationManager } + }); + }); +}); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 2683439..5f88c30 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -16,6 +16,8 @@ import "./init.js" import "./bootstrap/responsive.js" import "./bootstrap/vue.js" +import "./bootstrap/system-notifications.js" + import "./bootstrap/my-courses.js"; import "./studip-ui.js" diff --git a/resources/assets/stylesheets/scss/responsive.scss b/resources/assets/stylesheets/scss/responsive.scss index c4317e9..e84a8a8 100644 --- a/resources/assets/stylesheets/scss/responsive.scss +++ b/resources/assets/stylesheets/scss/responsive.scss @@ -537,6 +537,26 @@ $sidebarOut: -330px; } } } + + #system-notifications { + top: 0; + position: fixed; + left: 0; + width: 100%; + z-index: 1001; + } + + .system-notification { + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + transform: translateY(-100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + transform: translateY(0); + } + } } /* Settings especially for fullscreen mode */ diff --git a/resources/assets/stylesheets/scss/system-notifications.scss b/resources/assets/stylesheets/scss/system-notifications.scss new file mode 100644 index 0000000..1da1ec2 --- /dev/null +++ b/resources/assets/stylesheets/scss/system-notifications.scss @@ -0,0 +1,174 @@ +#system-notifications { + &.bottom-right { + bottom: 50px; + right: 15px; + + .system-notification { + box-shadow: 5px 5px var(--dark-gray-color-10); + + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + opacity: 0; + transform: translateX(100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + opacity: 1; + transform: translateX(0); + } + } + } + + &.top-center { + left: calc(50% - 300px); + top: 0; + + .system-notification { + box-shadow: 0 0 0 3px var(--dark-gray-color-10); + + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + opacity: 0; + transform: translateY(-100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + opacity: 1; + transform: translateY(0); + } + } + } + + &:not(.system-notifications-login) { + position: fixed; + width: 600px; + } + + &.system-notifications-login { + margin-bottom: 15px; + } + + overflow: hidden; + z-index: 1001; +} + +.system-notification { + background-color: var(--content-color-20); + border: thin solid var(--content-color-40); + color: var(--black); + cursor: pointer; + display: flex; + padding: 10px; + position: relative; + + &.system-notification-slide-enter-active, + &.system-notification-slide-leave-active { + transition: all var(--transition-duration-slow) ease-in-out; + } + + .system-notification-icon { + flex: 0; + padding: 10px; + } + + .system-notification-content { + flex: 1; + height: auto; + margin: auto 25px auto 10px; + word-wrap: break-word; + + .system-notification-details { + .system-notification-details-content { + padding-left: 10px; + } + } + } + + .system-notification-close { + align-self: normal; + flex: 0; + height: 20px; + width: 20px; + + img, svg { + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; + } + } + + .system-notification-timeout { + &.system-notification-timeout-enter-active, + &.system-notification-timeout-leave-active { + transition: width 5s linear; + } + + &.system-notification-timeout-enter { + width: 100%; + } + + &.system-notification-timeout-leave { + width: 0; + } + + background-color: var(--base-color-40); + bottom: 0; + height: 5px; + left: 0; + position: absolute; + width: 0; + } + + a:not(.system-notification-message) { + color: var(--black); + text-decoration-line: underline; + } + + a.system-notification-message { + color: var(--text-color); + text-decoration: unset; + } +} + +.system-notification-exception { + background-color: var(--red-40); + + .system-notification-timeout { + background-color: var(--red); + } +} + +.system-notification-error { + background-color: var(--red-20); + + .system-notification-timeout { + background-color: var(--red-80); + } +} + +.system-notification-warning { + background-color: var(--yellow-20); + + .system-notification-timeout { + background-color: var(--yellow-80); + } +} + +.system-notification-success { + background-color: var(--green-20); + + .system-notification-timeout { + background-color: var(--green-80); + } +} + +.system-notification-info { + background-color: var(--dark-violet-20); + + .system-notification-timeout { + background-color: var(--dark-violet-60); + } +} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 0f329a4..a322400 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -96,6 +96,7 @@ @import "scss/studygroup"; @import "scss/studip-overlay"; @import "scss/studip-selection"; +@import "scss/system-notifications"; @import "scss/table_of_contents"; @import "scss/tables"; @import "scss/tabs"; diff --git a/resources/vue/components/SystemNotification.vue b/resources/vue/components/SystemNotification.vue new file mode 100644 index 0000000..bf804ae --- /dev/null +++ b/resources/vue/components/SystemNotification.vue @@ -0,0 +1,156 @@ + + + diff --git a/resources/vue/components/SystemNotificationManager.vue b/resources/vue/components/SystemNotificationManager.vue new file mode 100644 index 0000000..72c4dbf --- /dev/null +++ b/resources/vue/components/SystemNotificationManager.vue @@ -0,0 +1,52 @@ + + + diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue index f617e5a..c40edd1 100644 --- a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue +++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue @@ -1,12 +1,3 @@ - - \ No newline at end of file + diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue index ff177f7..a45da11 100644 --- a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue +++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue @@ -1,23 +1,3 @@ - - diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js index dc92a23..d907ee4 100644 --- a/resources/vue/store/courseware/courseware-shelf.module.js +++ b/resources/vue/store/courseware/courseware-shelf.module.js @@ -249,33 +249,23 @@ export const actions = { } }, async companionInfo({ dispatch }, { info }) { - await dispatch('setStyleCompanionOverlay', 'default'); - await dispatch('setMsgCompanionOverlay', info); - return dispatch('setShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info}); }, async companionSuccess({ dispatch }, { info }) { - await dispatch('setStyleCompanionOverlay', 'happy'); - await dispatch('setMsgCompanionOverlay', info); - return dispatch('setShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info}); }, async companionError({ dispatch }, { info }) { - await dispatch('setStyleCompanionOverlay', 'sad'); - await dispatch('setMsgCompanionOverlay', info); - return dispatch('setShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info}); }, async companionWarning({ dispatch }, { info }) { - await dispatch('setStyleCompanionOverlay', 'alert'); - await dispatch('setMsgCompanionOverlay', info); - return dispatch('setShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info}); }, async companionSpecial({ dispatch }, { info }) { - await dispatch('setStyleCompanionOverlay', 'special'); - await dispatch('setMsgCompanionOverlay', info); - return dispatch('setShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info}); }, coursewareShowCompanionOverlay({dispatch}, { data }) { return dispatch('setShowCompanionOverlay', data); @@ -310,7 +300,7 @@ export const actions = { return dispatch(loadUnits, state.context.id); }, - + async sortUnits({ dispatch, state }, data) { let loadUnits = null; if (state.context.type === 'courses') { @@ -321,7 +311,7 @@ export const actions = { } await state.httpClient.post(`${state.context.type}/${state.context.id}/courseware-units/sort`, {data: data}); - + return dispatch(loadUnits, state.context.id); }, diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index c44bba1..784b750 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -874,33 +874,23 @@ export const actions = { }, async companionInfo({ dispatch }, { info }) { - await dispatch('coursewareStyleCompanionOverlay', 'default'); - await dispatch('coursewareMsgCompanionOverlay', info); - return dispatch('coursewareShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info}); }, async companionSuccess({ dispatch }, { info }) { - await dispatch('coursewareStyleCompanionOverlay', 'happy'); - await dispatch('coursewareMsgCompanionOverlay', info); - return dispatch('coursewareShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info}); }, async companionError({ dispatch }, { info }) { - await dispatch('coursewareStyleCompanionOverlay', 'sad'); - await dispatch('coursewareMsgCompanionOverlay', info); - return dispatch('coursewareShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info}); }, async companionWarning({ dispatch }, { info }) { - await dispatch('coursewareStyleCompanionOverlay', 'alert'); - await dispatch('coursewareMsgCompanionOverlay', info); - return dispatch('coursewareShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'exception', message: info}); }, async companionSpecial({ dispatch }, { info }) { - await dispatch('coursewareStyleCompanionOverlay', 'special'); - await dispatch('coursewareMsgCompanionOverlay', info); - return dispatch('coursewareShowCompanionOverlay', true); + STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info}); }, // adds a favorite block type using the `type` of the BlockType diff --git a/templates/layouts/base.php b/templates/layouts/base.php index 6597fd8..d7fc1f1 100644 --- a/templates/layouts/base.php +++ b/templates/layouts/base.php @@ -53,7 +53,9 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']); 'ACTIONMENU_THRESHOLD' => Config::get()->ACTION_MENU_THRESHOLD, 'ENTRIES_PER_PAGE' => Config::get()->ENTRIES_PER_PAGE, 'OPENGRAPH_ENABLE' => Config::get()->OPENGRAPH_ENABLE, - 'COURSEWARE_CERTIFICATES_ENABLE' => Config::get()->COURSEWARE_CERTIFICATES_ENABLE + 'COURSEWARE_CERTIFICATES_ENABLE' => Config::get()->COURSEWARE_CERTIFICATES_ENABLE, + 'PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED' => + (bool) User::findCurrent()->getConfiguration()->PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED, ]) ?>, } @@ -90,9 +92,12 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']); asImg(24) ?> - + diff --git a/templates/loginform.php b/templates/loginform.php index 69fc1f0..da717eb 100644 --- a/templates/loginform.php +++ b/templates/loginform.php @@ -32,20 +32,14 @@ $show_hidden_login = !$show_login && StudipAuthAbstract::isLoginEnabled();
- - - %1$s'), - $GLOBALS['UNI_CONTACT'] - ) - ]) ?> - - -
+ +

UNI_NAME_CLEAN) ?>

@@ -139,7 +133,6 @@ $show_hidden_login = !$show_login && StudipAuthAbstract::isLoginEnabled();
-