Add "my files" using resources #3792

pull/3904/head
Julio Montoya 4 years ago
parent b090544ba6
commit fcabbcc1ef
  1. 16
      assets/vue/App.vue
  2. 75
      assets/vue/components/personalfile/Form.vue
  3. 126
      assets/vue/components/personalfile/FormUpload.vue
  4. 9
      assets/vue/components/personalfile/Layout.vue
  5. 68
      assets/vue/components/personalfile/ResourceLinkForm.vue
  6. 25
      assets/vue/main.js
  7. 7
      assets/vue/mixins/ListMixin.js
  8. 4
      assets/vue/router/index.js
  9. 42
      assets/vue/router/personalfile.js
  10. 3
      assets/vue/services/personalfile.js
  11. 2
      assets/vue/utils/fetch.js
  12. 2
      assets/vue/views/account/Home.vue
  13. 63
      assets/vue/views/personalfile/Create.vue
  14. 43
      assets/vue/views/personalfile/Home.vue
  15. 363
      assets/vue/views/personalfile/List.vue
  16. 193
      assets/vue/views/personalfile/Show.vue
  17. 68
      assets/vue/views/personalfile/Update.vue
  18. 138
      assets/vue/views/personalfile/Upload.vue
  19. 8
      src/CoreBundle/Controller/Api/BaseResourceFileAction.php
  20. 23
      src/CoreBundle/Controller/Api/CreatePersonalFileAction.php
  21. 2
      src/CoreBundle/Controller/Api/UpdateDocumentFileAction.php
  22. 25
      src/CoreBundle/Controller/Api/UpdatePersonalFileAction.php
  23. 17
      src/CoreBundle/Controller/ResourceController.php
  24. 12
      src/CoreBundle/Entity/PersonalFile.php
  25. 5
      src/CoreBundle/Entity/ResourceNode.php
  26. 2
      src/CourseBundle/Entity/CDocument.php

