From 551d2de1771bfc6dac6b1c53c8cc97c26d04de75 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Tue, 26 Sep 2023 07:25:43 +0000
Subject: [PATCH] =?UTF-8?q?=C3=9Cberarbeitung=20der=20Verwaltungsseite:=20?=
 =?UTF-8?q?Werkzeuge?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #3089

Merge request studip/studip!2070
---
 .../components/ContentModulesEditTiles.vue    | 240 +++++++++++++-----
 .../components/ContentmodulesEditTable.vue    | 138 +++++++---
 resources/vue/mixins/ContentModulesMixin.js   |  15 +-
 3 files changed, 286 insertions(+), 107 deletions(-)

diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue
index 70f7da5f3bb..5aa5bf04a96 100644
--- a/resources/vue/components/ContentModulesEditTiles.vue
+++ b/resources/vue/components/ContentModulesEditTiles.vue
@@ -1,66 +1,149 @@
 <template>
-    <draggable v-model="sortedModules" handle=".dragarea">
-        <transition-group name="admin_contentmodules"
-                          class="admin_contentmodules studip-grid"
-                          tag="div"
-                          role="listbox"
+    <div class="content-modules-wrapper">
+        <draggable v-model="sortedModules" handle=".dragarea">
+            <transition-group
+                name="admin_contentmodules"
+                class="admin_contentmodules studip-grid"
+                tag="div"
+                role="listbox"
+            >
+                <div
+                    v-for="module in activeModules"
+                    :key="module.id"
+                    role="option"
+                    class="studip-grid-element"
+                    :class="getModuleCSSClasses(module, activated[module.id])"
+                    v-cloak
+                >
+                    <div>
+                        <a class="upper_part dragarea" :href="getDescriptionURL(module)" data-dialog>
+                            <div>
+                                <img :src="module.icon" width="40" height="40" v-if="module.icon" />
+                            </div>
+                            <div>
+                                <h3>{{ module.displayname }}</h3>
+                                {{ module.summary }}
+                            </div>
+                        </a>
+                        <div class="down_part">
+                            <div>
+                                <a
+                                    class="dragarea"
+                                    tabindex="0"
+                                    :aria-label="
+                                        $gettextInterpolate(
+                                            $gettext(
+                                                'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
+                                            ),
+                                            { module: module.displayname }
+                                        )
+                                    "
+                                    @keydown="keyboardHandler($event, module)"
+                                    v-if="filterCategory === null"
+                                    :ref="`draghandle-${module.id}`"
+                                >
+                                    <span class="drag-handle"></span>
+                                </a>
+                                <label v-if="!module.mandatory">
+                                    <input
+                                        type="checkbox"
+                                        :checked="activated[module.id]"
+                                        @click="toggleModule(module)"
+                                        :ref="'checkbox_' + module.id"
+                                    />
+                                    {{ $gettext('Werkzeug ist aktiv') }}
+                                </label>
+                            </div>
+
+                            <div class="icons_right">
+                                <a
+                                    href="#"
+                                    class="toggle_visibility"
+                                    role="checkbox"
+                                    v-if="!module.mandatory"
+                                    :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
+                                    @click.prevent="toggleModuleVisibility(module)"
+                                >
+                                    <studip-icon
+                                        :shape="
+                                            module.visibility !== 'tutor'
+                                                ? 'visibility-visible'
+                                                : 'visibility-invisible'
+                                        "
+                                        class="text-bottom"
+                                        :title="
+                                            $gettextInterpolate(
+                                                $gettext(
+                                                    'Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
+                                                ),
+                                                { name: module.displayname }
+                                            )
+                                        "
+                                    ></studip-icon>
+                                </a>
+                                <a :href="getRenameURL(module)" data-dialog="size=medium">
+                                    <studip-icon
+                                        shape="edit"
+                                        class="text-bottom"
+                                        :title="
+                                            $gettextInterpolate(
+                                                $gettext(
+                                                    'Umbenennen des Inhaltsmoduls %{ name }'
+                                                ),
+                                                { name: module.displayname }
+                                            )
+                                        "
+                                    ></studip-icon>
+                                </a>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </transition-group>
+        </draggable>
+        <transition-group
+            name="admin_contentmodules"
+            class="admin_contentmodules studip-grid inactive-modules"
+            tag="div"
+            role="listbox"
         >
