diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2026-01-15 17:10:04 +0100 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2026-01-15 17:10:04 +0100 |
| commit | c3e07e221b0bef64d3ad4da48c6371c75ca12cc3 (patch) | |
| tree | a2da9becb70c334b38cdf429f77257167d37993b /resources/vue | |
| parent | 083da68644d230b8a4b1d8201ed832a5206d3d5c (diff) | |
Resolve "Neues Forum Polishing", #6064, #6063, #6151
Closes #6165
Merge request studip/studip!4671
Diffstat (limited to 'resources/vue')
40 files changed, 1124 insertions, 936 deletions
diff --git a/resources/vue/apps/forum/categories/Edit.vue b/resources/vue/apps/forum/categories/Edit.vue index b3ca722..742c637 100644 --- a/resources/vue/apps/forum/categories/Edit.vue +++ b/resources/vue/apps/forum/categories/Edit.vue @@ -1,6 +1,6 @@ <script setup> -import {computed, onMounted, reactive, useTemplateRef} from "vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import {computed, onMounted, reactive, useTemplateRef} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; const CSRF = STUDIP.CSRF_TOKEN; @@ -10,7 +10,7 @@ const props = defineProps({ } }); -const categoryForm = reactive({ +const form = reactive({ ...props.category }); @@ -22,7 +22,7 @@ const formActionURL = computed(() => { return STUDIP.URLHelper.getURL(`dispatch.php/course/forum/categories/save`); }); -const nameInput = useTemplateRef('name-input'); +const nameInput = useTemplateRef('nameInput'); onMounted(() => { nameInput.value.focus(); @@ -53,8 +53,8 @@ onMounted(() => { required type="text" name="name" - ref="name-input" - v-model="categoryForm.name" + ref="nameInput" + v-model="form.name" class="max-w-full" /> </label> </section> @@ -62,7 +62,7 @@ onMounted(() => { <section> <label> {{ $gettext('Beschreibung') }} - <textarea rows="5" name="description" v-model="categoryForm.description"></textarea> + <textarea rows="5" name="description" v-model="form.description"></textarea> </label> </section> @@ -74,7 +74,7 @@ onMounted(() => { <input type="color" name="color" - v-model="categoryForm.color" /> + v-model="form.color" /> </label> </section> </fieldset> diff --git a/resources/vue/apps/forum/categories/Index.vue b/resources/vue/apps/forum/categories/Index.vue index 4329bc3..62a3397 100644 --- a/resources/vue/apps/forum/categories/Index.vue +++ b/resources/vue/apps/forum/categories/Index.vue @@ -1,18 +1,19 @@ <script setup> -import {computed, nextTick, onMounted, ref} from "vue"; -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import draggable from "vuedraggable"; -import { default as CreateCategory } from "@/vue/components/forum/categories/Create.vue"; -import CategoryItem from "@/vue/components/forum/categories/CategoryItem.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import {useSortable} from "../../../composables/useSortable"; import {debounce} from 'lodash'; +import Draggable from 'vuedraggable'; +import {computed, nextTick, onMounted, ref} from 'vue'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import { default as CreateCategory } from '@/vue/components/forum/categories/Create.vue'; +import CategoryItem from '@/vue/components/forum/categories/CategoryItem.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import {useSortable} from '@/vue/composables/useSortable'; const forumConfig = useForumConfig(); +const currentCategory = ref(null); const categories = ref([]); const pagination = ref({}); @@ -55,11 +56,11 @@ const fetchCategories = async (_, offset = 0) => { const updateCategoriesOrder = async () => { try { - const category_ids = sortedCategories.value.map(({ id }) => id); + const categoryIds = sortedCategories.value.map(({ id }) => id); const data = { attributes: { - 'category-ids': category_ids + 'category-ids': categoryIds }, relationships: { range: { @@ -109,6 +110,8 @@ const swapCategory = (categoryId, step) => { updateOrderDebounced(); }); } + +const showCategoryDialog = category => currentCategory.value = category; </script> <template> @@ -159,12 +162,16 @@ const swapCategory = (categoryId, step) => { tag="ul"> <template #item="{element}"> <li> - <CategoryItem :category="element" @swapCategory="swapCategory" /> + <CategoryItem + :category="element" + @swapCategory="swapCategory" + @showCategory="showCategoryDialog(element)" + /> </li> </template> <template v-if="forumConfig.isModerator" #footer> <li key="footer"> - <div class="topic-card --new-topic"> + <div class="topic-card topic-card--new-topic"> <CreateCategory class="--with-label" :label="$gettext('Neue Kategorie anlegen')" @@ -174,7 +181,7 @@ const swapCategory = (categoryId, step) => { </template> </draggable> <div v-else-if="forumConfig.isModerator" class="topic-cards-container"> - <div class="topic-card --new-topic"> + <div class="topic-card topic-card--new-topic"> <CreateCategory class="--with-label" :label="$gettext('Neue Kategorie anlegen')" @@ -182,7 +189,7 @@ const swapCategory = (categoryId, step) => { </div> </div> </div> - <table v-else class="default forum-table --topics-index"> + <table v-else class="default forum-table forum-table--topics-index"> <colgroup> <col> <col style="width: 15%;"> @@ -198,76 +205,87 @@ const swapCategory = (categoryId, step) => { :aria-sort="getAriaSortString('name')" :aria-label="getAriaSortLabel('name', $gettext('Name'))" > - <a - href="#" - @click.prevent="sortBy('name')" + <button + type="button" + class="as-link" + @click="sortBy('name')" :title="$gettext('Nach Name sortieren')"> {{ $gettext('Name') }} - </a> + </button> </th> <th :class="getSortClass('meta.discussions_count')" :aria-sort="getAriaSortString('meta.discussions_count')" :aria-label="getAriaSortLabel('meta.discussions_count', $gettext('Anzahl der Diskussionen'))" > - <a - href="#" - @click.prevent="sortBy('meta.discussions_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.discussions_count')" :title="$gettext('Nach Anzahl der Diskussionen sortieren')"> {{ $gettext('Diskussionen') }} - </a> + </button> </th> <th :class="getSortClass('meta.users_count')" :aria-sort="getAriaSortString('meta.users_count')" :aria-label="getAriaSortLabel('meta.users_count', $gettext('Anzahl der Teilnehmenden'))" > - <a - href="#" - @click.prevent="sortBy('meta.users_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.users_count')" :title="$gettext('Nach Anzahl der Teilnehmenden sortieren')"> {{ $gettext('Teilnehmende') }} - </a> + </button> </th> <th :class="getSortClass('meta.postings_count')" :aria-sort="getAriaSortString('meta.postings_count')" :aria-label="getAriaSortLabel('meta.postings_count', $gettext('Anzahl der Beiträge'))" > - <a - href="#" - @click.prevent="sortBy('meta.postings_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.postings_count')" :title="$gettext('Nach Anzahl der Beiträge sortieren')"> {{ $gettext('Beiträge') }} - </a> + </button> </th> <th :class="getSortClass('meta.recent_activity')" :aria-sort="getAriaSortString('meta.recent_activity')" :aria-label="getAriaSortLabel('meta.recent_activity', $gettext('Letzte Aktivität'))" > - <a - href="#" + <button + type="button" + class="as-link" @click.prevent="sortBy('meta.recent_activity')" :title="$gettext('Nach letzter Aktivität sortieren')"> {{ $gettext('Letzte Aktivität') }} - </a> + </button> </th> <th></th> </tr> </thead> - <draggable + <Draggable + v-if="sortedCategories.length" v-model="sortedCategories" item-key="category_id" :animation="200" - v-if="sortedCategories.length" @end="updateCategoriesOrder" :disabled="!forumConfig.isModerator" + handle=".drag-handle" tag="tbody"> <template #item="{element}"> - <CategoryItem :category="element" render-type="tr" @swapCategory="swapCategory" /> + <CategoryItem + renderType="tr" + :category="element" + @swapCategory="swapCategory" + @showCategory="showCategoryDialog(element)" + /> </template> - </draggable> + </Draggable> <tbody v-else> <tr> <td colspan="6"> diff --git a/resources/vue/apps/forum/categories/Show.vue b/resources/vue/apps/forum/categories/Show.vue index a9e7ab8..2d5d5b2 100644 --- a/resources/vue/apps/forum/categories/Show.vue +++ b/resources/vue/apps/forum/categories/Show.vue @@ -1,14 +1,14 @@ <script setup> -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import StudipDateTime from "../../../components/StudipDateTime.vue"; -import TopicsIndex from "@/vue/components/forum/topics/TopicsIndex.vue"; -import CreateTopic from "@/vue/components/forum/topics/CreateTopic.vue"; -import {computed, onMounted, ref} from "vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; +import {computed, onMounted, ref} from 'vue'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; +import TopicsIndex from '@/vue/components/forum/topics/TopicsIndex.vue'; +import CreateTopic from '@/vue/components/forum/topics/CreateTopic.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; const forumConfig = useForumConfig(); @@ -72,16 +72,19 @@ onMounted(async () => { {{ category.name }} </h2> <div class="mt-10 inline-flex gap-20 items-center"> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden an der Diskussion')" :aria-label="$gettext('Anzahl der Teilnehmenden an der Diskussion')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden an der Diskussion')" role="group"> <StudipIcon shape="community2" :size="15" role="info" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Teilnehmenden an der Diskussion') }}:</span> <small>{{ metadata.users_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" :size="15" role="info" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Beiträge') }}:</span> <small>{{ metadata.postings_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" :size="15" role="info" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Letzte Aktivität') }}:</span> <StudipDateTime v-if="metadata.recent_activity" :iso="metadata.recent_activity" :relative="true" /> <template v-else>{{ $gettext('Keine Aktivität') }}</template> </span> diff --git a/resources/vue/apps/forum/configs/Edit.vue b/resources/vue/apps/forum/configs/Edit.vue index 8357e05..d82fe73 100644 --- a/resources/vue/apps/forum/configs/Edit.vue +++ b/resources/vue/apps/forum/configs/Edit.vue @@ -1,6 +1,6 @@ <script setup> -import {reactive} from "vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import {reactive} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; const CSRF = STUDIP.CSRF_TOKEN; @@ -10,7 +10,7 @@ const props = defineProps({ } }); -const formState = reactive({ +const form = reactive({ ...props.config }); @@ -32,7 +32,7 @@ const formActionURL = STUDIP.URLHelper.getURL(`dispatch.php/course/forum/configs <section> <label> {{ $gettext('Wer darf das Forum moderieren?') }} - <select name="moderator" v-model="formState.moderator"> + <select name="moderator" v-model="form.moderator"> <option value="all"> {{ $gettext('Alle Teilnehmenden der Veranstaltung') }} </option> @@ -51,7 +51,7 @@ const formActionURL = STUDIP.URLHelper.getURL(`dispatch.php/course/forum/configs type="checkbox" :aria-label="$gettext('Kategorien ausblenden')" name="categories_navigation" - v-model="formState.categories_navigation" + v-model="form.categories_navigation" value="1" /> <span> diff --git a/resources/vue/apps/forum/discussions/Edit.vue b/resources/vue/apps/forum/discussions/Edit.vue index 68ef00f..df63764 100644 --- a/resources/vue/apps/forum/discussions/Edit.vue +++ b/resources/vue/apps/forum/discussions/Edit.vue @@ -1,26 +1,28 @@ <script setup> -import {computed, onMounted, reactive, useTemplateRef} from "vue"; -import SelectTopicInput from "@/vue/components/forum/topics/SelectTopicInput.vue"; -import SelectDiscussionType from "@/vue/components/forum/discussions/SelectDiscussionType.vue"; -import SelectTagsInput from "@/vue/components/forum/SelectTagsInput.vue"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import StudipWysiwyg from "../../../components/StudipWysiwyg.vue"; -import StudipSwitch from "../../../components/StudipSwitch.vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {computed, onMounted, reactive, useTemplateRef} from 'vue'; +import SelectTopicInput from '@/vue/components/forum/topics/SelectTopicInput.vue'; +import SelectDiscussionType from '@/vue/components/forum/discussions/SelectDiscussionType.vue'; +import SelectTagsInput from '@/vue/components/forum/SelectTagsInput.vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipWysiwyg from '@/vue/components/StudipWysiwyg.vue'; +import StudipSwitch from '@/vue/components/StudipSwitch.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); + const CSRF = STUDIP.CSRF_TOKEN; const props = defineProps({ discussion: { type: Object, + default: () => ({}) }, topics: { type: Array, required: true }, - discussion_types: { + discussionTypes: { type: Array, required: true }, @@ -35,7 +37,7 @@ const discussionForm = reactive({ closed_at: Boolean(props.discussion.closed_at), sticky: Boolean(props.discussion.sticky), topic: props.topics.find(({ topic_id }) => topic_id === props.discussion.topic_id), - type: props.discussion_types.find(({ type_id }) => type_id === parseInt(props.discussion.type_id)) + type: props.discussionTypes.find(({ type_id }) => type_id === parseInt(props.discussion.type_id)) }); const formActionURL = computed(() => { @@ -55,7 +57,7 @@ const availableTags = computed(() => { return props.tags; }); -const titleInput = useTemplateRef('title-input'); +const titleInput = useTemplateRef('titleInput'); onMounted(() => { titleInput.value.focus(); @@ -86,7 +88,7 @@ onMounted(() => { required type="text" name="title" - ref="title-input" + ref="titleInput" v-model="discussionForm.title" class="max-w-full" /> </label> @@ -137,7 +139,7 @@ onMounted(() => { </label> <SelectDiscussionType id="select-discussion-type" - :options="discussion_types" + :options="discussionTypes" v-model="discussionForm.type" /> <input v-if="discussionForm.type" type="hidden" name="type_id" :value="discussionForm.type.type_id"> diff --git a/resources/vue/apps/forum/discussions/Index.vue b/resources/vue/apps/forum/discussions/Index.vue index 26c29ae..7315a2a 100644 --- a/resources/vue/apps/forum/discussions/Index.vue +++ b/resources/vue/apps/forum/discussions/Index.vue @@ -1,18 +1,18 @@ <script setup> -import {onMounted, ref} from "vue"; -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import DiscussionIndex from "@/vue/components/forum/discussions/DiscussionIndex.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import { default as CreateDiscussion } from "@/vue/components/forum/discussions/Create.vue"; -import StudipDateTime from "../../../components/StudipDateTime.vue"; -import StudipIcon from "../../../components/StudipIcon.vue"; +import {onMounted, ref} from 'vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import DiscussionIndex from '@/vue/components/forum/discussions/DiscussionIndex.vue'; +import { default as CreateDiscussion } from '@/vue/components/forum/discussions/Create.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; defineProps({ metadata: { type: Object, - required: true, + required: true } }); @@ -60,16 +60,19 @@ onMounted(async () => { </h2> <div class="mt-10 inline-flex gap-20 items-center"> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden')" :aria-label="$gettext('Anzahl der Teilnehmenden')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden')" role="group"> <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Teilnehmenden') }}:</span> <small>{{ metadata.users_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true"/> + <span class="sr-only">{{ $gettext('Anzahl der Beiträge') }}:</span> <small>{{ metadata.postings_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Letzte Aktivität') }}:</span> <StudipDateTime v-if="metadata.recent_activity" :iso="metadata.recent_activity" :relative="true" /> <template v-else>{{ $gettext('Keine Aktivität') }}</template> </span> diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue index ca1cabc..35c81f2 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -27,11 +27,11 @@ const props = defineProps({ type: Object, required: true, }, - auth_user: { + authUser: { type: Object, required: true, }, - read_index: { + readIndex: { type: Number, required: true, default: 0 @@ -133,11 +133,11 @@ onMounted(async () => { postCreateForm.value = true; } jumpTo(document.getElementById(urlHash)) - } else if (props.read_index < posts.value.length) { - if (props.read_index === 0) { + } else if (props.readIndex < posts.value.length) { + if (props.readIndex === 0) { jumpTo(document.getElementById('discussion_start')); } else { - jumpTo(document.querySelector(`[data-index='${props.read_index}']`)); + jumpTo(document.querySelector(`[data-index='${props.readIndex}']`)); } } @@ -208,11 +208,12 @@ onMounted(async () => { </button> <SubscriptionDropdown v-if="!discussion.closed_at" + :context="discussion.title" :subject="{ id: discussion.discussion_id, type: 'forum-discussions' }" - :user_subscription="auth_user.subscription" + :userSubscription="authUser.subscription" /> </template> </div> @@ -220,7 +221,7 @@ onMounted(async () => { </header> <div class="discussion"> <template v-if="posts[0]"> - <Post :post="posts[0]" :auth_user="auth_user" :discussion="discussion" :readIndex="read_index" /> + <Post :post="posts[0]" :authUser="authUser" :discussion="discussion" :readIndex="readIndex" /> </template> <div v-else class="discussion__body"> <Loader v-if="isLoading" /> @@ -232,7 +233,7 @@ onMounted(async () => { <DiscussionFooter :discussion="discussion" :posts="posts" - :read_index="read_index" + :readIndex="readIndex" v-model:postCreateForm="postCreateForm" /> <hr class="m-0" /> @@ -241,10 +242,10 @@ onMounted(async () => { <template v-for="(post, index) in posts.slice(1)" :key="post.id"> <Post :post="post" - :auth_user="auth_user" + :authUser="authUser" :discussion="discussion" :index="index + 1" - :readIndex="read_index" + :readIndex="readIndex" /> <hr v-if="index < posts.length - 2" class="divider m-0" /> </template> @@ -261,8 +262,8 @@ onMounted(async () => { <div id="new-post" class="post-form-container"> <PostCreateForm v-if="postCreateForm && !discussion.closed_at" - :discussion_id="discussion.discussion_id" - :auth_user="auth_user" + :discussionId="discussion.discussion_id" + :authUser="authUser" @canceled="postCreateForm = false" @created="addPost" /> diff --git a/resources/vue/apps/forum/discussions_types/Edit.vue b/resources/vue/apps/forum/discussions_types/Edit.vue index 40084ea..cb30c49 100644 --- a/resources/vue/apps/forum/discussions_types/Edit.vue +++ b/resources/vue/apps/forum/discussions_types/Edit.vue @@ -1,13 +1,14 @@ <script setup> -import {computed, reactive} from "vue"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import {getDiscussionTypeStoreURL} from "../../../components/forum/helpers/urls"; +import {computed, reactive} from 'vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {getDiscussionTypeStoreURL} from '@/vue/components/forum/helpers/urls'; const CSRF = STUDIP.CSRF_TOKEN; const props = defineProps({ discussionType: { type: Object, + default: () => ({}) }, icons: { type: Array, @@ -15,7 +16,7 @@ const props = defineProps({ } }); -const formState = reactive({ +const form = reactive({ ...props.discussionType }); @@ -43,8 +44,9 @@ const formActionURL = computed(() => getDiscussionTypeStoreURL(props.discussionT required type="text" name="name" - v-model="formState.name" - maxlength="100" /> + v-model="form.name" + maxlength="100" + /> </label> </section> @@ -55,7 +57,7 @@ const formActionURL = computed(() => getDiscussionTypeStoreURL(props.discussionT </span> </label> <div id="studip_icons" class="studip-icons-container"> - <input type="hidden" v-model="formState.icon" name="icon" /> + <input type="hidden" v-model="form.icon" name="icon" /> <template v-for="icon in icons" :key="icon"> <button @@ -63,10 +65,10 @@ const formActionURL = computed(() => getDiscussionTypeStoreURL(props.discussionT type="button" :title="icon" :class="{ - 'disabled': formState.icon && formState.icon !== icon, - 'active': formState.icon === icon + 'disabled': form.icon && form.icon !== icon, + 'active': form.icon === icon }" - @click="formState.icon = icon"> + @click="form.icon = icon"> <StudipIcon :shape="icon" :size="35" /> </button> </template> @@ -74,7 +76,7 @@ const formActionURL = computed(() => getDiscussionTypeStoreURL(props.discussionT </section> </fieldset> <footer data-dialog-button> - <button :disabled="!formState.icon || !formState.name" class="button accept"> + <button :disabled="!form.icon || !form.name" class="button accept"> {{ $gettext('Speichern') }} </button> <button class="button cancel" type="button" data-dialog-close> diff --git a/resources/vue/apps/forum/discussions_types/Index.vue b/resources/vue/apps/forum/discussions_types/Index.vue index 6dad3f9..a5db3fc 100644 --- a/resources/vue/apps/forum/discussions_types/Index.vue +++ b/resources/vue/apps/forum/discussions_types/Index.vue @@ -1,13 +1,13 @@ <script setup> -import {onMounted, ref} from "vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import {useSortable} from "../../../composables/useSortable"; -import {getDiscussionTypeEditURL} from "../../../components/forum/helpers/urls"; -import StudipActionMenu from "../../../components/StudipActionMenu.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import Loader from "../../../components/forum/Loader.vue"; +import {onMounted, ref} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {useSortable} from '@/vue/composables/useSortable'; +import {getDiscussionTypeEditURL} from '@/vue/components/forum/helpers/urls'; +import StudipActionMenu from '@/vue/components/StudipActionMenu.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import Loader from '@/vue/components/forum/Loader.vue'; const discussionTypes = ref([]); const pagination = ref({}); @@ -43,8 +43,18 @@ const fetchDiscussionTypes = async (_, offset = 0) => { } } -const editType = type => STUDIP.Dialog.fromURL( - getDiscussionTypeEditURL(type.id), +const addType = () => { + STUDIP.Dialog.fromURL( + getDiscussionTypeEditURL(), + { + width: '700', + height: '650' + } + ); +} + +const editType = id => STUDIP.Dialog.fromURL( + getDiscussionTypeEditURL(id), { width: '700', height: '650' @@ -85,9 +95,14 @@ onMounted(() => { <caption> {{ $gettext('Diskussionstypen') }} <span class="actions"> - <a :href="getDiscussionTypeEditURL()" data-dialog="width=700;height=650" :title="$gettext('Neue Diskussionstyp anlegen')"> + <button + type="button" + class="button button--icon-only" + @click="addType" + :title="$gettext('Neuen Diskussionstyp anlegen')" + > <StudipIcon shape="add" aria-hidden="true" /> - </a> + </button> </span> </caption> @@ -105,12 +120,13 @@ onMounted(() => { :aria-sort="getAriaSortString('name')" :aria-label="getAriaSortLabel('name', $gettext('Name'))" > - <a - href="#" - @click.prevent="sortBy('name')" + <button + type="button" + class="as-link" + @click="sortBy('name')" :title="$gettext('Nach Name sortieren')"> {{ $gettext('Name') }} - </a> + </button> </th> <th>{{ $gettext('Aktionen') }}</th> @@ -122,19 +138,21 @@ onMounted(() => { <StudipIcon :shape="type.icon" role="info" :size="24" aria-hidden="true" /> </td> <td> - <a - :href="getDiscussionTypeEditURL(type.id)" - data-dialog="width=700;height=650" + <button + type="button" + class="as-link" + @click="editType(type.id)" :title="$gettext('Diskussionstyp bearbeiten')" > {{ type.name }} - </a> + </button> </td> <td class="actions"> <StudipActionMenu + :context="type.name" :items="actionMenusItems" - @edit="editType(type)" + @edit="editType(type.id)" @delete="deleteType(type)" /> </td> diff --git a/resources/vue/apps/forum/recent/Index.vue b/resources/vue/apps/forum/recent/Index.vue index aa0f327..b362e90 100644 --- a/resources/vue/apps/forum/recent/Index.vue +++ b/resources/vue/apps/forum/recent/Index.vue @@ -1,12 +1,12 @@ <script setup> -import {onMounted, ref} from "vue"; -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import DiscussionIndex from "@/vue/components/forum/discussions/DiscussionIndex.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; +import {onMounted, ref} from 'vue'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import DiscussionIndex from '@/vue/components/forum/discussions/DiscussionIndex.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; const props = defineProps({ - last_visit: { + lastVisit: { type: Number, required: true } @@ -25,7 +25,7 @@ const fetchDiscussions = async (_, offset = 0) => { data: { include: 'category,discussion-type,members,tags,user&fields[users]=id', filter: { - 'last-visit': props.last_visit + 'last-visit': props.lastVisit }, page: { offset } } @@ -38,7 +38,7 @@ const fetchDiscussions = async (_, offset = 0) => { links: response.links }; - discussions.value = await deserializeJSONAPIResponse(response) + discussions.value = await deserializeJSONAPIResponse(response); } catch (error) { STUDIP.Report.error(error); } finally { diff --git a/resources/vue/apps/forum/search/Index.vue b/resources/vue/apps/forum/search/Index.vue index ba5d9a8..42f9ce1 100644 --- a/resources/vue/apps/forum/search/Index.vue +++ b/resources/vue/apps/forum/search/Index.vue @@ -1,18 +1,18 @@ <script setup> -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import {computed, onMounted, reactive, ref} from "vue"; -import SelectTopicInput from "@/vue/components/forum/topics/SelectTopicInput.vue"; -import SelectTagsInput from "@/vue/components/forum/SelectTagsInput.vue"; -import SelectDiscussionType from "@/vue/components/forum/discussions/SelectDiscussionType.vue"; -import {getTopicURL} from "@/vue/components/forum/helpers/urls"; -import SelectUserInput from "@/vue/components/forum/SelectUserInput.vue"; -import DiscussionIndex from "@/vue/components/forum/discussions/DiscussionIndex.vue"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import StudipSelect from "../../../components/StudipSelect.vue"; -import {highlightText, removeHighlight} from "@/vue/components/forum/helpers"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {computed, onMounted, reactive, ref} from 'vue'; +import SelectTopicInput from '@/vue/components/forum/topics/SelectTopicInput.vue'; +import SelectTagsInput from '@/vue/components/forum/SelectTagsInput.vue'; +import SelectDiscussionType from '@/vue/components/forum/discussions/SelectDiscussionType.vue'; +import {getTopicURL} from '@/vue/components/forum/helpers/urls'; +import SelectUserInput from '@/vue/components/forum/SelectUserInput.vue'; +import DiscussionIndex from '@/vue/components/forum/discussions/DiscussionIndex.vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; +import {highlightText, removeHighlight} from '@/vue/components/forum/helpers'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; const discussionStatuses = [ { @@ -40,7 +40,7 @@ const props = defineProps({ type: Array, required: true }, - discussion_types: { + discussionTypes: { type: Array, required: true }, @@ -48,7 +48,7 @@ const props = defineProps({ type: Array, required: true }, - course_members: { + courseMembers: { type: Array, required: true } @@ -66,8 +66,8 @@ const searchForm = reactive({ ...(props.filter.status && { status: discussionStatuses.find(status => status.value === props.filter.status) }), ...(props.filter.topic_ids && { topics: props.topics.filter(({ topic_id }) => props.filter.topic_ids.includes(topic_id)) }), ...(props.filter.tag_ids && { tags: props.tags.filter(({ id }) => props.filter.tag_ids.includes(id.toString())) }), - ...(props.filter.type_ids && { types: props.discussion_types.filter(({ type_id }) => props.filter.type_ids.includes(type_id.toString())) }), - ...(props.filter.user_ids && { authors: props.course_members.filter(({ user_id }) => props.filter.user_ids.includes(user_id)) }), + ...(props.filter.type_ids && { types: props.discussionTypes.filter(({ type_id }) => props.filter.type_ids.includes(type_id.toString())) }), + ...(props.filter.user_ids && { authors: props.courseMembers.filter(({ user_id }) => props.filter.user_ids.includes(user_id)) }), }); const availableTags = computed(() => { @@ -91,10 +91,10 @@ const availableTopics = computed(() => { const availableTypes = computed(() => { if (searchForm.types && searchForm.types.length > 0) { const selectedTypesId = searchForm.types.map(({ type_id }) => type_id); - return props.discussion_types.filter(({ type_id }) => selectedTypesId.indexOf(type_id) < 0); + return props.discussionTypes.filter(({ type_id }) => selectedTypesId.indexOf(type_id) < 0); } - return props.discussion_types; + return props.discussionTypes; }); const resetSearchForm = () => { @@ -174,7 +174,7 @@ onMounted(async () => { await fetchDiscussions(); } - if(searchForm.keyword.length > 1 && discussions.value.length) { + if (searchForm.keyword?.length > 1 && discussions.value.length) { highlightText(searchForm.keyword, '.discussion-title'); // remove highlights @@ -258,7 +258,6 @@ onMounted(async () => { type="button" class="toggle-filter-button button-base" :title="isFilterVisible ? $gettext('Erweiterte Filter zuklappen') : $gettext('Erweiterte Filter aufklappen')" - :aria-label="isFilterVisible ? $gettext('Erweiterte Filter zuklappen') : $gettext('Erweiterte Filter aufklappen')" :aria-expanded="isFilterVisible.toString()" > {{ $gettext('Erweiterte Filter') }} @@ -318,8 +317,8 @@ onMounted(async () => { </StudipSelect> </div> <div class="date-inputs-container"> - <input type="date" v-model="searchForm.begin" :placeholder="$gettext('Von')" :aria-label="$gettext('Von')" autocomplete="off" /> - <input type="date" v-model="searchForm.end" :placeholder="$gettext('Bis')" :aria-label="$gettext('Bis')" autocomplete="off" /> + <input type="date" v-model="searchForm.begin" :placeholder="$gettext('Von')" autocomplete="off" /> + <input type="date" v-model="searchForm.end" :placeholder="$gettext('Bis')" autocomplete="off" /> </div> <div> <label for="select-user-input" class="sr-only"> @@ -327,7 +326,7 @@ onMounted(async () => { </label> <SelectUserInput id="select-user-input" - :options="course_members" + :options="courseMembers" multiple v-model="searchForm.authors" /> diff --git a/resources/vue/apps/forum/subscriptions/Index.vue b/resources/vue/apps/forum/subscriptions/Index.vue index 175fa9d..98798a3 100644 --- a/resources/vue/apps/forum/subscriptions/Index.vue +++ b/resources/vue/apps/forum/subscriptions/Index.vue @@ -1,23 +1,23 @@ <script setup> -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import {onMounted, ref} from "vue"; -import {getDiscussionURL, getTopicURL} from "@/vue/components/forum/helpers/urls"; -import {useSortable} from "../../../composables/useSortable"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import StudipDateTime from "../../../components/StudipDateTime.vue"; -import SubscriptionDropdown from "@/vue/components/forum/SubscriptionDropdown.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import {subscriptionTransformer} from "../../../components/forum/helpers/transformers"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import Loader from "../../../components/forum/Loader.vue"; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import {onMounted, ref} from 'vue'; +import {getDiscussionURL, getTopicURL} from '@/vue/components/forum/helpers/urls'; +import {useSortable} from '@/vue/composables/useSortable'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; +import SubscriptionDropdown from '@/vue/components/forum/SubscriptionDropdown.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import {subscriptionTransformer} from '@/vue/components/forum/helpers/transformers'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import Loader from '@/vue/components/forum/Loader.vue'; const subscriptions = ref([]); const pagination = ref({}); const isLoading = ref(false); -const removeSubscription = subscription_id => { - subscriptions.value = subscriptions.value.filter(({ id }) => id !== subscription_id); +const removeSubscription = subscriptionId => { + subscriptions.value = subscriptions.value.filter(({ id }) => id !== subscriptionId); } const getSubjectLabel = type => { @@ -34,9 +34,9 @@ const getSubjectLabel = type => { const getSubscriptionDropdownTitle = type => { switch (type) { case 'forum-discussions': - return $gettext('Diskussion abonnieren'); + return $gettext('Diskussion'); case 'forum-topics': - return $gettext('Thema abonnieren'); + return $gettext('Thema'); default: return $gettext('Abonnieren'); } @@ -59,7 +59,7 @@ const fetchSubscribedDiscussions = async (_, offset = 0) => { links: response.links }; - const data = await deserializeJSONAPIResponse(response) + const data = await deserializeJSONAPIResponse(response); subscriptions.value = data.map(subscriptionTransformer); } catch (error) { @@ -91,14 +91,10 @@ onMounted(async () => { {{ $gettext('Abonnements') }} </h2> </div> - - <div class="actions"> - - </div> </div> </header> <div class="py-10"> - <table class="default forum-table --subscription-index"> + <table class="default forum-table forum-table--subscription-index"> <colgroup> <col> <col style="width: 5%"> @@ -113,12 +109,13 @@ onMounted(async () => { :aria-sort="getAriaSortString('subject.name')" :aria-label="getAriaSortLabel('subject.name', $gettext('Thema Name'))" > - <a - href="#" - @click.prevent="sortBy('subject.name')" + <button + type="button" + class="as-link" + @click="sortBy('subject.name')" :title="$gettext('Nach Thema Name sortieren')"> {{ $gettext('Thema') }} - </a> + </button> </th> <th></th> <th @@ -126,24 +123,26 @@ onMounted(async () => { :aria-sort="getAriaSortString('subject.type')" :aria-label="getAriaSortLabel('subject.type', $gettext('Typ'))" > - <a - href="#" - @click.prevent="sortBy('subject.type')" + <button + type="button" + class="as-link" + @click="sortBy('subject.type')" :title="$gettext('Nach Typ sortieren')"> {{ $gettext('Typ') }} - </a> + </button> </th> <th :class="getSortClass('mkdate')" :aria-sort="getAriaSortString('mkdate')" :aria-label="getAriaSortLabel('mkdate', $gettext('Abonniert datum'))" > - <a - href="#" - @click.prevent="sortBy('mkdate')" + <button + type="button" + class="as-link" + @click="sortBy('mkdate')" :title="$gettext('Nach Abonniert am sortieren')"> {{ $gettext('Abonniert am') }} - </a> + </button> </th> <th class="actions" @@ -151,12 +150,13 @@ onMounted(async () => { :aria-sort="getAriaSortString('notification_type')" :aria-label="getAriaSortLabel('notification_type', $gettext('Typ des Abonnements'))" > - <a - href="#" - @click.prevent="sortBy('notification_type')" + <button + type="button" + class="as-link" + @click="sortBy('notification_type')" :title="$gettext('Nach Typ des Abonnements sortieren')"> {{ $gettext('Typ des Abonnements') }} - </a> + </button> </th> </tr> </thead> @@ -181,10 +181,10 @@ onMounted(async () => { <div class="title-with-actions__actions-xs"> <SubscriptionDropdown - :title="getSubscriptionDropdownTitle(subscription.subject.type)" + :type="getSubscriptionDropdownTitle(subscription.subject.type)" :subject="subscription.subject" :subject_id="subscription.subject_id" - :user_subscription="subscription" + :userSubscription="subscription" @deleted="removeSubscription(subscription.id)" /> </div> @@ -197,7 +197,8 @@ onMounted(async () => { :title="$gettext('Diskussion ist geschlossen')" shape="lock-locked2" :size="20" - role="inactive" /> + role="inactive" + /> </td> <td> {{ getSubjectLabel(subscription.subject.type) }} @@ -208,9 +209,9 @@ onMounted(async () => { <td class="actions"> <div class="inline-flex"> <SubscriptionDropdown - :title="getSubscriptionDropdownTitle(subscription.subject.type)" + :type="getSubscriptionDropdownTitle(subscription.subject.type)" :subject="subscription.subject" - :user_subscription="subscription" + :userSubscription="subscription" @deleted="removeSubscription(subscription.id)" /> </div> diff --git a/resources/vue/apps/forum/topics/Edit.vue b/resources/vue/apps/forum/topics/Edit.vue index b0c2018..ac96e93 100644 --- a/resources/vue/apps/forum/topics/Edit.vue +++ b/resources/vue/apps/forum/topics/Edit.vue @@ -1,13 +1,14 @@ <script setup> -import {computed, onMounted, reactive, useTemplateRef} from "vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipSelect from "../../../components/StudipSelect.vue"; +import {computed, onMounted, reactive, useTemplateRef} from 'vue'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; const CSRF = STUDIP.CSRF_TOKEN; const props = defineProps({ topic: { type: Object, + default: () => ({}) }, categories: { type: Array, @@ -61,7 +62,8 @@ onMounted(() => { name="name" ref="name-input" v-model="topicForm.name" - class="max-w-full" /> + class="max-w-full" + /> </label> </section> diff --git a/resources/vue/apps/forum/topics/Index.vue b/resources/vue/apps/forum/topics/Index.vue index 4e4ef35..1fc4ee9 100644 --- a/resources/vue/apps/forum/topics/Index.vue +++ b/resources/vue/apps/forum/topics/Index.vue @@ -1,15 +1,15 @@ <script setup> -import CreateTopic from "@/vue/components/forum/topics/CreateTopic.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import TopicsIndex from "@/vue/components/forum/topics/TopicsIndex.vue"; -import {computed, onMounted, ref} from "vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import {topicTransformer} from "../../../components/forum/helpers/transformers"; -import EmptyForum from "../../../components/forum/EmptyForum.vue"; +import {computed, onMounted, ref} from 'vue'; +import CreateTopic from '@/vue/components/forum/topics/CreateTopic.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import TopicsIndex from '@/vue/components/forum/topics/TopicsIndex.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import {topicTransformer} from '@/vue/components/forum/helpers/transformers'; +import EmptyForum from '@/vue/components/forum/EmptyForum.vue'; const forumConfig = useForumConfig(); diff --git a/resources/vue/apps/forum/topics/Show.vue b/resources/vue/apps/forum/topics/Show.vue index 0adda3d..37952c5 100644 --- a/resources/vue/apps/forum/topics/Show.vue +++ b/resources/vue/apps/forum/topics/Show.vue @@ -1,16 +1,16 @@ <script setup> -import {onMounted, ref} from "vue"; -import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import { default as CreateDiscussion } from "@/vue/components/forum/discussions/Create.vue"; -import DiscussionIndex from "@/vue/components/forum/discussions/DiscussionIndex.vue"; -import {getCategoryURL} from "@/vue/components/forum/helpers/urls"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipIcon from "../../../components/StudipIcon.vue"; -import StudipDateTime from "../../../components/StudipDateTime.vue"; -import SubscriptionDropdown from "@/vue/components/forum/SubscriptionDropdown.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipPagination from "../../../components/StudipPagination.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {onMounted, ref} from 'vue'; +import ForumApp from '@/vue/components/forum/ForumApp.vue'; +import { default as CreateDiscussion } from '@/vue/components/forum/discussions/Create.vue'; +import DiscussionIndex from '@/vue/components/forum/discussions/DiscussionIndex.vue'; +import {getCategoryURL} from '@/vue/components/forum/helpers/urls'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; +import SubscriptionDropdown from '@/vue/components/forum/SubscriptionDropdown.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipPagination from '@/vue/components/StudipPagination.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); const props = defineProps({ @@ -26,7 +26,7 @@ const props = defineProps({ type: Object, required: true, }, - user_subscription: { + userSubscription: { type: Object }, }); @@ -81,16 +81,19 @@ onMounted(async () => { </ul> <div class="mt-10 inline-flex gap-20 items-center"> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" :aria-label="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Teilnehmenden am Thema') }}:</span> <small>{{ metadata.users_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true"/> + <span class="sr-only">{{ $gettext('Anzahl der Beiträge') }}:</span> <small>{{ metadata.postings_count }}</small> </span> - <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + <span class="inline-flex gap-5 items-center" :title="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Letzte Aktivität') }}:</span> <StudipDateTime v-if="metadata.recent_activity" :iso="metadata.recent_activity" :relative="true" /> <template v-else>{{ $gettext('Keine Aktivität') }}</template> </span> @@ -100,12 +103,13 @@ onMounted(async () => { <div v-if="!forumConfig.allowGuestAccess" class="actions"> <CreateDiscussion :topic_id="topic.topic_id" /> <SubscriptionDropdown - :title="$gettext('Thema abonnieren')" + :type="$gettext('Thema')" + :context="topic.name" :subject="{ id: topic.topic_id, type: 'forum-topics' }" - :user_subscription="user_subscription" + :userSubscription="userSubscription" /> </div> </div> diff --git a/resources/vue/components/Dropdown.vue b/resources/vue/components/Dropdown.vue index 011bef3..c02f196 100644 --- a/resources/vue/components/Dropdown.vue +++ b/resources/vue/components/Dropdown.vue @@ -3,6 +3,7 @@ import {nextTick, onBeforeUnmount, ref, useTemplateRef, watch} from "vue"; import { createPopper } from '@popperjs/core'; import useDetectOutsideClick from "../composables/useDetectOutsideClick"; import StudipIcon from "./StudipIcon.vue"; +import {$gettext} from "../../assets/javascripts/lib/gettext"; defineProps({ title: { @@ -78,6 +79,7 @@ onBeforeUnmount(() => { type="button" v-if="withCloseButton" @click="isOpen = false" + :title="$gettext('Menü schließen')" class="dropdown__close-button button-base"> <StudipIcon shape="decline" :size="20" /> </button> diff --git a/resources/vue/components/forum/EmptyForum.vue b/resources/vue/components/forum/EmptyForum.vue index a63a52c..12f0709 100644 --- a/resources/vue/components/forum/EmptyForum.vue +++ b/resources/vue/components/forum/EmptyForum.vue @@ -1,12 +1,19 @@ <script setup> -import {getDiscussionCreateURL} from "./helpers/urls"; -import {$gettext} from "../../../assets/javascripts/lib/gettext"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; +import {getDiscussionCreateURL} from '@/vue/components/forum/helpers/urls'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; const emptyForumIllustration = `${STUDIP.ASSETS_URL}images/forum/forum-keyvisual-positive.svg`; -const openTour = (id) => { - STUDIP.Tour.init(id, 1); +const openTour = id => STUDIP.Tour.init(id, 1); +const addDiscussion = () => { + STUDIP.Dialog.fromURL( + getDiscussionCreateURL(), + { + width: '900', + height: '700' + } + ); } </script> @@ -28,16 +35,24 @@ const openTour = (id) => { </p> <div class="buttons-container"> - <button type="button" - class="button button--icon-label" - @click.prevent="openTour('ea68d2f9d7b81d01d2d3ea38a105c734')"> + <button + type="button" + class="button button--icon-label" + @click="openTour('ea68d2f9d7b81d01d2d3ea38a105c734')" + :title="$gettext('Tour ansehen')" + > <StudipIcon shape="lightbulb" :size="20" aria-hidden="true" /> {{ $gettext('Tour ansehen') }} </button> - <a :href="getDiscussionCreateURL()" data-dialog="width=900;height=700" class="button button--icon-label"> + <button + type="button" + class="button button--icon-label" + @click="addDiscussion" + :title="$gettext('Eine Diskussion starten')" + > <StudipIcon shape="add" :size="20" aria-hidden="true" /> {{ $gettext('Eine Diskussion starten') }} - </a> + </button> </div> </div> </div> diff --git a/resources/vue/components/forum/ForumApp.vue b/resources/vue/components/forum/ForumApp.vue index 7a0ea35..7b926e0 100644 --- a/resources/vue/components/forum/ForumApp.vue +++ b/resources/vue/components/forum/ForumApp.vue @@ -1,7 +1,8 @@ <script setup> -import {onMounted} from "vue"; -import {useForumConfig} from "../../store/pinia/forum/ForumConfig"; +import {onMounted} from 'vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +const CSRF = STUDIP.CSRF_TOKEN; const forumConfig = useForumConfig(); const fetchConfigs = async () => { try { @@ -40,5 +41,9 @@ onMounted(async () => { <slot name="sidebar" /> </div> </div> + + <form id="forum-delete-form" method="post"> + <input type="hidden" :name="CSRF.name" :value="CSRF.value" /> + </form> </div> </template> diff --git a/resources/vue/components/forum/ForumMembers.vue b/resources/vue/components/forum/ForumMembers.vue index 8b76c48..d8f4e6e 100644 --- a/resources/vue/components/forum/ForumMembers.vue +++ b/resources/vue/components/forum/ForumMembers.vue @@ -1,10 +1,10 @@ <script setup> -import {computed, ref} from "vue"; -import {$gettext} from "../../../assets/javascripts/lib/gettext"; -import Dropdown from "../Dropdown.vue"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import UserAvatar from "@/vue/components/avatar/UserAvatar.vue"; -import UserAvatarDropdown from "@/vue/components/avatar/UserAvatarDropdown.vue"; +import {computed, ref} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import Dropdown from '@/vue/components/Dropdown.vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import UserAvatar from '@/vue/components/avatar/UserAvatar.vue'; +import UserAvatarDropdown from '@/vue/components/avatar/UserAvatarDropdown.vue'; const props = defineProps({ members: { @@ -95,39 +95,41 @@ const isModerator = role => role === 'moderator'; </li> </ul> </div> - <hr /> - <div class="user-group"> - <p class="user-group__title">{{ $gettext('Autor:in') }}</p> - <ul class="user-group__list"> - <li v-for="(user, index) in authors" :key="index"> - <div - v-if="activeUserAvatar !== user.id" - class="user-item" - > - <div class="user-item__user"> - <img :src="user.avatar_url" :alt="user.name" /> - <p>{{ user.name }}</p> + <template v-if="authors.length > 0"> + <hr /> + <div class="user-group"> + <p class="user-group__title">{{ $gettext('Autor:in') }}</p> + <ul class="user-group__list"> + <li v-for="(user, index) in authors" :key="index"> + <div + v-if="activeUserAvatar !== user.id" + class="user-item" + > + <div class="user-item__user"> + <img :src="user.avatar_url" :alt="user.name" /> + <p>{{ user.name }}</p> + </div> + <button + @click="activeUserAvatar = user.id" + :title="$gettext('Aufklappen')" + :aria-label="$gettext('Aufklappen')" + class="show-avatar button-base"> + <StudipIcon shape="arr_1down" :size="15" aria-hidden="true" /> + </button> </div> <button - @click="activeUserAvatar = user.id" - :title="$gettext('Aufklappen')" - :aria-label="$gettext('Aufklappen')" - class="show-avatar button-base"> - <StudipIcon shape="arr_1down" :size="15" aria-hidden="true" /> + v-else + @click="activeUserAvatar = ''" + :title="$gettext('Zuklappen')" + :aria-label="$gettext('Zuklappen')" + class="hide-avatar button-base"> + <StudipIcon shape="arr_1up" :size="15" aria-hidden="true" /> </button> - </div> - <button - v-else - @click="activeUserAvatar = ''" - :title="$gettext('Zuklappen')" - :aria-label="$gettext('Zuklappen')" - class="hide-avatar button-base"> - <StudipIcon shape="arr_1up" :size="15" aria-hidden="true" /> - </button> - <UserAvatar v-if="activeUserAvatar === user.id" :user="user" /> - </li> - </ul> - </div> + <UserAvatar v-if="activeUserAvatar === user.id" :user="user" /> + </li> + </ul> + </div> + </template> </div> </template> </Dropdown> diff --git a/resources/vue/components/forum/SelectTagsInput.vue b/resources/vue/components/forum/SelectTagsInput.vue index 055fb5d..255b0b5 100644 --- a/resources/vue/components/forum/SelectTagsInput.vue +++ b/resources/vue/components/forum/SelectTagsInput.vue @@ -1,6 +1,6 @@ <script setup> -import {$gettext} from "../../../assets/javascripts/lib/gettext"; -import StudipSelect from "../StudipSelect.vue"; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; </script> <template> diff --git a/resources/vue/components/forum/SelectUserInput.vue b/resources/vue/components/forum/SelectUserInput.vue index 16b272c..d996b84 100644 --- a/resources/vue/components/forum/SelectUserInput.vue +++ b/resources/vue/components/forum/SelectUserInput.vue @@ -1,6 +1,6 @@ <script setup> -import {$gettext} from "../../../assets/javascripts/lib/gettext"; -import StudipSelect from "..//StudipSelect.vue"; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; </script> <template> diff --git a/resources/vue/components/forum/SubscriptionDropdown.vue b/resources/vue/components/forum/SubscriptionDropdown.vue index 98a4543..9976959 100644 --- a/resources/vue/components/forum/SubscriptionDropdown.vue +++ b/resources/vue/components/forum/SubscriptionDropdown.vue @@ -1,14 +1,14 @@ <script setup> -import {computed, ref} from "vue"; -import {$gettext} from "../../../assets/javascripts/lib/gettext"; -import Dropdown from "../Dropdown.vue"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import {SubscriptionNotificationType} from "@/vue/components/forum/enums/SubscriptionNotificationType"; -import {deserializeJSONAPIResponse} from "../../../assets/javascripts/lib/jsonapiUtils"; +import {computed, ref} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import Dropdown from '@/vue/components/Dropdown.vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {SubscriptionNotificationType} from '@/vue/components/forum/enums/SubscriptionNotificationType'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; const emit = defineEmits(['updated', 'deleted']); const props = defineProps({ - user_subscription: { + userSubscription: { type: Object, required: true }, @@ -16,14 +16,18 @@ const props = defineProps({ type: Object, required: true }, - title: { + type: { type: String, - default: $gettext('Diskussion abonnieren') + default: $gettext('Diskussion') + }, + context: { + type: String, + default: '' } }); const isOpen = ref(false); -const subscription = ref(props.user_subscription); +const subscription = ref(props.userSubscription); const isLoading = ref(false); const subscriptionButtonLabel = computed(() => { @@ -56,12 +60,15 @@ const subscriptionButtonIcon = computed(() => { return 'subscription-all'; }); -const getSubscriptionJSONAPIObject = (notification_type = 'all') => ({ +const title = computed(() => $gettext('%{type} abonnieren', {type: props.type })); +const computedContext = computed(() => props.context || props.subject.name || props.subject.title); + +const getSubscriptionJSONAPIObject = (notificationType = 'all') => ({ data: { id: subscription.value?.id, type: 'forum-subscriptions', attributes: { - 'notification-type': notification_type + 'notification-type': notificationType }, relationships: { subject: { @@ -101,14 +108,14 @@ const unSubscribe = async () => { } } -const subscribe = async (notification_type = 'all') => { +const subscribe = async (notificationType = 'all') => { try { isLoading.value = false; const response = await STUDIP.jsonapi.withPromises().POST( 'forum-subscriptions', { - data: getSubscriptionJSONAPIObject(notification_type) + data: getSubscriptionJSONAPIObject(notificationType) } ); @@ -130,10 +137,12 @@ const subscribe = async (notification_type = 'all') => { <template #trigger> <button type="button" - :title="title" class="button subscription-button" :class="subscriptionButtonLabel ? 'button--icon-label' : 'button--icon-only'" - :aria-pressed="isOpen" + :title="$gettext(`${type} abonnieren (Menü öffnen)`)" + :aria-label="$gettext('Menü zum Abonnieren für „%{ context }“ öffnen)', { context: computedContext ?? type })" + aria-haspopup="menu" + :aria-expanded="isOpen" @click="isOpen = !isOpen" > <span v-if="subscriptionButtonLabel"> diff --git a/resources/vue/components/forum/categories/CategoryItem.vue b/resources/vue/components/forum/categories/CategoryItem.vue index c761e12..e2d524e 100644 --- a/resources/vue/components/forum/categories/CategoryItem.vue +++ b/resources/vue/components/forum/categories/CategoryItem.vue @@ -1,15 +1,14 @@ <script setup> import {getCategoryDeleteURL, getCategoryEditURL, getCategoryURL} from "../helpers/urls"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipDateTime from "@/vue/components/StudipDateTime.vue"; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; import StudipActionMenu from "@/vue/components/StudipActionMenu.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {useForumConfig} from '../../../store/pinia/forum/ForumConfig'; import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {computed, ref} from "vue"; -import ShowCategory from "./ShowCategory.vue"; +import {computed} from "vue"; const forumConfig = useForumConfig(); -const emit = defineEmits(['swapCategory']); +const emit = defineEmits(['swapCategory', 'showCategory']); const props = defineProps({ category: { @@ -37,20 +36,22 @@ const categoryActionMenus = computed(() => { return menu; }); -const isCategoryDialogOpen = ref(false); - -const displayCategory = () => { - isCategoryDialogOpen.value = true; -} +const showCategory = () => emit('showCategory', props.category); const editCategory = () => STUDIP.Dialog.fromURL(getCategoryEditURL(props.category.id), { width: '700' }); -const deleteCategory = () => STUDIP.Dialog.confirm( +const showConfirmDelete = () => STUDIP.Dialog.confirm( $gettext('Wollen Sie diese "%{name}" Kategorie löschen?', {name: props.category.name}), - () => window.location = getCategoryDeleteURL(props.category.id), + () => deleteCategory(), STUDIP.Dialog.close() ); +const deleteCategory = () => { + const deleteForm = document.getElementById('forum-delete-form'); + deleteForm.action = getCategoryDeleteURL(props.category.id); + deleteForm.submit(); +} + const swapCategory = event => { const keyCodes = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown']; @@ -63,200 +64,210 @@ const swapCategory = event => { </script> <template> - <tr v-if="renderType === 'tr'"> - <td> - <div class="topic-overview"> - <div v-if="forumConfig.isModerator" class="drag-area"> - <a class="drag-link" - tabindex="0" - role="option" - :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: category.name})" - :id="`sort-handle-${category.id}`" - @keydown="swapCategory"> - <span class="drag-handle"></span> - </a> - </div> - <div class="flag" v-if="category.color" :style="{ backgroundColor: category.color}"></div> - <div class="content"> - <div class="flex-1"> - <div class="title-with-actions"> - <div class="title-with-actions__content"> - <a - class="title-with-actions__link" - :href="getCategoryURL(category.id)" - :title="$gettext('Zur Kategorie')"> - <span class="category-title line-clamp-2">{{ category.name }}</span> - <span - v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" - > + <template v-if="renderType === 'tr'"> + <tr v-bind="$attrs"> + <td> + <div class="topic-overview"> + <div v-if="forumConfig.isModerator" class="drag-area"> + <button + type="button" + :id="`sort-handle-${category.id}`" + class="drag-link styleless" + @keydown="swapCategory" + :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: category.name})" + + > + <span class="drag-handle"></span> + </button> + </div> + <div class="flag" v-if="category.color" :style="{ backgroundColor: category.color}"></div> + <div class="content"> + <div class="flex-1"> + <div class="title-with-actions"> + <div class="title-with-actions__content"> + <a + class="title-with-actions__link" + :href="getCategoryURL(category.id)" + :title="$gettext('Zur Kategorie')"> + <span class="category-title line-clamp-2">{{ category.name }}</span> + <span + v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + > {{ category.meta.unread_postings_count }} </span> - </a> - </div> + </a> + </div> - <div class="title-with-actions__actions-xs"> - <StudipActionMenu - :items="categoryActionMenus" - @show="displayCategory" - @edit="editCategory" - @delete="deleteCategory" - /> + <div class="title-with-actions__actions-xs"> + <StudipActionMenu + :context="category.name" + :items="categoryActionMenus" + @show="showCategory" + @edit="editCategory" + @delete="showConfirmDelete" + /> + </div> </div> + <p v-if="category.description"> + <small class="line-clamp-3">{{ category.description }}</small> + </p> </div> - <p v-if="category.description"> - <small class="line-clamp-3">{{ category.description }}</small> - </p> </div> </div> - </div> - <!--mobile display: start--> - <div class="details-xs"> - <dl> - <dt>{{ $gettext('Anzahl der Teilnehmenden in der Kategorie') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="community2" role="info" :size="15" /> - {{ category.meta.users_count }} - </dd> - </dl> + <!--mobile display: start--> + <div class="details-xs"> + <dl> + <dt>{{ $gettext('Anzahl der Teilnehmenden in der Kategorie') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="community2" role="info" :size="15" /> + {{ category.meta.users_count }} + </dd> + </dl> - <dl> - <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="forum" role="info" :size="15" /> - {{ category.meta.discussions_count }} - </dd> + <dl> + <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="forum" role="info" :size="15" /> + {{ category.meta.discussions_count }} + </dd> - <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="reply" role="info" :size="15" /> - {{ category.meta.postings_count }} - </dd> + <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="reply" role="info" :size="15" /> + {{ category.meta.postings_count }} + </dd> - <dt>{{ $gettext('Letzte Aktivität') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="activity" role="info" :size="15" /> - <StudipDateTime v-if="category.meta.recent_activity" :iso="category.meta.recent_activity" :relative="true" /> - <template v-else>{{ $gettext('Keine Aktivität') }}</template> - </dd> - </dl> - </div> - <!--mobile display: end--> - </td> - <td class="nowrap" :title="$gettext('Anzahl der Diskussionen')" :aria-label="$gettext('Anzahl der Diskussionen')"> - {{ category.meta.discussions_count }} {{ $gettext('Diskussion') }} - </td> - <td> + <dt>{{ $gettext('Letzte Aktivität') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="activity" role="info" :size="15" /> + <StudipDateTime v-if="category.meta.recent_activity" :iso="category.meta.recent_activity" :relative="true" /> + <template v-else>{{ $gettext('Keine Aktivität') }}</template> + </dd> + </dl> + </div> + <!--mobile display: end--> + </td> + <td class="nowrap" :title="$gettext('Anzahl der Diskussionen')" :aria-label="$gettext('Anzahl der Diskussionen')"> + {{ category.meta.discussions_count }} {{ $gettext('Diskussion') }} + </td> + <td> <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden in der Kategorie')" :aria-label="$gettext('Anzahl der Teilnehmenden in der Kategorie')" role="group"> <StudipIcon shape="community2" role="info" :size="20" aria-hidden="true" /> <span>{{ category.meta.users_count }}</span> </span> - </td> - <td> + </td> + <td> <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="20" aria-hidden="true" /> <span>{{ category.meta.postings_count }}</span> </span> - </td> - <td> + </td> + <td> <span class="inline-flex gap-10 items-center nowrap" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="20" aria-hidden="true"/> <StudipDateTime v-if="category.meta.recent_activity" :iso="category.meta.recent_activity" :relative="true" /> <template v-else>{{ $gettext('Keine Aktivität') }}</template> </span> - </td> - <td class="actions"> - <StudipActionMenu - :items="categoryActionMenus" - @show="displayCategory" - @edit="editCategory" - @delete="deleteCategory" - /> - </td> - </tr> - <a - v-else - :href="getCategoryURL(category.id)" - :title="$gettext('Zur Kategorie')" - class="styleless"> - <div - class="topic-card" - :class="{ - '--with-hover-style': category.color + </td> + <td class="actions"> + <StudipActionMenu + :context="category.name" + :items="categoryActionMenus" + @show="showCategory" + @edit="editCategory" + @delete="showConfirmDelete" + /> + </td> + </tr> + </template> + <template v-else> + <a + :href="getCategoryURL(category.id)" + :title="$gettext('Zur Kategorie')" + class="styleless" + v-bind="$attrs" + > + <div + class="topic-card" + :class="{ + 'topic-card--with-hover-style': category.color }" - :style="{ + :style="{ '--forum-topic-card-hover-border-color': category.color }" - > - <div v-if="category.color" class="topic-card__flag" :style="{ backgroundColor: category.color}"></div> - <div class="topic-card__content"> - <div class="topic-card__body"> - <div class="flex space-between"> - <div class="flex items-start gap-10"> + > + <div v-if="category.color" class="topic-card__flag" :style="{ backgroundColor: category.color}"></div> + <div class="topic-card__content"> + <div class="topic-card__body"> + <div class="flex space-between"> + <div class="flex items-start gap-10"> <span class="topic-card__title category-title line-clamp-2"> {{ category.name }} </span> - <span - v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" - > + <span + v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + > {{ category.meta.unread_postings_count }} </span> + </div> + <div class="actions"> + <StudipActionMenu + :context="category.name" + :items="categoryActionMenus" + @show="showCategory" + @edit="editCategory" + @delete="showConfirmDelete" + /> + </div> </div> - <div class="actions"> - <StudipActionMenu - :items="categoryActionMenus" - @show="displayCategory" - @edit="editCategory" - @delete="deleteCategory" - /> - </div> - </div> - <p> - <small class="line-clamp-3">{{ category.description }}</small> - </p> - </div> - <div> - <div v-if="forumConfig.isModerator" class="drag-area"> - <a class="drag-link" - tabindex="0" - role="option" - :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: category.name})" - :id="`sort-handle-${category.id}`" - @keydown="swapCategory"> - <span class="drag-handle"></span> - </a> + <p> + <small class="line-clamp-3">{{ category.description }}</small> + </p> </div> - <div class="topic-card__footer"> + <div> + <div v-if="forumConfig.isModerator" class="drag-area"> + <button + type="button" + :id="`sort-handle-${category.id}`" + class="drag-link styleless" + @keydown="swapCategory" + :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: category.name})" + > + <span class="drag-handle"></span> + </button> + </div> + <div class="topic-card__footer"> <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden in der Kategorie')" :aria-label="$gettext('Anzahl der Teilnehmenden in der Kategorie')" role="group"> <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true"/> <small>{{ category.meta.users_count }}</small> </span> - <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true"/> <small>{{ category.meta.postings_count }}</small> </span> - <span class="inline-flex gap-10 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + <span class="inline-flex gap-10 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true"/> <small v-if="category.meta.recent_activity"> <StudipDateTime :iso="category.meta.recent_activity" :relative="true" /> </small> <small v-else>{{ $gettext('Keine Aktivität') }}</small> </span> + </div> </div> </div> </div> - </div> - </a> - <ShowCategory :category="category" v-model:isOpen="isCategoryDialogOpen" /> + </a> + </template> </template> diff --git a/resources/vue/components/forum/categories/Create.vue b/resources/vue/components/forum/categories/Create.vue index 392785b..98693ba 100644 --- a/resources/vue/components/forum/categories/Create.vue +++ b/resources/vue/components/forum/categories/Create.vue @@ -1,31 +1,39 @@ <script setup> -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import {getCategoryCreateURL} from "../helpers/urls"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {getCategoryCreateURL} from '@/vue/components/forum/helpers/urls'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); + defineProps({ label: { type: String, default: '' } }); + +const addCategory = () => { + STUDIP.Dialog.fromURL( + getCategoryCreateURL(), + { + width: '700' + } + ); +} </script> <template> - <a + <button v-if="forumConfig.isModerator" - :href="getCategoryCreateURL()" - data-dialog="size=700" - :title="$gettext('Neue Kategorie anlegen')" - :aria-label="$gettext('Neue Kategorie anlegen')" + type="button" class="button" + @click="addCategory" + :title="$gettext('Neue Kategorie anlegen')" :class="label ? 'button--icon-label' : 'button--icon-only'" - role="button" > <StudipIcon shape="add" :size="20" aria-hidden="true" /> <span v-if="label" class="label">{{ label }}</span> - </a> + </button> </template> diff --git a/resources/vue/components/forum/categories/ShowCategory.vue b/resources/vue/components/forum/categories/ShowCategory.vue index 2ea389e..cbc1a80 100644 --- a/resources/vue/components/forum/categories/ShowCategory.vue +++ b/resources/vue/components/forum/categories/ShowCategory.vue @@ -1,5 +1,4 @@ <script setup> -import StudipDialog from "../../StudipDialog.vue"; import {$gettext} from "../../../../assets/javascripts/lib/gettext"; import StudipDateTime from "../../StudipDateTime.vue"; @@ -9,56 +8,40 @@ defineProps({ required: true, } }); - -const isOpen = defineModel('isOpen'); </script> <template> - <StudipDialog - v-if="isOpen" - :title="$gettext('Informationen')" - :closeText="$gettext('Schließen')" - closeClass="cancel" - height="700" - width="600" - @close="isOpen = false" - > - <template #dialogContent> - <div class="forum"> - <dl class="use-utility-classes"> - <dt>{{ $gettext('Title') }}</dt> - <dd>{{ category.name }}</dd> - - <dt>{{ $gettext('Beschreibung') }}</dt> - <dd class="break-word"> - <p>{{ category.description }}</p> - </dd> - - <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> - <dd>{{ category.meta.discussions_count }}</dd> - - <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> - <dd>{{ category.meta.postings_count }}</dd> - - <dt>{{ $gettext('Anzahl der Teilnehmenden an der Kategorie') }}</dt> - <dd>{{ category.meta.users_count }}</dd> - - <dt>{{ $gettext('Letzte Aktivität') }}</dt> - <dd> - <template v-if="category.meta.recent_activity"> - <StudipDateTime :iso="category.meta.recent_activity" /> - </template> - <template v-else> - {{ $gettext('Keine Aktivität') }} - </template> - </dd> - - <dt>{{ $gettext('Erstellt am') }}</dt> - <dd> - <StudipDateTime :iso="category.mkdate" /> - </dd> - </dl> - </div> - </template> - </StudipDialog> + <dl class="use-utility-classes"> + <dt>{{ $gettext('Title') }}</dt> + <dd>{{ category.name }}</dd> + + <dt>{{ $gettext('Beschreibung') }}</dt> + <dd class="break-word"> + <p>{{ category.description }}</p> + </dd> + + <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> + <dd>{{ category.meta.discussions_count }}</dd> + + <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> + <dd>{{ category.meta.postings_count }}</dd> + + <dt>{{ $gettext('Anzahl der Teilnehmenden an der Kategorie') }}</dt> + <dd>{{ category.meta.users_count }}</dd> + + <dt>{{ $gettext('Letzte Aktivität') }}</dt> + <dd> + <template v-if="category.meta.recent_activity"> + <StudipDateTime :iso="category.meta.recent_activity" /> + </template> + <template v-else> + {{ $gettext('Keine Aktivität') }} + </template> + </dd> + + <dt>{{ $gettext('Erstellt am') }}</dt> + <dd> + <StudipDateTime :iso="category.mkdate" /> + </dd> + </dl> </template> diff --git a/resources/vue/components/forum/discussions/Create.vue b/resources/vue/components/forum/discussions/Create.vue index fb4d541..568041b 100644 --- a/resources/vue/components/forum/discussions/Create.vue +++ b/resources/vue/components/forum/discussions/Create.vue @@ -1,9 +1,9 @@ <script setup> -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import {computed} from "vue"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {computed} from 'vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); const props = defineProps({ @@ -15,16 +15,26 @@ const props = defineProps({ const discussionCreateURL = computed(() => { return STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussions/edit?topic_id=${props.topic_id}`); }); + +const addDiscussion = () => { + STUDIP.Dialog.fromURL( + discussionCreateURL.value, + { + width: '900', + height: '750' + } + ); +} </script> <template> - <a + <button v-if="!forumConfig.allowGuestAccess" - :href="discussionCreateURL" + type="button" + @click="addDiscussion" :title="$gettext('Neue Diskussion starten')" - data-dialog="width=900;height=750" - role="button" - class="button button--icon-only"> + class="button button--icon-only" + > <StudipIcon shape="add" :size="20" aria-hidden="true" /> - </a> + </button> </template> diff --git a/resources/vue/components/forum/discussions/DiscussionFooter.vue b/resources/vue/components/forum/discussions/DiscussionFooter.vue index 9b801e2..b640d41 100644 --- a/resources/vue/components/forum/discussions/DiscussionFooter.vue +++ b/resources/vue/components/forum/discussions/DiscussionFooter.vue @@ -19,7 +19,7 @@ const props = defineProps({ type: Array, required: true }, - read_index: { + readIndex: { type: Number, default: -1 } @@ -27,8 +27,13 @@ const props = defineProps({ const recentActivity = computed(() => props.posts.at(-1)?.mkdate ?? props.discussion.mkdate); const hasUnreadPost = computed(() => { - return props.read_index === 0 && props.posts.length > 1 && props.posts[1].author.id !== STUDIP.USER_ID; + return props.readIndex === 0 && props.posts.length > 1 && props.posts[1].author.id !== STUDIP.USER_ID; }); + +const addPost = () => { + document.getElementById(`new-post`)?.scrollIntoView({ behavior: 'smooth' }); + postCreateForm.value = true; +} </script> <template> @@ -55,21 +60,20 @@ const hasUnreadPost = computed(() => { </div> </div> <ForumMembers :members="discussion.members" :limit="5" size="35px" /> - <a + <button v-if="!forumConfig.allowGuestAccess && !discussion.closed_at" - href="#new-post" + type="button" class="button button--icon-label" - role="button" + @click="addPost" :title="$gettext('Antworten')" :aria-label="$gettext('Antworten')" :class="{ 'disabled': postCreateForm }" - @click="postCreateForm = true" > <StudipIcon shape="reply" :size="20" aria-hidden="true" /> {{ $gettext('Antworten') }} - </a> + </button> </div> </div> </template> diff --git a/resources/vue/components/forum/discussions/DiscussionIndex.vue b/resources/vue/components/forum/discussions/DiscussionIndex.vue index 5478489..446565c 100644 --- a/resources/vue/components/forum/discussions/DiscussionIndex.vue +++ b/resources/vue/components/forum/discussions/DiscussionIndex.vue @@ -1,15 +1,15 @@ <script setup> +import {onMounted, toRef} from 'vue'; import {getDiscussionURL, getSearchURL} from "../helpers/urls"; -import {numberFormatter} from "../../../../assets/javascripts/lib/number_formatter"; -import ForumMembers from "../ForumMembers.vue"; -import {useSortable} from "../../../composables/useSortable"; -import {onMounted, toRef} from "vue"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipDateTime from "@/vue/components/StudipDateTime.vue"; -import StudipActionMenu from "@/vue/components/StudipActionMenu.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import Loader from "../Loader.vue"; +import {numberFormatter} from '@/assets/javascripts/lib/number_formatter'; +import ForumMembers from '@/vue/components/forum/ForumMembers.vue'; +import {useSortable} from '@/vue/composables/useSortable'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; +import StudipActionMenu from '@/vue/components/StudipActionMenu.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import Loader from '../Loader.vue'; const forumConfig = useForumConfig(); const props = defineProps({ @@ -59,21 +59,25 @@ const editDiscussion = id => STUDIP.Dialog.fromURL( } ); -const deleteDiscussion = id => STUDIP.Dialog.confirm( +const showConfirmDelete = id => STUDIP.Dialog.confirm( $gettext('Wollen Sie diese Diskussion löschen? Damit werden auch alle Beiträge gelöscht!'), - () => { - window.location = STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussions/delete/${id}`); - }, + () => deleteDiscussion(id), STUDIP.Dialog.close() ); +const deleteDiscussion = id => { + const deleteForm = document.getElementById('forum-delete-form'); + deleteForm.action = STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussions/delete/${id}`); + deleteForm.submit(); +} + onMounted(() => { sortBy('meta.recent_activity', 'desc'); }); </script> <template> - <table class="default forum-table --discussions-index"> + <table class="default forum-table forum-table--discussions-index"> <colgroup> <col> <col style="width: 15%;"> @@ -90,60 +94,65 @@ onMounted(() => { :aria-sort="getAriaSortString('title')" :aria-label="getAriaSortLabel('title', $gettext('Diskussionstitel'))" > - <a - href="#" - @click.prevent="sortBy('title')" + <button + type="button" + class="as-link" + @click="sortBy('title')" :title="$gettext('Nach Diskussionstitel sortieren')"> {{ $gettext('Diskussion') }} - </a> + </button> </th> <th :class="getSortClass('members')" :aria-sort="getAriaSortString('members')" :aria-label="getAriaSortLabel('members', $gettext('Anzahl der Teilnehmenden'))" > - <a - href="#" - @click.prevent="sortBy('members')" + <button + type="button" + class="as-link" + @click="sortBy('members')" :title="$gettext('Nach Anzahl der Teilnehmenden sortieren')"> {{ $gettext('Teilnehmende') }} - </a> + </button> </th> <th :class="getSortClass('meta.postings_count')" :aria-sort="getAriaSortString('meta.postings_count')" :aria-label="getAriaSortLabel('meta.postings_count', $gettext('Anzahl der Beiträge'))" > - <a - href="#" - @click.prevent="sortBy('meta.postings_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.postings_count')" :title="$gettext('Nach Anzahl der Beiträge sortieren')"> {{ $gettext('Beiträge') }} - </a> + </button> </th> <th :class="getSortClass('view_count')" :aria-sort="getAriaSortString('view_count')" :aria-label="getAriaSortLabel('view_count', $gettext('Anzahl der Aufrufe'))" > - <a - href="#" - @click.prevent="sortBy('view_count')" + <button + type="button" + class="as-link" + @click="sortBy('view_count')" :title="$gettext('Nach Anzahl der Aufrufe sortieren')"> {{ $gettext('Aufrufe') }} - </a> + </button> </th> <th :class="getSortClass('meta.recent_activity')" :aria-sort="getAriaSortString('meta.recent_activity')" :aria-label="getAriaSortLabel('meta.recent_activity', $gettext('Letzte Aktivität'))" > - <a - href="#" - @click.prevent="sortBy('meta.recent_activity')" + <button + type="button" + class="as-link" + @click="sortBy('meta.recent_activity')" :title="$gettext('Nach letzter Aktivität sortieren')"> {{ $gettext('Letzte Aktivität') }} - </a> + </button> </th> <th></th> <th v-if="withActions"></th> @@ -194,9 +203,10 @@ onMounted(() => { <div class="title-with-actions__actions-xs"> <StudipActionMenu v-if="withActions" + :context="discussion.title" :items="getActionMenusItems(discussion)" @edit="editDiscussion(discussion.id)" - @delete="deleteDiscussion(discussion.id)" + @delete="showConfirmDelete(discussion.id)" /> </div> </div> @@ -287,9 +297,10 @@ onMounted(() => { </td> <td v-if="withActions" class="actions"> <StudipActionMenu + :context="discussion.title" :items="getActionMenusItems(discussion)" @edit="editDiscussion(discussion.id)" - @delete="deleteDiscussion(discussion.id)" + @delete="showConfirmDelete(discussion.id)" /> </td> </tr> diff --git a/resources/vue/components/forum/discussions/SelectDiscussionType.vue b/resources/vue/components/forum/discussions/SelectDiscussionType.vue index 0b81988..32f1bff 100644 --- a/resources/vue/components/forum/discussions/SelectDiscussionType.vue +++ b/resources/vue/components/forum/discussions/SelectDiscussionType.vue @@ -1,7 +1,7 @@ <script setup> -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipSelect from "@/vue/components/StudipSelect.vue"; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; </script> <template> diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue index 34107a3..26bba20 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -1,16 +1,15 @@ <script setup> import {computed, onBeforeUnmount, onMounted, ref, useTemplateRef} from 'vue'; -import PostEditForm from './PostEditForm.vue'; -import PostCreateForm from './PostCreateForm.vue'; +import PostEditForm from '@/vue/components/forum/posts/PostEditForm.vue'; +import PostCreateForm from '@/vue/components/forum/posts/PostCreateForm.vue'; import PostContent from '@/vue/components/forum/posts/PostContent.vue'; -import PostReactions from './PostReactions.vue'; -import {getDiscussionURL} from '@/vue/components/forum/helpers/urls'; +import PostReactions from '@/vue/components/forum/posts/PostReactions.vue'; +import {getDiscussionURL, userProfileURL} from '@/vue/components/forum/helpers/urls'; import StudipDateTime from '@/vue/components/StudipDateTime.vue'; import StudipIcon from '@/vue/components/StudipIcon.vue'; import {$gettext} from '@/assets/javascripts/lib/gettext'; import LinksPreview from '@/vue/components/LinksPreview.vue'; import UserAvatarDropdown from '@/vue/components/avatar/UserAvatarDropdown.vue'; -import {userProfileURL} from '../helpers/urls'; import {useForumPost} from '@/vue/store/pinia/forum/ForumPost'; import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; @@ -25,7 +24,7 @@ const props = defineProps({ type: Object, required: true, }, - auth_user: { + authUser: { type: Object, required: true }, @@ -63,6 +62,7 @@ const editPost = () => { return; } + document.getElementById(`post_${props.post.id}`)?.scrollIntoView({ behavior: 'smooth' }); showPostEditForm.value = true; } @@ -91,18 +91,19 @@ const addPost = () => { showPostCreateForm.value = false; } -const addReply = post => { +const addReply = () => { + document.getElementById(`create_form_${props.post.id}`)?.scrollIntoView({ behavior: 'smooth' }); showPostCreateForm.value = true; - selectedText.value = post.content; + selectedText.value = props.post.content; } -const forwardPost = post => { +const forwardPost = () => { let messageBoyd = ` ${$gettext('Die Sender:in dieser Nachricht möchte Sie auf den folgenden Beitrag aufmerksam machen: ')} <br /> <br /> ${$gettext('Link zum Beitrag: ')} - <a href="${getDiscussionURL(props.discussion.discussion_id) + '#post_' + post.id}"> + <a href="${getDiscussionURL(props.discussion.discussion_id) + '#post_' + props.post.id}"> ${props.discussion.title} </a> `; @@ -122,7 +123,7 @@ const removePostHighlight = id => { console.error('Element not found!'); return; } - element.classList.remove('--highlight'); + element.classList.remove('post--highlight'); } let postObserver = null; @@ -218,7 +219,7 @@ onBeforeUnmount(() => postObserver.disconnect()); <StudipDateTime v-else :iso="post.mkdate" :relative="true" /> </div> <template v-if="showPostEditForm"> - <PostEditForm :post="post" :auth_user="auth_user" class="mt-10" @canceled="showPostEditForm = false" @updated="showPostEditForm = false"/> + <PostEditForm :post="post" :authUser="authUser" class="mt-10" @canceled="showPostEditForm = false" @updated="showPostEditForm = false" /> </template> <template v-else> <div class="post__text"> @@ -263,9 +264,8 @@ onBeforeUnmount(() => postObserver.disconnect()); <div></div> <div class="inline-flex items-center gap-40"> <div v-if="!forumConfig.allowGuestAccess" class="inline-flex items-center gap-10"> - <a + <button v-if="canEditPost" - :href="`#post_${post.id}`" @click="editPost" type="button" class="button button--icon-only" @@ -276,7 +276,7 @@ onBeforeUnmount(() => postObserver.disconnect()); :aria-label="$gettext('Beitrag bearbeiten')" > <StudipIcon shape="edit" :size="20" aria-hidden="true" /> - </a> + </button> <button v-if="canDeletePost" @click="deletePost" @@ -286,13 +286,17 @@ onBeforeUnmount(() => postObserver.disconnect()); > <StudipIcon shape="trash" :size="20" aria-hidden="true" /> </button> - <button type="button" @click="forwardPost(post)" class="button button--icon-only" :title="$gettext('Beitrag weiterleiten')" :aria-label="$gettext('Beitrag weiterleiten')"> + <button + type="button" + @click="forwardPost" + class="button button--icon-only" + :title="$gettext('Beitrag weiterleiten')" + :aria-label="$gettext('Beitrag weiterleiten')"> <StudipIcon shape="export" :size="20" aria-hidden="true" /> </button> - <a + <button v-if="!discussion.closed_at" - :href="`#create_form_${post.id}`" - @click="addReply(post)" + @click="addReply" type="button" class="button button--icon-only" :class="{ @@ -302,7 +306,7 @@ onBeforeUnmount(() => postObserver.disconnect()); :aria-label="$gettext('Zitieren und Antworten')" > <StudipIcon shape="quote" :size="20" aria-hidden="true" /> - </a> + </button> </div> </div> </div> @@ -311,9 +315,9 @@ onBeforeUnmount(() => postObserver.disconnect()); </div> <div v-if="showPostCreateForm && !discussion.closed_at" :id="`create_form_${post.id}`" class="post-form-container" style="scroll-margin-top: 200px;"> <PostCreateForm - :parent_id="post.id" - :discussion_id="props.discussion.discussion_id" - :auth_user="auth_user" + :parentId="post.id" + :discussionId="props.discussion.discussion_id" + :authUser="authUser" v-model:quote="selectedText" @canceled="showPostCreateForm = false; selectedText = ''" @created="addPost" diff --git a/resources/vue/components/forum/posts/PostContent.vue b/resources/vue/components/forum/posts/PostContent.vue index 1c29812..5403db5 100644 --- a/resources/vue/components/forum/posts/PostContent.vue +++ b/resources/vue/components/forum/posts/PostContent.vue @@ -1,5 +1,5 @@ <script setup> -import {onDeactivated, onMounted, useTemplateRef, watch} from "vue"; +import {onDeactivated, onMounted, useTemplateRef, watch} from 'vue'; const emit = defineEmits(['update:modelValue']); const props = defineProps({ diff --git a/resources/vue/components/forum/posts/PostCreateForm.vue b/resources/vue/components/forum/posts/PostCreateForm.vue index 458e580..ff1878c 100644 --- a/resources/vue/components/forum/posts/PostCreateForm.vue +++ b/resources/vue/components/forum/posts/PostCreateForm.vue @@ -1,30 +1,30 @@ <script setup> -import {onMounted, onUnmounted, ref} from "vue"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import {useForumPost} from "../../../store/pinia/forum/ForumPost"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipSwitch from "@/vue/components/StudipSwitch.vue"; -import StudipWysiwyg from "@/vue/components/StudipWysiwyg.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import {userProfileURL} from "../helpers/urls"; +import {onMounted, onUnmounted, ref} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import {useForumPost} from '@/vue/store/pinia/forum/ForumPost'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipSwitch from '@/vue/components/StudipSwitch.vue'; +import StudipWysiwyg from '@/vue/components/StudipWysiwyg.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import {userProfileURL} from '@/vue/components/forum/helpers/urls'; const forumConfig = useForumConfig(); const forumDiscussionPost = useForumPost(); const emit = defineEmits(['canceled', 'created', 'update:quote']); const props = defineProps({ - discussion_id: { + discussionId: { type: String, required: true, }, - auth_user: { + authUser: { type: Object, - required: true, + required: true }, quote: { type: String, }, - parent_id: { + parentId: { type: String, } }); @@ -77,13 +77,13 @@ const getPostJSONAPIObject = () => ({ discussion: { data: { type: 'forum-discussions', - id: props.discussion_id + id: props.discussionId } }, posting: { data: { type: 'forum-postings', - id: props.parent_id + id: props.parentId } } } @@ -118,14 +118,14 @@ const storePost = async () => { <form @submit.prevent="storePost" class="default post-form forum-quote"> <div class="post-form__author"> <a - :href="userProfileURL(auth_user.username)" + :href="userProfileURL(authUser.username)" class="post-form__author-image profile-image-container" :title="$gettext('Zum Profil')" - :aria-label="$gettext('Zum Profil von %{name}', { name: auth_user.name })" + :aria-label="$gettext('Zum Profil von %{name}', { name: authUser.name })" > - <img :src="auth_user.avatar_url" :alt="auth_user.name" /> + <img :src="authUser.avatar_url" :alt="authUser.name" /> </a> - <p class="post-form__author-name">{{ auth_user.name }}</p> + <p class="post-form__author-name">{{ authUser.name }}</p> </div> <StudipWysiwyg :required="true" v-model="content" /> <div v-if="forumConfig.anonymousPost" class="mt-10"> @@ -139,7 +139,7 @@ const storePost = async () => { :title="$gettext('Speichern')" :aria-label="$gettext('Speichern')" > - <StudipIcon shape="reply" :size="20" aria-hidden="true" /> + <StudipIcon shape="accept" :size="20" aria-hidden="true" /> {{ $gettext('Speichern') }} </button> <button diff --git a/resources/vue/components/forum/posts/PostEditForm.vue b/resources/vue/components/forum/posts/PostEditForm.vue index 10821a1..cd16d7a 100644 --- a/resources/vue/components/forum/posts/PostEditForm.vue +++ b/resources/vue/components/forum/posts/PostEditForm.vue @@ -1,24 +1,24 @@ <script setup> -import {onMounted, onUnmounted, ref} from "vue"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import {useForumPost} from "../../../store/pinia/forum/ForumPost"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipSwitch from "@/vue/components/StudipSwitch.vue"; -import StudipWysiwyg from "@/vue/components/StudipWysiwyg.vue"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; +import {onMounted, onUnmounted, ref} from 'vue'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import {useForumPost} from '@/vue/store/pinia/forum/ForumPost'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipSwitch from '@/vue/components/StudipSwitch.vue'; +import StudipWysiwyg from '@/vue/components/StudipWysiwyg.vue'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; const forumDiscussionPost = useForumPost(); const forumConfig = useForumConfig(); const emit = defineEmits(['canceled', 'updated']); const props = defineProps({ - auth_user: { + authUser: { type: Object, - required: true, + required: true }, post: { type: Object, - required: true, + required: true } }); @@ -93,7 +93,7 @@ onUnmounted(() => { :value="$gettext('Speichern')" :title="$gettext('Speichern')" > - <StudipIcon shape="reply" :size="20" aria-hidden="true" /> + <StudipIcon shape="accept" :size="20" aria-hidden="true" /> {{ $gettext('Speichern') }} </button> <button diff --git a/resources/vue/components/forum/posts/PostReactionShow.vue b/resources/vue/components/forum/posts/PostReactionShow.vue index e5790b6..400fa4e 100644 --- a/resources/vue/components/forum/posts/PostReactionShow.vue +++ b/resources/vue/components/forum/posts/PostReactionShow.vue @@ -54,24 +54,26 @@ onMounted(() => { :aria-sort="getAriaSortString('user.formatted_name')" :aria-label="getAriaSortLabel('user.formatted_name', $gettext('Name'))" > - <a - href="#" - @click.prevent="sortBy('user.formatted_name')" + <button + type="button" + class="as-link" + @click="sortBy('user.formatted_name')" :title="$gettext('Nach Name sortieren')"> {{ $gettext('Name') }} - </a> + </button> </th> <th :class="getSortClass('mkdate')" :aria-sort="getAriaSortString('mkdate')" :aria-label="getAriaSortLabel('mkdate', $gettext('Datum'))" > - <a - href="#" - @click.prevent="sortBy('mkdate')" + <button + type="button" + class="as-link" + @click="sortBy('mkdate')" :title="$gettext('Nach Datum sortieren')"> {{ $gettext('Datum') }} - </a> + </button> </th> </tr> </thead> diff --git a/resources/vue/components/forum/posts/PostReactions.vue b/resources/vue/components/forum/posts/PostReactions.vue index ca5e5a2..58af213 100644 --- a/resources/vue/components/forum/posts/PostReactions.vue +++ b/resources/vue/components/forum/posts/PostReactions.vue @@ -1,15 +1,15 @@ <script setup> -import {computed, reactive, ref, useTemplateRef} from "vue"; -import {REACTION_ICONS} from "./reactions"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {numberFormatter} from "../../../../assets/javascripts/lib/number_formatter"; -import useDetectOutsideClick from "../../../composables/useDetectOutsideClick"; -import {useForumPost} from "../../../store/pinia/forum/ForumPost"; -import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; -import StudipIcon from "../../StudipIcon.vue"; -import PostReactionShow from "./PostReactionShow.vue"; -import StudipDialog from "../../StudipDialog.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {computed, reactive, ref, useTemplateRef} from 'vue'; +import {REACTION_ICONS} from './reactions'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {numberFormatter} from '@/assets/javascripts/lib/number_formatter'; +import useDetectOutsideClick from '@/vue/composables/useDetectOutsideClick'; +import {useForumPost} from '@/vue/store/pinia/forum/ForumPost'; +import {deserializeJSONAPIResponse} from '@/assets/javascripts/lib/jsonapiUtils'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import PostReactionShow from '@/vue/components/forum/posts/PostReactionShow.vue'; +import StudipDialog from '@/vue/components/StudipDialog.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); const forumDiscussionPost = useForumPost(); @@ -126,7 +126,7 @@ const reactionShowDialog = reactive({ type="button" class="post-reaction button-base" :class="{ - '--active': findUserReaction(emoji, reaction) + 'post-reaction--active': findUserReaction(emoji, reaction) }" :title="findUserReaction(emoji, reaction) ? $gettext('Reaktion zurücknehmen') : $gettext('Reaktion hinzufügen')" :aria-label="findUserReaction(emoji, reaction) ? $gettext('Reaktion zurücknehmen') : $gettext('Reaktion hinzufügen')" @@ -164,7 +164,7 @@ const reactionShowDialog = reactive({ <button type="button" :class="{ - '--active': findUserReaction(emoji.value) + 'post-reaction--active': findUserReaction(emoji.value) }" :title="$gettext('Auf diesen Beitrag reagieren')" :aria-label="$gettext('Auf diesen Beitrag mit %{emojiName} reagieren', { emojiName: emoji.value })" diff --git a/resources/vue/components/forum/topics/CreateTopic.vue b/resources/vue/components/forum/topics/CreateTopic.vue index 88a9ad9..688c4a8 100644 --- a/resources/vue/components/forum/topics/CreateTopic.vue +++ b/resources/vue/components/forum/topics/CreateTopic.vue @@ -1,10 +1,11 @@ <script setup> -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import {computed} from "vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {computed} from 'vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); + const props = defineProps({ category_id: { type: String, @@ -22,20 +23,27 @@ const topicCreateURL = computed(() => { return STUDIP.URLHelper.getURL('dispatch.php/course/forum/topics/edit'); }); + +const addTopic = () => { + STUDIP.Dialog.fromURL( + topicCreateURL.value, + { + width: '700' + } + ); +} </script> <template> - <a + <button v-if="forumConfig.isModerator" - :href="topicCreateURL" - data-dialog="width=700" - :title="$gettext('Neues Thema anlegen')" - :aria-label="$gettext('Neues Thema anlegen')" + type="button" class="button button--icon-only" + @click="addTopic" + :title="$gettext('Neues Thema anlegen')" :class="label ? 'button--icon-label' : 'button--icon-only'" - role="button" > <StudipIcon shape="add" :size="20" aria-hidden="true" /> <span v-if="label" class="label">{{ label }}</span> - </a> + </button> </template> diff --git a/resources/vue/components/forum/topics/SelectTopicInput.vue b/resources/vue/components/forum/topics/SelectTopicInput.vue index a5a76fa..8187115 100644 --- a/resources/vue/components/forum/topics/SelectTopicInput.vue +++ b/resources/vue/components/forum/topics/SelectTopicInput.vue @@ -1,6 +1,6 @@ <script setup> -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import StudipSelect from "@/vue/components/StudipSelect.vue"; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipSelect from '@/vue/components/StudipSelect.vue'; const selectedTopics = defineModel(); </script> diff --git a/resources/vue/components/forum/topics/ShowTopic.vue b/resources/vue/components/forum/topics/ShowTopic.vue index 0682fba..18a7bcd 100644 --- a/resources/vue/components/forum/topics/ShowTopic.vue +++ b/resources/vue/components/forum/topics/ShowTopic.vue @@ -1,9 +1,6 @@ <script setup> -import StudipDialog from "../../StudipDialog.vue"; -import {$gettext} from "../../../../assets/javascripts/lib/gettext"; -import StudipDateTime from "../../StudipDateTime.vue"; - -defineEmits(['close']); +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; defineProps({ topic: { @@ -12,60 +9,45 @@ defineProps({ } }); -const isOpen = defineModel('isOpen'); </script> <template> - <StudipDialog - v-if="isOpen" - :title="$gettext('Informationen')" - :closeText="$gettext('Schließen')" - closeClass="cancel" - height="700" - width="600" - @close="isOpen = false" - > - <template #dialogContent> - <div class="forum"> - <dl class="use-utility-classes"> - <dt>{{ $gettext('Title') }}</dt> - <dd>{{ topic.name }}</dd> - - <dt>{{ $gettext('Beschreibung') }}</dt> - <dd class="break-word"> - <p>{{ topic.description }}</p> - </dd> - - <template v-if="topic.category"> - <dt>{{ $gettext('Kategorie') }}</dt> - <dd>{{ topic.category.name }}</dd> - </template> - - <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> - <dd>{{ topic.meta.discussions_count }}</dd> - - <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> - <dd>{{ topic.meta.postings_count }}</dd> - - <dt>{{ $gettext('Anzahl der Teilnehmenden am Thema') }}</dt> - <dd>{{ topic.meta.users_count }}</dd> - - <dt>{{ $gettext('Letzte Aktivität') }}</dt> - <dd> - <template v-if="topic.meta.recent_activity"> - <StudipDateTime :iso="topic.meta.recent_activity" /> - </template> - <template v-else> - {{ $gettext('Keine Aktivität') }} - </template> - </dd> - - <dt>{{ $gettext('Erstellt am') }}</dt> - <dd> - <StudipDateTime :iso="topic.mkdate" /> - </dd> - </dl> - </div> + <dl class="use-utility-classes"> + <dt>{{ $gettext('Title') }}</dt> + <dd>{{ topic.name }}</dd> + + <dt>{{ $gettext('Beschreibung') }}</dt> + <dd class="break-word"> + <p>{{ topic.description }}</p> + </dd> + + <template v-if="topic.category"> + <dt>{{ $gettext('Kategorie') }}</dt> + <dd>{{ topic.category.name }}</dd> </template> - </StudipDialog> + + <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> + <dd>{{ topic.meta.discussions_count }}</dd> + + <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> + <dd>{{ topic.meta.postings_count }}</dd> + + <dt>{{ $gettext('Anzahl der Teilnehmenden am Thema') }}</dt> + <dd>{{ topic.meta.users_count }}</dd> + + <dt>{{ $gettext('Letzte Aktivität') }}</dt> + <dd> + <template v-if="topic.meta.recent_activity"> + <StudipDateTime :iso="topic.meta.recent_activity" /> + </template> + <template v-else> + {{ $gettext('Keine Aktivität') }} + </template> + </dd> + + <dt>{{ $gettext('Erstellt am') }}</dt> + <dd> + <StudipDateTime :iso="topic.mkdate" /> + </dd> + </dl> </template> diff --git a/resources/vue/components/forum/topics/TopicItem.vue b/resources/vue/components/forum/topics/TopicItem.vue index 581c47f..30ae4d7 100644 --- a/resources/vue/components/forum/topics/TopicItem.vue +++ b/resources/vue/components/forum/topics/TopicItem.vue @@ -1,14 +1,13 @@ <script setup> -import {getTopicDeleteURL, getTopicEditURL, getTopicURL} from "../helpers/urls"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import StudipActionMenu from "@/vue/components/StudipActionMenu.vue"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import StudipDateTime from "@/vue/components/StudipDateTime.vue"; -import {computed, ref} from "vue"; -import ShowTopic from "./ShowTopic.vue"; - -const emit = defineEmits(['swapTopic']); +import {computed} from 'vue'; +import {getTopicDeleteURL, getTopicEditURL, getTopicURL} from '../helpers/urls'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import StudipActionMenu from '@/vue/components/StudipActionMenu.vue'; +import StudipIcon from '@/vue/components/StudipIcon.vue'; +import StudipDateTime from '@/vue/components/StudipDateTime.vue'; + +const emit = defineEmits(['swapTopic', 'showTopic']); const forumConfig = useForumConfig(); const props = defineProps({ @@ -37,20 +36,22 @@ const topicActionMenus = computed(() => { return menu; }); -const isTopicDialogOpen = ref(false); - -const displayTopic = () => { - isTopicDialogOpen.value = true; -} +const showTopic = () => emit('showTopic', props.topic); const editTopic = () => STUDIP.Dialog.fromURL(getTopicEditURL(props.topic.id),{ width: '700' }); -const deleteTopic = () => STUDIP.Dialog.confirm( +const showConfirmDelete = () => STUDIP.Dialog.confirm( $gettext('Wollen Sie dieses "%{name}" Thema löschen? Dann werden auch alle Diskussionen gelöscht!', {name: props.topic.name}), - () => window.location = getTopicDeleteURL(props.topic.id), + () => deleteTopic(), STUDIP.Dialog.close() ); +const deleteTopic = () => { + const deleteForm = document.getElementById('forum-delete-form'); + deleteForm.action = getTopicDeleteURL(props.topic.id); + deleteForm.submit(); +} + const swapTopic = event => { const keyCodes = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown']; @@ -63,180 +64,194 @@ const swapTopic = event => { </script> <template> - <tr v-if="renderType === 'tr'"> - <td> - <div class="topic-overview"> - <div v-if="forumConfig.isModerator" class="drag-area"> - <a class="drag-link" - tabindex="0" - role="option" - :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: topic.name})" - :id="`sort-handle-${topic.id}`" - @keydown="swapTopic"> - <span class="drag-handle"></span> - </a> - </div> - <div class="content"> - <div class="flex-1"> - <div class="title-with-actions"> - <div class="title-with-actions__content"> - <a class="title-with-actions__link" :href="getTopicURL(topic.id)" :title="$gettext('Zum Thema')"> - <span class="topic-title line-clamp-2">{{ topic.name }}</span> - <span - v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" - > + <template v-if="renderType === 'tr'"> + <tr v-bind="$attrs"> + <td> + <div class="topic-overview"> + <div v-if="forumConfig.isModerator" class="drag-area"> + <button + type="button" + :id="`sort-handle-${topic.id}`" + class="drag-link styleless" + @keydown="swapTopic" + :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: topic.name})" + > + <span class="drag-handle"></span> + </button> + </div> + <div class="content"> + <div class="flex-1"> + <div class="title-with-actions"> + <div class="title-with-actions__content"> + <a class="title-with-actions__link" :href="getTopicURL(topic.id)" :title="$gettext('Zum Thema')"> + <span class="topic-title line-clamp-2">{{ topic.name }}</span> + <span + v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + > {{ topic.meta.unread_postings_count }} </span> - </a> - </div> + </a> + </div> - <div class="title-with-actions__actions-xs"> - <StudipActionMenu - :items="topicActionMenus" - @show="displayTopic" - @edit="editTopic" - @delete="deleteTopic" - /> + <div class="title-with-actions__actions-xs"> + <StudipActionMenu + :context="topic.name" + :items="topicActionMenus" + @show="showTopic" + @edit="editTopic" + @delete="showConfirmDelete" + /> + </div> </div> + <p v-if="topic.description"> + <small class="line-clamp-3">{{ topic.description }}</small> + </p> </div> - <p v-if="topic.description"> - <small class="line-clamp-3">{{ topic.description }}</small> - </p> </div> </div> - </div> - <!--mobile display: start--> - <div class="details-xs"> - <dl> - <dt>{{ $gettext('Anzahl der Teilnehmenden am Thema') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true"/> - {{ topic.meta.users_count }} - </dd> - </dl> - - <dl> - <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="forum" role="info" :size="15" aria-hidden="true"/> - {{ topic.meta.discussions_count }} - </dd> - - <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true"/> - {{ topic.meta.postings_count }} - </dd> - - <dt>{{ $gettext('Letzte Aktivität') }}</dt> - <dd class="inline-flex gap-5 items-center"> - <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true"/> - <StudipDateTime v-if="topic.meta.recent_activity" :iso="topic.meta.recent_activity" :relative="true" /> - <template v-else>{{ $gettext('Keine Aktivität') }}</template> - </dd> - </dl> - </div> - <!--mobile display: end--> - </td> - <td class="nowrap" :title="$gettext('Anzahl der Diskussionen')" :aria-label="$gettext('Anzahl der Diskussionen')"> - {{ topic.meta.discussions_count }} {{ $gettext('Diskussionen') }} - </td> - <td> - <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" :aria-label="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> + <!--mobile display: start--> + <div class="details-xs"> + <dl> + <dt>{{ $gettext('Anzahl der Teilnehmenden am Thema') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true"/> + {{ topic.meta.users_count }} + </dd> + </dl> + + <dl> + <dt>{{ $gettext('Anzahl der Diskussionen') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="forum" role="info" :size="15" aria-hidden="true"/> + {{ topic.meta.discussions_count }} + </dd> + + <dt>{{ $gettext('Anzahl der Beiträge') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true"/> + {{ topic.meta.postings_count }} + </dd> + + <dt>{{ $gettext('Letzte Aktivität') }}</dt> + <dd class="inline-flex gap-5 items-center"> + <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true"/> + <StudipDateTime v-if="topic.meta.recent_activity" :iso="topic.meta.recent_activity" :relative="true" /> + <template v-else>{{ $gettext('Keine Aktivität') }}</template> + </dd> + </dl> + </div> + <!--mobile display: end--> + </td> + <td class="nowrap" :title="$gettext('Anzahl der Diskussionen')" :aria-label="$gettext('Anzahl der Diskussionen')"> + {{ topic.meta.discussions_count }} {{ $gettext('Diskussionen') }} + </td> + <td> + <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> <StudipIcon shape="community2" role="info" :size="20" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Teilnehmenden am Thema') }}:</span> <span>{{ topic.meta.users_count }}</span> </span> - </td> - <td> - <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + </td> + <td> + <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="20" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Beiträge') }}:</span> <span>{{ topic.meta.postings_count }}</span> </span> - </td> - <td> - <span class="inline-flex gap-10 items-center nowrap" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + </td> + <td> + <span class="inline-flex gap-10 items-center nowrap" :title="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="20" aria-hidden="true"/> + <span class="sr-only">{{ $gettext('Letzte Aktivität') }}:</span> <StudipDateTime v-if="topic.meta.recent_activity" :iso="topic.meta.recent_activity" :relative="true" /> <template v-else>{{ $gettext('Keine Aktivität') }}</template> </span> - </td> - <td class="actions"> - <StudipActionMenu - :items="topicActionMenus" - @show="displayTopic" - @edit="editTopic" - @delete="deleteTopic" - /> - </td> - </tr> - <a - v-else - :href="getTopicURL(topic.id)" - :title="$gettext('Zum Thema')" - class="styleless" - > - <div class="topic-card"> - <div class="topic-card__content"> - <div class="topic-card__body"> - <div class="flex space-between"> - <div class="flex items-start gap-10"> + </td> + <td class="actions"> + <StudipActionMenu + :context="topic.name" + :items="topicActionMenus" + @show="showTopic" + @edit="editTopic" + @delete="showConfirmDelete" + /> + </td> + </tr> + </template> + <template v-else> + <a + :href="getTopicURL(topic.id)" + :title="$gettext('Zum Thema')" + class="styleless" + v-bind="$attrs" + > + <div class="topic-card"> + <div class="topic-card__content"> + <div class="topic-card__body"> + <div class="flex space-between"> + <div class="flex items-start gap-10"> <span class="topic-card__title topic-title line-clamp-2"> {{ topic.name }} </span> - <span - v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" - > + <span + v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + > {{ topic.meta.unread_postings_count }} </span> - </div> + </div> - <div class="actions"> - <StudipActionMenu - :items="topicActionMenus" - @show="displayTopic" - @edit="editTopic" - @delete="deleteTopic" - /> + <div class="actions"> + <StudipActionMenu + :context="topic.name" + :items="topicActionMenus" + @show="showTopic" + @edit="editTopic" + @delete="showConfirmDelete" + /> + </div> </div> + <p> + <small class="line-clamp-3">{{ topic.description }}</small> + </p> </div> - <p> - <small class="line-clamp-3">{{ topic.description }}</small> - </p> - </div> - <div> - <div v-if="forumConfig.isModerator" class="drag-area"> - <a class="drag-link" - tabindex="0" - role="option" - :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: topic.name})" - :id="`sort-handle-${topic.id}`" - @keydown="swapTopic"> - <span class="drag-handle"></span> - </a> - </div> - <div class="topic-card__footer"> - <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" :aria-label="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> + <div> + <div v-if="forumConfig.isModerator" class="drag-area"> + <button + type="button" + :id="`sort-handle-${topic.id}`" + class="drag-link styleless" + @keydown="swapTopic" + :title="$gettext('Sortierelement für Element %{name}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {name: topic.name})" + > + <span class="drag-handle"></span> + </button> + </div> + <div class="topic-card__footer"> + <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Teilnehmenden am Thema')" role="group"> <StudipIcon shape="community2" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Teilnehmenden am Thema') }}:</span> <small>{{ topic.meta.users_count }}</small> </span> - <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" :aria-label="$gettext('Anzahl der Beiträge')" role="group"> + <span class="inline-flex gap-10 items-center" :title="$gettext('Anzahl der Beiträge')" role="group"> <StudipIcon shape="reply" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Anzahl der Beiträge') }}:</span> <small>{{ topic.meta.postings_count }}</small> </span> - <span class="inline-flex gap-10 items-center" :title="$gettext('Letzte Aktivität')" :aria-label="$gettext('Letzte Aktivität')" role="group"> + <span class="inline-flex gap-10 items-center" :title="$gettext('Letzte Aktivität')" role="group"> <StudipIcon shape="activity" role="info" :size="15" aria-hidden="true" /> + <span class="sr-only">{{ $gettext('Letzte Aktivität') }}:</span> <small v-if="topic.meta.recent_activity"> <StudipDateTime :iso="topic.meta.recent_activity" :relative="true" /> </small> @@ -244,10 +259,10 @@ const swapTopic = event => { {{ $gettext('Keine Aktivität') }} </small> </span> + </div> </div> </div> </div> - </div> - </a> - <ShowTopic :topic="topic" v-model:isOpen="isTopicDialogOpen" /> + </a> + </template> </template> diff --git a/resources/vue/components/forum/topics/TopicsIndex.vue b/resources/vue/components/forum/topics/TopicsIndex.vue index e937932..3da69c6 100644 --- a/resources/vue/components/forum/topics/TopicsIndex.vue +++ b/resources/vue/components/forum/topics/TopicsIndex.vue @@ -1,15 +1,18 @@ <script setup> -import draggable from "vuedraggable"; -import {nextTick, ref, toRef} from "vue"; -import CreateTopic from "./CreateTopic.vue"; -import TopicItem from "./TopicItem.vue"; -import Loader from "../Loader.vue"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; -import {$gettext} from "@/assets/javascripts/lib/gettext"; -import EmptyForum from "../EmptyForum.vue"; -import CategoryItem from "../categories/CategoryItem.vue"; -import {useSortable} from "../../../composables/useSortable"; -import {debounce} from "lodash"; +import {debounce} from 'lodash'; +import draggable from 'vuedraggable'; +import {nextTick, ref, toRef} from 'vue'; +import CreateTopic from './CreateTopic.vue'; +import TopicItem from './TopicItem.vue'; +import Loader from '../Loader.vue'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; +import {$gettext} from '@/assets/javascripts/lib/gettext'; +import EmptyForum from '../EmptyForum.vue'; +import CategoryItem from '../categories/CategoryItem.vue'; +import {useSortable} from '@/vue/composables/useSortable'; +import StudipDialog from '@/vue/components/StudipDialog.vue'; +import ShowTopic from "./ShowTopic.vue"; +import ShowCategory from "../categories/ShowCategory.vue"; const forumConfig = useForumConfig(); @@ -31,6 +34,8 @@ const props = defineProps({ } }); +const currentTopic = ref(null); +const currentCategory = ref(null); const topicsRef = toRef(props, 'topics'); const { @@ -94,6 +99,9 @@ const swapItem = (itemId, step) => { updateOrderDebounced(); }); } + +const showTopicDialog = topic => currentTopic.value = topic; +const showCategoryDialog = category => currentCategory.value = category; </script> <template> @@ -118,13 +126,23 @@ const swapItem = (itemId, step) => { tag="ul"> <template #item="{element}"> <li> - <CategoryItem v-if="element.category" :category="element.category" @swapCategory="swapItem" /> - <TopicItem v-else :topic="element" @swapTopic="swapItem" /> + <CategoryItem + v-if="element.category" + :category="element.category" + @swapCategory="swapItem" + @showCategory="showCategoryDialog(element)" + /> + <TopicItem + v-else + :topic="element" + @swapTopic="swapItem" + @showTopic="showTopicDialog(element)" + /> </li> </template> <template v-if="forumConfig.isModerator" #footer> <li key="footer"> - <div class="topic-card --new-topic"> + <div class="topic-card topic-card--new-topic"> <CreateTopic class="--with-label" :category_id="categoryId" @@ -135,7 +153,7 @@ const swapItem = (itemId, step) => { </template> </draggable> <div v-else-if="forumConfig.isModerator" class="topic-cards-container"> - <div class="topic-card --new-topic"> + <div class="topic-card topic-card--new-topic"> <CreateTopic :category_id="categoryId" class="--with-label" @@ -144,7 +162,7 @@ const swapItem = (itemId, step) => { </div> </div> </div> - <table v-else class="default forum-table --topics-index"> + <table v-else class="default forum-table forum-table--topics-index"> <colgroup> <col> <col style="width: 15%;"> @@ -160,60 +178,65 @@ const swapItem = (itemId, step) => { :aria-sort="getAriaSortString('name')" :aria-label="getAriaSortLabel('name', $gettext('Name'))" > - <a - href="#" - @click.prevent="sortBy('name')" + <button + type="button" + class="as-link" + @click="sortBy('name')" :title="$gettext('Nach Name sortieren')"> {{ $gettext('Name') }} - </a> + </button> </th> <th :class="getSortClass('meta.discussions_count')" :aria-sort="getAriaSortString('meta.discussions_count')" :aria-label="getAriaSortLabel('meta.discussions_count', $gettext('Anzahl der Diskussionen'))" > - <a - href="#" - @click.prevent="sortBy('meta.discussions_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.discussions_count')" :title="$gettext('Nach Anzahl der Diskussionen sortieren')"> {{ $gettext('Diskussionen') }} - </a> + </button> </th> <th :class="getSortClass('meta.users_count')" :aria-sort="getAriaSortString('meta.users_count')" :aria-label="getAriaSortLabel('meta.users_count', $gettext('Anzahl der Teilnehmenden'))" > - <a - href="#" - @click.prevent="sortBy('meta.users_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.users_count')" :title="$gettext('Nach Anzahl der Teilnehmenden sortieren')"> {{ $gettext('Teilnehmende') }} - </a> + </button> </th> <th :class="getSortClass('meta.postings_count')" :aria-sort="getAriaSortString('meta.postings_count')" :aria-label="getAriaSortLabel('meta.postings_count', $gettext('Anzahl der Beiträge'))" > - <a - href="#" - @click.prevent="sortBy('meta.postings_count')" + <button + type="button" + class="as-link" + @click="sortBy('meta.postings_count')" :title="$gettext('Nach Anzahl der Beiträge sortieren')"> {{ $gettext('Beiträge') }} - </a> + </button> </th> <th :class="getSortClass('meta.recent_activity')" :aria-sort="getAriaSortString('meta.recent_activity')" :aria-label="getAriaSortLabel('meta.recent_activity', $gettext('Letzte Aktivität'))" > - <a - href="#" - @click.prevent="sortBy('meta.recent_activity')" + <button + type="button" + class="as-link" + @click="sortBy('meta.recent_activity')" :title="$gettext('Nach letzter Aktivität sortieren')"> {{ $gettext('Letzte Aktivität') }} - </a> + </button> </th> <th></th> </tr> @@ -226,11 +249,22 @@ const swapItem = (itemId, step) => { @end="updateTopicsOrder" :disabled="!forumConfig.isModerator" handle=".drag-handle" - role="listbox" tag="tbody"> <template #item="{element}"> - <CategoryItem v-if="element.category" :category="element.category" render-type="tr" @swapCategory="swapItem" /> - <TopicItem v-else :topic="element" render-type="tr" @swapTopic="swapItem" /> + <CategoryItem + v-if="element.category" + renderType="tr" + :category="element.category" + @swapCategory="swapItem" + @showCategory="showCategoryDialog(element)" + /> + <TopicItem + v-else + renderType="tr" + :topic="element" + @swapTopic="swapItem" + @showTopic="showTopicDialog(element)" + /> </template> </draggable> <tbody v-else> @@ -255,6 +289,36 @@ const swapItem = (itemId, step) => { </tfoot> </table> <slot name="pagination" /> + + <StudipDialog + v-if="currentTopic?.id" + :title="$gettext('Detaillierte Information')" + :closeText="$gettext('Schließen')" + height="700" + width="600" + @close="currentTopic = null" + > + <template #dialogContent> + <div class="forum"> + <ShowTopic :topic="currentTopic" /> + </div> + </template> + </StudipDialog> + + <StudipDialog + v-if="currentCategory?.id" + :title="$gettext('Detaillierte Information')" + :closeText="$gettext('Schließen')" + height="700" + width="600" + @close="currentCategory = null" + > + <template #dialogContent> + <div class="forum"> + <ShowCategory :category="currentCategory" /> + </div> + </template> + </StudipDialog> </template> <EmptyForum v-else-if="showEmptyForumLayout" /> </template> |
