aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2025-07-30 09:48:20 +0200
committerDavid Siegfried <david.siegfried@uni-vechta.de>2025-07-30 07:48:20 +0000
commit8b5da1acae37d9bc983394c3f27508c24024d69b (patch)
tree93360533b83eeff9f6c78e60cedadeb026bcad8e /resources
parentd6ce47b2ea667524acafda4e539a81695158c07d (diff)
Resolve "Forum in freien Veranstaltungen sind nicht aufrufbar"issue-5760
Closes #5699 Merge request studip/studip!4371
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/javascripts/lib/dates.js4
-rw-r--r--resources/assets/stylesheets/scss/buttons.scss35
-rw-r--r--resources/assets/stylesheets/scss/forum.scss112
-rw-r--r--resources/assets/stylesheets/studip.scss4
-rw-r--r--resources/vue/apps/forum/categories/Index.vue3
-rw-r--r--resources/vue/apps/forum/discussions/Show.vue114
-rw-r--r--resources/vue/apps/forum/topics/Index.vue6
-rw-r--r--resources/vue/apps/forum/topics/Show.vue7
-rw-r--r--resources/vue/components/Dropdown.vue1
-rw-r--r--resources/vue/components/UserAvatar.vue10
-rw-r--r--resources/vue/components/forum/ForumApp.vue13
-rw-r--r--resources/vue/components/forum/SubscriptionDropdown.vue127
-rw-r--r--resources/vue/components/forum/UserAvatarDropdown.vue1
-rw-r--r--resources/vue/components/forum/categories/CategoryItem.vue31
-rw-r--r--resources/vue/components/forum/categories/Create.vue3
-rw-r--r--resources/vue/components/forum/categories/ShowCategory.vue64
-rw-r--r--resources/vue/components/forum/discussions/Create.vue3
-rw-r--r--resources/vue/components/forum/discussions/DiscussionFooter.vue65
-rw-r--r--resources/vue/components/forum/discussions/DiscussionIndex.vue43
-rw-r--r--resources/vue/components/forum/posts/Post.vue22
-rw-r--r--resources/vue/components/forum/posts/PostReactionShow.vue5
-rw-r--r--resources/vue/components/forum/posts/PostReactions.vue45
-rw-r--r--resources/vue/components/forum/topics/CreateTopic.vue3
-rw-r--r--resources/vue/components/forum/topics/SelectTopicInput.vue1
-rw-r--r--resources/vue/components/forum/topics/ShowTopic.vue71
-rw-r--r--resources/vue/components/forum/topics/TopicItem.vue29
-rw-r--r--resources/vue/store/pinia/forum/ForumConfig.js18
27 files changed, 538 insertions, 302 deletions
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,