-            <div v-for="module in sortedModules"
-                 :key="module.id"
-                 role="option"
-                 class="studip-grid-element"
-                 :class="getModuleCSSClasses(module, activated[module.id])"
-                 v-cloak
+            <div
+                v-for="module in inactiveModules"
+                :key="module.id"
+                role="option"
+                class="studip-grid-element"
+                :class="getModuleCSSClasses(module, activated[module.id])"
+                v-cloak
             >
                 <div>
-                    <a :class="'upper_part' + (module.active && filterCategory === null ? ' dragarea' : '')" :href="getDescriptionURL(module)" data-dialog>
+                    <a class="upper_part" :href="getDescriptionURL(module)" data-dialog>
                         <div>
-                            <img :src="module.icon" width="40" height="40" v-if="module.icon">
+                            <img :src="module.icon" width="40" height="40" v-if="module.icon" />
                         </div>
                         <div>
-                            <h3>{{module.displayname}}</h3>
-                            {{module.summary}}
+                            <h3>{{ module.displayname }}</h3>
+                            {{ module.summary }}
                         </div>
                     </a>
                     <div class="down_part">
                         <div>
-                            <a class="dragarea"
-                               tabindex="0"
-                               :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})"
-                               @keydown="keyboardHandler($event, module)"
-                               v-if="module.active && filterCategory === null"
-                               :ref="`draghandle-${module.id}`">
-                                <span class="drag-handle"></span>
-                            </a>
                             <label v-if="!module.mandatory">
-                                <input type="checkbox" :checked="activated[module.id]" @click="toggleModule(module)" :ref="'checkbox_' + module.id">
-                                {{ module.active ? $gettext('Werkzeug ist aktiv') : $gettext('Werkzeug ist inaktiv') }}
+                                <input
+                                    type="checkbox"
+                                    :checked="activated[module.id]"
+                                    @click="toggleModule(module)"
+                                    :ref="'checkbox_' + module.id"
+                                />
+                                {{ $gettext('Werkzeug ist inaktiv') }}
                             </label>
                         </div>
-
-                        <div class="icons_right">
-                            <a href="#"
-                               class="toggle_visibility"
-                               role="checkbox"
-                               v-if="module.active && !module.mandatory"
-                               :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
-                               @click.prevent="toggleModuleVisibility(module)">
-                                <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
-                                             class="text-bottom"
-                                             :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon>
-                            </a>
-                            <a :href="getRenameURL(module)" data-dialog="size=medium" v-if="module.active">
-                                <studip-icon shape="edit"
-                                             class="text-bottom"
-                                             :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon>
-                            </a>
-                        </div>
                     </div>
                 </div>
             </div>
         </transition-group>
-    </draggable>
+    </div>
 </template>
+
 <script>
 import Vue from 'vue';
 import { mapState } from 'vuex';
@@ -74,42 +157,41 @@ export default {
         timeouts: {},
     }),
     computed: {
-        ...mapState('contentmodules', [
-            'modules'
-        ]),
+        ...mapState('contentmodules', ['modules']),
     },
     methods: {
         toggleModule(module) {
             Vue.set(this.activated, module.id, !this.activated[module.id]);
-
-            if (this.timeouts[module.id] ?? null) {
-                clearTimeout(this.timeouts[module.id] ?? null);
-                this.timeouts[module.id] = null;
-            }  else {
-                this.timeouts[module.id] = setTimeout(() => {
-                    this.toggleModuleActivation(module);
-                    this.timeouts[module.id] = null;
-                }, 700);
-            }
+            this.toggleModuleActivation(module);
         },
     },
     watch: {
         modules: {
             immediate: true,
             handler(current) {
-                current.forEach(module => Vue.set(this.activated, module.id, module.active));
-            }
-        }
-    }
-}
+                current.forEach((module) => Vue.set(this.activated, module.id, module.active));
+            },
+        },
+    },
+};
 </script>
+
 <style lang="scss" scoped>
