User friends: Add friends list

pull/3924/head
Julio Montoya 4 years ago
parent af2527dd47
commit 6c9edb976f
  1. 13
      assets/vue/components/layout/DashboardLayout.vue
  2. 9
      assets/vue/components/user/Layout.vue
  3. 9
      assets/vue/components/usergroup/Layout.vue
  4. 9
      assets/vue/components/userreluser/Layout.vue
  5. 31
      assets/vue/main.js
  6. 10
      assets/vue/router/index.js
  7. 15
      assets/vue/router/user.js
  8. 20
      assets/vue/router/usergroup.js
  9. 20
      assets/vue/router/userreluser.js
  10. 3
      assets/vue/services/user.js
  11. 3
      assets/vue/services/usergroup.js
  12. 3
      assets/vue/services/userreluser.js
  13. 15
      assets/vue/views/account/Home.vue
  14. 2
      assets/vue/views/message/List.vue
  15. 2
      assets/vue/views/message/Reply.vue
  16. 563
      assets/vue/views/usergroup/List.vue
  17. 242
      assets/vue/views/usergroup/Show.vue
  18. 190
      assets/vue/views/userreluser/Add.vue
  19. 381
      assets/vue/views/userreluser/List.vue
  20. 7
      src/CoreBundle/Entity/UserRelUser.php

@ -33,9 +33,12 @@
<q-space />
<div class="q-gutter-sm row items-center no-wrap">
<!-- <q-btn v-if="isAuthenticated" round dense flat color="grey-8" icon="people">-->
<!-- <q-tooltip>Friends</q-tooltip>-->
<!-- </q-btn>-->
<q-btn v-if="isAuthenticated" round dense flat color="grey-8"
icon="person"
:to="'/account/home'"
>
<q-tooltip>Profile</q-tooltip>
</q-btn>
<q-btn v-if="isAuthenticated" round dense flat color="grey-8"
icon="inbox"
@ -303,9 +306,9 @@ export default {
//let payload = {isAuthenticated: isAuthenticated, user: this.user};
//this.$store.dispatch("security/onRefresh", payload);
if (isAuthenticated) {
/*if (isAuthenticated) {
this.linksUser.unshift({icon: 'mdi-account', url: '/account/home', text: this.currentUser.username});
}
}*/
},
computed: {
...mapGetters({

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'UserLayout'
}
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'UserGroupLayout'
}
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'UserRelUserLayout'
}
</script>

@ -4,6 +4,9 @@ import i18n from './i18n';
import router from './router';
import store from './store';
import axios from 'axios'
// Services
import courseCategoryService from './services/coursecategory';
import documentsService from './services/documents';
import courseService from './services/course';
@ -11,6 +14,11 @@ import personalFileService from './services/personalfile';
import resourceLinkService from './services/resourcelink';
import resourceNodeService from './services/resourcenode';
import messageService from './services/message';
import userService from './services/user';
import userGroupService from './services/usergroup';
import userRelUserService from './services/userreluser';
import makeCrudModule from './store/modules/crud';
//import vuetify from './plugins/vuetify' // path to vuetify export
@ -79,6 +87,29 @@ store.registerModule(
})
);
store.registerModule(
'userreluser',
makeCrudModule({
service: userRelUserService
})
);
store.registerModule(
'user',
makeCrudModule({
service: userService
})
);
store.registerModule(
'usergroup',
makeCrudModule({
service: userGroupService
})
);
// Vuetify.
import '@mdi/font/css/materialdesignicons.css';
import 'vuetify/lib/styles/main.sass';

@ -3,12 +3,13 @@ import courseRoutes from './course';
import accountRoutes from './account';
import personalFileRoutes from './personalfile';
import messageRoutes from './message';
import userRoutes from './user';
import userGroupRoutes from './usergroup';
import userRelUserRoutes from './userreluser';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
import store from '../store';
//import Legacy from '../views/Legacy.vue';
//import Home from '../views/Home.vue';
import MyCourseList from '../views/user/courses/List.vue';
import MySessionList from '../views/user/sessions/List.vue';
@ -91,7 +92,10 @@ const router = createRouter({
documents,
accountRoutes,
personalFileRoutes,
messageRoutes
messageRoutes,
userRoutes,
userGroupRoutes,
userRelUserRoutes,
]
});