@ -101,11 +101,13 @@ export default {
}
}*/
// This code below will handle the legacy content to be loaded.
let url = window.location.href;
var n = url.indexOf("main/");
if (n > 0) {
if (this.firstTime) {
console.log('firstTime: 1.');
console.log('App.vue: firstTime: 1.');
let content = document.querySelector("#sectionMainContent");
if (content) {
console.log('legacyContent updated');
@ -145,7 +147,7 @@ export default {
}
} else {
if (this.firstTime) {
console.log('firstTime 2.');
console.log('App.vue: firstTime 2');
let content = document.querySelector("#sectionMainContent");
if (content) {
console.log('legacyContent updated');
@ -172,15 +174,15 @@ export default {
},
created() {
console.log('APP created');
console.log('App.vue created');
this.legacyContent = '';
console.log('updated empty created');
console.log('App.vue legacyContent cleaned');
let app = document.getElementById('app');
let isAuthenticated = false;
if (!isEmpty(window.user)) {
console.log('is logged in as ' + window.user.username);
console.log('userAvatar ' + window.userAvatar);
console.log('APP.vue: is logged in as ' + window.user.username);
console.log('APP.vue: userAvatar ' + window.userAvatar);
this.user = window.user;
this.userAvatar = window.userAvatar;
isAuthenticated = true;
@ -222,7 +224,7 @@ export default {
});
},
mounted() {
console.log('app.vue mounted');
console.log('App.vue mounted');
this.firstTime = true;
},
mixins: [NotificationMixin],

@ -0,0 +1,75 @@
<template>
<q-form>
<q-input
id="item_title"
v-model="item.title"
:placeholder="$t('Title')"
:error="v$.item.title.$error"
@input="v$.item.title.$touch()"
@blur="v$.item.title.$touch()"
:error-message="titleErrors"
/>
<slot></slot>
</q-form>
</template>
<script>
import has from 'lodash/has';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
export default {
name: 'PersonalFileForm',
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
},
},
data() {
return {
title: null,
parentResourceNodeId: null,
};
},
computed: {
item() {
return this.initialValues || this.values;
},
titleErrors() {
const errors = [];
if (!this.v$.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
if (this.v$.item.title.required) {
return this.$t('Field is required')
}
return errors;
},
violations() {
return this.errors || {};
}
},
validations: {
item: {
title: {
required,
},
parentResourceNodeId: {
},
}
}
};
</script>

@ -0,0 +1,126 @@
<template>
<q-form>
<div class="input-group mb-3">
<div class="custom-file">
<input
id="file_upload"
type="file"
class="custom-file-input"
ref="fileList"
multiple
placeholder="File upload"
@change="selectFile"
/>
<label
class="custom-file-label"
for="file_upload"
aria-describedby="File upload">
Choose file
</label>
</div>
</div>
<div class="field">
<div
v-for="(file, index) in files"
:key="index"
:class="{ error : file.invalidMessage}"
>
<div>
{{ file.name }}
<span v-if="file.invalidMessage">
- {{ file.invalidMessage }}
</span>
<span>
<a @click.prevent="files.splice(index, 1)"
class="delete"
>
<FontAwesomeIcon icon="trash" />
</a>
</span>
</div>
</div>
</div>
</q-form>
</template>
<script>
import has from 'lodash/has';
import map from 'lodash/map';
import useVuelidate from '@vuelidate/core';
export default {
name: 'PersonalFileFormUpload',
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Array,
required: true
},
parentResourceNodeId: {
type: Number
},
resourceLinkList: {
type: String,
},
errors: {
type: Object,
default: () => {}
},
processFiles: {
type: Function,
required: false
},
},
data() {
return {
fileList:[],
files: [],
};
},
computed: {
titleErrors() {
const errors = [];
if (!this.$v.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
!this.$v.item.title.required && errors.push(this.$t('Field is required'));
return errors;
},
violations() {
return this.errors || {};
}
},
methods: {
selectFile() {
const files = this.$refs.fileList.files;
this.files = [
...this.files,
...map(files, file => ({
name: file.name,
size: file.size,
type: file.type,
filetype: 'file',
parentResourceNodeId: this.parentResourceNodeId,
resourceLinkList: this.resourceLinkList,
uploadFile: file,
invalidMessage: this.validate(file),
}))
]
},
validate(file) {
if (file) {
return '';
}
return 'error';
}
},
validations: {
files: {}
}
};
</script>

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

@ -0,0 +1,68 @@
<template>
<div v-if="item && item['resourceLinkListFromEntity']">
<ul>
<li
v-for="link in item['resourceLinkListFromEntity']"
>
<div v-if="link['course']">
{{ $t('Course') }}: {{ link.course.resourceNode.title }}
</div>
<div v-if="link['session']">
{{ $t('Session') }}: {{ link.session.name }}
</div>
<div v-if="link['group']">
{{ $t('Group') }}: {{ link.session.resourceNode.title }}
</div>
<q-separator />
<q-select
filled
v-model="link.visibility"
:options="visibilityList"
label="Status"
emit-value
map-options
/>
</li>
</ul>
</div>
</template>
<script>
import has from 'lodash/has';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
export default {
name: 'ResourceLinkForm',
setup () {
const visibilityList = [
{value: 2, label: 'Published'},
{value: 0, label: 'Draft'},
];
return {v$: useVuelidate(), visibilityList};
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
},
},
computed: {
item() {
return this.initialValues || this.values;
},
},
};
</script>

@ -7,6 +7,7 @@ import axios from 'axios'
import courseCategoryService from './services/coursecategory';
import documentsService from './services/documents';
import courseService from './services/course';
import personalFileService from './services/personalfile';
import resourceLinkService from './services/resourcelink';
import resourceNodeService from './services/resourcenode';
import makeCrudModule from './store/modules/crud';
@ -32,6 +33,7 @@ const toastOptions = {
import VueFlatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
// @todo move in a file:
store.registerModule(
'course',
makeCrudModule({
@ -40,30 +42,37 @@ store.registerModule(
);
store.registerModule(
'resourcelink',
'coursecategory',
makeCrudModule({
service: resourceLinkService
service: courseCategoryService
})
);
store.registerModule(
'resourcenode',
'documents',
makeCrudModule({
service: resourceNodeService
service: documentsService
})
);
store.registerModule(
'coursecategory',
'personalfile',
makeCrudModule({
service: courseCategoryService
service: personalFileService
})
);
store.registerModule(
'documents',
'resourcelink',
makeCrudModule({
service: documentsService
service: resourceLinkService
})
);
store.registerModule(
'resourcenode',
makeCrudModule({
service: resourceNodeService
})
);

@ -119,7 +119,7 @@ export default {
});
},
onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc, totalItems } = {}) {
console.log('onUpdateOptions');
console.log('ListMixin.js: onUpdateOptions');
let params = {
...this.filters
}
@ -247,8 +247,7 @@ export default {
this.resetList = true;
let resourceId = item['resourceNode']['id'];
this.$route.params.node = resourceId;
//this.onUpdateOptions(this.options);
//folderParams['node'] = resourceId;
this.$router.push({
name: `${this.$options.servicePrefix}List`,
@ -269,7 +268,7 @@ export default {
let folderParams = this.$route.query;
folderParams['id'] = item['@id'];
if ('folder' === item.filetype) {
if ('folder' === item.filetype || isEmpty(item.filetype)) {
this.$router.push({
name: `${this.$options.servicePrefix}Update`,
params: { id: item['@id'] },

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router';
import courseRoutes from './course';
import accountRoutes from './account';
import personalFileRoutes from './personalfile';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
@ -87,7 +88,8 @@ const router = createRouter({
courseRoutes,
//courseCategoryRoutes,
documents,
accountRoutes
accountRoutes,
personalFileRoutes
]
});

@ -0,0 +1,42 @@
export default {
path: '/resources/personal_files',
meta: { requiresAuth: true },
name: 'personal_files',
component: () => import('../views/personalfile/Home.vue'),
children: [
{
name: 'personal_files',
path: ':node/',
component: () => import('../components/personalfile/Layout.vue'),
redirect: { name: 'PersonalFileList' },
children: [
{
name: 'PersonalFileList',
path: '',
component: () => import('../views/personalfile/List.vue')
},
{
name: 'PersonalFileCreate',
path: 'new',
component: () => import('../views/personalfile/Create.vue')
},
{
name: 'PersonalFileUploadFile',
path: 'upload',
component: () => import('../views/personalfile/Upload.vue')
},
{
name: 'PersonalFileUpdate',
//path: ':id/edit',
path: 'edit_file',
component: () => import('../views/personalfile/Update.vue')
},
{
name: 'PersonalFileShow',
path: 'show',
component: () => import('../views/personalfile/Show.vue')
}
]
},
]
};

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

@ -124,6 +124,8 @@ export default function(id, options = {}) {
});
}*/
console.log('ready to fetch');
return global.fetch(new URL(id, entryPoint), options).then(response => {
console.log(response, 'global.fetch');

@ -6,7 +6,7 @@
<q-route-tab to="/courses" label="Posts" />
<q-route-tab to="/courses" label="Friends" />
<q-route-tab to="/" label="Posts" />
<q-route-tab to="/sessions" label="My files" />
<q-route-tab to="/resources/personal_files" label="My files" />
</q-tabs>
<a href="/account/edit" class="btn btn-primary">

@ -0,0 +1,63 @@
<template>
<div>
<Toolbar
:handle-submit="onSendForm"
:handle-reset="resetForm"
/>
<DocumentsForm
ref="createForm"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/personalfile/Form.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import CreateMixin from '../../mixins/CreateMixin';
const servicePrefix = 'PersonalFile';
const { mapFields } = createHelpers({
getterType: 'personal_file/getField',
mutationType: 'personal_file/updateField'
});
export default {
name: 'PersonalFileCreate',
servicePrefix,
components: {
Loading,
Toolbar,
DocumentsForm
},
mixins: [CreateMixin],
data() {
return {
item: {},
type: 'folder'
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
created() {
this.item.parentResourceNodeId = this.$route.params.node;
this.item.resourceLinkList = JSON.stringify([{
gid: this.$route.query.gid,
sid: this.$route.query.sid,
c_id: this.$route.query.cid,
visibility: 2, // visible by default
}]);
},
methods: {
...mapActions('personalfile', ['create', 'reset'])
}
};
</script>

@ -0,0 +1,43 @@
<template>
<router-view></router-view>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import Loading from '../../components/Loading.vue';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar.vue';
import isEmpty from "lodash/isEmpty";
const servicePrefix = 'PersonalFile';
export default {
name: 'PersonalFileHome',
servicePrefix,
components: {
Loading,
Toolbar
},
created() {
console.log('CREATED HOME');
let resourceNodeId = this.currentUser.resourceNode['id'];
console.log(resourceNodeId);
this.$router
.push({ name: `${this.$options.servicePrefix}List`, params: { node: resourceNodeId },})
.catch(() => {});
},
computed: {
// From crud.js list function
...mapGetters('resourcenode', {
resourceNode: 'getResourceNode'
}),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'currentUser': 'security/getUser',
}),
}
};
</script>

@ -0,0 +1,363 @@
<template>
<div v-if="isAuthenticated" class="q-card">
<div class="p-4 flex flex-row gap-1 mb-2">
<div class="flex flex-row gap-2" >
<Button label="New folder" icon="fa fa-folder-plus" class="btn btn-primary" @click="openNew" />
<Button label="Upload" icon="fa fa-file-upload" class="btn btn-primary" @click="uploadDocumentHandler()" />
<Button label="Delete" icon="pi pi-trash" class="btn btn-danger " @click="confirmDeleteMultiple" :disabled="!selectedItems || !selectedItems.length" />
</div>
</div>
</div>
<DataTable
class="p-datatable-sm"
:value="items"
v-model:selection="selectedItems"
dataKey="iid"
v-model:filters="filters"
filterDisplay="menu"
: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="['resourceNode.title', 'resourceNode.updatedAt']">
<span v-if="isCurrentTeacher">
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
</span>
<Column field="resourceNode.title" :header="$t('Title')" :sortable="true">
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<ResourceFileLink :resource="slotProps.data" />
</div>
<div v-else>
<a
v-if="slotProps.data"
@click="handleClick(slotProps.data)"
class="cursor-pointer " >
<FontAwesomeIcon
icon="folder"
size="lg"
/>
{{ slotProps.data.resourceNode.title }}
</a>
</div>
</template>
</Column>
<Column field="resourceNode.resourceFile.size" :header="$t('Size')" :sortable="true">
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile ? $filters.prettyBytes(slotProps.data.resourceNode.resourceFile.size) : ''
}}
</template>
</Column>
<Column field="resourceNode.updatedAt" :header="$t('Modified')" :sortable="true">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.resourceNode.updatedAt).toRelative() }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button icon="fa fa-info-circle" class="btn btn-primary " @click="showHandler(slotProps.data)" />
<Button v-if="isAuthenticated" icon="pi pi-pencil" class="btn btn-primary p-mr-2" @click="editHandler(slotProps.data)" />
<Button v-if="isAuthenticated" icon="pi pi-trash" class="btn btn-danger" @click="confirmDeleteItem(slotProps.data)" />
</div>
</template>
</Column>
<!-- <template #paginatorLeft>-->
<!-- <Button type="button" icon="pi pi-refresh" class="p-button-text" />-->
<!-- </template>-->
<!-- <template #paginatorRight>-->
<!-- <Button type="button" icon="pi pi-cloud" class="p-button-text" />-->
<!-- </template>-->
</DataTable>
<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 } from 'vue-router'
import DataFilter from '../../components/DataFilter';
//import DocumentsFilterForm from '../../components/personalfile/Filter';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import moment from "moment";
export default {
name: 'PersonalFileList',
servicePrefix: 'PersonalFile',
components: {
//8Toolbar,
ActionCell,
ResourceFileIcon,
ResourceFileLink,
//DocumentsFilterForm,
DataFilter
},
mixins: [ListMixin],
data() {
return {
sortBy: 'title',
sortDesc: false,
columnsQua: [
{align: 'left', name: 'resourceNode.title', label: this.$i18n.t('Title'), field: 'resourceNode.title', sortable: true},
{align: 'left', name: 'resourceNode.updatedAt', label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', sortable: true},
{name: 'resourceNode.resourceFile.size', label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', sortable: true},
{name: 'action', label: this.$i18n.t('Actions'), field: 'action', sortable: false}
],
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', name: 'size', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [10, 20, 50, this.$i18n.t('All')],
selected: [],
isBusy: false,
options: [],
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
filters: {},
submitted: false,
};
},
created() {
console.log('CREATED');
let resourceNodeId = this.currentUser.resourceNode['id'];
if (isEmpty(this.$route.params.node)) {
this.$route.params.node = resourceNodeId;
}
//this.item.parentResourceNodeId = this.$route.params.node;
this.filters['resourceNode.parent'] = resourceNodeId;
},
mounted() {
const route = useRoute()
/*let nodeId = route.params['node'];
if (!isEmpty(nodeId)) {
this.findResourceNode('/api/resource_nodes/' + nodeId);
}*/
console.log(this.options);
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('personalfile', {
items: 'list',
}),
//...getters
// From ListMixin
...mapFields('personalfile', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
}),
},
methods: {
// prime
onPage(event) {
console.log(event);
console.log(event.page);
console.log(event.sortField);
console.log(event.sortOrder);
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 {
let resourceNodeId = this.currentUser.resourceNode['id'];
if (!isEmpty(this.$route.params.node)) {
resourceNodeId = this.$route.params.node;
}
this.item.parentResourceNodeId = resourceNodeId;
this.item.resourceLinkList = JSON.stringify([{
gid: 0,
sid: 0,
c_id: 0,
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;
},
deleteMultipleItems() {
console.log('deleteMultipleItems');
console.log(this.selectedItems);
this.deleteMultipleAction(this.selectedItems);
this.onRequest({
pagination: this.pagination,
});
this.deleteMultipleDialog = false;
this.selectedItems = null;
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
},
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,
});
console.log('end -- deleteSelected');
},
//...actions,
// From ListMixin
...mapActions('personalfile', {
getPage: 'fetchAll',
create: 'create',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),
...mapActions('resourcenode', {
findResourceNode: 'findResourceNode',
}),
}
};
</script>

@ -0,0 +1,193 @@
<template>
<div>
<Toolbar
v-if="item && isCurrentTeacher"
:handle-edit="editHandler"
:handle-delete="del"
>
<template slot="left">
<!-- <v-toolbar-title v-if="item">-->
<!-- {{-->
<!-- `${$options.servicePrefix} ${item['@id']}`-->
<!-- }}-->
<!-- </v-toolbar-title>-->
</template>
</Toolbar>
<p class="text-lg" v-if="item">
{{ item['title'] }}
</p>
<div v-if="item" class="flex flex-row">
<div class="w-1/2">
<div class ="flex justify-center" v-if="item['resourceNode']['resourceFile']">
<div class="w-64">
<q-img
spinner-color="primary"
v-if="item['resourceNode']['resourceFile']['image']"
:src="item['contentUrl'] + '&w=300'"
/>
<span v-else-if="item['resourceNode']['resourceFile']['video']">
<video controls>
<source :src="item['contentUrl']" />
</video>
</span>
<span v-else>
<q-btn
class="btn btn-primary"
:to="item['downloadUrl']"
>
<FontAwesomeIcon icon="file-download" />
{{ $t('Download file') }}
</q-btn>
</span>
</div>
</div>
<div class ="flex justify-center" v-else>
<FontAwesomeIcon
icon="folder"
size="7x"
/>
</div>
</div>
<span class="w-1/2">
<q-markup-table>
<tbody>
<tr>
<td><strong>{{ $t('Author') }}</strong></td>
<td>
{{ item['resourceNode'].creator.username }}
</td>
<td></td>
<td />
</tr>
<tr>
<td><strong>{{ $t('Comment') }}</strong></td>
<td>
{{ item['comment'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('Created at') }}</strong></td>
<td>
{{ item['resourceNode'] ? $luxonDateTime.fromISO(item['resourceNode'].createdAt).toRelative() : ''}}
</td>
<td />
</tr>
<tr>
<td><strong>{{ $t('Updated at') }}</strong></td>
<td>
{{ item['resourceNode'] ? $luxonDateTime.fromISO(item['resourceNode'].updatedAt).toRelative() : ''}}
</td>
<td />
</tr>
<tr v-if="item['resourceNode']['resourceFile']">
<td><strong>{{ $t('File') }}</strong></td>
<td>
<div>
<a
class="btn btn-primary"
:href="item['downloadUrl']"
>
<FontAwesomeIcon icon="file-download" />
{{ $t('Download file') }}
</a>
</div>
</td>
<td />
</tr>
</tbody>
</q-markup-table>
<hr />
<span v-if="item['resourceLinkListFromEntity']">
<h2>{{ $t('Shared') }}</h2>
<span
v-for="link in item['resourceLinkListFromEntity']"
>
<q-markup-table>
<tbody>
<tr>
<td>
{{ $t('Status') }}
</td>
<td>
{{ link.visibilityName }}
</td>
</tr>
<tr v-if="link['course']">
<td>
{{ $t('Course') }}
</td>
<td>
{{ link.course.resourceNode.title }}
</td>
</tr>
<tr v-if="link['session']">
<td>
{{ $t('Session') }}
</td>
<td>
{{ link.session.name }}
</td>
</tr>
<tr v-if="link['group']">
<td>
{{ $t('Group') }}
</td>
<td>
{{ link.group.resourceNode.title }}
</td>
</tr>
</tbody>
</q-markup-table>
</span>
</span>
</span>
</div>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } 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';
const servicePrefix = 'PersonalFile';
export default {
name: 'PersonalFileShow',
components: {
Loading,
Toolbar
},
mixins: [ShowMixin],
computed: {
...mapFields('personalfile', {
isLoading: 'isLoading'
}),
...mapGetters('personalfile', ['find']),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
},
methods: {
...mapActions('personalfile', {
deleteItem: 'del',
reset: 'resetShow',
retrieve: 'loadWithQuery'
}),
},
servicePrefix
};
</script>

@ -0,0 +1,68 @@
<template>
<div v-if="!isLoading && item && isCurrentTeacher">
<!-- :handle-delete="del"-->
<Toolbar
:handle-submit="onSendForm"
:handle-reset="resetForm"
/>
<DocumentsForm
ref="updateForm"
:values="item"
:errors="violations"
>
<ResourceLinkForm
v-if="item && !isLoading"
ref="resourceLinkForm"
:values="item"
/>
</DocumentsForm>
<Loading :visible="isLoading || deleteLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import DocumentsForm from '../../components/personalfile/Form.vue';
import ResourceLinkForm from '../../components/personalfile/ResourceLinkForm.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import UpdateMixin from '../../mixins/UpdateMixin';
const servicePrefix = 'PersonalFile';
export default {
name: 'PersonalFileUpdate',
servicePrefix,
components: {
Loading,
Toolbar,
DocumentsForm,
ResourceLinkForm
},
mixins: [UpdateMixin],
computed: {
...mapFields('personalfile', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('personalfile', ['find']),
...mapGetters({
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
},
methods: {
...mapActions('personalfile', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateReset: 'resetUpdate'
})
}
};
</script>

@ -0,0 +1,138 @@
<template>
<div>
<DocumentsForm
ref="createForm"
:values="files"
:parentResourceNodeId="parentResourceNodeId"
:resourceLinkList="resourceLinkList"
:errors="violations"
:process-files="processFiles"
/>
<Toolbar
:handle-submit="onUploadForm"
/>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import {mapActions, mapGetters} from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/personalfile/FormUpload.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import UploadMixin from '../../mixins/UploadMixin';
import { ref, onMounted } from 'vue'
import isEmpty from 'lodash/isEmpty';
const servicePrefix = 'PersonalFile';
const { mapFields } = createHelpers({
getterType: 'personalfile/getField',
mutationType: 'personalfile/updateField'
});
export default {
name: 'PersonalFileUploadFile',
servicePrefix,
components: {
Loading,
Toolbar,
DocumentsForm
},
setup() {
const createForm = ref(null);
return {
createForm
}
},
mixins: [UploadMixin],
data() {
return {
files : [],
parentResourceNodeId: 0,
resourceLinkList: '',
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations']),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'currentUser': 'security/getUser',
}),
},
created() {
console.log('created');
let nodeId = this.$route.params.node;
if (isEmpty(nodeId)) {
nodeId = this.currentUser.resourceNode['id']
}
console.log(nodeId)
this.parentResourceNodeId = Number(nodeId);
this.resourceLinkList = JSON.stringify([{
gid: this.$route.query.gid,
sid: this.$route.query.sid,
c_id: this.$route.query.cid,
visibility: 2,
}]);
this.files = [];
},
methods: {
async processFiles(files) {
/*this.files = [
...this.files,
...map(files, file => ({
title: file.name,
name: file.name,
size: file.size,
type: file.type,
filetype: 'file',
parentResourceNodeId: this.parentResourceNodeId,
resourceLinkList: this.resourceLinkList,
uploadFile: file,
invalidMessage: this.validate(file),
}))
];*/
return new Promise((resolve) => {
for (let i = 0; i < files.length; i++) {
files[i].title = files[i].name;
files[i].parentResourceNodeId = this.parentResourceNodeId;
files[i].resourceLinkList = this.resourceLinkList;
files[i].uploadFile = files[i];
this.createFile(files[i]);
}
resolve(files);
/*console.log(file);
file.title = file.name;
file.parentResourceNodeId = this.parentResourceNodeId;
file.resourceLinkList = this.resourceLinkList;
file.uploadFile = file;
this.create(file);
resolve(file);*/
/*for (let i = 0; i < this.files.length; i++) {
this.create(this.files[i]);
}
resolve(true);*/
}).then(() => {
this.files = [];
});
},
validate(file) {
if (file) {
return '';
}
return 'error';
},
...mapActions('personalfile', ['uploadMany', 'create', 'createFile'])
}
};
</script>

@ -8,7 +8,7 @@ namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceLink;
use Chamilo\CourseBundle\Entity\CDocument;
use Exception;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class BaseResourceFileAction
{
protected function handleCreateRequest(AbstractResource $resource, Request $request)
protected function handleCreateRequest(AbstractResource $resource, Request $request): void
{
error_log('handleCreateRequest');
$contentData = $request->getContent();
@ -36,13 +36,13 @@ class BaseResourceFileAction
}
if (empty($fileType)) {
throw new \Exception('filetype needed: folder or file');
throw new Exception('filetype needed: folder or file');
}
$nodeId = (int) $request->get('parentResourceNodeId');
if (0 === $nodeId) {
throw new \Exception('parentResourceNodeId int value needed');
throw new Exception('parentResourceNodeId int value needed');
}
$resource->setParentResourceNode($nodeId);

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\PersonalFile;
use Symfony\Component\HttpFoundation\Request;
class CreatePersonalFileAction extends BaseResourceFileAction
{
public function __invoke(Request $request): PersonalFile
{
error_log('CreatePersonalFileAction __invoke');
$document = new PersonalFile();
$this->handleCreateRequest($document, $request);
return $document;
}
}

@ -6,10 +6,8 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\ResourceLink;
use Chamilo\CourseBundle\Entity\CDocument;
use Chamilo\CourseBundle\Repository\CDocumentRepository;
use DateTime;
use Symfony\Component\HttpFoundation\Request;
class UpdateDocumentFileAction extends BaseResourceFileAction

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\PersonalFile;
use Chamilo\CourseBundle\Repository\CDocumentRepository;
use Symfony\Component\HttpFoundation\Request;
class UpdatePersonalFileAction extends BaseResourceFileAction
{
public function __invoke(PersonalFile $document, Request $request, CDocumentRepository $repo): PersonalFile
{
error_log('UpdatePersonalFileAction __invoke');
$this->handleUpdateRequest($document, $repo, $request);
error_log('Finish update resource node file action');
return $document;
}
}

@ -51,7 +51,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
private string $fileContentName = 'file_content';
/**
* @deprecated in favor of vue CRUD methods
* @deprecated Use Vue
*
* @Route("/{tool}/{type}", name="chamilo_core_resource_index")
*
@ -85,7 +85,8 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated in favor of vue CRUD methods
* @deprecated Use Vue
*
* @Route("/{tool}/{type}/{id}/list", name="chamilo_core_resource_list")
*
* If node has children show it
@ -118,7 +119,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated in favor of vue CRUD methods
* @deprecated Use Vue
*
* @Route("/{tool}/{type}/{id}/new_folder", methods={"GET", "POST"}, name="chamilo_core_resource_new_folder")
*/
@ -128,7 +129,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated in favor of vue CRUD methods
* @deprecated Use Vue
*
* @Route("/{tool}/{type}/{id}/new", methods={"GET", "POST"}, name="chamilo_core_resource_new")
*/
@ -213,7 +214,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated in favor of vue CRUD methods
* @deprecated Use Vue
*
* @Route("/{tool}/{type}/{id}/edit", methods={"GET", "POST"})
*/
@ -294,6 +295,8 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated Use Vue
*
* Shows a resource information.
*
* @Route("/{tool}/{type}/{id}/info", methods={"GET", "POST"}, name="chamilo_core_resource_info")
@ -411,6 +414,8 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated Use Vue + api platform
*
* @Route("/{tool}/{type}/{id}/delete", name="chamilo_core_resource_delete")
*/
public function deleteAction(Request $request): Response
@ -447,6 +452,8 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
/**
* @deprecated Use Vue + api platform
*
* @Route("/{tool}/{type}/{id}/delete_mass", methods={"DELETE"}, name="chamilo_core_resource_delete_mass")
*/
public function deleteMassAction($primaryKeys, $allPrimaryKeys, Request $request): Response

@ -11,8 +11,8 @@ use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Chamilo\CoreBundle\Controller\Api\CreateResourceNodeFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdateResourceNodeFileAction;
use Chamilo\CoreBundle\Controller\Api\CreatePersonalFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdatePersonalFileAction;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\Serializer\Annotation\Groups;
@ -20,11 +20,11 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* normalizationContext={"groups"={"personal_file:read"}},
* normalizationContext={"groups"={"personal_file:read", "resource_node:read"}},
* denormalizationContext={"groups"={"personal_file:write"}},
* itemOperations={
* "put"={
* "controller"=UpdateResourceNodeFileAction::class,
* "controller"=UpdatePersonalFileAction::class,
* "deserialize"=false,
* "security"="is_granted('EDIT', object.resourceNode)",
* },
@ -37,7 +37,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* },
* collectionOperations={
* "post"={
* "controller"=CreateResourceNodeFileAction::class,
* "controller"=CreatePersonalFileAction::class,
* "deserialize"=false,
* "security"="is_granted('ROLE_USER')",
* "validation_groups"={"Default", "media_object_create", "personal_file:write"},
@ -115,6 +115,7 @@ class PersonalFile extends AbstractResource implements ResourceInterface
use TimestampableEntity;
/**
* @Groups({"personal_file:read"})
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
@ -122,6 +123,7 @@ class PersonalFile extends AbstractResource implements ResourceInterface
protected int $id;
/**
* @Groups({"personal_file:read"})
* @Assert\NotBlank()
* @ORM\Column(name="title", type="string", length=255, nullable=false)
*/

@ -56,7 +56,7 @@ class ResourceNode
public const PATH_SEPARATOR = '/';
/**
* @Groups({"resource_node:read", "document:read", "ctool:read"})
* @Groups({"resource_node:read", "document:read", "ctool:read", "user_json:read"})
* @ORM\Id
* @ORM\Column(type="bigint")
* @ORM\GeneratedValue(strategy="AUTO")
@ -210,9 +210,6 @@ class ResourceNode
$this->fileEditableText = false;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getPathForDisplay();

@ -11,8 +11,8 @@ use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\CreateDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CourseBundle\Traits\ShowCourseResourcesInSessionTrait;

Loading…
Cancel
Save