diff options
43 files changed, 667 insertions, 380 deletions
diff --git a/app/controllers/course/forum/categories.php b/app/controllers/course/forum/categories.php index 8ed7950..df86089 100644 --- a/app/controllers/course/forum/categories.php +++ b/app/controllers/course/forum/categories.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; use Forum\Category; diff --git a/app/controllers/course/forum/configs.php b/app/controllers/course/forum/configs.php index 209799f..c99f718 100644 --- a/app/controllers/course/forum/configs.php +++ b/app/controllers/course/forum/configs.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; class Course_Forum_ConfigsController extends Forum\BaseController { @@ -7,6 +6,10 @@ class Course_Forum_ConfigsController extends Forum\BaseController { parent::before_filter($action, $args); + if (!$this->user_id) { + throw new LoginException(); + } + if (! $this->is_admin) { throw new AccessDeniedException(); } diff --git a/app/controllers/course/forum/discussions.php b/app/controllers/course/forum/discussions.php index ee6e1d1..e6cec64 100644 --- a/app/controllers/course/forum/discussions.php +++ b/app/controllers/course/forum/discussions.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; use Studip\Markup; use Forum\Discussion; @@ -59,27 +58,38 @@ class Course_Forum_DiscussionsController extends Forum\BaseController PageLayout::setTitle($discussion->title); - $auth_user = User::findCurrent(); - $discussion->view_count += 1; $discussion->store(); - $posting_read = PostingRead::findOneBySQL( - "discussion_id = :discussion_id AND user_id = :user_id", - [ - 'discussion_id' => $discussion->getId(), - 'user_id' => User::findCurrent()->user_id - ] - ); - - $user_subscription = Subscription::findOneBySQL( - "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", - [ - 'subject' => 'discussion', - 'subject_id' => $discussion->getId(), - 'user_id' => $auth_user->user_id - ] - ); + $auth = User::findCurrent(); + $posting_read = null; + $auth_user = []; + if ($auth) { + $posting_read = PostingRead::findOneBySQL( + "discussion_id = :discussion_id AND user_id = :user_id", + [ + 'discussion_id' => $discussion->getId(), + 'user_id' => $auth->user_id + ] + ); + + $user_subscription = Subscription::findOneBySQL( + "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", + [ + 'subject' => 'discussion', + 'subject_id' => $discussion->getId(), + 'user_id' => $auth->user_id + ] + ); + + $auth_user = [ + 'id' => $auth->id, + 'username' => $auth->username, + 'name' => $auth->getFullName(), + 'avatar_url' => Avatar::getAvatar($auth->user_id)->getURL(Avatar::NORMAL), + 'subscription' => $user_subscription ? $user_subscription->toRawArray() : [] + ]; + } $category = $discussion->getCategory(); $tags = array_map(fn(TagDTO $tag) => $tag->toRawArray(), $discussion->tags); @@ -88,13 +98,7 @@ class Course_Forum_DiscussionsController extends Forum\BaseController $this->render_vue_app( Studip\VueApp::create('forum/discussions/Show') ->withProps([ - 'auth_user' => [ - 'id' => $auth_user->id, - 'username' => $auth_user->username, - 'name' => $auth_user->getFullName(), - 'avatar_url' => Avatar::getAvatar($auth_user->user_id)->getURL(Avatar::NORMAL), - 'subscription' => $user_subscription ? $user_subscription->toRawArray() : [] - ], + 'auth_user' => $auth_user, 'discussion' => [ ...$discussion->transformData(), 'topic' => $discussion->topic->toRawArray(), @@ -157,7 +161,7 @@ class Course_Forum_DiscussionsController extends Forum\BaseController $discussion = Discussion::find($discussion_id); } else { $discussion = new Discussion(); - $discussion->user_id = User::findCurrent()->user_id; + $discussion->user_id = $this->user_id; } $discussion->title = Request::get('title'); @@ -189,7 +193,7 @@ class Course_Forum_DiscussionsController extends Forum\BaseController 'range_id' => $this->range_id, 'discussion_id' => $discussion->discussion_id, 'content' => Markup::purifyHtml(Markup::markAsHtml(Request::get('content'))), - 'user_id' => User::findCurrent()->user_id + 'user_id' => $this->user_id ]); } else { TagRelation::deleteBySQL("range_id = ? AND range_type = 'forum'", [$discussion->discussion_id]); @@ -228,7 +232,7 @@ class Course_Forum_DiscussionsController extends Forum\BaseController throw new AccessDeniedException(); } - if (!$this->is_moderator && $discussion->user_id !== User::findCurrent()->user_id) { + if (!$this->is_moderator && $discussion->user_id !== $this->user_id) { throw new AccessDeniedException(); } diff --git a/app/controllers/course/forum/recent.php b/app/controllers/course/forum/recent.php index 7a117cd..3585c2c 100644 --- a/app/controllers/course/forum/recent.php +++ b/app/controllers/course/forum/recent.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; class Course_Forum_RecentController extends Forum\BaseController { diff --git a/app/controllers/course/forum/search.php b/app/controllers/course/forum/search.php index f6fc223..706674c 100644 --- a/app/controllers/course/forum/search.php +++ b/app/controllers/course/forum/search.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; use Forum\DiscussionType; use Forum\DTO\Tag as TagDTO; diff --git a/app/controllers/course/forum/subscriptions.php b/app/controllers/course/forum/subscriptions.php index 1f5582c..13df712 100644 --- a/app/controllers/course/forum/subscriptions.php +++ b/app/controllers/course/forum/subscriptions.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; class Course_Forum_SubscriptionsController extends Forum\BaseController { @@ -7,6 +6,10 @@ class Course_Forum_SubscriptionsController extends Forum\BaseController { parent::before_filter($action, $args); + if (!$this->user_id) { + throw new LoginException(); + } + Navigation::activateItem('course/forum/subscriptions'); } diff --git a/app/controllers/course/forum/topics.php b/app/controllers/course/forum/topics.php index b6a1307..ca22f89 100644 --- a/app/controllers/course/forum/topics.php +++ b/app/controllers/course/forum/topics.php @@ -1,5 +1,4 @@ <?php -require_once 'BaseController.php'; use Forum\Category; use Forum\Subscription; @@ -33,14 +32,17 @@ class Course_Forum_TopicsController extends Forum\BaseController PageLayout::setTitle($topic->name); - $user_subscription = Subscription::findOneBySQL( - "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", - [ - 'subject' => 'topic', - 'subject_id' => $topic->getId(), - 'user_id' => User::findCurrent()->user_id - ] - ); + $user_subscription = null; + if ($this->user_id) { + $user_subscription = Subscription::findOneBySQL( + "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", + [ + 'subject' => 'topic', + 'subject_id' => $topic->getId(), + 'user_id' => $this->user_id + ] + ); + } $this->render_vue_app( Studip\VueApp::create('forum/topics/Show') diff --git a/app/controllers/course/forum/BaseController.php b/lib/classes/Forum/BaseController.php index 6525250..aa1bfd2 100644 --- a/app/controllers/course/forum/BaseController.php +++ b/lib/classes/Forum/BaseController.php @@ -9,21 +9,27 @@ use Request; use SearchWidget; use Sidebar; use StudipController; +use User; abstract class BaseController extends StudipController { protected $with_session = true; + protected $is_admin = false; + protected $is_moderator = false; public function before_filter(&$action, &$args) { object_set_visit_module('forum'); $this->range_id = Context::getId(); - $this->is_moderator = CoreForum::isModerator($this->range_id); - $this->is_admin = CoreForum::isAdmin($this->range_id); + $this->user_id = User::findCurrent()?->user_id; - $this->buildSidebar(); + if ($this->user_id) { + $this->is_admin = CoreForum::isAdmin($this->range_id); + $this->is_moderator = CoreForum::isModerator($this->range_id); + } + $this->buildSidebar(); parent::before_filter($action, $args); } @@ -31,11 +37,13 @@ abstract class BaseController extends StudipController { $actions = new ActionsWidget(); - $actions->addLink( - _('Neue Diskussion starten'), - $this->url_for('course/forum/discussions/edit'), - Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Diskussion starten')]) - )->asDialog('width=900;height=750'); + if ($this->user_id) { + $actions->addLink( + _('Neue Diskussion starten'), + $this->url_for('course/forum/discussions/edit'), + Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Diskussion starten')]) + )->asDialog('width=900;height=750'); + } if ($this->is_admin) { $actions->addLink( diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 235f4b6..47bd666 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -169,6 +169,7 @@ class RouteMap } $this->addUnauthenticatedTreeRoutes($group); + $this->addUnAuthenticatedForumRoutes($group); } private function getAuthenticator(): callable @@ -675,44 +676,31 @@ class RouteMap { $group->group('/courses/{range_id}', function ($forum) { $forum->get('/forum-configs', Routes\Forum\ConfigIndex::class); - $forum->get('/forum-categories', Routes\Forum\CategoryIndex::class); - $forum->get('/forum-discussions', Routes\Forum\DiscussionIndex::class); - $forum->get('/forum-topics', Routes\Forum\TopicIndex::class); $forum->get('/forum-subscriptions', Routes\Forum\SubscriptionIndex::class); }); - $group->group('/forum-subscriptions', function ($forum) { - $forum->post('', Routes\Forum\SubscriptionStore::class); - $forum->get('/{subscription_id}', Routes\Forum\SubscriptionShow::class); - $forum->delete('/{subscription_id}', Routes\Forum\SubscriptionDelete::class); - }); - $group->group('/forum-topics', function ($forum) { - $forum->get('/{topic_id}', Routes\Forum\TopicShow::class); - $forum->get('/{topic_id}/discussions', Routes\Forum\TopicDiscussions::class); $forum->patch('/sort', Routes\Forum\TopicUpdateSort::class); }); $group->group('/forum-categories', function ($forum) { - $forum->get('/{category_id}', Routes\Forum\CategoryShow::class); - $forum->get('/{category_id}/topics', Routes\Forum\CategoryTopics::class); $forum->patch('/sort', Routes\Forum\CategoryUpdateSort::class); }); + $group->group('/forum-subscriptions', function ($forum) { + $forum->post('', Routes\Forum\SubscriptionStore::class); + $forum->get('/{subscription_id}', Routes\Forum\SubscriptionShow::class); + $forum->delete('/{subscription_id}', Routes\Forum\SubscriptionDelete::class); + }); + $group->group('/forum-discussion-types', function ($forum) { $forum->get('', Routes\Forum\DiscussionTypeIndex::class); $forum->get('/{type_id}', Routes\Forum\DiscussionTypeShow::class); }); - $group->group('/forum-discussions', function ($forum) { - $forum->get('/{discussion_id}', Routes\Forum\DiscussionShow::class); - $forum->get('/{discussion_id}/postings', Routes\Forum\DiscussionPostings::class); - }); - $group->group('/forum-postings', function ($forum) { $forum->post('', Routes\Forum\PostingStore::class); $forum->get('/{posting_id}', Routes\Forum\PostingShow::class); - $forum->get('/{posting_id}/reactions', Routes\Forum\PostingReactions::class); $forum->patch('/{posting_id}', Routes\Forum\PostingUpdate::class); $forum->delete('/{posting_id}', Routes\Forum\PostingDelete::class); }); @@ -724,6 +712,34 @@ class RouteMap }); } + private function addUnAuthenticatedForumRoutes(RouteCollectorProxy $group): void + { + $group->group('/courses/{range_id}', function ($forum) { + $forum->get('/forum-categories', Routes\Forum\CategoryIndex::class); + $forum->get('/forum-discussions', Routes\Forum\DiscussionIndex::class); + $forum->get('/forum-topics', Routes\Forum\TopicIndex::class); + }); + + $group->group('/forum-topics', function ($forum) { + $forum->get('/{topic_id}', Routes\Forum\TopicShow::class); + $forum->get('/{topic_id}/discussions', Routes\Forum\TopicDiscussions::class); + }); + + $group->group('/forum-categories', function ($forum) { + $forum->get('/{category_id}', Routes\Forum\CategoryShow::class); + $forum->get('/{category_id}/topics', Routes\Forum\CategoryTopics::class); + }); + + $group->group('/forum-discussions', function ($forum) { + $forum->get('/{discussion_id}', Routes\Forum\DiscussionShow::class); + $forum->get('/{discussion_id}/postings', Routes\Forum\DiscussionPostings::class); + }); + + $group->group('/forum-postings', function ($forum) { + $forum->get('/{posting_id}/reactions', Routes\Forum\PostingReactions::class); + }); + } + private function addAuthenticatedStockImagesRoutes(RouteCollectorProxy $group): void { $group->get('/stock-images', Routes\StockImages\StockImagesIndex::class); diff --git a/lib/classes/JsonApi/Routes/Forum/DiscussionPostings.php b/lib/classes/JsonApi/Routes/Forum/DiscussionPostings.php index 8b64021..b40e4c4 100644 --- a/lib/classes/JsonApi/Routes/Forum/DiscussionPostings.php +++ b/lib/classes/JsonApi/Routes/Forum/DiscussionPostings.php @@ -40,7 +40,9 @@ class DiscussionPostings extends JsonApiController $postings = $discussion->postings ?? \SimpleORMapCollection::createFromArray([]); - PostingRead::updateUserReadPoint($user->user_id, $discussion->discussion_id, count($postings)); + if ($user) { + PostingRead::updateUserReadPoint($user->user_id, $discussion->discussion_id, count($postings)); + } return $this->getPaginatedContentResponse( $postings->limit(...$this->getOffsetAndLimit()), diff --git a/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php b/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php new file mode 100644 index 0000000..0a0017f --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php @@ -0,0 +1,14 @@ +<?php + +namespace JsonApi\Routes\Forum; + +use Range; +use User; + +class ForumAuthority +{ + public static function canShowForum(Range $range, ?User $user = null): bool + { + return $range->isAccessibleToUser($user?->id); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/PostingShow.php b/lib/classes/JsonApi/Routes/Forum/PostingShow.php index c062169..730dc4f 100644 --- a/lib/classes/JsonApi/Routes/Forum/PostingShow.php +++ b/lib/classes/JsonApi/Routes/Forum/PostingShow.php @@ -12,6 +12,7 @@ class PostingShow extends JsonApiController { protected $allowedIncludePaths = [ \JsonApi\Schemas\Forum\Posting::REL_DISCUSSION, + \JsonApi\Schemas\Forum\Posting::REL_AUTHOR, \JsonApi\Schemas\Forum\Posting::REL_POSTING, \JsonApi\Schemas\Forum\Posting::REL_OPENGRAPH_URLS, \JsonApi\Schemas\Forum\Posting::REL_REACTIONS, diff --git a/lib/classes/JsonApi/Schemas/Forum/PostingReaction.php b/lib/classes/JsonApi/Schemas/Forum/PostingReaction.php index 9441b63..4104e01 100644 --- a/lib/classes/JsonApi/Schemas/Forum/PostingReaction.php +++ b/lib/classes/JsonApi/Schemas/Forum/PostingReaction.php @@ -55,14 +55,15 @@ class PostingReaction extends SchemaProvider return $relationships; } - private function addUserRelationship(array $relationships, $discussion, bool $withUser = false) + private function addUserRelationship(array $relationships, $postingReaction, bool $withUser = false) { - if ($withUser) { + $user = $postingReaction->user; + if ($withUser && $user) { $relationships[self::REL_USER] = [ self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->createLinkToResource($discussion->user) + Link::RELATED => $this->createLinkToResource($user) ], - self::RELATIONSHIP_DATA => $discussion->user + self::RELATIONSHIP_DATA => $user ]; } diff --git a/lib/models/Forum/Category.php b/lib/models/Forum/Category.php index 73c96e1..6ee52c8 100644 --- a/lib/models/Forum/Category.php +++ b/lib/models/Forum/Category.php @@ -80,7 +80,7 @@ class Category extends \SimpleORMap WHERE `forum_topics`.`category_id` = :category_id", [ 'category_id' => $this->category_id, - 'user_id' => User::findCurrent()->user_id + 'user_id' => User::findCurrent()?->user_id ] ); } diff --git a/lib/models/Forum/Discussion.php b/lib/models/Forum/Discussion.php index 96bd85a..de82422 100644 --- a/lib/models/Forum/Discussion.php +++ b/lib/models/Forum/Discussion.php @@ -242,7 +242,7 @@ class Discussion extends SimpleORMap public function getMetaData(int $last_visit = 0): array { - $user_id = \User::findCurrent()->user_id; + $user_id = User::findCurrent()?->user_id; if (!$last_visit) { $plugin_id = \PluginEngine::getPlugin(\CoreForum::class)->getPluginId(); diff --git a/lib/models/Forum/Topic.php b/lib/models/Forum/Topic.php index a6b298f..4ea8361 100644 --- a/lib/models/Forum/Topic.php +++ b/lib/models/Forum/Topic.php @@ -141,7 +141,7 @@ class Topic extends SimpleORMap WHERE `forum_discussions`.`topic_id` = :topic_id", [ 'topic_id' => $this->topic_id, - 'user_id' => User::findCurrent()->user_id + 'user_id' => User::findCurrent()?->user_id ] ); } diff --git a/resources/assets/javascripts/lib/dates.js b/resources/assets/javascripts/lib/dates.js index ccb67a8..04b2ec5 100644 --- a/resources/assets/javascripts/lib/dates.js +++ b/resources/assets/javascripts/lib/dates.js @@ -52,7 +52,9 @@ const Dates = { }).done(function() { $('.topic_' + termin_id + '_' + topic_id).remove(); }); - } + }, + stringToUnixTimestamp: date => ((new Date(date)).getTime() / 1000), + unixTimestampToISO: timestamp => new Date(timestamp * 1000).toISOString() }; export default Dates; diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index 0f39cb7..63457fd 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -98,8 +98,23 @@ button.button { } } +@mixin button-base() { + color: var(--base-color); + transition: color var(--transition-duration); + + &:hover, + &:active, + &.active, + &[aria-pressed="true"] { + color: var(--active-color); + text-decoration: none; + } +} + button, .button { + @include button-base; + &.as-link, &.styleless, &.undecorated, @@ -114,22 +129,10 @@ button, margin: 0; padding: 0; } +} - &.as-link, - &.undecorated[formaction] { - color: var(--base-color); - transition: color var(--transition-duration); - - &:hover, - &:active { - color: var(--active-color); - text-decoration: none; - } - - &[disabled] { - pointer-events: none; - } - } +.button-base { + @include button-base; } label { @@ -203,4 +206,4 @@ $square-button-size: 130px; color: var(--color--highlight-hover); border-color: var(--color--highlight); } -}
\ No newline at end of file +} diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss index 72c66a9..68a5967 100644 --- a/resources/assets/stylesheets/scss/forum.scss +++ b/resources/assets/stylesheets/scss/forum.scss @@ -63,7 +63,7 @@ $card-max-width: 300px; justify-content: end; padding: 0; &:hover { - color: var(--color--highlight); + color: var(--active-color); } } } @@ -304,6 +304,8 @@ $card-max-width: 300px; p { margin-top: 5px; color: var(--color--font-secondary); + word-break: break-word; + overflow-wrap: break-word; } } @@ -318,6 +320,8 @@ $card-max-width: 300px; p { margin-top: 5px; color: var(--color--font-secondary); + word-break: break-word; + overflow-wrap: break-word; } .discussion-category { @@ -437,6 +441,8 @@ $card-max-width: 300px; p { margin-top: 5px; color: var(--color--font-secondary); + word-break: break-word; + overflow-wrap: break-word; } &__content { @@ -500,9 +506,8 @@ $card-max-width: 300px; } &__text { - p { - color: var(--color--font-primary); - } + word-break: break-word; + overflow-wrap: break-word; img { max-width: 100% !important; @@ -765,7 +770,7 @@ $card-max-width: 300px; &:hover, &:focus, - &.active { + &[aria-ppressed="true"] { position: relative; z-index: 1; @@ -1094,60 +1099,58 @@ $card-max-width: 300px; .dropdown__items { max-width: 300px; + li { - display: flex; - align-items: center; - gap: 15px; - padding: 10px 15px; + padding: 0; - .subscription-option { - flex: 1; + button { + cursor: pointer; + background: none; + border: none; + width: 100%; display: flex; align-items: center; - justify-content: space-between; - gap: 5px; - } + gap: 15px; + padding: 10px 15px; - .option-title { - font-size: 14px; - color: var(--color--font-primary); - font-weight: 400; - margin: 0; - } + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } - p { - color: var(--color--font-secondary); - margin-top: 6px; - font-size: small; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - } + .subscription-option { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + } - &.all { - background-color: $green-20; - cursor: default; - } + .option-title { + font-size: 14px; + font-weight: 400; + margin: 0; + } - &.replies_only { - background-color: $activity-color-20; - cursor: default; - } + &.all { + background-color: $green-20; + cursor: default; + } - &.none { - background-color: $dark-gray-color-20; - cursor: default; - } + &.replies_only { + background-color: $activity-color-20; + cursor: default; + } - &.--active { - background-color: $dark-gray-color-10; - cursor: default; - } + &.none { + background-color: $dark-gray-color-20; + cursor: default; + } - &.--disabled { - opacity: 0.5; - cursor: not-allowed; + &.active { + background-color: $dark-gray-color-10; + cursor: default; + } } } } @@ -1301,6 +1304,10 @@ $card-max-width: 300px; .color-font-secondary { color: var(--color--font-secondary); } + + .break-word { + word-break: break-word; + } } .vs__actions { @@ -1468,6 +1475,7 @@ $card-max-width: 300px; .tab { &__buttons { display: flex; + flex-wrap: wrap; gap: 10px; border-bottom: 2px solid var(--color--divider); } @@ -1475,8 +1483,6 @@ $card-max-width: 300px; &__button { label { position: relative; - transition: color 0.3s ease; - color: var(--color--highlight); font-weight: bold; padding: 6px 12px; cursor: pointer; @@ -1485,15 +1491,13 @@ $card-max-width: 300px; gap: 5px; &:hover { - color: var(--color--highlight-hover); - &::after { background-color: var(--color--focus); } } &:hover, - &.is-checked { + &.active { &::after { content: ''; position: absolute; @@ -1504,7 +1508,7 @@ $card-max-width: 300px; } } - &.is-checked { + &.active { &::after { background-color: var(--color--highlight); } diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index f0f17d4..284734c 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -788,8 +788,8 @@ input.allow-plaintext-toggle { display: flex; align-items: center; justify-content: center; - background: transparent; - border: transparent; + background: none; + border: none; height: 20px; width: 20px; cursor: pointer; diff --git a/resources/vue/apps/forum/categories/Index.vue b/resources/vue/apps/forum/categories/Index.vue index 93977cf..647a6a5 100644 --- a/resources/vue/apps/forum/categories/Index.vue +++ b/resources/vue/apps/forum/categories/Index.vue @@ -122,7 +122,7 @@ const swapCategory = (categoryId, step) => { </div> <div class="actions"> - <CreateCategory v-if="forumConfig.isModerator" /> + <CreateCategory /> <button v-if="forumConfig.tileLayout" @click="forumConfig.toggleForumLayout()" @@ -176,7 +176,6 @@ const swapCategory = (categoryId, step) => { <div v-else-if="forumConfig.isModerator" class="topic-cards-container"> <div class="topic-card --new-topic"> <CreateCategory - v-if="forumConfig.isModerator" class="--with-label" :label="$gettext('Neue Kategorie anlegen')" /> diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue index 1b09a7b..b0a6f66 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -1,8 +1,6 @@ <script setup> import {onMounted, computed, ref} from "vue"; import ForumApp from "@/vue/components/forum/ForumApp.vue"; -import ForumMembers from "@/vue/components/forum/ForumMembers.vue"; -import {numberFormatter} from "../../../../assets/javascripts/lib/number_formatter"; import {useForumPost} from "../../../store/pinia/forum/ForumPost"; import {$gettext} from "../../../../assets/javascripts/lib/gettext"; import Post from "@/vue/components/forum/posts/Post.vue"; @@ -15,6 +13,7 @@ import SubscriptionDropdown from "@/vue/components/forum/SubscriptionDropdown.vu import {highlightText, removeHighlight} from "@/vue/components/forum/helpers"; import {getSearchURL, getTopicURL, getDiscussionIndexURL} from "@/vue/components/forum/helpers/urls"; import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; +import DiscussionFooter from "../../../components/forum/discussions/DiscussionFooter.vue"; const forumConfig = useForumConfig(); const forumPostStore = useForumPost(); @@ -115,7 +114,6 @@ const fetchPostings = async () => { }; - onMounted(async () => { isLoading.value = true; @@ -194,17 +192,19 @@ onMounted(async () => { </em> <StudipIcon shape="lock-locked2" :size="20" role="inactive" /> </div> - <button v-if="canEditDiscussion" @click="editDiscussion(discussion.discussion_id)" type="button" :title="$gettext('Diskussion bearbeiten')" class="button button--icon-only"> - <StudipIcon shape="edit" :size="20" /> - </button> - <SubscriptionDropdown - v-if="!discussion.closed_at" - :subject="{ - id: discussion.discussion_id, - type: 'forum-discussions' - }" - :user_subscription="auth_user.subscription" - /> + <template v-if="!forumConfig.allowGuestAccess"> + <button v-if="canEditDiscussion" @click="editDiscussion(discussion.discussion_id)" type="button" :title="$gettext('Diskussion bearbeiten')" class="button button--icon-only"> + <StudipIcon shape="edit" :size="20" /> + </button> + <SubscriptionDropdown + v-if="!discussion.closed_at" + :subject="{ + id: discussion.discussion_id, + type: 'forum-discussions' + }" + :user_subscription="auth_user.subscription" + /> + </template> </div> </div> </header> @@ -219,43 +219,12 @@ onMounted(async () => { </p> </div> <hr /> - <div class="discussion__status"> - <div class="flex items-start gap-20"> - <div class="text-center"> - <p>{{ $gettext('Erstellt') }}</p> - <StudipDateTime :iso="discussion.mkdate" :date_only="true"/> - </div> - <div class="text-center"> - <p>{{ $gettext('Beiträge') }}</p> - <p>{{ posts.length }}</p> - </div> - <div class="text-center"> - <p>{{ $gettext('Aufrufe') }}</p> - <p>{{ numberFormatter(discussion.view_count, 1) }}</p> - </div> - <div class="text-center"> - <p>{{ $gettext('Aktivität') }}</p> - <StudipDateTime v-if="posts[posts.length -1]" :iso="posts[posts.length -1].mkdate" :relative="true" /> - <StudipDateTime v-else :iso="discussion.mkdate" :relative="true"/> - </div> - </div> - <ForumMembers :members="discussion.members" :limit="5" size="35px" /> - <a - v-if="!discussion.closed_at" - href="#new-post" - class="button button--icon-label" - role="button" - :title="$gettext('Antworten')" - :aria-label="$gettext('Antworten')" - :class="{ - 'disabled': postCreateForm - }" - @click="postCreateForm = true" - > - <StudipIcon shape="reply" :size="20" aria-hidden="true" /> - {{ $gettext('Antworten') }} - </a> - </div> + <DiscussionFooter + :discussion="discussion" + :posts_count="posts.length" + :recent_activity="posts[posts.length - 1] ? posts[posts.length - 1].mkdate : null" + v-model:postCreateForm="postCreateForm" + /> <hr /> </div> <div class="posts-container"> @@ -271,43 +240,12 @@ onMounted(async () => { </div> <div v-if="posts.length > 3" class="discussion"> - <div class="discussion__status"> - <div class="flex items-start gap-20"> - <div class="text-center"> - <p>{{ $gettext('Erstellt') }}</p> - <StudipDateTime :iso="discussion.mkdate" :date_only="true"/> - </div> - <div class="text-center"> - <p>{{ $gettext('Beiträge') }}</p> - <p>{{ posts.length }}</p> - </div> - <div class="text-center"> - <p>{{ $gettext('Aufrufe') }}</p> - <p>{{ numberFormatter(discussion.view_count, 1) }}</p> - </div> - <div class="text-center"> - <p>{{ $gettext('Aktivität') }}</p> - <StudipDateTime v-if="posts[posts.length -1]" :iso="posts[posts.length -1].mkdate" :relative="true" /> - <StudipDateTime v-else :iso="discussion.mkdate" :relative="true"/> - </div> - </div> - <ForumMembers :members="discussion.members" :limit="5" size="35px" /> - <a - v-if="!discussion.closed_at" - href="#new-post" - class="button button--icon-label" - role="button" - :title="$gettext('Antworten')" - :aria-label="$gettext('Antworten')" - :class="{ - 'disabled': postCreateForm - }" - @click="postCreateForm = true" - > - <StudipIcon shape="reply" :size="20" aria-hidden="true" /> - {{ $gettext('Antworten') }} - </a> - </div> + <DiscussionFooter + :discussion="discussion" + :posts_count="posts.length" + :recent_activity="posts[posts.length - 1].mkdate" + v-model:postCreateForm="postCreateForm" + /> </div> <div id="new-post" class="post-form-container"> diff --git a/resources/vue/apps/forum/topics/Index.vue b/resources/vue/apps/forum/topics/Index.vue index 9df0a78..75c5267 100644 --- a/resources/vue/apps/forum/topics/Index.vue +++ b/resources/vue/apps/forum/topics/Index.vue @@ -49,8 +49,8 @@ const fetchTopics = async (_, offset = 0) => { } onMounted(async () => { - await fetchTopics() -}) + await fetchTopics(); +}); </script> <template> @@ -63,7 +63,7 @@ onMounted(async () => { {{ $gettext('Themen') }} </h2> <div class="actions"> - <CreateTopic v-if="forumConfig.isModerator" /> + <CreateTopic /> <button v-if="forumConfig.tileLayout" @click="forumConfig.toggleForumLayout();" diff --git a/resources/vue/apps/forum/topics/Show.vue b/resources/vue/apps/forum/topics/Show.vue index 619df47..58319fa 100644 --- a/resources/vue/apps/forum/topics/Show.vue +++ b/resources/vue/apps/forum/topics/Show.vue @@ -10,7 +10,9 @@ 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"; +const forumConfig = useForumConfig(); const props = defineProps({ topic: { type: Object, @@ -25,8 +27,7 @@ const props = defineProps({ required: true, }, user_subscription: { - type: Object, - required: true + type: Object }, }); @@ -96,7 +97,7 @@ onMounted(async () => { </div> </div> - <div class="actions"> + <div v-if="!forumConfig.allowGuestAccess" class="actions"> <CreateDiscussion :topic_id="topic.topic_id" /> <SubscriptionDropdown :title="$gettext('Thema abonnieren')" diff --git a/resources/vue/components/Dropdown.vue b/resources/vue/components/Dropdown.vue index 02980ee..1063516 100644 --- a/resources/vue/components/Dropdown.vue +++ b/resources/vue/components/Dropdown.vue @@ -54,6 +54,7 @@ watch(isOpen, async (open) => { aria-labelledby="dropdown-title" > <button + type="button" v-if="withCloseButton" @click="isOpen = false" class="dropdown__close-button"> diff --git a/resources/vue/components/UserAvatar.vue b/resources/vue/components/UserAvatar.vue index b9c0b1c..441a41c 100644 --- a/resources/vue/components/UserAvatar.vue +++ b/resources/vue/components/UserAvatar.vue @@ -10,7 +10,7 @@ const props = defineProps({ }); const isOpen = defineModel({ default: false }); -const AUTH_ID = STUDIP.USER_ID +const AUTH_ID = STUDIP.USER_ID; const vCardDownloadURL = STUDIP.URLHelper.getURL('dispatch.php/contact/vcard', {'user[]': props.user.username}); const userProfileURL = STUDIP.URLHelper.getURL('dispatch.php/profile', {username: props.user.username}); @@ -26,7 +26,7 @@ const writeMessage = () => { } ); - isOpen.value = false + isOpen.value = false; } const openBlubberChat = () => { @@ -37,7 +37,7 @@ const openBlubberChat = () => { } ); - isOpen.value = false + isOpen.value = false; } </script> <template> @@ -55,7 +55,7 @@ const openBlubberChat = () => { <button v-if="user.id !== AUTH_ID" @click="openBlubberChat" - class="action-item as-link" + class="action-item" :title="$gettext('Blubber diesen Nutzer an')" :aria-label="$gettext('Blubber diesen Nutzer an')" > @@ -77,7 +77,7 @@ const openBlubberChat = () => { <li> <button v-if="user.id !== AUTH_ID" - class="action-item as-link" + class="action-item" :title="$gettext('Nachricht schreiben')" :aria-label="$gettext('Nachricht schreiben')" @click="writeMessage()" diff --git a/resources/vue/components/forum/ForumApp.vue b/resources/vue/components/forum/ForumApp.vue index 60c8f8d..372a077 100644 --- a/resources/vue/components/forum/ForumApp.vue +++ b/resources/vue/components/forum/ForumApp.vue @@ -3,8 +3,7 @@ import {onMounted} from "vue"; import {useForumConfig} from "../../store/pinia/forum/ForumConfig"; const forumConfig = useForumConfig(); - -onMounted(async () => { +const fetchConfigs = async () => { try { const response = await STUDIP.jsonapi.withPromises().GET(`courses/${STUDIP.URLHelper.parameters.cid}/forum-configs`); @@ -17,6 +16,16 @@ onMounted(async () => { } catch (error) { STUDIP.Report.error(error.statusText); } +} + +onMounted(async () => { + if (STUDIP.USER_ID === 'nobody') { + forumConfig.$patch({ + allowGuestAccess: true + }); + } else { + await fetchConfigs(); + } }) </script> diff --git a/resources/vue/components/forum/SubscriptionDropdown.vue b/resources/vue/components/forum/SubscriptionDropdown.vue index 422c0ae..944805d 100644 --- a/resources/vue/components/forum/SubscriptionDropdown.vue +++ b/resources/vue/components/forum/SubscriptionDropdown.vue @@ -95,7 +95,7 @@ const unSubscribe = async () => { STUDIP.Report.success($gettext('Sie haben das Abonnement erfolgreich beendet.')); } catch (error) { - STUDIP.Report.error(error.statusText); + STUDIP.Report.error(error); } finally { isLoading.value = false; } @@ -133,6 +133,7 @@ const subscribe = async (notification_type = 'all') => { :title="title" class="button subscription-button" :class="subscriptionButtonLabel ? 'button--icon-label' : 'button--icon-only'" + :aria-pressed="isOpen" @click="isOpen = !isOpen" > <span v-if="subscriptionButtonLabel"> @@ -143,70 +144,72 @@ const subscribe = async (notification_type = 'all') => { </template> <template #items> - <li - tabindex="0" - :class="{ - '--active': subscription?.notification_type === SubscriptionNotificationType.All - }" - @keydown.enter="subscribe(SubscriptionNotificationType.All)" - @click="subscribe(SubscriptionNotificationType.All)" - > - <StudipIcon shape="subscription-all" :size="25" /> - <div class="subscription-option"> - <p class="option-title">{{ $gettext('Alle Benachrichtigungen') }}</p> - <StudipIcon - v-if="subscription?.notification_type === SubscriptionNotificationType.All" - shape="accept" - :size="20" - role="accept" /> - </div> + <li> + <button + type="button" + :class="{ + 'active': subscription?.notification_type === SubscriptionNotificationType.All + }" + @click="subscribe(SubscriptionNotificationType.All)" + > + <StudipIcon shape="subscription-all" :size="20" /> + <span class="subscription-option"> + <span class="option-title">{{ $gettext('Alle Benachrichtigungen') }}</span> + <StudipIcon + v-if="subscription?.notification_type === SubscriptionNotificationType.All" + shape="accept" + :size="20" + role="accept" /> + </span> + </button> </li> - <li - tabindex="0" - :class="{ - '--active': subscription?.notification_type === SubscriptionNotificationType.RepliesOnly - }" - @keydown.enter="subscribe(SubscriptionNotificationType.RepliesOnly)" - @click="subscribe(SubscriptionNotificationType.RepliesOnly)" - > - <StudipIcon shape="subscription-quotes" :size="25" /> - <div class="subscription-option"> - <p class="option-title">{{ $gettext('Nur Zitat') }}</p> - <StudipIcon - v-if="subscription?.notification_type === SubscriptionNotificationType.RepliesOnly" - shape="accept" - :size="20" - role="accept" /> - </div> + <li> + <button + type="button" + :class="{ + 'active': subscription?.notification_type === SubscriptionNotificationType.RepliesOnly + }" + @click="subscribe(SubscriptionNotificationType.RepliesOnly)" + > + <StudipIcon shape="subscription-quotes" :size="20" /> + <span class="subscription-option"> + <span class="option-title">{{ $gettext('Nur Zitat') }}</span> + <StudipIcon + v-if="subscription?.notification_type === SubscriptionNotificationType.RepliesOnly" + shape="accept" + :size="20" + role="accept" /> + </span> + </button> </li> - <li - tabindex="0" - :class="{ - '--active': subscription?.notification_type === SubscriptionNotificationType.None - }" - @keydown.enter="subscribe(SubscriptionNotificationType.None)" - @click="subscribe(SubscriptionNotificationType.None)" - > - <StudipIcon shape="subscription-none" :size="25" /> - <div class="subscription-option"> - <p class="option-title">{{ $gettext('Keine') }}</p> - <StudipIcon - v-if="subscription?.notification_type === SubscriptionNotificationType.None" - shape="accept" - :size="20" - role="accept" /> - </div> + <li> + <button + type="button" + :class="{ + 'active': subscription?.notification_type === SubscriptionNotificationType.None + }" + @click="subscribe(SubscriptionNotificationType.None)" + > + <StudipIcon shape="subscription-none" :size="20" /> + <span class="subscription-option"> + <span class="option-title">{{ $gettext('Keine') }}</span> + <StudipIcon + v-if="subscription?.notification_type === SubscriptionNotificationType.None" + shape="accept" + :size="20" + role="accept" /> + </span> + </button> </li> - <li - :tabindex="subscription ? 0 : -1" - :class="{ - '--disabled': !subscription?.notification_type - }" - @keydown.enter="unSubscribe" - @click="unSubscribe" - > - <StudipIcon shape="subscription-end" :size="25" /> - <p class="option-title">{{ $gettext('Abonnieren beenden') }}</p> + <li> + <button + type="button" + :disabled="!subscription?.notification_type" + @click="unSubscribe" + > + <StudipIcon shape="subscription-end" :size="20" /> + <p class="option-title">{{ $gettext('Abonnieren beenden') }}</p> + </button> </li> </template> </Dropdown> diff --git a/resources/vue/components/forum/UserAvatarDropdown.vue b/resources/vue/components/forum/UserAvatarDropdown.vue index dec1658..82f693d 100644 --- a/resources/vue/components/forum/UserAvatarDropdown.vue +++ b/resources/vue/components/forum/UserAvatarDropdown.vue @@ -32,6 +32,7 @@ const isOpen = defineModel({ default: false }); }" :title="label ?? user.name" :aria-label="label ?? $gettext('vCard')" + :aria-pressed="isOpen" > <img class="user-profile" :src="user.avatar_url" :style="{ width: size, height: size }" :alt="user.name" /> </button> diff --git a/resources/vue/components/forum/categories/CategoryItem.vue b/resources/vue/components/forum/categories/CategoryItem.vue index ed79140..72b646b 100644 --- a/resources/vue/components/forum/categories/CategoryItem.vue +++ b/resources/vue/components/forum/categories/CategoryItem.vue @@ -5,10 +5,11 @@ 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 {computed} from "vue"; +import {computed, ref} from "vue"; +import ShowCategory from "./ShowCategory.vue"; -const emit = defineEmits(['swapCategory']); const forumConfig = useForumConfig(); +const emit = defineEmits(['swapCategory']); const props = defineProps({ category: { @@ -22,16 +23,26 @@ const props = defineProps({ }); const categoryActionMenus = computed(() => { + let menu = [ + { label: $gettext('Informationen'), icon: 'info', emit: 'show'}, + ]; + if (forumConfig.isModerator) { - return [ + menu.push( { label: $gettext('Kategorie bearbeiten'), icon: 'edit', emit: 'edit'}, { label: $gettext('Kategorie löschen'), icon: 'trash', emit: 'delete'} - ]; + ); } - return []; + return menu; }); +const isCategoryDialogOpen = ref(false); + +const displayCategory = () => { + isCategoryDialogOpen.value = true; +} + const editCategory = () => STUDIP.Dialog.fromURL(getCategoryEditURL(props.category.id), { width: '700' }); const deleteCategory = () => STUDIP.Dialog.confirm( @@ -67,7 +78,7 @@ const swapCategory = event => { </div> <div class="flag" v-if="category.color" :style="{ backgroundColor: category.color}"></div> <div class="content"> - <div> + <div class="flex-1"> <div class="title-with-actions"> <div class="title-with-actions__content"> <a @@ -76,7 +87,7 @@ const swapCategory = event => { :title="$gettext('Zur Kategorie')"> <span class="category-title line-clamp-2">{{ category.name }}</span> <span - v-if="category.meta.postings_count > category.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && category.meta.postings_count > category.meta.user_read_index" class="unread-items-badge" role="status" aria-live="polite" @@ -91,6 +102,7 @@ const swapCategory = event => { <div class="title-with-actions__actions-xs"> <StudipActionMenu :items="categoryActionMenus" + @show="displayCategory" @edit="editCategory" @delete="deleteCategory" /> @@ -161,6 +173,7 @@ const swapCategory = event => { <td class="actions"> <StudipActionMenu :items="categoryActionMenus" + @show="displayCategory" @edit="editCategory" @delete="deleteCategory" /> @@ -190,7 +203,7 @@ const swapCategory = event => { </span> <span - v-if="category.meta.postings_count > category.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && category.meta.postings_count > category.meta.user_read_index" class="unread-items-badge" role="status" aria-live="polite" @@ -203,6 +216,7 @@ const swapCategory = event => { <div class="actions"> <StudipActionMenu :items="categoryActionMenus" + @show="displayCategory" @edit="editCategory" @delete="deleteCategory" /> @@ -244,4 +258,5 @@ const swapCategory = event => { </div> </div> </a> + <ShowCategory :category="category" v-model:isOpen="isCategoryDialogOpen" /> </template> diff --git a/resources/vue/components/forum/categories/Create.vue b/resources/vue/components/forum/categories/Create.vue index a7b47df..392785b 100644 --- a/resources/vue/components/forum/categories/Create.vue +++ b/resources/vue/components/forum/categories/Create.vue @@ -3,7 +3,9 @@ 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"; +const forumConfig = useForumConfig(); defineProps({ label: { type: String, @@ -14,6 +16,7 @@ defineProps({ <template> <a + v-if="forumConfig.isModerator" :href="getCategoryCreateURL()" data-dialog="size=700" :title="$gettext('Neue Kategorie anlegen')" diff --git a/resources/vue/components/forum/categories/ShowCategory.vue b/resources/vue/components/forum/categories/ShowCategory.vue new file mode 100644 index 0000000..2ea389e --- /dev/null +++ b/resources/vue/components/forum/categories/ShowCategory.vue @@ -0,0 +1,64 @@ +<script setup> +import StudipDialog from "../../StudipDialog.vue"; +import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import StudipDateTime from "../../StudipDateTime.vue"; + +defineProps({ + category: { + type: Object, + 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> +</template> diff --git a/resources/vue/components/forum/discussions/Create.vue b/resources/vue/components/forum/discussions/Create.vue index ee76c72..fb4d541 100644 --- a/resources/vue/components/forum/discussions/Create.vue +++ b/resources/vue/components/forum/discussions/Create.vue @@ -3,7 +3,9 @@ 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"; +const forumConfig = useForumConfig(); const props = defineProps({ topic_id: { type: String, @@ -17,6 +19,7 @@ const discussionCreateURL = computed(() => { <template> <a + v-if="!forumConfig.allowGuestAccess" :href="discussionCreateURL" :title="$gettext('Neue Diskussion starten')" data-dialog="width=900;height=750" diff --git a/resources/vue/components/forum/discussions/DiscussionFooter.vue b/resources/vue/components/forum/discussions/DiscussionFooter.vue new file mode 100644 index 0000000..5def908 --- /dev/null +++ b/resources/vue/components/forum/discussions/DiscussionFooter.vue @@ -0,0 +1,65 @@ +<script setup> +import StudipDateTime from "../../StudipDateTime.vue"; +import StudipIcon from "../../StudipIcon.vue"; +import ForumMembers from "../ForumMembers.vue"; +import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import {numberFormatter} from "../../../../assets/javascripts/lib/number_formatter"; +import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; + +const postCreateForm = defineModel('postCreateForm'); + +const forumConfig = useForumConfig(); +defineProps({ + discussion: { + type: Object, + required: true + }, + posts_count: { + type: Number, + default: 0 + }, + recent_activity: { + type: String, + } +}); +</script> + +<template> + <div class="discussion__status"> + <div class="flex items-start gap-20"> + <div class="text-center"> + <p>{{ $gettext('Erstellt') }}</p> + <StudipDateTime :iso="discussion.mkdate" :date_only="true" /> + </div> + <div class="text-center"> + <p>{{ $gettext('Beiträge') }}</p> + <p>{{ posts_count }}</p> + </div> + <div class="text-center"> + <p>{{ $gettext('Aufrufe') }}</p> + <p>{{ numberFormatter(discussion.view_count, 1) }}</p> + </div> + <div class="text-center"> + <p>{{ $gettext('Aktivität') }}</p> + <StudipDateTime v-if="recent_activity" :iso="recent_activity" :relative="true" /> + <StudipDateTime v-else :iso="discussion.mkdate" :relative="true"/> + </div> + </div> + <ForumMembers :members="discussion.members" :limit="5" size="35px" /> + <a + v-if="!forumConfig.allowGuestAccess && !discussion.closed_at" + href="#new-post" + class="button button--icon-label" + role="button" + :title="$gettext('Antworten')" + :aria-label="$gettext('Antworten')" + :class="{ + 'disabled': postCreateForm + }" + @click="postCreateForm = true" + > + <StudipIcon shape="reply" :size="20" aria-hidden="true" /> + {{ $gettext('Antworten') }} + </a> + </div> +</template> diff --git a/resources/vue/components/forum/discussions/DiscussionIndex.vue b/resources/vue/components/forum/discussions/DiscussionIndex.vue index d59c523..f5295fa 100644 --- a/resources/vue/components/forum/discussions/DiscussionIndex.vue +++ b/resources/vue/components/forum/discussions/DiscussionIndex.vue @@ -12,7 +12,6 @@ import {$gettext} from "@/assets/javascripts/lib/gettext"; import Loader from "../Loader.vue"; const forumConfig = useForumConfig(); - const props = defineProps({ discussions: { type: Array, @@ -168,26 +167,28 @@ onMounted(() => { :href="getDiscussionURL(discussion.id, {redirect})" :title="$gettext('Zur Diskussion')"> <span class="title-with-actions_title discussion-title line-clamp-2 m-0">{{ discussion.title }}</span> - <span - v-if="redirect !== 'recent' && discussion.meta.postings_count > discussion.meta.user_read_index" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" - > - {{ discussion.meta.postings_count - discussion.meta.user_read_index }} - </span> - <span - v-if="redirect === 'recent' && discussion.meta.recent_postings_count" - class="unread-items-badge" - role="status" - aria-live="polite" - :aria-label="$gettext('%{count} neue Beiträge seit Ihrem letzten Besuch.', {count: discussion.meta.recent_postings_count})" - :title="$gettext('%{count} neue Beiträge seit Ihrem letzten Besuch.', {count: discussion.meta.recent_postings_count})" - > - {{ discussion.meta.recent_postings_count }} - </span> + <template v-if="!forumConfig.allowGuestAccess"> + <span + v-if="redirect !== 'recent' && discussion.meta.postings_count > discussion.meta.user_read_index" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" + > + {{ discussion.meta.postings_count - discussion.meta.user_read_index }} + </span> + <span + v-if="redirect === 'recent' && discussion.meta.recent_postings_count" + class="unread-items-badge" + role="status" + aria-live="polite" + :aria-label="$gettext('%{count} neue Beiträge seit Ihrem letzten Besuch.', {count: discussion.meta.recent_postings_count})" + :title="$gettext('%{count} neue Beiträge seit Ihrem letzten Besuch.', {count: discussion.meta.recent_postings_count})" + > + {{ discussion.meta.recent_postings_count }} + </span> + </template> </a> </div> <div class="title-with-actions__actions-xs"> diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue index 7ac84f7..523fa51 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -12,7 +12,9 @@ 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"; +const forumConfig = useForumConfig(); const forumDiscussionPost = useForumPost(); const props = defineProps({ discussion: { @@ -107,7 +109,7 @@ const removePostHighlight = id => { <template> <div :id="'post_'+post.id" class="post" @click="removePostHighlight('post_'+post.id)"> - <div v-if="isUnread" class="post__unread"> + <div v-if="!forumConfig.allowGuestAccess && isUnread" class="post__unread"> </div> <div class="post__body"> <div class="post__author"> @@ -178,7 +180,7 @@ const removePostHighlight = id => { <a :href="`#create_form_${post.id}`" class="ballon-action__button" - v-if="!postCreateForm && !discussion.closed_at" + v-if="!forumConfig.allowGuestAccess && !postCreateForm && !discussion.closed_at" @click="postCreateForm = true; postContent.removeSelection()" :title="$gettext('Auswahl zitieren und antworten')" :aria-label="$gettext('Auswahl zitieren und antworten')" @@ -208,7 +210,7 @@ const removePostHighlight = id => { <div class="post__footer"> <div></div> <div class="inline-flex items-center gap-40"> - <div v-if="!discussion.closed_at" class="inline-flex items-center gap-10"> + <div v-if="!forumConfig.allowGuestAccess && !discussion.closed_at" class="inline-flex items-center gap-10"> <template v-if="post.author?.id === auth_user.id"> <a :href="`#post_${post.id}`" @@ -230,9 +232,19 @@ const removePostHighlight = id => { <button type="button" @click="forwardPost(post)" class="button button--icon-only" :title="$gettext('Beitrage weiterleiten')" :aria-label="$gettext('Beitrage weiterleiten')"> <StudipIcon shape="export" :size="20" aria-hidden="true" /> </button> - <button :disabled="postCreateForm" @click="addReply(post)" type="button" class="button button--icon-only" :title="$gettext('Zitieren und antworten')" :aria-label="$gettext('Zitieren und Antworten')"> + <a + :href="`#create_form_${post.id}`" + @click="addReply(post)" + type="button" + class="button button--icon-only" + :class="{ + 'disabled': postCreateForm + }" + :title="$gettext('Zitieren und antworten')" + :aria-label="$gettext('Zitieren und Antworten')" + > <StudipIcon shape="quote" :size="20" aria-hidden="true" /> - </button> + </a> </div> </div> </div> diff --git a/resources/vue/components/forum/posts/PostReactionShow.vue b/resources/vue/components/forum/posts/PostReactionShow.vue index a15df91..8ad13a2 100644 --- a/resources/vue/components/forum/posts/PostReactionShow.vue +++ b/resources/vue/components/forum/posts/PostReactionShow.vue @@ -81,6 +81,7 @@ onMounted(() => { <div class="user-reaction"> <UserAvatarDropdown size="30px" + v-if="reaction.user.id" :user="{ id: reaction.user.id, username: reaction.user.username, @@ -94,6 +95,7 @@ onMounted(() => { </td> <td> <a + v-if="reaction.user.id" :href="userProfileURL(reaction.user.username)" :title="$gettext('Zum Profil')" :aria-label="$gettext('Zum Profil von %{name}', { name: reaction.user.formatted_name })" @@ -101,6 +103,9 @@ onMounted(() => { > {{ reaction.user.formatted_name }} </a> + <p v-else class="author-name"> + {{ $gettext('Unbekannt') }} + </p> </td> <td> <StudipDateTime :iso="reaction.mkdate" :relative="true" /> diff --git a/resources/vue/components/forum/posts/PostReactions.vue b/resources/vue/components/forum/posts/PostReactions.vue index c93bacf..eff8d6b 100644 --- a/resources/vue/components/forum/posts/PostReactions.vue +++ b/resources/vue/components/forum/posts/PostReactions.vue @@ -9,7 +9,9 @@ import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jso import StudipIcon from "../../StudipIcon.vue"; import PostReactionShow from "./PostReactionShow.vue"; import StudipDialog from "../../StudipDialog.vue"; +import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +const forumConfig = useForumConfig(); const forumDiscussionPost = useForumPost(); const props = defineProps({ posting_id: { @@ -26,7 +28,14 @@ const props = defineProps({ const showReactions = ref(false); const reactionStatusMessage = ref(null); -const groupedReactions = computed(() => Object.groupBy(props.reactions, ({ emoji }) => emoji)); +const transformedReactions = computed(() => props.reactions.map(reaction => { + return { + ...reaction, + ...(!reaction?.user ? { user: { formatted_name: $gettext('Unbekannt') } } : {}) + } +})); + +const groupedReactions = computed(() => Object.groupBy(transformedReactions.value, ({ emoji }) => emoji)); const announceToScreenReader = message => reactionStatusMessage.value.textContent = message; @@ -74,7 +83,11 @@ const deleteReaction = async (reactionId) => { } } -const toggleReaction = async (emoji, reactions = props.reactions) => { +const toggleReaction = async (emoji, reactions = transformedReactions.value) => { + if (forumConfig.allowGuestAccess) { + return; + } + const userReaction = findUserReaction(emoji, reactions); if (userReaction) { @@ -86,7 +99,7 @@ const toggleReaction = async (emoji, reactions = props.reactions) => { } } -const findUserReaction = (emoji, reactions = props.reactions) => reactions.find(reaction => reaction.user.id === STUDIP.USER_ID && reaction.emoji === emoji); +const findUserReaction = (emoji, reactions = transformedReactions.value) => reactions.find(reaction => reaction.user.id === STUDIP.USER_ID && reaction.emoji === emoji); const reactionCreate = useTemplateRef('reactionCreate'); useDetectOutsideClick(reactionCreate, () => showReactions.value = false); @@ -101,11 +114,11 @@ const reactionShowDialog = reactive({ <div class="post-reactions-container"> <div aria-live="polite" class="sr-only" role="status" ref="reactionStatusMessage"></div> - <template v-if="reactions.length"> + <template v-if="transformedReactions.length"> <template v-for="(reaction, emoji) in groupedReactions" :key="emoji"> <button type="button" - class="post-reaction as-link" + class="post-reaction" :class="{ '--active': findUserReaction(emoji, reaction) }" @@ -120,21 +133,23 @@ const reactionShowDialog = reactive({ <div ref="reactionCreate" class="post-reactions"> <div class="post-reactions__button-group"> <button + v-if="!forumConfig.allowGuestAccess" type="button" - class="post-reactions__add-reaction as-link" + class="post-reactions__add-reaction" :title="$gettext('Reagieren')" :aria-label="$gettext('Reagieren')" + :aria-pressed="showReactions" @click="showReactions = !showReactions"> <StudipIcon shape="add-reaction" class="add-reaction-icon" :size="18" /> </button> <button - v-if="reactions.length" + v-if="transformedReactions.length" type="button" - class="post-reactions__show-reactions as-link" + class="post-reactions__show-reactions" :title="$gettext('Reaktionen anzeigen')" - :aria-label="$gettext('%{count} Reaktionen anzeigen', { count: reactions.length })" + :aria-label="$gettext('%{count} Reaktionen anzeigen', { count: transformedReactions.length })" @click="reactionShowDialog.isOpen = true"> - {{ numberFormatter(reactions.length, 1) }} + {{ numberFormatter(transformedReactions.length, 1) }} </button> </div> <Transition name="fade"> @@ -158,7 +173,7 @@ const reactionShowDialog = reactive({ </div> <StudipDialog - v-if="reactionShowDialog.isOpen && reactions.length" + v-if="reactionShowDialog.isOpen && transformedReactions.length" :title="$gettext('Reaktionen anzeigen')" :closeText="$gettext('Schließen')" closeClass="cancel" @@ -178,9 +193,9 @@ const reactionShowDialog = reactive({ value="all" v-model="reactionShowDialog.emoji" /> - <label for="reaction-all" :class="{ 'is-checked': reactionShowDialog.emoji === 'all' }"> + <label class="button-base" for="reaction-all" :class="{ 'active': reactionShowDialog.emoji === 'all' }"> {{ $gettext('Alle') }} - <span>{{ numberFormatter(reactions.length, 1) }}</span> + <span>{{ numberFormatter(transformedReactions.length, 1) }}</span> </label> </div> <div @@ -195,7 +210,7 @@ const reactionShowDialog = reactive({ :value="emoji" v-model="reactionShowDialog.emoji" /> - <label :for="`reaction-${emoji}`" :class="{ 'is-checked': reactionShowDialog.emoji === emoji }"> + <label class="button-base" :for="`reaction-${emoji}`" :class="{ 'active': reactionShowDialog.emoji === emoji }"> <span class="emoji-icon" v-html="REACTION_ICONS[emoji].icon" aria-hidden="true"></span> <span class="sr-only">{{ emoji }}</span> <span>{{ numberFormatter(reaction.length, 1) }}</span> @@ -203,7 +218,7 @@ const reactionShowDialog = reactive({ </div> </div> <div class="tab__content"> - <PostReactionShow :reactions="reactions" :emoji="reactionShowDialog.emoji" /> + <PostReactionShow :reactions="transformedReactions" :emoji="reactionShowDialog.emoji" /> </div> </div> </div> diff --git a/resources/vue/components/forum/topics/CreateTopic.vue b/resources/vue/components/forum/topics/CreateTopic.vue index 03b7542..88a9ad9 100644 --- a/resources/vue/components/forum/topics/CreateTopic.vue +++ b/resources/vue/components/forum/topics/CreateTopic.vue @@ -2,7 +2,9 @@ <script setup> import StudipIcon from "@/vue/components/StudipIcon.vue"; import {computed} from "vue"; +import {useForumConfig} from "../../../store/pinia/forum/ForumConfig"; +const forumConfig = useForumConfig(); const props = defineProps({ category_id: { type: String, @@ -24,6 +26,7 @@ const topicCreateURL = computed(() => { <template> <a + v-if="forumConfig.isModerator" :href="topicCreateURL" data-dialog="width=700" :title="$gettext('Neues Thema anlegen')" diff --git a/resources/vue/components/forum/topics/SelectTopicInput.vue b/resources/vue/components/forum/topics/SelectTopicInput.vue index 24c0e00..0b4d0c1 100644 --- a/resources/vue/components/forum/topics/SelectTopicInput.vue +++ b/resources/vue/components/forum/topics/SelectTopicInput.vue @@ -2,7 +2,6 @@ import StudipIcon from "@/vue/components/StudipIcon.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 new file mode 100644 index 0000000..0682fba --- /dev/null +++ b/resources/vue/components/forum/topics/ShowTopic.vue @@ -0,0 +1,71 @@ +<script setup> +import StudipDialog from "../../StudipDialog.vue"; +import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import StudipDateTime from "../../StudipDateTime.vue"; + +defineEmits(['close']); + +defineProps({ + topic: { + type: Object, + 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>{{ 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> + </template> + </StudipDialog> +</template> diff --git a/resources/vue/components/forum/topics/TopicItem.vue b/resources/vue/components/forum/topics/TopicItem.vue index 82f573d..544c49d 100644 --- a/resources/vue/components/forum/topics/TopicItem.vue +++ b/resources/vue/components/forum/topics/TopicItem.vue @@ -5,7 +5,8 @@ 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} from "vue"; +import {computed, ref} from "vue"; +import ShowTopic from "./ShowTopic.vue"; const emit = defineEmits(['swapTopic']); const forumConfig = useForumConfig(); @@ -22,16 +23,26 @@ const props = defineProps({ }); const topicActionMenus = computed(() => { + let menu = [ + { label: $gettext('Informationen'), icon: 'info', emit: 'show'}, + ]; + if (forumConfig.isModerator) { - return [ + menu.push( { label: $gettext('Thema bearbeiten'), icon: 'edit', emit: 'edit'}, { label: $gettext('Thema löschen'), icon: 'trash', emit: 'delete'} - ]; + ); } - return []; + return menu; }); +const isTopicDialogOpen = ref(false); + +const displayTopic = () => { + isTopicDialogOpen.value = true; +} + const editTopic = () => STUDIP.Dialog.fromURL(getTopicEditURL(props.topic.id),{ width: '700' }); const deleteTopic = () => STUDIP.Dialog.confirm( @@ -66,13 +77,13 @@ const swapTopic = event => { </a> </div> <div class="content"> - <div> + <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="topic.meta.postings_count > topic.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && topic.meta.postings_count > topic.meta.user_read_index" class="unread-items-badge" role="status" aria-live="polite" @@ -87,6 +98,7 @@ const swapTopic = event => { <div class="title-with-actions__actions-xs"> <StudipActionMenu :items="topicActionMenus" + @show="displayTopic" @edit="editTopic" @delete="deleteTopic" /> @@ -157,6 +169,7 @@ const swapTopic = event => { <td class="actions"> <StudipActionMenu :items="topicActionMenus" + @show="displayTopic" @edit="editTopic" @delete="deleteTopic" /> @@ -178,7 +191,7 @@ const swapTopic = event => { </span> <span - v-if="topic.meta.postings_count > topic.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && topic.meta.postings_count > topic.meta.user_read_index" class="unread-items-badge" role="status" aria-live="polite" @@ -192,6 +205,7 @@ const swapTopic = event => { <div class="actions"> <StudipActionMenu :items="topicActionMenus" + @show="displayTopic" @edit="editTopic" @delete="deleteTopic" /> @@ -235,4 +249,5 @@ const swapTopic = event => { </div> </div> </a> + <ShowTopic :topic="topic" v-model:isOpen="isTopicDialogOpen" /> </template> diff --git a/resources/vue/store/pinia/forum/ForumConfig.js b/resources/vue/store/pinia/forum/ForumConfig.js index d2de6b6..26b008c 100644 --- a/resources/vue/store/pinia/forum/ForumConfig.js +++ b/resources/vue/store/pinia/forum/ForumConfig.js @@ -4,6 +4,7 @@ import {ref} from "vue"; export const useForumConfig = defineStore( 'forum_config', () => { + const allowGuestAccess = ref(false); const isAdmin = ref(false); const isModerator = ref(false); const anonymousPost = ref(false); @@ -12,18 +13,21 @@ export const useForumConfig = defineStore( function toggleForumLayout() { tileLayout.value = !tileLayout.value; - const configId = `${STUDIP.USER_ID}_FORUM_TILE_LAYOUT`; + if (!allowGuestAccess.value) { + const configId = `${STUDIP.USER_ID}_FORUM_TILE_LAYOUT`; - const data = { - id: configId, - type: 'config-values', - attributes: { value: tileLayout.value } - }; + const data = { + id: configId, + type: 'config-values', + attributes: { value: tileLayout.value } + }; - STUDIP.jsonapi.PATCH(`config-values/${configId}`, { data: { data } }); + STUDIP.jsonapi.PATCH(`config-values/${configId}`, { data: { data } }); + } } return { + allowGuestAccess, isAdmin, isModerator, anonymousPost, |