@ -0,0 +1,15 @@
export default {
path: '/resources/users',
meta: { requiresAuth: true },
name: 'users',
component: () => import('../components/user/Layout.vue'),
//redirect: { name: 'UserGroupList' },
children: [
{
name: 'UserGroupShow',
//path: ':id',
path: 'show',
component: () => import('../views/usergroup/Show.vue')
}
]
};

@ -0,0 +1,20 @@
export default {
path: '/resources/usergroups',
meta: { requiresAuth: true },
name: 'usergroups',
component: () => import('../components/usergroup/Layout.vue'),
redirect: { name: 'UserGroupList' },
children: [
{
name: 'UserGroupList',
path: '',
component: () => import('../views/usergroup/List.vue')
},
{
name: 'UserGroupShow',
//path: ':id',
path: 'show',
component: () => import('../views/usergroup/Show.vue')
}
]
};

@ -0,0 +1,20 @@
export default {
path: '/resources/friends',
meta: { requiresAuth: true },
name: 'friends',
component: () => import('../components/userreluser/Layout.vue'),
redirect: { name: 'UserGroupList' },
children: [
{
name: 'UserRelUserList',
path: '',
component: () => import('../views/userreluser/List.vue')
},
{
name: 'UserRelUserAdd',
//path: ':id',
path: 'add',
component: () => import('../views/userreluser/Add.vue')
}
]
};

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('user');

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('usergroups');

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('user_rel_users');

@ -1,18 +1,21 @@
<template>
<div class="card">
<v-card>
<q-avatar size="64px">
<img :src="user.illustrationUrl + '?w=80&h=80&fit=crop'" />
</q-avatar>
<h6>{{ user.firstname }} {{ user.lastname }} </h6>
<q-tabs align="left" dense inline-label no-caps>
<q-route-tab to="/resources/messages" label="Inbox" />
<q-route-tab to="/courses" label="Posts" />
<q-route-tab to="/courses" label="Friends" />
<q-route-tab to="/" label="Posts" />
<q-route-tab to="/resources/friends" label="My friends" />
<q-route-tab to="/resources/personal_files" label="My files" />
</q-tabs>
<a href="/account/edit" class="btn btn-primary">
Edit profile
</a>
</div>
</v-card>
</template>
<script>

