Merge pull request #5156 from christianbeeznest/ofaj-21101-6
Social: Add social group UI enhancements and API integration - refs BT#21101pull/5168/head
commit
3905a6da0f
@ -0,0 +1,38 @@ |
||||
<template> |
||||
<BaseCard plain> |
||||
<div class="p-4 text-center"> |
||||
<img |
||||
:src="groupInfo.image" |
||||
class="mb-4 w-24 h-24 mx-auto rounded-full" |
||||
alt="Group picture" |
||||
/> |
||||
<hr /> |
||||
<BaseButton |
||||
:label="t('Edit this group')" |
||||
type="primary" |
||||
class="mt-4" |
||||
@click="editGroup" |
||||
icon="edit" |
||||
/> |
||||
</div> |
||||
</BaseCard> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { computed, inject, onMounted, ref, watch } from "vue" |
||||
import { useStore } from 'vuex' |
||||
import BaseCard from "../basecomponents/BaseCard.vue" |
||||
import BaseButton from "../basecomponents/BaseButton.vue" |
||||
import { useI18n } from "vue-i18n" |
||||
import { useRoute } from "vue-router" |
||||
|
||||
const { t } = useI18n() |
||||
const store = useStore() |
||||
const route = useRoute() |
||||
const groupInfo = inject('group-info') |
||||
const isGroup = inject('is-group') |
||||
|
||||
const editGroup = () => { |
||||
window.location = "/account/edit" |
||||
} |
||||
</script> |
@ -0,0 +1,60 @@ |
||||
<template> |
||||
<BaseCard class="social-side-menu mt-4"> |
||||
<template #header> |
||||
<div class="px-4 py-2 -mb-2 bg-gray-15"> |
||||
<h2 class="text-h5">{{ t('Social Group') }}</h2> |
||||
</div> |
||||
</template> |
||||
<hr class="-mt-2 mb-4 -mx-4"> |
||||
<ul class="menu-list"> |
||||
<li class="menu-item"> |
||||
<router-link to="/social"> |
||||
<i class="mdi mdi-home" aria-hidden="true"></i> |
||||
{{ t("Home") }} |
||||
</router-link> |
||||
</li> |
||||
<li class="menu-item"> |
||||
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }"> |
||||
<i class="mdi mdi-account-multiple-outline" aria-hidden="true"></i> |
||||
{{ t("Waiting list") }} |
||||
</router-link> |
||||
</li> |
||||
<li class="menu-item"> |
||||
<router-link :to="{ name: 'UserGroupInvite', params: { group_id: groupInfo.id } }"> |
||||
<i class="mdi mdi-account-plus" aria-hidden="true"></i> |
||||
{{ t("Invite friends") }} |
||||
</router-link> |
||||
</li> |
||||
<li class="menu-item"> |
||||
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }"> |
||||
<i class="mdi mdi-exit-to-app" aria-hidden="true"></i> |
||||
{{ t("Leave group") }} |
||||
</router-link> |
||||
</li> |
||||
</ul> |
||||
</BaseCard> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import BaseCard from "../basecomponents/BaseCard.vue" |
||||
import { useRoute } from 'vue-router' |
||||
import { useI18n } from "vue-i18n" |
||||
import { onMounted, computed, ref, inject, watchEffect } from "vue" |
||||
import { useStore } from "vuex" |
||||
import { useSecurityStore } from "../../store/securityStore" |
||||
|
||||
const { t } = useI18n() |
||||
const route = useRoute() |
||||
const store = useStore() |
||||
const securityStore = useSecurityStore() |
||||
|
||||
const groupInfo = inject('group-info') |
||||
const isGroup = inject('is-group') |
||||
|
||||
const isActive = (path, filterType = null) => { |
||||
const pathMatch = route.path.startsWith(path) |
||||
const hasQueryParams = Object.keys(route.query).length > 0 |
||||
const filterMatch = filterType ? (route.query.filterType === filterType && hasQueryParams) : !hasQueryParams |
||||
return pathMatch && filterMatch |
||||
} |
||||
</script> |
@ -0,0 +1,55 @@ |
||||
<template> |
||||
<div> |
||||
<div class="discussions-header"> |
||||
<h2>Discussions</h2> |
||||
<a :href="threadCreationUrl" class="btn btn-primary create-thread-btn ajax"> |
||||
<i class="pi pi-plus"></i> {{ t("Create thread") }} |
||||
</a> |
||||
</div> |
||||
<div class="discussion-item" v-for="discussion in discussions" :key="discussion.id"> |
||||
<div class="discussion-content"> |
||||
<div class="discussion-title" v-html="discussion.title"></div> |
||||
<div class="discussion-details"> |
||||
<i class="mdi mdi-message-reply-text icon"></i> |
||||
<span>{{ discussion.repliesCount }} {{ t("Replies") }}</span> |
||||
<i class="mdi mdi-clock-outline icon"></i> |
||||
<span>Created {{ new Date(discussion.sendDate).toLocaleDateString() }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="discussion-author"> |
||||
<img v-if="discussion.sender.illustrationUrl" :src="discussion.sender.illustrationUrl" class="author-avatar-icon"> |
||||
<i v-else class="mdi mdi-account-circle-outline author-avatar-icon"></i> |
||||
<span class="author-name">{{ discussion.sender.name }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref, onMounted, computed } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import axios from 'axios' |
||||
import { useI18n } from "vue-i18n" |
||||
const route = useRoute() |
||||
const discussions = ref([]) |
||||
const groupId = ref(route.params.group_id) |
||||
const { t } = useI18n() |
||||
|
||||
onMounted(async () => { |
||||
if (groupId.value) { |
||||
try { |
||||
const response = await axios.get(`/api/messages/by-group/list?groupId=${groupId.value}`) |
||||
discussions.value = response.data['hydra:member'].map(discussion => ({ |
||||
...discussion, |
||||
repliesCount: discussion.receiversTo.length + discussion.receiversCc.length |
||||
})) |
||||
} catch (error) { |
||||
console.error('Error fetching discussions:', error) |
||||
discussions.value = [] |
||||
} |
||||
} |
||||
}) |
||||
const threadCreationUrl = computed(() => { |
||||
return `/main/social/message_for_group_form.inc.php?view_panel=1&user_friend=1&group_id=${groupId.value}&action=add_message_group` |
||||
}) |
||||
</script> |
@ -0,0 +1,63 @@ |
||||
<template> |
||||
<div class="group-members"> |
||||
<div class="edit-members"> |
||||
<BaseButton |
||||
label="Edit members list" |
||||
type="primary" |
||||
class="edit-members-btn" |
||||
icon="pi pi-plus" |
||||
@click="editMembers" |
||||
/> |
||||
</div> |
||||
<div class="members-grid"> |
||||
<div class="member-card" v-for="member in members" :key="member.id"> |
||||
<div class="member-avatar"> |
||||
<img v-if="member.avatar" :src="member.avatar" alt="Member avatar"> |
||||
<i v-else class="mdi mdi-account-circle-outline"></i> |
||||
</div> |
||||
<div class="member-name"> |
||||
{{ member.name }} |
||||
<i v-if="member.isAdmin" class="mdi mdi-star-outline admin-icon"></i> |
||||
</div> |
||||
<div class="member-role" v-if="member.role">{{ member.role }}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref, onMounted } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import BaseButton from "../basecomponents/BaseButton.vue" |
||||
import axios from "axios" |
||||
|
||||
const route = useRoute() |
||||
const members = ref([]) |
||||
const groupId = ref(route.params.group_id) |
||||
|
||||
const fetchMembers = async (groupId) => { |
||||
if (groupId.value) { |
||||
try { |
||||
const response = await axios.get(`/api/usergroups/${groupId.value}/members`) |
||||
members.value = response.data['hydra:member'].map(member => ({ |
||||
id: member.id, |
||||
name: member.username, |
||||
role: member.relationType === 1 ? 'Admin' : 'Member', |
||||
avatar: null, |
||||
isAdmin: member.relationType === 1 |
||||
})) |
||||
} catch (error) { |
||||
console.error('Error fetching group members:', error) |
||||
members.value = [] |
||||
} |
||||
} |
||||
} |
||||
const editMembers = () => { |
||||
} |
||||
|
||||
onMounted(() => { |
||||
if (groupId) { |
||||
fetchMembers(groupId) |
||||
} |
||||
}) |
||||
</script> |
@ -1,9 +1,34 @@ |
||||
<template> |
||||
<router-view></router-view> |
||||
<div class="flex flex-col md:flex-row gap-4"> |
||||
<div class="md:basis-1/3 lg:basis-1/4 2xl:basis-1/6 flex flex-col"> |
||||
<UserProfileCard v-if="!isLoading && !isGroup" /> |
||||
<GroupInfoCard v-if="!isLoading && isGroup" /> |
||||
<SocialSideMenu v-if="!isLoading && !isGroup" /> |
||||
<SocialGroupMenu v-if="!isLoading && isGroup" /> |
||||
</div> |
||||
<div class="md:basis-2/3 lg:basis-3/4 2xl:basis-5/6"> |
||||
<router-view></router-view> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import UserProfileCard from "../social/UserProfileCard.vue" |
||||
import SocialSideMenu from "../social/SocialSideMenu.vue" |
||||
import { useStore } from "vuex" |
||||
import { useRoute } from "vue-router" |
||||
import { onMounted, provide, readonly, ref, watch } from "vue" |
||||
import { useSocialInfo } from "../../composables/useSocialInfo" |
||||
import SocialGroupMenu from "../social/SocialGroupMenu.vue" |
||||
import GroupInfoCard from "../social/GroupInfoCard.vue" |
||||
|
||||
const store = useStore() |
||||
const route = useRoute() |
||||
|
||||
const { user, isCurrentUser, groupInfo, isGroup, loadGroup, loadUser, isLoading } = useSocialInfo() |
||||
|
||||
provide("social-user", user) |
||||
provide("is-current-user", isCurrentUser) |
||||
provide("group-info", groupInfo) |
||||
provide("is-group", isGroup) |
||||
|
||||
<script> |
||||
export default { |
||||
name: 'UserGroupLayout' |
||||
} |
||||
</script> |
||||
|
@ -0,0 +1,80 @@ |
||||
import { ref, readonly, onMounted } from "vue"; |
||||
import { useStore } from "vuex"; |
||||
import { useRoute } from "vue-router"; |
||||
import axios from "axios"; |
||||
|
||||
export function useSocialInfo() { |
||||
const store = useStore(); |
||||
const route = useRoute(); |
||||
|
||||
const user = ref({}); |
||||
const isCurrentUser = ref(true); |
||||
const groupInfo = ref({}); |
||||
const isGroup = ref(false); |
||||
|
||||
const isLoading = ref(true); |
||||
|
||||
const loadGroup = async (groupId) => { |
||||
isLoading.value = true; |
||||
if (groupId) { |
||||
try { |
||||
const response = await axios.get(`/api/usergroup/${groupId}`); |
||||
const groupData = response.data; |
||||
const extractedId = groupData['@id'].split('/').pop(); |
||||
|
||||
groupInfo.value = { |
||||
...groupData, |
||||
id: extractedId |
||||
}; |
||||
|
||||
isGroup.value = true; |
||||
} catch (error) { |
||||
console.error("Error loading group:", error); |
||||
groupInfo.value = {}; |
||||
isGroup.value = false; |
||||
} |
||||
isLoading.value = false; |
||||
} else { |
||||
isGroup.value = false; |
||||
groupInfo.value = {}; |
||||
} |
||||
}; |
||||
|
||||
const loadUser = async () => { |
||||
try { |
||||
if (route.query.id) { |
||||
user.value = await store.dispatch("user/load", '/api/users/' + route.query.id) |
||||
isCurrentUser.value = false |
||||
} else { |
||||
user.value = store.getters["security/getUser"] |
||||
isCurrentUser.value = true |
||||
} |
||||
} catch (e) { |
||||
user.value = {} |
||||
isCurrentUser.value = true |
||||
} |
||||
}; |
||||
|
||||
onMounted(async () => { |
||||
try { |
||||
//if (!route.params.group_id) {
|
||||
await loadUser(); |
||||
//}
|
||||
if (route.params.group_id) { |
||||
await loadGroup(route.params.group_id); |
||||
} |
||||
} finally { |
||||
isLoading.value = false; |
||||
} |
||||
}); |
||||
|
||||
return { |
||||
user: readonly(user), |
||||
isCurrentUser: readonly(isCurrentUser), |
||||
groupInfo: readonly(groupInfo), |
||||
isGroup: readonly(isGroup), |
||||
loadGroup, |
||||
loadUser, |
||||
isLoading, |
||||
}; |
||||
} |
@ -0,0 +1,112 @@ |
||||
<template> |
||||
<div class="invite-friends-container invite-friends"> |
||||
<div class="invite-friends-header"> |
||||
<h2>{{ t('Invite Friends to Group') }}</h2> |
||||
</div> |
||||
<div class="invite-friends-body"> |
||||
<div class="friends-list"> |
||||
<div class="list-header"> |
||||
<h3>{{ t('Available Friends') }}</h3> |
||||
</div> |
||||
<div class="list-content"> |
||||
<div class="friend-entry" v-for="friend in availableFriends" :key="friend.id"> |
||||
<div class="friend-info"> |
||||
<img :src="friend.avatar" alt="avatar" class="friend-avatar" /> |
||||
<span class="friend-name">{{ friend.name }}</span> |
||||
</div> |
||||
<button @click="selectFriend(friend)" class="invite-btn">+</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="selected-friends-list"> |
||||
<div class="list-header"> |
||||
<h3>{{ t('Selected Friends') }}</h3> |
||||
</div> |
||||
<div class="list-content"> |
||||
<div class="friend-entry" v-for="friend in selectedFriends" :key="friend.id"> |
||||
<div class="friend-info"> |
||||
<img :src="friend.avatar" alt="avatar" class="friend-avatar" /> |
||||
<span class="friend-name">{{ friend.name }}</span> |
||||
</div> |
||||
<button @click="removeFriend(friend)" class="remove-btn">-</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="invite-friends-footer"> |
||||
<button @click="sendInvitations" class="send-invites-btn">{{ t('Send Invitations') }}</button> |
||||
</div> |
||||
<div class="invited-friends-list mt-4"> |
||||
<div class="list-header"> |
||||
<h3>{{ t('Users Already Invited') }}</h3> |
||||
</div> |
||||
<div class="invited-users-grid mt-4"> |
||||
<div class="user-card" v-for="user in invitedFriends" :key="user.id"> |
||||
<img :src="user.avatar" alt="avatar" class="user-avatar" /> |
||||
<span class="user-name">{{ user.name }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { inject, onMounted, ref, computed} from "vue" |
||||
import { useI18n } from 'vue-i18n' |
||||
import { useRoute } from 'vue-router' |
||||
import { useStore } from "vuex" |
||||
import axios from "axios" |
||||
|
||||
const { t } = useI18n() |
||||
const route = useRoute() |
||||
const store = useStore() |
||||
const currentUser = computed(() => store.getters["security/getUser"]) |
||||
const availableFriends = ref([]) |
||||
const selectedFriends = ref([]) |
||||
const invitedFriends = ref([]) |
||||
const selectFriend = (friend) => { |
||||
availableFriends.value = availableFriends.value.filter((f) => f.id !== friend.id) |
||||
selectedFriends.value.push(friend) |
||||
} |
||||
const removeFriend = (friend) => { |
||||
selectedFriends.value = selectedFriends.value.filter((f) => f.id !== friend.id) |
||||
availableFriends.value.push(friend) |
||||
} |
||||
const loadAvailableFriends = async () => { |
||||
const groupId = route.params.group_id |
||||
const userId = currentUser.value.id |
||||
try { |
||||
const response = await axios.get(`/social-network/invite-friends/${userId}/${groupId}`) |
||||
availableFriends.value = response.data.friends |
||||
} catch (error) { |
||||
console.error('Error loading available friends:', error) |
||||
} |
||||
} |
||||
onMounted(() => { |
||||
loadAvailableFriends() |
||||
loadInvitedFriends() |
||||
}) |
||||
const sendInvitations = async () => { |
||||
const groupId = route.params.group_id |
||||
const userIds = selectedFriends.value.map(friend => friend.id) |
||||
try { |
||||
await axios.post(`/social-network/add-users-to-group/${groupId}`, { |
||||
userIds, |
||||
}) |
||||
console.log('Users added to group successfully!') |
||||
selectedFriends.value = [] |
||||
loadInvitedFriends() |
||||
} catch (error) { |
||||
console.error('Error adding users to group:', error) |
||||
} |
||||
} |
||||
const loadInvitedFriends = async () => { |
||||
const groupId = route.params.group_id |
||||
try { |
||||
const response = await axios.get(`/social-network/group/${groupId}/invited-users`) |
||||
invitedFriends.value = response.data.invitedUsers |
||||
} catch (error) { |
||||
console.error('Error loading invited friends:', error) |
||||
} |
||||
} |
||||
</script> |
@ -1,642 +1,205 @@ |
||||
<template> |
||||
<Toolbar> |
||||
<template v-slot:right> |
||||
<v-btn |
||||
icon |
||||
tile |
||||
@click="composeHandler" |
||||
> |
||||
<v-icon icon="mdi-email-plus-outline" /> |
||||
</v-btn> |
||||
|
||||
<v-btn |
||||
:loading="isLoading" |
||||
icon |
||||
tile |
||||
@click="reloadHandler" |
||||
> |
||||
<v-icon icon="mdi-refresh" /> |
||||
</v-btn> |
||||
|
||||
<v-btn |
||||
:class="[!selectedItems || !selectedItems.length ? 'hidden' : '']" |
||||
icon |
||||
tile |
||||
@click="confirmDeleteMultiple" |
||||
> |
||||
<v-icon icon="mdi-delete" /> |
||||
</v-btn> |
||||
<!-- :disabled="!selectedItems || !selectedItems.length"--> |
||||
<v-btn |
||||
:class="[!selectedItems || !selectedItems.length ? 'hidden' : '']" |
||||
icon |
||||
tile |
||||
@click="markAsUnReadMultiple" |
||||
> |
||||
<v-icon icon="mdi-email" /> |
||||
</v-btn> |
||||
|
||||
<v-btn |
||||
:class="[!selectedItems || !selectedItems.length ? 'hidden' : '']" |
||||
icon |
||||
tile |
||||
> |
||||
<v-icon icon="mdi-email-open" /> |
||||
</v-btn> |
||||
</template> |
||||
</Toolbar> |
||||
|
||||
<div class="flex flex-row pt-2"> |
||||
<div class="w-1/5"> |
||||
<v-card |
||||
max-width="300" |
||||
tile |
||||
> |
||||
<v-list dense> |
||||
<!-- v-model="selectedItem"--> |
||||
<v-list-item-group color="primary"> |
||||
<v-list-item @click="goToInbox"> |
||||
<v-list-item-icon> |
||||
<v-icon icon="mdi-inbox"></v-icon> |
||||
</v-list-item-icon> |
||||
<v-list-item-content> |
||||
<v-list-item-title>Inbox</v-list-item-title> |
||||
</v-list-item-content> |
||||
</v-list-item> |
||||
|
||||
<v-list-item @click="goToSent"> |
||||
<v-list-item-icon> |
||||
<v-icon icon="mdi-send-outline"></v-icon> |
||||
</v-list-item-icon> |
||||
<v-list-item-content> |
||||
<v-list-item-title>Sent</v-list-item-title> |
||||
</v-list-item-content> |
||||
</v-list-item> |
||||
|
||||
<v-list-item @click="goToUnread"> |
||||
<v-list-item-icon> |
||||
<v-icon icon="mdi-email-outline"></v-icon> |
||||
</v-list-item-icon> |
||||
<v-list-item-content> |
||||
<v-list-item-title>Unread</v-list-item-title> |
||||
</v-list-item-content> |
||||
</v-list-item> |
||||
|
||||
<v-list-item |
||||
v-for="(tag, i) in tags" |
||||
:key="i" |
||||
@click="goToTag(tag)" |
||||
> |
||||
<v-list-item-icon> |
||||
<v-icon icon="mdi-label-outline"></v-icon> |
||||
</v-list-item-icon> |
||||
<v-list-item-content> |
||||
<v-list-item-title v-text="tag.tag"></v-list-item-title> |
||||
</v-list-item-content> |
||||
</v-list-item> |
||||
</v-list-item-group> |
||||
</v-list> |
||||
</v-card> |
||||
</div> |
||||
<div class="w-4/5 pl-4"> |
||||
<div class="text-h4 q-mb-md">{{ title }}</div> |
||||
<DataTable |
||||
v-model:filters="filters" |
||||
v-model:selection="selectedItems" |
||||
:globalFilterFields="['title', 'sendDate']" |
||||
:lazy="true" |
||||
:loading="isLoading" |
||||
:paginator="true" |
||||
:rows="10" |
||||
:rowsPerPageOptions="[5, 10, 20, 50]" |
||||
:totalRecords="totalItems" |
||||
:value="items" |
||||
class="p-datatable-sm" |
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}" |
||||
dataKey="id" |
||||
filterDisplay="menu" |
||||
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown" |
||||
responsiveLayout="scroll" |
||||
sortBy="sendDate" |
||||
sortOrder="asc" |
||||
@page="onPage($event)" |
||||
@sort="sortingChanged($event)" |
||||
> |
||||
<Column |
||||
:exportable="false" |
||||
selectionMode="multiple" |
||||
style="width: 3rem" |
||||
></Column> |
||||
|
||||
<Column |
||||
:header="$t('From')" |
||||
:sortable="false" |
||||
field="sender" |
||||
> |
||||
<template #body="slotProps"> |
||||
<q-avatar size="40px"> |
||||
<img :src="slotProps.data.sender.illustrationUrl + '?w=80&h=80&fit=crop'" /> |
||||
</q-avatar> |
||||
|
||||
<a |
||||
v-if="slotProps.data" |
||||
:class="[true === slotProps.data.read ? 'font-normal' : 'font-semibold']" |
||||
class="cursor-pointer" |
||||
@click="showHandler(slotProps.data)" |
||||
> |
||||
{{ slotProps.data.sender.username }} |
||||
</a> |
||||
</template> |
||||
</Column> |
||||
|
||||
<Column |
||||
:header="$t('Title')" |
||||
:sortable="false" |
||||
field="title" |
||||
> |
||||
<template #body="slotProps"> |
||||
<a |
||||
v-if="slotProps.data" |
||||
class="cursor-pointer" |
||||
v-bind:class="{ 'font-semibold': !slotProps.data.read }" |
||||
@click="showHandler(slotProps.data)" |
||||
> |
||||
{{ slotProps.data.title }} |
||||
</a> |
||||
|
||||
<div class="flex flex-row"> |
||||
<v-chip v-for="tag in slotProps.data.tags"> |
||||
{{ tag.tag }} |
||||
</v-chip> |
||||
<div class="p-grid p-nogutter social-groups"> |
||||
<div class="p-col-12"> |
||||
<div class="p-d-flex p-jc-between p-ai-center p-mb-4"> |
||||
<h1>Social groups</h1> |
||||
<Button label="Create a social group" icon="pi pi-plus" class="create-group-button" @click="showCreateGroupDialog = true" /> |
||||
</div> |
||||
<TabView class="social-group-tabs"> |
||||
<TabPanel header="Newest" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Newest' }"> |
||||
<div class="group-list"> |
||||
<div class="group-item" v-for="group in newestGroups" :key="group['@id']"> |
||||
<i class="mdi mdi-account-group-outline group-icon"></i> |
||||
<div class="group-details"> |
||||
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> |
||||
<div class="group-info"> |
||||
<span class="group-member-count">{{ group.memberCount }} Member</span> |
||||
<span class="group-description">{{ group.description }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<!-- <template #filter="{filterModel}">--> |
||||
<!-- <InputText type="text" v-model="filterModel.value" class="p-column-filter" placeholder="Search by name"/>--> |
||||
<!-- </template>--> |
||||
<!-- --> |
||||
|
||||
<!-- <template #filter="{filterModel}">--> |
||||
<!-- <InputText type="text" v-model="filterModel.value" class="p-column-filter" placeholder="Search by title"/>--> |
||||
<!-- </template>--> |
||||
<!-- <template #filterclear="{filterCallback}">--> |
||||
<!-- <Button type="button" icon="pi pi-times" @click="filterCallback()" class="p-button-secondary"></Button>--> |
||||
<!-- </template>--> |
||||
<!-- <template #filterapply="{filterCallback}">--> |
||||
<!-- <Button type="button" icon="pi pi-check" @click="filterCallback()" class="p-button-success"></Button>--> |
||||
<!-- </template>--> |
||||
</Column> |
||||
|
||||
<Column |
||||
:header="$t('Send date')" |
||||
:sortable="true" |
||||
field="sendDate" |
||||
> |
||||
<template #body="slotProps"> |
||||
{{ relativeDatetime(slotProps.data.sendDate) }} |
||||
</template> |
||||
</Column> |
||||
|
||||
<Column :exportable="false"> |
||||
<template #body="slotProps"> |
||||
<div class="flex flex-row gap-2"> |
||||
<v-btn |
||||
icon |
||||
tile |
||||
@click="confirmDeleteItem(slotProps.data)" |
||||
> |
||||
<v-icon icon="mdi-delete" /> |
||||
</v-btn> |
||||
</div> |
||||
</TabPanel> |
||||
<TabPanel header="Popular" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Popular' }"> |
||||
<div class="group-list"> |
||||
<div class="group-item" v-for="group in popularGroups" :key="group['@id']"> |
||||
<i class="mdi mdi-account-group-outline group-icon"></i> |
||||
<div class="group-details"> |
||||
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> |
||||
<div class="group-info"> |
||||
<span class="group-member-count">{{ group.memberCount }} Member</span> |
||||
<span class="group-description">{{ group.description }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</Column> |
||||
</DataTable> |
||||
</div> </TabPanel> |
||||
<TabPanel header="My groups" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'My groups' }"> |
||||
<div class="group-list"> |
||||
<div class="group-item" v-for="group in myGroups" :key="group['@id']"> |
||||
<i class="mdi mdi-account-group-outline group-icon"></i> |
||||
<div class="group-details"> |
||||
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> |
||||
<div class="group-info"> |
||||
<span class="group-member-count">{{ group.memberCount }} Member</span> |
||||
<span class="group-description">{{ group.description }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</TabPanel> |
||||
</TabView> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Dialogs--> |
||||
|
||||
<Dialog |
||||
v-model:visible="itemDialog" |
||||
:header="$t('New folder')" |
||||
:modal="true" |
||||
:style="{ width: '450px' }" |
||||
class="p-fluid" |
||||
> |
||||
<div class="p-field"> |
||||
<label for="title">{{ $t("Name") }}</label> |
||||
<InputText |
||||
id="title" |
||||
v-model.trim="item.title" |
||||
:class="{ 'p-invalid': submitted && !item.title }" |
||||
autocomplete="off" |
||||
autofocus |
||||
required="true" |
||||
/> |
||||
<small |
||||
v-if="submitted && !item.title" |
||||
class="p-error" |
||||
>$t('Title is required')</small |
||||
> |
||||
</div> |
||||
|
||||
<template #footer> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-times" |
||||
label="Cancel" |
||||
@click="hideDialog" |
||||
/> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-check" |
||||
label="Save" |
||||
@click="saveItem" |
||||
/> |
||||
</template> |
||||
</Dialog> |
||||
|
||||
<Dialog |
||||
v-model:visible="deleteItemDialog" |
||||
:modal="true" |
||||
:style="{ width: '450px' }" |
||||
header="Confirm" |
||||
> |
||||
<div class="confirmation-content"> |
||||
<i |
||||
class="pi pi-exclamation-triangle p-mr-3" |
||||
style="font-size: 2rem" |
||||
/> |
||||
<span v-if="item" |
||||
>Are you sure you want to delete <b>{{ item.title }}</b |
||||
>?</span |
||||
> |
||||
</div> |
||||
<template #footer> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-times" |
||||
label="No" |
||||
@click="deleteItemDialog = false" |
||||
/> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-check" |
||||
label="Yes" |
||||
@click="deleteItemButton" |
||||
/> |
||||
</template> |
||||
</Dialog> |
||||
|
||||
<Dialog |
||||
v-model:visible="deleteMultipleDialog" |
||||
:modal="true" |
||||
:style="{ width: '450px' }" |
||||
header="Confirm" |
||||
> |
||||
<div class="confirmation-content"> |
||||
<i |
||||
class="pi pi-exclamation-triangle p-mr-3" |
||||
style="font-size: 2rem" |
||||
/> |
||||
<span v-if="item">Are you sure you want to delete the selected items?</span> |
||||
</div> |
||||
<template #footer> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-times" |
||||
label="No" |
||||
@click="deleteMultipleDialog = false" |
||||
/> |
||||
<Button |
||||
class="p-button-text" |
||||
icon="pi pi-check" |
||||
label="Yes" |
||||
@click="deleteMultipleItems" |
||||
/> |
||||
</template> |
||||
<Dialog header="Add" :visible="showCreateGroupDialog" :modal="true" :closable="true" @hide="showCreateGroupDialog = false"> |
||||
<form @submit.prevent="createGroup"> |
||||
<div class="p-fluid"> |
||||
<BaseInputTextWithVuelidate |
||||
v-model="groupForm.name" |
||||
label="Name*" |
||||
:vuelidate-property="v$.groupForm.name" |
||||
/> |
||||
|
||||
<BaseInputTextWithVuelidate |
||||
v-model="groupForm.description" |
||||
label="Description" |
||||
:vuelidate-property="v$.groupForm.description" |
||||
as="textarea" |
||||
rows="3" |
||||
/> |
||||
|
||||
<BaseInputTextWithVuelidate |
||||
v-model="groupForm.url" |
||||
label="URL" |
||||
:vuelidate-property="v$.groupForm.url" |
||||
/> |
||||
<BaseFileUpload |
||||
:label="t('Add a picture')" |
||||
accept="image" |
||||
size="small" |
||||
@file-selected="selectedFile = $event" |
||||
/> |
||||
<div class="p-field mt-2"> |
||||
<label for="groupPermissions">Group Permissions</label> |
||||
<Dropdown id="groupPermissions" v-model="groupForm.permissions" :options="permissionsOptions" optionLabel="label" placeholder="Select Permission" /> |
||||
</div> |
||||
<div class="p-field-checkbox mt-2"> |
||||
<BaseCheckbox |
||||
id="leaveGroup" |
||||
v-model="groupForm.allowLeave" |
||||
:label="$t('Allow members to leave group')" |
||||
name="leaveGroup" |
||||
/> |
||||
</div> |
||||
|
||||
</div> |
||||
<Button label="Add" icon="pi pi-check" class="p-button-rounded p-button-text" @click="createGroup" /> |
||||
</form> |
||||
</Dialog> |
||||
</template> |
||||
|
||||
<script> |
||||
import { mapActions, mapGetters, useStore } from "vuex" |
||||
import { mapFields } from "vuex-map-fields" |
||||
import ListMixin from "../../mixins/ListMixin" |
||||
import ActionCell from "../../components/ActionCell.vue" |
||||
import Toolbar from "../../components/Toolbar.vue" |
||||
import ResourceIcon from "../../components/documents/ResourceIcon.vue" |
||||
import ResourceFileLink from "../../components/documents/ResourceFileLink.vue" |
||||
import DataFilter from "../../components/DataFilter" |
||||
import DocumentsFilterForm from "../../components/documents/Filter" |
||||
import { ref } from "vue" |
||||
<script setup> |
||||
import Button from 'primevue/button' |
||||
import TabView from 'primevue/tabview' |
||||
import TabPanel from 'primevue/tabpanel' |
||||
import { ref, onMounted } from 'vue' |
||||
import useVuelidate from '@vuelidate/core' |
||||
import { required } from '@vuelidate/validators' |
||||
import BaseInputTextWithVuelidate from "../../components/basecomponents/BaseInputTextWithVuelidate.vue" |
||||
import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue" |
||||
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue" |
||||
import { useI18n } from "vue-i18n" |
||||
import axios from "axios" |
||||
import { ENTRYPOINT } from "../../config/entrypoint" |
||||
import { RESOURCE_LINK_PUBLISHED } from "../../components/resource_links/visibility" |
||||
import { MESSAGE_TYPE_INBOX, MESSAGE_TYPE_OUTBOX } from "../../components/message/constants" |
||||
import { useFormatDate } from "../../composables/formatDate" |
||||
import { useI18n } from "vue-i18n" |
||||
|
||||
export default { |
||||
name: "UserGroupList", |
||||
servicePrefix: "usergroups", |
||||
components: { |
||||
Toolbar, |
||||
ActionCell, |
||||
ResourceIcon, |
||||
ResourceFileLink, |
||||
DocumentsFilterForm, |
||||
DataFilter, |
||||
}, |
||||
mixins: [ListMixin], |
||||
setup() { |
||||
const { t } = useI18n() |
||||
const { relativeDatetime } = useFormatDate() |
||||
|
||||
const store = useStore() |
||||
const filters = ref([]) |
||||
const filtersSent = ref([]) |
||||
const user = store.getters["security/getUser"] |
||||
const tags = ref([]) |
||||
const title = ref("Inbox") |
||||
|
||||
filtersSent.value = { |
||||
msgType: MESSAGE_TYPE_OUTBOX, |
||||
sender: user.id, |
||||
} |
||||
|
||||
// inbox |
||||
filters.value = { |
||||
msgType: MESSAGE_TYPE_INBOX, |
||||
userReceiver: user.id, |
||||
const {t} = useI18n() |
||||
const newestGroups = ref([]) |
||||
const popularGroups = ref([]) |
||||
const myGroups = ref([]) |
||||
const activeTab = ref('Newest') |
||||
const showCreateGroupDialog = ref(false) |
||||
const selectedFile = ref(null) |
||||
|
||||
const groupForm = ref({ |
||||
name: '', |
||||
description: '', |
||||
url: '', |
||||
picture: null, |
||||
}) |
||||
const v$ = useVuelidate({ |
||||
groupForm: { |
||||
name: { required }, |
||||
description: {}, |
||||
url: {}, |
||||
} |
||||
}, { groupForm }) |
||||
const permissionsOptions = [ |
||||
{ label: 'Open', value: '1' }, |
||||
{ label: 'Closed', value: '2' }, |
||||
] |
||||
const createGroup = async () => { |
||||
v$.value.$touch() |
||||
if (!v$.value.$invalid) { |
||||
const groupData = { |
||||
title: groupForm.value.name, |
||||
description: groupForm.value.description, |
||||
url: groupForm.value.url, |
||||
visibility: groupForm.value.permissions.value, |
||||
allowMembersToLeaveGroup: groupForm.value.allowLeave ? 1 : 0, |
||||
groupType: 1, |
||||
} |
||||
|
||||
// Get user tags. |
||||
axios |
||||
.get(ENTRYPOINT + "message_tags", { |
||||
params: { |
||||
user: user["@id"], |
||||
try { |
||||
const response = await axios.post(ENTRYPOINT + 'usergroups', groupData, { |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
}) |
||||
.then((response) => { |
||||
let data = response.data |
||||
tags.value = data["hydra:member"] |
||||
}) |
||||
|
||||
function goToInbox() { |
||||
title.value = "Inbox" |
||||
filters.value = { |
||||
msgType: MESSAGE_TYPE_INBOX, |
||||
userReceiver: user.id, |
||||
} |
||||
store.dispatch("message/resetList") |
||||
store.dispatch("message/fetchAll", filters.value) |
||||
} |
||||
|
||||
function goToUnread() { |
||||
title.value = "Unread" |
||||
filters.value = { |
||||
msgType: MESSAGE_TYPE_INBOX, |
||||
userReceiver: user.id, |
||||
read: false, |
||||
} |
||||
store.dispatch("message/resetList") |
||||
store.dispatch("message/fetchAll", filters.value) |
||||
} |
||||
|
||||
function goToSent() { |
||||
title.value = "Sent" |
||||
filters.value = { |
||||
msgType: MESSAGE_TYPE_OUTBOX, |
||||
sender: user.id, |
||||
} |
||||
store.dispatch("message/resetList") |
||||
store.dispatch("message/fetchAll", filters.value) |
||||
} |
||||
|
||||
function goToTag(tag) { |
||||
title.value = tag.tag |
||||
filters.value = { |
||||
msgType: MESSAGE_TYPE_INBOX, |
||||
userReceiver: user.id, |
||||
tags: [tag], |
||||
} |
||||
store.dispatch("message/resetList") |
||||
store.dispatch("message/fetchAll", filters.value) |
||||
} |
||||
/*if (selectedFile.value && response.data && response.data.id) { |
||||
const formData = new FormData() |
||||
formData.append('picture', selectedFile.value) |
||||
await axios.post(`/social-network/upload-group-picture/${response.data.id}`, formData, { |
||||
headers: { |
||||
'Content-Type': 'multipart/form-data', |
||||
}, |
||||
}) |
||||
}*/ |
||||
|
||||
return { |
||||
goToInbox, |
||||
goToSent, |
||||
goToTag, |
||||
goToUnread, |
||||
tags, |
||||
filters, |
||||
title, |
||||
relativeDatetime, |
||||
showCreateGroupDialog.value = false |
||||
await updateGroupsList() |
||||
} catch (error) { |
||||
console.error('Failed to create group or upload picture:', error.response.data) |
||||
} |
||||
}, |
||||
data() { |
||||
const {t} = useI18n() |
||||
|
||||
return { |
||||
columns: [ |
||||
{ label: t("Title"), field: "title", name: "title", sortable: true }, |
||||
{ label: t("Sender"), field: "sender", name: "userSender", sortable: true }, |
||||
{ label: t("Modified"), field: "sendDate", name: "updatedAt", sortable: true }, |
||||
{ label: t("Actions"), name: "action", sortable: false }, |
||||
], |
||||
pageOptions: [10, 20, 50, t("All")], |
||||
selected: [], |
||||
isBusy: false, |
||||
options: { |
||||
sortBy: "sendDate", |
||||
sortDesc: "asc", |
||||
}, |
||||
selectedItems: [], |
||||
// prime vue |
||||
itemDialog: false, |
||||
deleteItemDialog: false, |
||||
deleteMultipleDialog: false, |
||||
item: {}, |
||||
submitted: false, |
||||
} |
||||
} |
||||
const fetchGroups = async (endpoint) => { |
||||
try { |
||||
const response = await fetch(ENTRYPOINT + `${endpoint}`) |
||||
if (!response.ok) { |
||||
throw new Error('Failed to fetch groups') |
||||
} |
||||
}, |
||||
mounted() { |
||||
this.onUpdateOptions(this.options) |
||||
}, |
||||
computed: { |
||||
// From crud.js list function |
||||
...mapGetters("resourcenode", { |
||||
resourceNode: "getResourceNode", |
||||
}), |
||||
...mapGetters({ |
||||
isAuthenticated: "security/isAuthenticated", |
||||
isAdmin: "security/isAdmin", |
||||
currentUser: "security/getUser", |
||||
}), |
||||
|
||||
...mapGetters("message", { |
||||
items: "list", |
||||
}), |
||||
|
||||
//...getters |
||||
|
||||
// From ListMixin |
||||
...mapFields("message", { |
||||
deletedItem: "deleted", |
||||
error: "error", |
||||
isLoading: "isLoading", |
||||
resetList: "resetList", |
||||
totalItems: "totalItems", |
||||
view: "view", |
||||
}), |
||||
}, |
||||
methods: { |
||||
composeHandler() { |
||||
let folderParams = this.$route.query |
||||
this.$router.push({ name: `${this.$options.servicePrefix}Create`, query: folderParams }) |
||||
}, |
||||
|
||||
// prime |
||||
onPage(event) { |
||||
this.options.itemsPerPage = event.rows |
||||
this.options.page = event.page + 1 |
||||
this.options.sortBy = event.sortField |
||||
this.options.sortDesc = event.sortOrder === -1 |
||||
|
||||
this.onUpdateOptions(this.options) |
||||
}, |
||||
sortingChanged(event) { |
||||
console.log("sortingChanged") |
||||
console.log(event) |
||||
this.options.sortBy = event.sortField |
||||
this.options.sortDesc = event.sortOrder === -1 |
||||
|
||||
this.onUpdateOptions(this.options) |
||||
// ctx.sortBy ==> Field key for sorting by (or null for no sorting) |
||||
// ctx.sortDesc ==> true if sorting descending, false otherwise |
||||
}, |
||||
openNew() { |
||||
this.item = {} |
||||
this.submitted = false |
||||
this.itemDialog = true |
||||
}, |
||||
hideDialog() { |
||||
this.itemDialog = false |
||||
this.submitted = false |
||||
}, |
||||
saveItem() { |
||||
this.submitted = true |
||||
|
||||
if (this.item.title.trim()) { |
||||
if (this.item.id) { |
||||
} else { |
||||
//this.products.push(this.product); |
||||
this.item.filetype = "folder" |
||||
this.item.parentResourceNodeId = this.$route.params.node |
||||
this.item.resourceLinkList = JSON.stringify([ |
||||
{ |
||||
gid: this.$route.query.gid, |
||||
sid: this.$route.query.sid, |
||||
cid: this.$route.query.cid, |
||||
visibility: RESOURCE_LINK_PUBLISHED, // visible by default |
||||
}, |
||||
]) |
||||
|
||||
this.create(this.item) |
||||
this.showMessage("Saved") |
||||
} |
||||
|
||||
this.itemDialog = false |
||||
this.item = {} |
||||
} |
||||
}, |
||||
editItem(item) { |
||||
this.item = { ...item } |
||||
this.itemDialog = true |
||||
}, |
||||
confirmDeleteItem(item) { |
||||
this.item = item |
||||
this.deleteItemDialog = true |
||||
}, |
||||
confirmDeleteMultiple() { |
||||
this.deleteMultipleDialog = true |
||||
}, |
||||
markAsReadMultiple() { |
||||
console.log("markAsReadMultiple") |
||||
this.selectedItems.forEach((message) => { |
||||
message.read = true |
||||
this.update(message) |
||||
}) |
||||
this.selectedItems = null |
||||
this.resetList = true |
||||
}, |
||||
reloadHandler() { |
||||
this.onUpdateOptions(this.options) |
||||
}, |
||||
markAsUnReadMultiple() { |
||||
console.log("markAsUnReadMultiple") |
||||
this.selectedItems.forEach((message) => { |
||||
message.read = false |
||||
this.update(message) |
||||
}) |
||||
this.selectedItems = null |
||||
this.resetList = true |
||||
//this.onUpdateOptions(this.options); |
||||
}, |
||||
deleteMultipleItems() { |
||||
console.log("deleteMultipleItems") |
||||
console.log(this.selectedItems) |
||||
this.deleteMultipleAction(this.selectedItems) |
||||
this.onRequest({ |
||||
pagination: this.pagination, |
||||
}) |
||||
this.deleteMultipleDialog = false |
||||
this.selectedItems = null |
||||
//this.onUpdateOptions(this.options); |
||||
}, |
||||
deleteItemButton() { |
||||
console.log("deleteItem") |
||||
this.deleteItem(this.item) |
||||
//this.items = this.items.filter(val => val.iid !== this.item.iid); |
||||
this.deleteItemDialog = false |
||||
this.item = {} |
||||
this.onUpdateOptions(this.options) |
||||
}, |
||||
onRowSelected(items) { |
||||
this.selected = items |
||||
}, |
||||
selectAllRows() { |
||||
this.$refs.selectableTable.selectAllRows() |
||||
}, |
||||
clearSelected() { |
||||
this.$refs.selectableTable.clearSelected() |
||||
}, |
||||
async deleteSelected() { |
||||
console.log("deleteSelected") |
||||
/*for (let i = 0; i < this.selected.length; i++) { |
||||
let item = this.selected[i]; |
||||
//this.deleteHandler(item); |
||||
this.deleteItem(item); |
||||
}*/ |
||||
const data = await response.json() |
||||
console.log('hidra menber ::: ', data['hydra:member']) |
||||
|
||||
return data['hydra:member'] |
||||
} catch (error) { |
||||
console.error(error) |
||||
return [] |
||||
} |
||||
} |
||||
const updateGroupsList = async () => { |
||||
newestGroups.value = await fetchGroups('usergroup/list/newest') |
||||
popularGroups.value = await fetchGroups('usergroup/list/popular') |
||||
myGroups.value = await fetchGroups('usergroup/list/my') |
||||
} |
||||
|
||||
this.deleteMultipleAction(this.selected) |
||||
this.onRequest({ |
||||
pagination: this.pagination, |
||||
}) |
||||
}, |
||||
//...actions, |
||||
// From ListMixin |
||||
...mapActions("message", { |
||||
getPage: "fetchAll", |
||||
create: "create", |
||||
update: "update", |
||||
deleteItem: "del", |
||||
deleteMultipleAction: "delMultiple", |
||||
}), |
||||
...mapActions("resourcenode", { |
||||
findResourceNode: "findResourceNode", |
||||
}), |
||||
}, |
||||
const extractGroupId = (group) => { |
||||
const match = group['@id'].match(/\/api\/usergroup\/(\d+)/) |
||||
return match ? match[1] : null |
||||
} |
||||
const redirectToGroupDetails = (groupId) => { |
||||
router.push({ name: 'UserGroupShow', params: { group_id: groupId } }) |
||||
} |
||||
onMounted(async () => { |
||||
await updateGroupsList() |
||||
}) |
||||
</script> |
||||
|
@ -0,0 +1,69 @@ |
||||
<template> |
||||
<div class="search-container social-groups"> |
||||
<div class="search-header"> |
||||
<h2>{{ t('Results and feedback') }} {{ searchTerm }}</h2> |
||||
<div class="p-inputgroup"> |
||||
<BaseInputText |
||||
v-model="searchTerm" |
||||
placeholder="Search term..." |
||||
class="search-term-input" |
||||
label="Search term ..."/> |
||||
<BaseButton |
||||
label="Search" |
||||
icon="pi pi-search" |
||||
@click="performSearch" |
||||
type="button"/> |
||||
</div> |
||||
</div> |
||||
<div class="p-grid search-results"> |
||||
<div class="p-col-12 p-md-4" v-for="group in searchResults" :key="group.id"> |
||||
<div class="group-card"> |
||||
<div class="group-image"> |
||||
<i v-if="!group.pictureUrl" class="pi pi-users large-icon"></i> |
||||
<img v-else :src="group.pictureUrl" alt="Group" /> |
||||
</div> |
||||
<div class="group-details"> |
||||
<h4 class="group-title">{{ group.title }}</h4> |
||||
<p class="group-description">{{ group.description }}</p> |
||||
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ t('See more') }}</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { onMounted, ref } from "vue" |
||||
import { useI18n } from 'vue-i18n' |
||||
import BaseInputText from "../../components/basecomponents/BaseInputText.vue" |
||||
import BaseButton from "../../components/basecomponents/BaseButton.vue" |
||||
import axios from 'axios' |
||||
import { useRoute } from "vue-router" |
||||
|
||||
const { t } = useI18n() |
||||
const route = useRoute() |
||||
const searchTerm = ref('') |
||||
const searchResults = ref([]) |
||||
onMounted(() => { |
||||
if (route.query.q) { |
||||
searchTerm.value = route.query.q |
||||
performSearch() |
||||
} |
||||
}) |
||||
const performSearch = async () => { |
||||
try { |
||||
const response = await axios.get('/api/usergroups/search', { |
||||
params: { search: searchTerm.value }, |
||||
}) |
||||
searchResults.value = response.data['hydra:member'] |
||||
} catch (error) { |
||||
console.error('Error performing search:', error) |
||||
searchResults.value = [] |
||||
} |
||||
} |
||||
const extractGroupId = (group) => { |
||||
const match = group['@id'].match(/\/api\/usergroup\/(\d+)/) |
||||
return match ? match[1] : null |
||||
} |
||||
</script> |
@ -1,262 +1,40 @@ |
||||
<template> |
||||
<div v-if="item"> |
||||
<Toolbar :handle-delete="del"> |
||||
<template v-slot:right> |
||||
<!-- <v-toolbar-title v-if="item">--> |
||||
<!-- {{--> |
||||
<!-- `${$options.servicePrefix} ${item['@id']}`--> |
||||
<!-- }}--> |
||||
<!-- </v-toolbar-title>--> |
||||
|
||||
<v-btn |
||||
:loading="isLoading" |
||||
icon |
||||
tile |
||||
@click="reply" |
||||
> |
||||
<v-icon icon="mdi-reply" /> |
||||
</v-btn> |
||||
</template> |
||||
</Toolbar> |
||||
|
||||
<VueMultiselect |
||||
v-model="item.tags" |
||||
:internal-search="false" |
||||
:loading="isLoadingSelect" |
||||
:multiple="true" |
||||
:options="tags" |
||||
:searchable="true" |
||||
:taggable="true" |
||||
label="tag" |
||||
placeholder="Tags" |
||||
tag-placeholder="Add this as new tag" |
||||
track-by="id" |
||||
@remove="removeTagFromMessage" |
||||
@select="addTagToMessage" |
||||
@tag="addTag" |
||||
@search-change="asyncFind" |
||||
/> |
||||
|
||||
<p class="text-lg"> |
||||
From: |
||||
<q-avatar size="32px"> |
||||
<img :src="item['sender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" /> |
||||
</q-avatar> |
||||
{{ item["sender"]["username"] }} |
||||
</p> |
||||
|
||||
<p class="text-lg"> |
||||
{{ relativeDatetime(item["sendDate"]) }} |
||||
</p> |
||||
<div class="social-group-show"> |
||||
<div class="group-header"> |
||||
<h1 class="group-title">mi grupo 0002</h1> |
||||
<p class="group-description">test</p> |
||||
</div> |
||||
|
||||
<h3 class="text-lg">{{ item.title }}</h3> |
||||
<ul class="tabs"> |
||||
<li :class="{ active: activeTab === 'discussions' }" @click="activeTab = 'discussions'">Discussions</li> |
||||
<li :class="{ active: activeTab === 'members' }" @click="activeTab = 'members'">Members</li> |
||||
</ul> |
||||
|
||||
<div class="flex flex-row"> |
||||
<div class="w-full"> |
||||
<p v-html="item.content" /> |
||||
</div> |
||||
<div class="tab-content"> |
||||
<GroupDiscussions v-if="activeTab === 'discussions'" :group-id="groupId" /> |
||||
<GroupMembers v-if="activeTab === 'members'" :group-id="groupId" /> |
||||
</div> |
||||
<Loading :visible="isLoading" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style> |
||||
|
||||
<script> |
||||
import { mapActions, mapGetters, useStore } from "vuex" |
||||
import { mapFields } from "vuex-map-fields" |
||||
import Loading from "../../components/Loading.vue" |
||||
import ShowMixin from "../../mixins/ShowMixin" |
||||
import Toolbar from "../../components/Toolbar.vue" |
||||
import VueMultiselect from "vue-multiselect" |
||||
import { ref } from "vue" |
||||
import isEmpty from "lodash/isEmpty" |
||||
import axios from "axios" |
||||
import { ENTRYPOINT } from "../../config/entrypoint" |
||||
import useVuelidate from "@vuelidate/core" |
||||
import { useRoute, useRouter } from "vue-router" |
||||
import NotificationMixin from "../../mixins/NotificationMixin" |
||||
import { useFormatDate } from "../../composables/formatDate" |
||||
|
||||
const servicePrefix = "usergroups" |
||||
|
||||
export default { |
||||
name: "UserGroupShow", |
||||
components: { |
||||
Loading, |
||||
Toolbar, |
||||
VueMultiselect, |
||||
}, |
||||
setup() { |
||||
const tags = ref([]) |
||||
const isLoadingSelect = ref(false) |
||||
const store = useStore() |
||||
const user = store.getters["security/getUser"] |
||||
const find = store.getters["message/find"] |
||||
const route = useRoute() |
||||
const router = useRouter() |
||||
|
||||
const { relativeDatetime } = useFormatDate() |
||||
|
||||
let id = route.params.id |
||||
if (isEmpty(id)) { |
||||
id = route.query.id |
||||
} |
||||
|
||||
console.log(id) |
||||
console.log(decodeURIComponent(id)) |
||||
|
||||
let item = find(decodeURIComponent(id)) |
||||
|
||||
// Change to read |
||||
if (false === item.read) { |
||||
axios |
||||
.put(ENTRYPOINT + "messages/" + item.id, { |
||||
read: true, |
||||
}) |
||||
.then((response) => { |
||||
console.log(response) |
||||
}) |
||||
.catch(function (error) { |
||||
console.log(error) |
||||
}) |
||||
} |
||||
|
||||
function addTag(newTag) { |
||||
axios |
||||
.post(ENTRYPOINT + "message_tags", { |
||||
user: user["@id"], |
||||
tag: newTag, |
||||
}) |
||||
.then((response) => { |
||||
addTagToMessage(response.data) |
||||
//this.showMessage('Added'); |
||||
item.tags.push(response.data) |
||||
console.log(response) |
||||
isLoadingSelect.value = false |
||||
}) |
||||
.catch(function (error) { |
||||
isLoadingSelect.value = false |
||||
console.log(error) |
||||
}) |
||||
} |
||||
|
||||
function addTagToMessage(newTag) { |
||||
console.log("addTagToMessage") |
||||
let tagsToUpdate = [] |
||||
item.tags.forEach((tagItem) => { |
||||
tagsToUpdate.push(tagItem["@id"]) |
||||
}) |
||||
tagsToUpdate.push(newTag["@id"]) |
||||
console.log(tagsToUpdate) |
||||
|
||||
axios |
||||
.put(ENTRYPOINT + "messages/" + item.id, { |
||||
tags: tagsToUpdate, |
||||
}) |
||||
.then((response) => { |
||||
//this.showMessage('Added'); |
||||
console.log(response) |
||||
isLoadingSelect.value = false |
||||
}) |
||||
.catch(function (error) { |
||||
isLoadingSelect.value = false |
||||
console.log(error) |
||||
}) |
||||
} |
||||
|
||||
function removeTagFromMessage() { |
||||
let tagsToUpdate = [] |
||||
item.tags.forEach((tagItem) => { |
||||
tagsToUpdate.push(tagItem["@id"]) |
||||
}) |
||||
|
||||
axios |
||||
.put(ENTRYPOINT + "messages/" + item.id, { |
||||
tags: tagsToUpdate, |
||||
}) |
||||
.then((response) => { |
||||
console.log(response) |
||||
isLoadingSelect.value = false |
||||
}) |
||||
.catch(function (error) { |
||||
isLoadingSelect.value = false |
||||
console.log(error) |
||||
}) |
||||
} |
||||
|
||||
axios |
||||
.get(ENTRYPOINT + "message_tags", { |
||||
params: { |
||||
user: user["@id"], |
||||
}, |
||||
}) |
||||
.then((response) => { |
||||
isLoadingSelect.value = false |
||||
let data = response.data |
||||
tags.value = data["hydra:member"] |
||||
}) |
||||
|
||||
function reply() { |
||||
let params = route.query |
||||
router.push({ name: `${servicePrefix}Reply`, query: params }) |
||||
} |
||||
|
||||
function asyncFind(query) { |
||||
if (query.toString().length < 3) { |
||||
return |
||||
} |
||||
|
||||
isLoadingSelect.value = true |
||||
axios |
||||
.get(ENTRYPOINT + "message_tags", { |
||||
params: { |
||||
user: user["@id"], |
||||
}, |
||||
}) |
||||
.then((response) => { |
||||
isLoadingSelect.value = false |
||||
let data = response.data |
||||
tags.value = data["hydra:member"] |
||||
}) |
||||
.catch(function (error) { |
||||
isLoadingSelect.value = false |
||||
console.log(error) |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
v$: useVuelidate(), |
||||
tags, |
||||
isLoadingSelect, |
||||
item, |
||||
addTag, |
||||
addTagToMessage, |
||||
removeTagFromMessage, |
||||
asyncFind, |
||||
reply, |
||||
relativeDatetime, |
||||
} |
||||
}, |
||||
mixins: [ShowMixin, NotificationMixin], |
||||
computed: { |
||||
...mapFields("message", { |
||||
isLoading: "isLoading", |
||||
}), |
||||
...mapGetters("message", ["find"]), |
||||
...mapGetters({ |
||||
isAuthenticated: "security/isAuthenticated", |
||||
isAdmin: "security/isAdmin", |
||||
currentUser: "security/getUser", |
||||
}), |
||||
}, |
||||
methods: { |
||||
...mapActions("message", { |
||||
deleteItem: "del", |
||||
reset: "resetShow", |
||||
retrieve: "loadWithQuery", |
||||
}), |
||||
}, |
||||
servicePrefix, |
||||
} |
||||
<script setup> |
||||
import { ref, onMounted } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import axios from 'axios' |
||||
import GroupDiscussions from "../../components/usergroup/GroupDiscussions.vue" |
||||
import GroupMembers from "../../components/usergroup/GroupMembers.vue" |
||||
const route = useRoute() |
||||
const activeTab = ref('discussions') |
||||
const groupId = ref(route.params.group_id) |
||||
const group = ref(null) |
||||
onMounted(async () => { |
||||
if (groupId.value) { |
||||
try { |
||||
const response = await axios.get(`/api/usergroup/${groupId.value}`) |
||||
group.value = response.data |
||||
} catch (error) { |
||||
console.error('Error fetching group details:', error) |
||||
} |
||||
} |
||||
}) |
||||
</script> |
||||
|
@ -0,0 +1,39 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\DataProvider; |
||||
|
||||
use ApiPlatform\Metadata\Operation; |
||||
use ApiPlatform\State\ProviderInterface; |
||||
use Chamilo\CoreBundle\Entity\Usergroup; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
|
||||
final class GroupMembersDataProvider implements ProviderInterface |
||||
{ |
||||
private EntityManagerInterface $entityManager; |
||||
|
||||
public function __construct(EntityManagerInterface $entityManager) |
||||
{ |
||||
$this->entityManager = $entityManager; |
||||
} |
||||
|
||||
public function supports(Operation $operation, array $uriVariables = [], array $context = []): bool |
||||
{ |
||||
return Usergroup::class === $operation->getClass() && 'get_group_members' === $operation->getName(); |
||||
} |
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable |
||||
{ |
||||
$groupId = $uriVariables['id'] ?? null; |
||||
|
||||
if (null === $groupId) { |
||||
return []; |
||||
} |
||||
|
||||
$usergroupRepository = $this->entityManager->getRepository(Usergroup::class); |
||||
$users = $usergroupRepository->getUsersByGroup((int)$groupId); |
||||
|
||||
return $users; |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\DataProvider; |
||||
|
||||
use ApiPlatform\Metadata\Operation; |
||||
use ApiPlatform\State\ProviderInterface; |
||||
use Chamilo\CoreBundle\Entity\Message; |
||||
use Chamilo\CoreBundle\Repository\MessageRepository; |
||||
|
||||
final class MessageByGroupDataProvider implements ProviderInterface |
||||
{ |
||||
private MessageRepository $messageRepository; |
||||
|
||||
public function __construct(MessageRepository $messageRepository) |
||||
{ |
||||
$this->messageRepository = $messageRepository; |
||||
} |
||||
|
||||
public function supports(Operation $operation, array $uriVariables = [], array $context = []): bool |
||||
{ |
||||
return Message::class === $operation->getClass() && 'get_messages_by_group' === $operation->getName(); |
||||
} |
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable |
||||
{ |
||||
$groupId = $context['filters']['groupId'] ?? null; |
||||
|
||||
if (null === $groupId) { |
||||
|
||||
return []; |
||||
} |
||||
|
||||
return $this->messageRepository->findByGroupId((int) $groupId); |
||||
} |
||||
} |
@ -0,0 +1,117 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\CoreBundle\DataProvider; |
||||
|
||||
use ApiPlatform\Metadata\Operation; |
||||
use ApiPlatform\State\ProviderInterface; |
||||
use Chamilo\CoreBundle\Entity\Usergroup; |
||||
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository; |
||||
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository; |
||||
use Symfony\Component\Security\Core\Security; |
||||
|
||||
|
||||
final class UsergroupDataProvider implements ProviderInterface |
||||
{ |
||||
private $security; |
||||
private $usergroupRepository; |
||||
private $illustrationRepository; |
||||
|
||||
public function __construct(Security $security, UsergroupRepository $usergroupRepository, IllustrationRepository $illustrationRepository) |
||||
{ |
||||
$this->security = $security; |
||||
$this->usergroupRepository = $usergroupRepository; |
||||
$this->illustrationRepository = $illustrationRepository; |
||||
} |
||||
|
||||
/** |
||||
* @param Operation $operation |
||||
* @param array $uriVariables |
||||
* @param array $context |
||||
* @return iterable |
||||
*/ |
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable |
||||
{ |
||||
$operationName = $operation->getName(); |
||||
if ($operationName === 'get_usergroup') { |
||||
$groupId = $uriVariables['id'] ?? null; |
||||
|
||||
if (!$groupId) { |
||||
throw new \Exception("Group ID is required for 'get_usergroup' operation"); |
||||
} |
||||
|
||||
$group = $this->usergroupRepository->findGroupById($groupId); |
||||
|
||||
if (!$group) { |
||||
throw new \Exception("Group not found"); |
||||
} |
||||
|
||||
$this->setGroupDetails($group); |
||||
|
||||
return [$group]; |
||||
} |
||||
|
||||
if ($operationName === 'search_usergroups') { |
||||
$searchTerm = $context['filters']['search'] ?? ''; |
||||
$groups = $this->usergroupRepository->searchGroups($searchTerm); |
||||
foreach ($groups as $group) { |
||||
$this->setGroupDetails($group); |
||||
} |
||||
return $groups; |
||||
} |
||||
|
||||
switch ($operationName) { |
||||
case 'get_my_usergroups': |
||||
$userId = $context['request_attributes']['_api_filters']['userId'] ?? null; |
||||
if (!$userId) { |
||||
$user = $this->security->getUser(); |
||||
$userId = $user ? $user->getId() : null; |
||||
} |
||||
if (!$userId) { |
||||
throw new \Exception("User ID is required"); |
||||
} |
||||
$groups = $this->usergroupRepository->getGroupsByUser($userId, 0); |
||||
break; |
||||
|
||||
case 'get_newest_usergroups': |
||||
$groups = $this->usergroupRepository->getNewestGroups(); |
||||
break; |
||||
|
||||
case 'get_popular_usergroups': |
||||
$groups = $this->usergroupRepository->getPopularGroups(); |
||||
break; |
||||
|
||||
default: |
||||
$groups = []; |
||||
break; |
||||
} |
||||
|
||||
if (in_array($operationName, ['get_my_usergroups', 'get_newest_usergroups', 'get_popular_usergroups'])) { |
||||
/* @var Usergroup $group */ |
||||
foreach ($groups as $group) { |
||||
$this->setGroupDetails($group); |
||||
} |
||||
} |
||||
|
||||
return $groups; |
||||
} |
||||
|
||||
|
||||
public function supports(Operation $operation, array $uriVariables = [], array $context = []): bool |
||||
{ |
||||
return Usergroup::class === $operation->getClass(); |
||||
} |
||||
|
||||
private function setGroupDetails(Usergroup $group): void |
||||
{ |
||||
$memberCount = $this->usergroupRepository->countMembers($group->getId()); |
||||
$group->setMemberCount($memberCount); |
||||
|
||||
if ($this->illustrationRepository->hasIllustration($group)) { |
||||
$picture = $this->illustrationRepository->getIllustrationUrl($group); |
||||
$group->setPictureUrl($picture); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\State; |
||||
|
||||
use ApiPlatform\Metadata\Operation; |
||||
use ApiPlatform\State\ProcessorInterface; |
||||
use Chamilo\CoreBundle\Entity\Usergroup; |
||||
use Chamilo\CoreBundle\Entity\UsergroupRelUser; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use Symfony\Component\HttpFoundation\RequestStack; |
||||
use Symfony\Component\Security\Core\Security; |
||||
|
||||
class UsergroupPostProcessor implements ProcessorInterface |
||||
{ |
||||
private ProcessorInterface $processor; |
||||
private EntityManagerInterface $entityManager; |
||||
private Security $security; |
||||
private RequestStack $requestStack; |
||||
|
||||
public function __construct( |
||||
ProcessorInterface $processor, |
||||
EntityManagerInterface $entityManager, |
||||
Security $security, |
||||
RequestStack $requestStack |
||||
) { |
||||
$this->processor = $processor; |
||||
$this->entityManager = $entityManager; |
||||
$this->security = $security; |
||||
$this->requestStack = $requestStack; |
||||
} |
||||
|
||||
public function process($data, Operation $operation, array $uriVariables = [], array $context = []) |
||||
{ |
||||
/** @var Usergroup $usergroup */ |
||||
$usergroup = $this->processor->process($data, $operation, $uriVariables, $context); |
||||
|
||||
if ($usergroup instanceof Usergroup) { |
||||
$this->associateCurrentUser($usergroup); |
||||
$this->entityManager->flush(); |
||||
} |
||||
|
||||
return $usergroup; |
||||
} |
||||
|
||||
private function associateCurrentUser(Usergroup $usergroup) |
||||
{ |
||||
$currentUser = $this->security->getUser(); |
||||
if ($currentUser) { |
||||
$usergroupRelUser = new UsergroupRelUser(); |
||||
$usergroupRelUser->setUsergroup($usergroup); |
||||
$usergroupRelUser->setUser($currentUser); |
||||
$usergroupRelUser->setRelationType(Usergroup::GROUP_USER_PERMISSION_ADMIN); |
||||
|
||||
$this->entityManager->persist($usergroupRelUser); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue