diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2026-01-15 17:10:04 +0100 |
|---|---|---|
| committer | Jan-Hendrik Willms <tleilax+studip@gmail.com> | 2026-02-27 15:29:11 +0100 |
| commit | 04b813348082ef0dbf883d3b6a5b63077331eda6 (patch) | |
| tree | 496371e19fecf107e6f95d4ffe3545cfd6f9c2ab | |
| parent | e6a106f2314239d8c7f7781058dbf7e99d403675 (diff) | |
Resolve "Neues Forum Polishing", #6064, #6063, #6151
Closes #6165
Merge request studip/studip!4671
50 files changed, 1219 insertions, 1017 deletions
diff --git a/app/controllers/course/forum/categories.php b/app/controllers/course/forum/categories.php index df86089..ff1ddfa 100644 --- a/app/controllers/course/forum/categories.php +++ b/app/controllers/course/forum/categories.php @@ -4,7 +4,7 @@ use Forum\Category; class Course_Forum_CategoriesController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); @@ -15,19 +15,17 @@ class Course_Forum_CategoriesController extends Forum\BaseController } } - public function index_action() + public function index_action(): void { $this->render_vue_app( Studip\VueApp::create('forum/categories/Index') ); } - public function show_action($category_id) + public function show_action(Category $category): void { - $category = Category::find($category_id); - if (!$category) { - throw new AccessDeniedException(); + throw new NotFoundException(); } PageLayout::setTitle($category->name); @@ -45,7 +43,7 @@ class Course_Forum_CategoriesController extends Forum\BaseController ); } - public function edit_action($category_id = null) + public function edit_action($category_id = null): void { if (!$this->is_moderator) { throw new AccessDeniedException(); @@ -71,7 +69,7 @@ class Course_Forum_CategoriesController extends Forum\BaseController ); } - public function save_action($category_id = null) + public function save_action($category_id = null): void { if (!$this->is_moderator) { throw new AccessDeniedException(); @@ -100,16 +98,16 @@ class Course_Forum_CategoriesController extends Forum\BaseController $this->relocate('course/forum/categories'); } - public function delete_action($category_id) + public function delete_action(Category $category): void { + CSRFProtection::verifyUnsafeRequest(); + if (!$this->is_moderator) { throw new AccessDeniedException(); } - $category = Category::findOneBySQL("range_id = ? AND category_id = ?", [$this->range_id, $category_id]); - if (!$category) { - throw new AccessDeniedException(); + throw new NotFoundException(); } $category->delete(); diff --git a/app/controllers/course/forum/configs.php b/app/controllers/course/forum/configs.php index c99f718..c2fdc3d 100644 --- a/app/controllers/course/forum/configs.php +++ b/app/controllers/course/forum/configs.php @@ -2,7 +2,7 @@ class Course_Forum_ConfigsController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); @@ -15,12 +15,12 @@ class Course_Forum_ConfigsController extends Forum\BaseController } } - public function edit_action() + public function edit_action(): void { $this->config = Context::get()->getConfiguration(); } - public function save_action() + public function save_action(): void { CSRFProtection::verifyUnsafeRequest(); diff --git a/app/controllers/course/forum/discussion_types.php b/app/controllers/course/forum/discussion_types.php index 68005a5..f350991 100644 --- a/app/controllers/course/forum/discussion_types.php +++ b/app/controllers/course/forum/discussion_types.php @@ -24,14 +24,14 @@ class Course_Forum_DiscussionTypesController extends AuthenticatedController Sidebar::Get()->addWidget($actions); } - public function index_action() + public function index_action(): void { $this->discussion_types = DiscussionType::findBySQL("TRUE ORDER BY mkdate DESC"); } - public function edit_action(DiscussionType $discussion_type = null) + public function edit_action(?DiscussionType $discussion_type = null): void { - if ($discussion_type->isNew()) { + if ($discussionType->isNew()) { PageLayout::setTitle(_('Neuen Diskussionstyp anlegen')); } else { PageLayout::setTitle(_('Diskussionstyp bearbeiten')); @@ -51,23 +51,24 @@ class Course_Forum_DiscussionTypesController extends AuthenticatedController } $this->render_vue_app( - Studip\VueApp::create('forum/discussions_types/Edit')->withProps([ - 'icons' => array_unique($icons), - 'discussion_type' => $discussion_type->toRawArray() - ]) + Studip\VueApp::create('forum/discussions_types/Edit') + ->withProps([ + 'icons' => array_unique($icons), + 'discussion_type' => $discussion_type->toRawArray() + ]) ); } - public function save_action(DiscussionType $discussion_type = null) + public function save_action(?DiscussionType $discussion_type = null): void { CSRFProtection::verifyUnsafeRequest(); - $discussion_type->name = Request::get('name'); - $discussion_type->icon = Request::get('icon'); + $discussionType->name = Request::get('name'); + $discussionType->icon = Request::get('icon'); - $discussion_type->store(); + $discussionType->store(); - PageLayout::postSuccess(sprintf(_('Der Diskussionstyp „%s“ wurde gespeichert.'), $discussion_type->name)); + PageLayout::postSuccess(sprintf(_('Der Diskussionstyp „%s“ wurde gespeichert.'), $discussionType->name)); $this->relocate('course/forum/discussion_types/index'); } diff --git a/app/controllers/course/forum/discussions.php b/app/controllers/course/forum/discussions.php index 219666d..c2bae73 100644 --- a/app/controllers/course/forum/discussions.php +++ b/app/controllers/course/forum/discussions.php @@ -12,7 +12,7 @@ use Forum\Topic; class Course_Forum_DiscussionsController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); @@ -23,8 +23,8 @@ class Course_Forum_DiscussionsController extends Forum\BaseController } } - public function index_action() { - + public function index_action(): void + { $metadata = DBManager::get()->fetchOne( "SELECT COUNT(posting_id) as 'postings_count', @@ -48,12 +48,10 @@ class Course_Forum_DiscussionsController extends Forum\BaseController ); } - public function show_action($discussion_id) + public function show_action(Discussion $discussion): void { - $discussion = Discussion::find($discussion_id); - if (!$discussion) { - throw new AccessDeniedException(); + throw new NotFoundException(); } PageLayout::setTitle($discussion->title); @@ -98,7 +96,7 @@ class Course_Forum_DiscussionsController extends Forum\BaseController $this->render_vue_app( Studip\VueApp::create('forum/discussions/Show') ->withProps([ - 'auth_user' => $auth_user, + 'authUser' => $auth_user, 'discussion' => [ ...$discussion->transformData(), 'topic' => $discussion->topic->toRawArray(), @@ -107,14 +105,14 @@ class Course_Forum_DiscussionsController extends Forum\BaseController 'type' => !empty($discussion->discussion_type) ? $discussion->discussion_type->toRawArray() : [] ], 'category' => $category ? $category->toRawArray() : [], - 'read_index' => (int) ($posting_read ? $posting_read->read_index : 0), + 'readIndex' => (int) ($posting_read ? $posting_read->read_index : 0), 'redirect' => Request::option('redirect'), - 'search_keyword' => Request::get('q', $_SESSION['forum'][$this->range_id]['search_filter']['keyword'] ?? '') + 'searchKeyword' => Request::get('q', $_SESSION['forum'][$this->range_id]['search_filter']['keyword'] ?? '') ]) ); } - public function edit_action(Discussion $discussion = null) + public function edit_action(?Discussion $discussion = null): void { if ($discussion->isNew()) { PageLayout::setTitle(_('Neue Diskussion starten')); @@ -134,9 +132,9 @@ class Course_Forum_DiscussionsController extends Forum\BaseController ['range_id' => $this->range_id] ); - $all_tags = array_map(fn(TagDTO $tag) => $tag->toRawArray(), TagDTO::getForumTags()); - $discussion_tags = array_map(fn(TagDTO $tag) => $tag->toRawArray(), $discussion->tags); - $discussion_types = array_map(fn(DiscussionType $discussion_type) => $discussion_type->toRawArray(), DiscussionType::getForumDiscussionType()); + $allTags = array_map(fn(TagDTO $tag) => $tag->toRawArray(), TagDTO::getForumTags()); + $discussionTags = array_map(fn(TagDTO $tag) => $tag->toRawArray(), $discussion->tags); + $discussionTypes = array_map(fn(DiscussionType $discussion_type) => $discussion_type->toRawArray(), DiscussionType::getForumDiscussionType()); $this->render_vue_app( Studip\VueApp::create('forum/discussions/Edit') @@ -144,16 +142,16 @@ class Course_Forum_DiscussionsController extends Forum\BaseController 'discussion' => [ ...$discussion->transformData(), 'topic_id' => !empty($discussion->topic_id) ? $discussion->topic_id : Request::option('topic_id'), - 'tags' => $discussion_tags + 'tags' => $discussionTags ], 'topics' => $topics, - 'tags' => $all_tags, - 'discussion_types' => $discussion_types + 'tags' => $allTags, + 'discussionTypes' => $discussionTypes ]) ); } - public function save_action($discussion_id = null) + public function save_action($discussion_id = null): void { CSRFProtection::verifyUnsafeRequest(); @@ -223,12 +221,12 @@ class Course_Forum_DiscussionsController extends Forum\BaseController ); } - public function delete_action($discussion_id) + public function delete_action(Discussion $discussion): void { - $discussion = Discussion::find($discussion_id); + CSRFProtection::verifyUnsafeRequest(); if (!$discussion) { - throw new AccessDeniedException(); + throw new NotFoundException(); } if (!$this->is_moderator && $discussion->user_id !== $this->user_id) { diff --git a/app/controllers/course/forum/recent.php b/app/controllers/course/forum/recent.php index 3585c2c..46f6acc 100644 --- a/app/controllers/course/forum/recent.php +++ b/app/controllers/course/forum/recent.php @@ -2,21 +2,21 @@ class Course_Forum_RecentController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); Navigation::activateItem('course/forum/topics'); } - public function index_action() + public function index_action(): void { PageLayout::setTitle(_('Neueste Beiträge')); $this->render_vue_app( Studip\VueApp::create('forum/recent/Index') ->withProps([ - 'last_visit' => Request::int('last_visit') + 'lastVisit' => Request::int('last_visit') ]) ); } diff --git a/app/controllers/course/forum/search.php b/app/controllers/course/forum/search.php index 706674c..b9b3532 100644 --- a/app/controllers/course/forum/search.php +++ b/app/controllers/course/forum/search.php @@ -5,14 +5,14 @@ use Forum\DTO\Tag as TagDTO; class Course_Forum_SearchController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); Navigation::activateItem('course/forum'); } - public function index_action() + public function index_action(): void { $topics = DBManager::get()->fetchAll( "SELECT @@ -43,9 +43,9 @@ class Course_Forum_SearchController extends Forum\BaseController ->withProps([ 'filter' => $this->getForumFilter(), 'topics' => $topics, - 'discussion_types' => $discussion_types, + 'discussionTypes' => $discussion_types, 'tags' => $tags, - 'course_members' => $course_members, + 'courseMembers' => $course_members, ]) ); } diff --git a/app/controllers/course/forum/subscriptions.php b/app/controllers/course/forum/subscriptions.php index 13df712..77b5e0f 100644 --- a/app/controllers/course/forum/subscriptions.php +++ b/app/controllers/course/forum/subscriptions.php @@ -2,7 +2,7 @@ class Course_Forum_SubscriptionsController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); @@ -13,7 +13,7 @@ class Course_Forum_SubscriptionsController extends Forum\BaseController Navigation::activateItem('course/forum/subscriptions'); } - public function index_action() + public function index_action(): void { $this->render_vue_app( Studip\VueApp::create('forum/subscriptions/Index') diff --git a/app/controllers/course/forum/topics.php b/app/controllers/course/forum/topics.php index ca22f89..56ffeee 100644 --- a/app/controllers/course/forum/topics.php +++ b/app/controllers/course/forum/topics.php @@ -6,7 +6,7 @@ use Forum\Topic; class Course_Forum_TopicsController extends Forum\BaseController { - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { parent::before_filter($action, $args); @@ -15,19 +15,17 @@ class Course_Forum_TopicsController extends Forum\BaseController Navigation::activateItem('course/forum/topics'); } - public function index_action() + public function index_action(): void { $this->render_vue_app( Studip\VueApp::create('forum/topics/Index') ); } - public function show_action($topic_id) + public function show_action(Topic $topic): void { - $topic = Topic::find($topic_id); - if (!$topic) { - throw new AccessDeniedException(); + throw new NotFoundException(); } PageLayout::setTitle($topic->name); @@ -49,7 +47,7 @@ class Course_Forum_TopicsController extends Forum\BaseController ->withProps([ 'topic' => $topic->transformData(), 'category' => $topic->category ? $topic->category->transformData() : [], - 'user_subscription' => $user_subscription ? $user_subscription->toRawArray() : [], + 'userSubscription' => $user_subscription ? $user_subscription->toRawArray() : [], 'metadata' => [ 'postings_count' => (int) $topic->metadata['postings_count'], 'users_count' => (int) $topic->metadata['users_count'], @@ -59,7 +57,7 @@ class Course_Forum_TopicsController extends Forum\BaseController ); } - public function edit_action($topic_id = null) + public function edit_action($topic_id = null): void { if (!$this->is_moderator) { throw new AccessDeniedException(); @@ -92,7 +90,7 @@ class Course_Forum_TopicsController extends Forum\BaseController ); } - public function save_action($topic_id = null) + public function save_action($topic_id = null): void { if (!$this->is_moderator) { throw new AccessDeniedException(); @@ -138,15 +136,15 @@ class Course_Forum_TopicsController extends Forum\BaseController $this->relocate('course/forum/topics/show/' . $topic->topic_id); } - public function delete_action($topic_id) + public function delete_action(Topic $topic): void { - if (!$this->is_moderator) { - throw new AccessDeniedException(); - } - - $topic = Topic::getCourseTopic($this->range_id, $topic_id); + CSRFProtection::verifyUnsafeRequest(); if (!$topic) { + throw new NotFoundException(); + } + + if (!$this->is_moderator) { throw new AccessDeniedException(); } diff --git a/lib/classes/Forum/BaseController.php b/lib/classes/Forum/BaseController.php index a3f438b..48706c3 100644 --- a/lib/classes/Forum/BaseController.php +++ b/lib/classes/Forum/BaseController.php @@ -17,7 +17,7 @@ abstract class BaseController extends StudipController protected $is_admin = false; protected $is_moderator = false; - public function before_filter(&$action, &$args) + public function before_filter(&$action, &$args): void { object_set_visit_module('forum'); @@ -56,9 +56,8 @@ abstract class BaseController extends StudipController $actions->addLink( _('Forum verwalten'), $this->url_for('course/forum/configs/edit'), - Icon::create('admin', Icon::ROLE_CLICKABLE, ['title' => _('Forum verwalten')]), - ['data-dialog' => 'width=500;height=300'] - ); + Icon::create('admin', Icon::ROLE_CLICKABLE, ['title' => _('Forum verwalten')]) + )->asDialog('width=500;height=350'); } Sidebar::Get()->addWidget($actions); diff --git a/lib/exceptions/NotFoundException.php b/lib/exceptions/NotFoundException.php new file mode 100644 index 0000000..d1ed900 --- /dev/null +++ b/lib/exceptions/NotFoundException.php @@ -0,0 +1,29 @@ +<?php +declare(strict_types=1); + +class NotFoundException extends Exception +{ + private array $details = []; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct( + $message ?: _('Die angeforderte Ressource wurde nicht gefunden.'), + $code ?: 404, + $previous + ); + } + + public function setDetails(array $details): void + { + $this->details = $details; + } + + public function getDetails(): array + { + return $this->details; + } +} diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index bedd67c..090128b 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -113,6 +113,11 @@ button, border: 0; } + &.styleless { + padding: 0; + margin: 0; + } + &.as-link, &.undecorated { cursor: pointer; diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss index e42fe7d..d2d258f 100644 --- a/resources/assets/stylesheets/scss/forum.scss +++ b/resources/assets/stylesheets/scss/forum.scss @@ -1,5 +1,5 @@ -$card-min-width: 250px; -$card-max-width: 300px; +$cardMinWidth: 300px; +$cardMaxWidth: 350px; .forum { hr { @@ -10,9 +10,9 @@ $card-max-width: 300px; } .forum-table { - &.--discussions-index, - &.--subscription-index, - &.--topics-index { + &--discussions-index, + &--subscription-index, + &--topics-index { .details-xs { display: none; margin-top: 5px; @@ -56,7 +56,7 @@ $card-max-width: 300px; } } - &.--subscription-index { + &--subscription-index { .subscription-button { background-color: transparent; border-color: transparent; @@ -179,16 +179,14 @@ $card-max-width: 300px; } } + table tr.sortable th button { + font-weight: bold; + } + .header { display: flex; background-color: var(--color--fieldset-header); - &.--sticky-top { - position: sticky; - top: 50px; - z-index: 10; - } - .flag { width: 10px; } @@ -374,11 +372,11 @@ $card-max-width: 300px; padding: 0; list-style-type: none; display: grid; - grid-template-columns: repeat(auto-fit, minmax($card-min-width, $card-max-width)); + grid-template-columns: repeat(auto-fit, minmax($cardMinWidth, $cardMaxWidth)); grid-gap: 15px; &.--fill-free-space { - grid-template-columns: repeat(auto-fit, minmax($card-min-width, 1fr)); + grid-template-columns: repeat(auto-fit, minmax($cardMinWidth, 1fr)); } .card-group { @@ -409,7 +407,7 @@ $card-max-width: 300px; width: 10px; } - &.--new-topic { + &--new-topic { display: flex; justify-content: center; align-items: center; @@ -465,8 +463,8 @@ $card-max-width: 300px; background-color: var(--color--tile-background-hover); } - &.--with-hover-style:hover, - &.--with-hover-style:focus { + &--with-hover-style:hover, + &--with-hover-style:focus { outline-color: var(--forum-topic-card-hover-border-color); outline-style: solid; border-color: var(--forum-topic-card-hover-border-color); @@ -481,7 +479,7 @@ $card-max-width: 300px; display: flex; transition: background-color 1s ease; - &.--highlight { + &--highlight { background-color: var(--content-color-10); } @@ -1070,7 +1068,7 @@ $card-max-width: 300px; gap: 5px; cursor: pointer; - &.--active { + &--active { background-color: var(--base-color-20); border-color: var(--base-color-60); } @@ -1597,9 +1595,9 @@ $card-max-width: 300px; } .forum-table { - &.--discussions-index, - &.--subscription-index, - &.--topics-index { + &--discussions-index, + &--subscription-index, + &--topics-index { th:not(:first-child), td:not(:first-child), col:not(:first-child) { 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/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 0e86d82..b812553 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -26,11 +26,11 @@ const props = defineProps({ type: Object, required: true, }, - auth_user: { + authUser: { type: Object, required: true, }, - read_index: { + readIndex: { type: Number, required: true, default: 0 @@ -123,9 +123,13 @@ onMounted(async () => { if (urlHash === 'new-post') { postCreateForm.value = true; } - document.getElementById(urlHash)?.scrollIntoView(); - } else if (props.read_index < posts.value.length) { - document.querySelectorAll(".post")[props.read_index].scrollIntoView(); + jumpTo(document.getElementById(urlHash)) + } else if (props.readIndex < posts.value.length) { + if (props.readIndex === 0) { + jumpTo(document.getElementById('discussion_start')); + } else { + jumpTo(document.querySelector(`[data-index='${props.readIndex}']`)); + } } if (props.search_keyword !== "") { @@ -195,11 +199,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> @@ -207,7 +212,7 @@ onMounted(async () => { </header> <div class="discussion"> <template v-if="posts[0]"> - <Post :post="posts[0]" :auth_user="auth_user" :discussion="discussion" :is_unread="read_index === 0" /> + <Post :post="posts[0]" :authUser="authUser" :discussion="discussion" :readIndex="readIndex" /> </template> <div v-else class="discussion__body"> <Loader v-if="isLoading" /> @@ -219,7 +224,7 @@ onMounted(async () => { <DiscussionFooter :discussion="discussion" :posts="posts" - :read_index="read_index" + :readIndex="readIndex" v-model:postCreateForm="postCreateForm" /> <hr /> @@ -228,9 +233,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" - :is_unread="read_index < index + 2" + :index="index + 1" + :readIndex="readIndex" /> <hr v-if="index < posts.slice(1).length - 1" class="divider"/> </template> @@ -247,8 +253,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 1d5b48b..ea1eeab 100644 --- a/resources/vue/apps/forum/discussions_types/Edit.vue +++ b/resources/vue/apps/forum/discussions_types/Edit.vue @@ -1,12 +1,14 @@ <script setup> -import {computed, reactive} from "vue"; -import StudipIcon from "../../../components/StudipIcon.vue"; +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({ discussion_type: { type: Object, + default: () => ({}) }, icons: { type: Array, @@ -14,8 +16,8 @@ const props = defineProps({ } }); -const formSate = reactive({ - ...props.discussion_type +const form = reactive({ + ...props.discussionType }); const formActionURL = computed(() => { @@ -48,8 +50,9 @@ const formActionURL = computed(() => { required type="text" name="name" - v-model="formSate.name" - maxlength="100" /> + v-model="form.name" + maxlength="100" + /> </label> </section> @@ -60,7 +63,7 @@ const formActionURL = computed(() => { </span> </label> <div id="studip_icons" class="studip-icons-container"> - <input type="hidden" v-model="formSate.icon" name="icon" required /> + <input type="hidden" v-model="form.icon" name="icon" /> <template v-for="icon in icons" :key="icon"> <button @@ -68,10 +71,10 @@ const formActionURL = computed(() => { type="button" :title="icon" :class="{ - 'disabled': formSate.icon && formSate.icon !== icon, - 'active': formSate.icon === icon + 'disabled': form.icon && form.icon !== icon, + 'active': form.icon === icon }" - @click="formSate.icon = icon"> + @click="form.icon = icon"> <StudipIcon :shape="icon" :size="35" /> </button> </template> @@ -79,7 +82,7 @@ const formActionURL = computed(() => { </section> </fieldset> <footer data-dialog-button> - <button :disabled="!formSate.icon || !formSate.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/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 f68f485..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,7 +79,8 @@ onBeforeUnmount(() => { type="button" v-if="withCloseButton" @click="isOpen = false" - class="dropdown__close-button"> + :title="$gettext('Menü schließen')" + class="dropdown__close-button button-base"> <StudipIcon shape="decline" :size="20" /> </button> <div v-if="title" class="dropdown__header"> 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 65bace7..54d29c4 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 UserAvatarDropdown from "./UserAvatarDropdown.vue"; -import Dropdown from "../Dropdown.vue"; -import StudipIcon from "@/vue/components/StudipIcon.vue"; -import UserAvatar from "../UserAvatar.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/UserAvatar.vue'; +import UserAvatarDropdown from '@/vue/components/forum/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"> - <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"> - <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 7711f0b..4a569f0 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 291f14f..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" v-html="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" v-html="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 f5c5d32..f65f250 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 146bcb2..c0fae1d 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -1,18 +1,17 @@ <script setup> -import {computed, ref, useTemplateRef} from "vue"; -import PostEditForm from "./PostEditForm.vue"; -import PostCreateForm from "./PostCreateForm.vue"; -import PostContent from "@/vue/components/forum/posts/PostContent.vue"; -import PostReactions from "./PostReactions.vue"; -import {useForumPost} from "../../../store/pinia/forum/ForumPost"; -import {getDiscussionURL} 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 "../UserAvatarDropdown.vue"; -import {userProfileURL} from "../helpers/urls"; -import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +import {computed, ref, useTemplateRef} from '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 '@/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/forum/UserAvatarDropdown.vue'; +import {useForumPost} from '@/vue/store/pinia/forum/ForumPost'; +import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig'; const forumConfig = useForumConfig(); const forumDiscussionPost = useForumPost(); @@ -25,7 +24,7 @@ const props = defineProps({ type: Object, required: true, }, - auth_user: { + authUser: { type: Object, required: true }, @@ -58,6 +57,7 @@ const editPost = () => { return; } + document.getElementById(`post_${props.post.id}`)?.scrollIntoView({ behavior: 'smooth' }); showPostEditForm.value = true; } @@ -85,18 +85,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> `; @@ -116,7 +117,7 @@ const removePostHighlight = id => { console.error("Element not found!"); return; } - element.classList.remove('--highlight'); + element.classList.remove('post--highlight'); } </script> @@ -184,7 +185,7 @@ const removePostHighlight = id => { <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"> @@ -229,9 +230,8 @@ const removePostHighlight = id => { <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" @@ -242,7 +242,7 @@ const removePostHighlight = id => { :aria-label="$gettext('Beitrag bearbeiten')" > <StudipIcon shape="edit" :size="20" aria-hidden="true" /> - </a> + </button> <button v-if="canDeletePost" @click="deletePost" @@ -252,13 +252,17 @@ const removePostHighlight = id => { > <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="{ @@ -268,7 +272,7 @@ const removePostHighlight = id => { :aria-label="$gettext('Zitieren und Antworten')" > <StudipIcon shape="quote" :size="20" aria-hidden="true" /> - </a> + </button> </div> </div> </div> @@ -277,9 +281,9 @@ const removePostHighlight = id => { </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 8ad13a2..a80e46b 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 a7d62d2..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" v-html="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" v-html="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> |
