aboutsummaryrefslogtreecommitdiff
path: root/resources/vue
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2026-01-15 17:10:04 +0100
committerMurtaza Sultani <sultani@data-quest.de>2026-01-15 17:10:04 +0100
commitc3e07e221b0bef64d3ad4da48c6371c75ca12cc3 (patch)
treea2da9becb70c334b38cdf429f77257167d37993b /resources/vue
parent083da68644d230b8a4b1d8201ed832a5206d3d5c (diff)
Resolve "Neues Forum Polishing", #6064, #6063, #6151
Closes #6165 Merge request studip/studip!4671
Diffstat (limited to 'resources/vue')
-rw-r--r--resources/vue/apps/forum/categories/Edit.vue16
-rw-r--r--resources/vue/apps/forum/categories/Index.vue98
-rw-r--r--resources/vue/apps/forum/categories/Show.vue29
-rw-r--r--resources/vue/apps/forum/configs/Edit.vue10
-rw-r--r--resources/vue/apps/forum/discussions/Edit.vue30
-rw-r--r--resources/vue/apps/forum/discussions/Index.vue29
-rw-r--r--resources/vue/apps/forum/discussions/Show.vue25
-rw-r--r--resources/vue/apps/forum/discussions_types/Edit.vue24
-rw-r--r--resources/vue/apps/forum/discussions_types/Index.vue62
-rw-r--r--resources/vue/apps/forum/recent/Index.vue16
-rw-r--r--resources/vue/apps/forum/search/Index.vue49
-rw-r--r--resources/vue/apps/forum/subscriptions/Index.vue87
-rw-r--r--resources/vue/apps/forum/topics/Edit.vue10
-rw-r--r--resources/vue/apps/forum/topics/Index.vue22
-rw-r--r--resources/vue/apps/forum/topics/Show.vue40
-rw-r--r--resources/vue/components/Dropdown.vue2
-rw-r--r--resources/vue/components/forum/EmptyForum.vue35
-rw-r--r--resources/vue/components/forum/ForumApp.vue9
-rw-r--r--resources/vue/components/forum/ForumMembers.vue74
-rw-r--r--resources/vue/components/forum/SelectTagsInput.vue4
-rw-r--r--resources/vue/components/forum/SelectUserInput.vue4
-rw-r--r--resources/vue/components/forum/SubscriptionDropdown.vue41
-rw-r--r--resources/vue/components/forum/categories/CategoryItem.vue327
-rw-r--r--resources/vue/components/forum/categories/Create.vue30
-rw-r--r--resources/vue/components/forum/categories/ShowCategory.vue83
-rw-r--r--resources/vue/components/forum/discussions/Create.vue30
-rw-r--r--resources/vue/components/forum/discussions/DiscussionFooter.vue18
-rw-r--r--resources/vue/components/forum/discussions/DiscussionIndex.vue85
-rw-r--r--resources/vue/components/forum/discussions/SelectDiscussionType.vue6
-rw-r--r--resources/vue/components/forum/posts/Post.vue50
-rw-r--r--resources/vue/components/forum/posts/PostContent.vue2
-rw-r--r--resources/vue/components/forum/posts/PostCreateForm.vue40
-rw-r--r--resources/vue/components/forum/posts/PostEditForm.vue24
-rw-r--r--resources/vue/components/forum/posts/PostReactionShow.vue18
-rw-r--r--resources/vue/components/forum/posts/PostReactions.vue26
-rw-r--r--resources/vue/components/forum/topics/CreateTopic.vue28
-rw-r--r--resources/vue/components/forum/topics/SelectTopicInput.vue4
-rw-r--r--resources/vue/components/forum/topics/ShowTopic.vue96
-rw-r--r--resources/vue/components/forum/topics/TopicItem.vue335
-rw-r--r--resources/vue/components/forum/topics/TopicsIndex.vue142
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>