aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2025-09-05 15:47:50 +0200
committerMurtaza Sultani <sultani@data-quest.de>2025-09-05 15:47:50 +0200
commit9bb68bf5d075ff0e127b9fe5309889f366ccf127 (patch)
tree992cce643bf95fc3e6985920aea86cf54335cbec /resources
parent8a6831f7d910f3ff7791d27fdf3988028982caa5 (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.scss2
-rw-r--r--resources/assets/stylesheets/scss/forum.scss9
-rw-r--r--resources/vue/apps/forum/discussions/Show.vue6
-rw-r--r--resources/vue/apps/forum/discussions_types/Edit.vue27
-rw-r--r--resources/vue/apps/forum/discussions_types/Index.vue163
-rw-r--r--resources/vue/components/Dropdown.vue2
-rw-r--r--resources/vue/components/UserAvatar.vue4
-rw-r--r--resources/vue/components/forum/SubscriptionDropdown.vue4
-rw-r--r--resources/vue/components/forum/helpers/urls.js4
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}`);