+.content-modules-wrapper {
+    max-width: 1410px;
+}
+.inactive-modules {
+    margin-top: 1em;
+    border-top: solid thin var(--content-color-40);
+    padding-top: 1em;
+}
 .studip-grid-element {
     display: flex;
     flex-direction: row;
     background-color: var(--white);
     border-left: 1px solid var(--dark-gray-color-60);
-    transition: all var(--transition-duration) ease;
+    margin: 2px 0;
+
     &.visibility-visible {
         border-left-color: var(--green);
         > div {
@@ -122,6 +204,38 @@ export default {
             border-left-color: var(--yellow);
         }
     }
+
+    &.sortable-ghost {
+        border: dashed 2px var(--content-color-40);
+        margin: 0;
+        * {
+            opacity: 0;
+        }
+    }
+    &.pulse:not(.sortable-ghost) {
+        box-shadow: 0 0 0 0 rgb(255, 189, 51, 1);
+        animation: pulse 2s;
+        animation-iteration-count: 1;
+    }
+
+    @keyframes pulse {
+        0% {
+            box-shadow: 0 0 0 0 rgba(255, 189, 51, 1);
+        }
+        25% {
+            box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.8);
+        }
+        50% {
+            box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.6);
+        }
+        75% {
+            box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.4);
+        }
+        100% {
+            box-shadow: 0 0 0 0 rgba(255, 189, 51, 0);
+        }
+    }
+
     > div {
         display: flex;
         flex-direction: column;
diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue
index 28e28435af5..a9ab50c2a70 100644
--- a/resources/vue/components/ContentmodulesEditTable.vue
+++ b/resources/vue/components/ContentmodulesEditTable.vue
@@ -1,69 +1,116 @@
 <template>
     <table class="admin_contentmodules table default">
         <colgroup>
-            <col style="width: 20px" v-if="filterCategory === null">
-            <col style="width: 20px">
-            <col>
-            <col style="width: 24px">
+            <col style="width: 20px" v-if="filterCategory === null" />
+            <col style="width: 20px" />
+            <col />
+            <col style="width: 24px" />
         </colgroup>
         <thead>
-        <tr>
-            <th v-if="filterCategory === null"></th>
-            <th></th>
-            <th>{{ $gettext('Name') }}</th>
-            <th class="actions">{{ $gettext('Aktionen') }}</th>
-        </tr>
+            <tr>
+                <th v-if="filterCategory === null"></th>
+                <th></th>
+                <th>{{ $gettext('Name') }}</th>
+                <th class="actions">{{ $gettext('Aktionen') }}</th>
+            </tr>
         </thead>
-
         <draggable v-model="sortedModules" handle=".dragarea" tag="tbody">
-            <tr v-for="module in sortedModules"
-                :key="module.id"
-                :class="getModuleCSSClasses(module)"
-                v-cloak>
+            <tr v-for="module in activeModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak>
                 <td v-if="filterCategory === null">
-                    <a class="dragarea"
-                       tabindex="0"
-                       :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})"
-                       @keydown="keyboardHandler($event, module)"
-                       v-if="module.active"
-                       :ref="`draghandle-${module.id}`"
+                    <a
+                        class="dragarea"
+                        tabindex="0"
+                        :aria-label="
+                            $gettextInterpolate(
+                                $gettext(
+                                    'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
+                                ),
+                                { module: module.displayname }
+                            )
+                        "
+                        @keydown="keyboardHandler($event, module)"
+                        v-if="module.active"
+                        :ref="`draghandle-${module.id}`"
                     >
                         <span class="drag-handle"></span>
                     </a>
                 </td>
                 <td>
-                    <input type="checkbox"
-                           v-model="module.active"
-                           @click="toggleModuleActivation(module)"
-                           v-if="!module.mandatory"
-                           :ref="'checkbox_' + module.id">
+                    <input
+                        type="checkbox"
+                        v-model="module.active"
+                        @click="toggleModuleActivation(module)"
+                        v-if="!module.mandatory"
+                        :ref="'checkbox_' + module.id"
+                    />
                 </td>
                 <td>
-                    <a class="upper_part"
-                       :class="{ dragrea: module.active }"
-                       :href="getDescriptionURL(module)"
-                       data-dialog
+                    <a
+                        class="upper_part"
+                        :class="{ dragrea: module.active }"
+                        :href="getDescriptionURL(module)"
+                        data-dialog
                     >
-                        <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom">
+                        <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom" />
                         {{ module.displayname }}
                     </a>
                 </td>
                 <td class="actions">
-                    <a href="#"
-                       v-if="module.active && !module.mandatory"
-                       role="checkbox"
-                       :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
-                       @click.prevent="toggleModuleVisibility(module)">
-                        <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
-                                     class="text-bottom"
-                                     :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname })"></studip-icon>
+                    <a
+                        href="#"
+                        v-if="module.active && !module.mandatory"
+                        role="checkbox"
+                        :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
+                        @click.prevent="toggleModuleVisibility(module)"
+                    >
+                        <studip-icon
+                            :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
+                            class="text-bottom"
+                            :title="
+                                $gettextInterpolate(
+                                    $gettext(
+                                        'Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
+                                    ),
+                                    { name: module.displayname }
+                                )
+                            "
+                        ></studip-icon>
                     </a>
                     <a :href="getRenameURL(module)" data-dialog="size=auto" v-if="module.active">
-                        <studip-icon shape="edit" class="text-bottom" :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon>
+                        <studip-icon
+                            shape="edit"
+                            class="text-bottom"
+                            :title="
+                                $gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), {
+                                    name: module.displayname,
+                                })
+                            "
+                        ></studip-icon>
                     </a>
                 </td>
             </tr>
         </draggable>
+        <tbody>
+            <tr v-for="module in inactiveModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak>
+                <td></td>
+                <td>
+                    <input
+                        type="checkbox"
+                        v-model="module.active"
+                        @click="toggleModuleActivation(module)"
+                        v-if="!module.mandatory"
+                        :ref="'checkbox_' + module.id"
+                    />
+                </td>
+                <td>
+                    <a class="upper_part" :href="getDescriptionURL(module)" data-dialog>
+                        <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom" />
+                        {{ module.displayname }}
+                    </a>
+                </td>
+                <td></td>
+            </tr>
+        </tbody>
     </table>
 </template>
 
@@ -74,10 +121,17 @@ export default {
     name: 'contentmodules-edit-table',
 
     mixins: [ContentModulesMixin],
-}
+};
 </script>
 <style lang="scss">
-table.admin_contentmodules > tbody > tr  {
+@use '../../assets/stylesheets/mixins/colors.scss';
+
+table.admin_contentmodules > tbody > tr {
+    &.sortable-ghost {
+        * {
+            opacity: 0;
+        }
+    }
     > td:first-child {
         background-image: linear-gradient(var(--dark-gray-color-60), var(--dark-gray-color-60));
         background-repeat: no-repeat;
diff --git a/resources/vue/mixins/ContentModulesMixin.js b/resources/vue/mixins/ContentModulesMixin.js
index 7df586dda28..86a29ffb52a 100644
--- a/resources/vue/mixins/ContentModulesMixin.js
+++ b/resources/vue/mixins/ContentModulesMixin.js
@@ -19,6 +19,9 @@ export default {
         activeModules() {
             return this.sortedModules.filter(module => module.active);
         },
+        inactiveModules() {
+            return this.sortedModules.filter(module => !module.active);
+        },
         sortedModules: {
             get() {
                 return Object.values(this.modules)
@@ -89,6 +92,7 @@ export default {
             });
         },
         toggleModuleActivation(module) {
+            module.pulse = true;
             this.setModuleActive({
                 moduleId: module.id,
                 active: !module.active,
@@ -96,6 +100,7 @@ export default {
                 if (output.tabs) {
                     $('.tabs_wrapper').replaceWith(output.tabs);
                 }
+                module.pulse = false;
             });
         },
         toggleModuleVisibility(module) {
@@ -115,11 +120,17 @@ export default {
             return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/info/${module.id}`);
         },
         getModuleCSSClasses(module, active= null) {
+            let classes = [];
+            classes.push(module.pulse ? 'pulse' : '');
+
             if (!(active ?? module.active)) {
-                return 'inactive';
+                classes.push('inactive');
+            } else {
+                classes.push(module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible');
             }
 
-            return module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible';
+
+            return classes.join(' ');
         },
     },
 };
-- 
GitLab