@ -353,8 +353,8 @@ export default {
return {
goToInbox,
goToSent,
goToTag,
goToUnread,
goToTag,
tags,
filters,
title,

@ -84,8 +84,6 @@ export default {
const users = ref([]);
const isLoadingSelect = ref(false);
function asyncFind (query) {
if (query.toString().length < 3) {
return;

@ -0,0 +1,563 @@
<template>
<Toolbar
>
<template v-slot:right>
<v-btn
tile
icon
@click="composeHandler">
<v-icon icon="mdi-email-plus-outline" />
</v-btn>
<v-btn
tile
icon
:loading="isLoading"
@click="reloadHandler">
<v-icon icon="mdi-refresh" />
</v-btn>
<v-btn
tile
icon
@click="confirmDeleteMultiple"
:class="[ !selectedItems || !selectedItems.length ? 'hidden': '']"
>
<v-icon icon="mdi-delete" />
</v-btn>
<!-- :disabled="!selectedItems || !selectedItems.length"-->
<v-btn
icon
tile
@click="markAsUnReadMultiple"
:class="[ !selectedItems || !selectedItems.length ? 'hidden': '']"
>
<v-icon icon="mdi-email" />
</v-btn>
<v-btn
tile
icon
:class="[ !selectedItems || !selectedItems.length ? 'hidden': '']"
>
<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
class="p-datatable-sm"
:value="items"
v-model:selection="selectedItems"
dataKey="id"
v-model:filters="filters"
filterDisplay="menu"
sortBy="sendDate"
sortOrder="asc"
:lazy="true"
:paginator="true"
:rows="10"
:totalRecords="totalItems"
:loading="isLoading"
@page="onPage($event)"
@sort="sortingChanged($event)"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 20, 50]"
responsiveLayout="scroll"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
:globalFilterFields="['title', 'sendDate']">
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="userSender" :header="$t('From')" :sortable="false">
<template #body="slotProps">
<q-avatar size="40px">
<img :src="slotProps.data.userSender.illustrationUrl + '?w=80&h=80&fit=crop'" />
</q-avatar>
<a
v-if="slotProps.data"
@click="showHandler(slotProps.data)"
class="cursor-pointer"
:class="[ true === slotProps.data.read ? 'font-normal': 'font-semibold']"
>
{{ slotProps.data.userSender.username }}
</a>
</template>
</Column>
<Column field="title" :header="$t('Title')" :sortable="false">
<template #body="slotProps">
<a
v-if="slotProps.data"
@click="showHandler(slotProps.data)"
class="cursor-pointer"
v-bind:class="{ 'font-semibold': !slotProps.data.read }"
>
{{ slotProps.data.title }}
</a>
<div class="flex flex-row">
<v-chip v-for="tag in slotProps.data.tags" >
{{ tag.tag }}
</v-chip>
</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 field="sendDate" :header="$t('Send date')" :sortable="true">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.sendDate).toRelative() }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<v-btn
tile
icon
@click="confirmDeleteItem(slotProps.data)" >
<v-icon icon="mdi-delete" />
</v-btn>
</div>
</template>
</Column>
</DataTable>
</div>
</div>
<!-- Dialogs-->
<Dialog v-model:visible="itemDialog" :style="{width: '450px'}" :header="$t('New folder')" :modal="true" class="p-fluid">
<div class="p-field">
<label for="name">{{ $t('Name') }}</label>
<InputText
autocomplete="off"
id="title"
v-model.trim="item.title"
required="true"
autofocus
:class="{'p-invalid': submitted && !item.title}"
/>
<small class="p-error" v-if="submitted && !item.title">$t('Title is required')</small>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="hideDialog"/>
<Button label="Save" icon="pi pi-check" class="p-button-text" @click="saveItem" />
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<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 label="No" icon="pi pi-times" class="p-button-text" @click="deleteItemDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteItemButton" />
</template>
</Dialog>
<Dialog v-model:visible="deleteMultipleDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<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 label="No" icon="pi pi-times" class="p-button-text" @click="deleteMultipleDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteMultipleItems" />
</template>
</Dialog>
</template>
<script>
import { mapActions, mapGetters } 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 ResourceFileIcon from '../../components/documents/ResourceFileIcon.vue';
import ResourceFileLink from '../../components/documents/ResourceFileLink.vue';
import {useRoute, useRouter} from 'vue-router'
import DataFilter from '../../components/DataFilter';
import DocumentsFilterForm from '../../components/documents/Filter';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import moment from "moment";
import toInteger from "lodash/toInteger";
import useState from "../../hooks/useState";
import axios from "axios";
import {ENTRYPOINT} from "../../config/entrypoint";
export default {
name: 'UserGroupList',
servicePrefix: 'usergroups',
components: {
Toolbar,
ActionCell,
ResourceFileIcon,
ResourceFileLink,
DocumentsFilterForm,
DataFilter
},
mixins: [ListMixin],
setup() {
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: 2,
userSender: user.id
}
// inbox
filters.value = {
msgType: 1,
userReceiver: user.id
};
// Get user tags.
axios.get(ENTRYPOINT + 'message_tags', {
params: {
user: user['@id']
}
}).then(response => {
let data = response.data;
tags.value = data['hydra:member'];
});
function goToInbox() {
title.value = 'Inbox';
filters.value = {
msgType: 1,
userReceiver: user.id,
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
}
function goToUnread() {
title.value = 'Unread';
filters.value = {
msgType: 1,
userReceiver: user.id,
read: false
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
}
function goToSent() {
title.value = 'Sent';
filters.value = {
msgType: 2,
userSender: user.id
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
}
function goToTag(tag) {
title.value = tag.tag;
filters.value = {
msgType: 1,
userReceiver: user.id,
tags: [tag]
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
}
return {
goToInbox,
goToSent,
goToTag,
goToUnread,
tags,
filters,
title,
}
},
data() {
return {
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Sender'), field: 'userSender', name: 'userSender', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'sendDate', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [10, 20, 50, this.$i18n.t('All')],
selected: [],
isBusy: false,
options: {
sortBy: 'sendDate',
sortDesc: 'asc',
},
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
submitted: false,
};
},
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: 2, // 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);
}*/
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',
}),
}
};
</script>

@ -0,0 +1,242 @@
<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"
tile
icon
@click="reply"
>
<v-icon icon="mdi-reply" />
</v-btn>
</template>
</Toolbar>
<VueMultiselect
placeholder="Tags"
v-model="item.tags"
:loading="isLoadingSelect"
tag-placeholder="Add this as new tag"
:options="tags"
:multiple="true"
:searchable="true"
:internal-search="false"
@search-change="asyncFind"
@select="addTagToMessage"
@remove="removeTagFromMessage"
:taggable="true"
@tag="addTag"
label="tag"
track-by="id"
/>
<p class="text-lg">
From:
<q-avatar size="32px">
<img :src="item['userSender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" />
<!-- <q-icon name="person" ></q-icon>-->
</q-avatar>
{{ item['userSender']['username'] }}
</p>
<p class="text-lg">
{{ $luxonDateTime.fromISO(item['sendDate']).toRelative() }}
</p>
<p class="text-lg">
<h3>{{ item.title }}</h3>
</p>
<div class="flex flex-row">
<div class="w-full">
<p v-html="item.content" />
</div>
</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 {computed, 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";
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();
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
};
},
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>

