diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2025-09-05 15:47:50 +0200 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2025-09-05 15:47:50 +0200 |
| commit | 9bb68bf5d075ff0e127b9fe5309889f366ccf127 (patch) | |
| tree | 992cce643bf95fc3e6985920aea86cf54335cbec /resources | |
| parent | 8a6831f7d910f3ff7791d27fdf3988028982caa5 (diff) | |
Resolve "Forum: Discussion-Type Index auf Vue umsetzen"
Closes #5782
Merge request studip/studip!4406
Diffstat (limited to 'resources')
| -rw-r--r-- | resources/assets/stylesheets/scss/buttons.scss | 2 | ||||
| -rw-r--r-- | resources/assets/stylesheets/scss/forum.scss | 9 | ||||
| -rw-r--r-- | resources/vue/apps/forum/discussions/Show.vue | 6 | ||||
| -rw-r--r-- | resources/vue/apps/forum/discussions_types/Edit.vue | 27 | ||||
| -rw-r--r-- | resources/vue/apps/forum/discussions_types/Index.vue | 163 | ||||
| -rw-r--r-- | resources/vue/components/Dropdown.vue | 2 | ||||
| -rw-r--r-- | resources/vue/components/UserAvatar.vue | 4 | ||||
| -rw-r--r-- | resources/vue/components/forum/SubscriptionDropdown.vue | 4 | ||||
| -rw-r--r-- | resources/vue/components/forum/helpers/urls.js | 4 |
9 files changed, 193 insertions, 28 deletions
diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index bedd67c..e4197c0 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -99,7 +99,7 @@ button.button { } @mixin button-base() { - color: var(--base-color); + color: var(--color--brand-primary); transition: color var(--transition-duration); } diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss index b0a02e9..ccfe15f 100644 --- a/resources/assets/stylesheets/scss/forum.scss +++ b/resources/assets/stylesheets/scss/forum.scss @@ -665,10 +665,6 @@ $card-max-width: 300px; .discussion { background-color: var(--color--main-navigation-background); - hr { - margin: 0; - } - &__status, &__body, &__form-container { @@ -1018,8 +1014,11 @@ $card-max-width: 300px; gap: 15px; padding: 15px; justify-content: space-between; - border: thin solid var(--color--button-inactive-border); + border: 1px solid var(--light-gray-color-40); max-width: calc(48em - 30px); + border-radius: var(--border-radius-default); + max-height: 300px; + overflow-y: scroll; .button { display: flex; diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue index 9c0d1c0..7e52669 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -216,14 +216,14 @@ onMounted(async () => { {{ $gettext('Es sind noch keine Beiträge vorhanden.') }} </p> </div> - <hr /> + <hr class="m-0" /> <DiscussionFooter :discussion="discussion" :posts="posts" :read_index="read_index" v-model:postCreateForm="postCreateForm" /> - <hr /> + <hr class="m-0" /> </div> <div class="posts-container"> <template v-for="(post, index) in posts.slice(1)" :key="post.id"> @@ -233,7 +233,7 @@ onMounted(async () => { :discussion="discussion" :is_unread="read_index < index + 2" /> - <hr v-if="index < posts.slice(1).length - 1" class="divider"/> + <hr v-if="index < posts.length - 2" class="divider m-0" /> </template> </div> diff --git a/resources/vue/apps/forum/discussions_types/Edit.vue b/resources/vue/apps/forum/discussions_types/Edit.vue index 1d5b48b..40084ea 100644 --- a/resources/vue/apps/forum/discussions_types/Edit.vue +++ b/resources/vue/apps/forum/discussions_types/Edit.vue @@ -1,11 +1,12 @@ <script setup> import {computed, reactive} from "vue"; import StudipIcon from "../../../components/StudipIcon.vue"; +import {getDiscussionTypeStoreURL} from "../../../components/forum/helpers/urls"; const CSRF = STUDIP.CSRF_TOKEN; const props = defineProps({ - discussion_type: { + discussionType: { type: Object, }, icons: { @@ -14,17 +15,11 @@ const props = defineProps({ } }); -const formSate = reactive({ - ...props.discussion_type +const formState = reactive({ + ...props.discussionType }); -const formActionURL = computed(() => { - if (props.discussion_type.type_id) { - return STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussion_types/save/${props.discussion_type.type_id}`); - } - - return STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussion_types/save`); -}); +const formActionURL = computed(() => getDiscussionTypeStoreURL(props.discussionType.id)); </script> <template> @@ -48,7 +43,7 @@ const formActionURL = computed(() => { required type="text" name="name" - v-model="formSate.name" + v-model="formState.name" maxlength="100" /> </label> </section> @@ -60,7 +55,7 @@ const formActionURL = computed(() => { </span> </label> <div id="studip_icons" class="studip-icons-container"> - <input type="hidden" v-model="formSate.icon" name="icon" required /> + <input type="hidden" v-model="formState.icon" name="icon" /> <template v-for="icon in icons" :key="icon"> <button @@ -68,10 +63,10 @@ const formActionURL = computed(() => { type="button" :title="icon" :class="{ - 'disabled': formSate.icon && formSate.icon !== icon, - 'active': formSate.icon === icon + 'disabled': formState.icon && formState.icon !== icon, + 'active': formState.icon === icon }" - @click="formSate.icon = icon"> + @click="formState.icon = icon"> <StudipIcon :shape="icon" :size="35" /> </button> </template> @@ -79,7 +74,7 @@ const formActionURL = computed(() => { </section> </fieldset> <footer data-dialog-button> - <button :disabled="!formSate.icon || !formSate.name" class="button accept"> + <button :disabled="!formState.icon || !formState.name" class="button accept"> {{ $gettext('Speichern') }} </button> <button class="button cancel" type="button" data-dialog-close> diff --git a/resources/vue/apps/forum/discussions_types/Index.vue b/resources/vue/apps/forum/discussions_types/Index.vue new file mode 100644 index 0000000..6dad3f9 --- /dev/null +++ b/resources/vue/apps/forum/discussions_types/Index.vue @@ -0,0 +1,163 @@ +<script setup> +import {onMounted, ref} from "vue"; +import {$gettext} from "../../../../assets/javascripts/lib/gettext"; +import StudipIcon from "../../../components/StudipIcon.vue"; +import {useSortable} from "../../../composables/useSortable"; +import {getDiscussionTypeEditURL} from "../../../components/forum/helpers/urls"; +import StudipActionMenu from "../../../components/StudipActionMenu.vue"; +import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils"; +import StudipPagination from "../../../components/StudipPagination.vue"; +import Loader from "../../../components/forum/Loader.vue"; + +const discussionTypes = ref([]); +const pagination = ref({}); +const isLoading = ref(false); + +const actionMenusItems = [ + { label: $gettext('Diskussionstyp bearbeiten'), icon: 'edit', emit: 'edit'}, + { label: $gettext('Diskussionstyp löschen'), icon: 'trash', emit: 'delete'} +]; + +const fetchDiscussionTypes = async (_, offset = 0) => { + try { + isLoading.value = true; + + const response = await STUDIP.jsonapi.withPromises().GET( + 'forum-discussion-types', + { + data: { page: { offset } } + } + ); + + pagination.value = { + ...response.meta.page, + currentPage: response.meta.page.offset / response.meta.page.limit, + links: response.links + }; + + discussionTypes.value = await deserializeJSONAPIResponse(response); + } catch (error) { + STUDIP.Report.error(error); + } finally { + isLoading.value = false; + } +} + +const editType = type => STUDIP.Dialog.fromURL( + getDiscussionTypeEditURL(type.id), + { + width: '700', + height: '650' + } +); + +const deleteType = type => STUDIP.Dialog.confirm( + $gettext('Wollen Sie „%{ name }“ löschen?', { name: type.name }), + async () => { + try { + await STUDIP.jsonapi.withPromises().DELETE(`forum-discussion-types/${type.id}`); + discussionTypes.value = discussionTypes.value.filter(({ id }) => id !== type.id); + + STUDIP.Report.success($gettext('Der Diskussionstyp wurde gelöscht.')); + } catch (error) { + STUDIP.Report.error(error); + } + }, + STUDIP.Dialog.close() +); + +const { + sortedData, + sortBy, + getSortClass, + getAriaSortString, + getAriaSortLabel +} = useSortable(discussionTypes); + +onMounted(() => { + fetchDiscussionTypes(); +}); +</script> + +<template> + <div class="forum"> + <table class="default"> + <caption> + {{ $gettext('Diskussionstypen') }} + <span class="actions"> + <a :href="getDiscussionTypeEditURL()" data-dialog="width=700;height=650" :title="$gettext('Neue Diskussionstyp anlegen')"> + <StudipIcon shape="add" aria-hidden="true" /> + </a> + </span> + </caption> + + <colgroup> + <col style="width: 10%"> + <col> + <col style="width: 24px"> + </colgroup> + + <thead> + <tr class="sortable"> + <th>{{ $gettext('Icon') }}</th> + <th + :class="getSortClass('name')" + :aria-sort="getAriaSortString('name')" + :aria-label="getAriaSortLabel('name', $gettext('Name'))" + > + <a + href="#" + @click.prevent="sortBy('name')" + :title="$gettext('Nach Name sortieren')"> + {{ $gettext('Name') }} + </a> + </th> + + <th>{{ $gettext('Aktionen') }}</th> + </tr> + </thead> + <tbody> + <tr v-for="type in sortedData" :key="type.id"> + <td> + <StudipIcon :shape="type.icon" role="info" :size="24" aria-hidden="true" /> + </td> + <td> + <a + :href="getDiscussionTypeEditURL(type.id)" + data-dialog="width=700;height=650" + :title="$gettext('Diskussionstyp bearbeiten')" + > + {{ type.name }} + </a> + </td> + + <td class="actions"> + <StudipActionMenu + :items="actionMenusItems" + @edit="editType(type)" + @delete="deleteType(type)" + /> + </td> + </tr> + + <tr v-if="isLoading" > + <td colspan="3"> + <Loader /> + </td> + </tr> + + <tr v-if="sortedData.length === 0"> + <td colspan="3" class="text-center"> + {{ $gettext('Es sind noch keine Diskussionstypen vorhanden.') }} + </td> + </tr> + </tbody> + </table> + <StudipPagination + v-if="pagination.total > pagination.limit" + :currentPage="pagination.currentPage" + :totalItems="pagination.total" + :itemsPerPage="pagination.limit" + @pageUpdated="fetchDiscussionTypes" /> + </div> +</template> diff --git a/resources/vue/components/Dropdown.vue b/resources/vue/components/Dropdown.vue index f68f485..011bef3 100644 --- a/resources/vue/components/Dropdown.vue +++ b/resources/vue/components/Dropdown.vue @@ -78,7 +78,7 @@ onBeforeUnmount(() => { type="button" v-if="withCloseButton" @click="isOpen = false" - class="dropdown__close-button"> + class="dropdown__close-button button-base"> <StudipIcon shape="decline" :size="20" /> </button> <div v-if="title" class="dropdown__header"> diff --git a/resources/vue/components/UserAvatar.vue b/resources/vue/components/UserAvatar.vue index 441a41c..91153e9 100644 --- a/resources/vue/components/UserAvatar.vue +++ b/resources/vue/components/UserAvatar.vue @@ -55,7 +55,7 @@ const openBlubberChat = () => { <button v-if="user.id !== AUTH_ID" @click="openBlubberChat" - class="action-item" + class="action-item button-base" :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" + class="action-item button-base" :title="$gettext('Nachricht schreiben')" :aria-label="$gettext('Nachricht schreiben')" @click="writeMessage()" diff --git a/resources/vue/components/forum/SubscriptionDropdown.vue b/resources/vue/components/forum/SubscriptionDropdown.vue index 944805d..2d214ff 100644 --- a/resources/vue/components/forum/SubscriptionDropdown.vue +++ b/resources/vue/components/forum/SubscriptionDropdown.vue @@ -147,6 +147,7 @@ const subscribe = async (notification_type = 'all') => { <li> <button type="button" + class="button-base" :class="{ 'active': subscription?.notification_type === SubscriptionNotificationType.All }" @@ -166,6 +167,7 @@ const subscribe = async (notification_type = 'all') => { <li> <button type="button" + class="button-base" :class="{ 'active': subscription?.notification_type === SubscriptionNotificationType.RepliesOnly }" @@ -185,6 +187,7 @@ const subscribe = async (notification_type = 'all') => { <li> <button type="button" + class="button-base" :class="{ 'active': subscription?.notification_type === SubscriptionNotificationType.None }" @@ -204,6 +207,7 @@ const subscribe = async (notification_type = 'all') => { <li> <button type="button" + class="button-base" :disabled="!subscription?.notification_type" @click="unSubscribe" > diff --git a/resources/vue/components/forum/helpers/urls.js b/resources/vue/components/forum/helpers/urls.js index b4357de..36b9944 100644 --- a/resources/vue/components/forum/helpers/urls.js +++ b/resources/vue/components/forum/helpers/urls.js @@ -15,3 +15,7 @@ export const getCategoryDeleteURL = id => STUDIP.URLHelper.getURL(`dispatch.php/ export const getSearchURL = (hashtags='') => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/search?${hashtags}`); export const userProfileURL = username => STUDIP.URLHelper.getURL('dispatch.php/profile', {username}); + +// Discussion Types: +export const getDiscussionTypeEditURL = (id = null) => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussion_types/edit/${id}`); +export const getDiscussionTypeStoreURL = (id = null) => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussion_types/save/${id}`); |
