diff options
Diffstat (limited to 'resources/vue/components/forum')
16 files changed, 405 insertions, 121 deletions
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> |