@ -0,0 +1,190 @@
<template>
<Toolbar >
<template v-slot:right>
<v-btn
tile
icon
:loading="isLoading"
@click="reloadHandler">
<v-icon icon="mdi-account-plus-outline" />
</v-btn>
</template>
</Toolbar>
<div class="flex flex-row pt-2">
<div class="w-full">
<div class="text-h4 q-mb-md">Search</div>
<VueMultiselect
placeholder="Add"
:loading="isLoadingSelect"
:options="users"
:multiple="true"
:searchable="true"
:internal-search="false"
@search-change="asyncFind"
@select="addFriend"
limit-text="3"
limit="3"
label="username"
track-by="id"
/>
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import Toolbar from '../../components/Toolbar.vue';
import VueMultiselect from 'vue-multiselect'
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import axios from "axios";
import {ENTRYPOINT} from "../../config/entrypoint";
import useVuelidate from "@vuelidate/core";
export default {
name: 'UserRelUserAdd',
servicePrefix: 'userreluser',
components: {
Toolbar,
VueMultiselect
},
mixins: [ListMixin],
setup() {
const users = ref([]);
const isLoadingSelect = ref(false);
const store = useStore();
const user = store.getters["security/getUser"];
function asyncFind (query) {
if (query.toString().length < 3) {
return;
}
isLoadingSelect.value = true;
axios.get(ENTRYPOINT + 'users', {
params: {
username: query
}
}).then(response => {
isLoadingSelect.value = false;
let data = response.data;
users.value = data['hydra:member'];
}).catch(function (error) {
isLoadingSelect.value = false;
console.log(error);
});
}
function addFriend(friend) {
axios.post(ENTRYPOINT + 'user_rel_users', {
user: user['@id'],
friend: friend['@id'],
relationType: 10,
}).then(response => {
console.log(response);
isLoadingSelect.value = false;
}).catch(function (error) {
isLoadingSelect.value = false;
console.log(error);
});
}
return {v$: useVuelidate(), users, asyncFind, addFriend, isLoadingSelect};
},
data() {
return {
selected: [],
isBusy: false,
options: {
sortBy: 'sendDate',
sortDesc: 'asc',
},
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
submitted: false,
};
},
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: {
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() {
this.deleteMultipleAction(this.selected);
this.onRequest({
pagination: this.pagination,
});
},
//...actions,
// From ListMixin
...mapActions('userreluser', {
getPage: 'fetchAll',
create: 'create',
update: 'update',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),
...mapActions('resourcenode', {
findResourceNode: 'findResourceNode',
}),
}
};
</script>

@ -0,0 +1,381 @@
<template>
<Toolbar >
<template v-slot:right>
<v-btn
tile
icon
:loading="isLoading"
:to="'/resources/friends/add'"
>
<v-icon icon="mdi-account-plus-outline" />
</v-btn>
<v-btn
tile
icon
:loading="isLoading"
@click="reloadHandler"
>
<v-icon icon="mdi-refresh" />
</v-btn>
<v-btn
tile
icon
@click="confirmDeleteMultiple"
:class="[ !selectedItems || !selectedItems.length ? 'hidden': '']"
>
<v-icon icon="mdi-delete" />
</v-btn>
<!-- :disabled="!selectedItems || !selectedItems.length"-->
</template>
</Toolbar>
<div class="flex flex-row pt-2">
<div class="w-full">
<div class="text-h4 q-mb-md">Friends</div>
<DataTable
class="p-datatable-sm"
:value="items"
v-model:selection="selectedItems"
dataKey="id"
v-model:filters="filters"
filterDisplay="menu"
sortBy="sendDate"
sortOrder="asc"
:lazy="true"
:paginator="false"
:totalRecords="totalItems"
:loading="isLoading"
@page="onPage($event)"
@sort="sortingChanged($event)"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 20, 50]"
responsiveLayout="scroll"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
>
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="userSender" :header="$t('User')" :sortable="false">
<template #body="slotProps">
<q-avatar size="40px">
<img :src="slotProps.data.friend.illustrationUrl + '?w=80&h=80&fit=crop'" />
</q-avatar>
{{ slotProps.data.friend.username }}
</template>
</Column>
<Column field="createdAt" :header="$t('Sent date')" :sortable="true">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.createdAt).toRelative() }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<!-- class="flex flex-row gap-2"-->
<v-icon v-if="slotProps.data.relationType == 3" icon="mdi-check" />
<v-btn
v-if="slotProps.data.relationType == 10"
tile
icon
@click="addFriend(slotProps.data)" >
<v-icon icon="mdi-plus" />
</v-btn>
<v-btn
tile
icon
@click="confirmDeleteItem(slotProps.data)" >
<v-icon icon="mdi-delete" />
</v-btn>
</template>
</Column>
</DataTable>
</div>
</div>
<Dialog v-model:visible="itemDialog" :style="{width: '450px'}" :header="$t('New folder')" :modal="true" class="p-fluid">
<div class="p-field">
<label for="name">{{ $t('Name') }}</label>
<InputText
autocomplete="off"
id="title"
v-model.trim="item.title"
required="true"
autofocus
:class="{'p-invalid': submitted && !item.title}"
/>
<small class="p-error" v-if="submitted && !item.title">$t('Title is required')</small>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="hideDialog"/>
<Button label="Save" icon="pi pi-check" class="p-button-text" @click="saveItem" />
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<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 label="No" icon="pi pi-times" class="p-button-text" @click="deleteItemDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteItemButton" />
</template>
</Dialog>
<Dialog v-model:visible="deleteMultipleDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<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 label="No" icon="pi pi-times" class="p-button-text" @click="deleteMultipleDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteMultipleItems" />
</template>
</Dialog>
</template>
<script>
import { mapActions, mapGetters } 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 ResourceFileIcon from '../../components/documents/ResourceFileIcon.vue';
import ResourceFileLink from '../../components/documents/ResourceFileLink.vue';
import DataFilter from '../../components/DataFilter';
import DocumentsFilterForm from '../../components/documents/Filter';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import axios from "axios";
import {ENTRYPOINT} from "../../config/entrypoint";
export default {
name: 'UserRelUserList',
servicePrefix: 'userreluser',
components: {
Toolbar,
},
mixins: [ListMixin],
setup() {
const store = useStore();
const user = store.getters["security/getUser"];
const isLoadingSelect = ref(false);
function addFriend(friend) {
axios.put(friend['@id'], {
relationType: 3,
}).then(response => {
console.log(response);
isLoadingSelect.value = false;
}).catch(function (error) {
isLoadingSelect.value = false;
console.log(error);
});
}
return {
addFriend,
}
},
data() {
return {
columns: [
{ label: this.$i18n.t('User'), field: 'friend.username', name: 'friend', sortable: true},
{ label: this.$i18n.t('Sent'), field: 'createdAt', name: 'createdAt', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [10, 20, 50, this.$i18n.t('All')],
selected: [],
isBusy: false,
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
submitted: false,
};
},
mounted() {
console.log('mounted');
this.filters = {
friend: this.currentUser.id
};
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('userreluser', {
items: 'list',
}),
//...getters
// From ListMixin
...mapFields('userreluser', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
}),
},
methods: {
// 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: 2, // 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() {
this.deleteMultipleAction(this.selected);
this.onRequest({
pagination: this.pagination,
});
},
//...actions,
// From ListMixin
...mapActions('userreluser', {
getPage: 'fetchAll',
create: 'create',
update: 'update',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),
...mapActions('resourcenode', {
findResourceNode: 'findResourceNode',
}),
}
};
</script>

@ -6,7 +6,9 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Chamilo\CoreBundle\Traits\UserTrait;
use Doctrine\ORM\Mapping as ORM;
@ -53,6 +55,11 @@ use Symfony\Component\Validator\Constraints as Assert;
'groups' => ['user_rel_user:read', 'timestampable_created:read'],
],
)]
#[ApiFilter(SearchFilter::class, properties: [
'user' => 'exact',
'friend' => 'exact',
'relationType' => 'exact',
])]
class UserRelUser
{
use UserTrait;

Loading…
Cancel
Save