Merge remote-tracking branch 'origin/master'

pull/5692/head
Angel Fernando Quiroz Campos 1 year ago
commit f861532b76
No known key found for this signature in database
GPG Key ID: B284841AE3E562CD
  1. 43
      assets/css/app.scss
  2. 56
      assets/css/scss/_documents.scss
  3. 36
      assets/vue/components/basecomponents/BaseContextMenu.vue
  4. 15
      assets/vue/components/course/CourseTool.vue
  5. 223
      assets/vue/components/filemanager/CourseDocuments.vue
  6. 224
      assets/vue/components/filemanager/PersonalFiles.vue
  7. 388
      assets/vue/composables/useFileManager.js
  8. 6
      assets/vue/router/filemanager.js
  9. 33
      assets/vue/views/documents/DocumentsUpload.vue
  10. 579
      assets/vue/views/filemanager/List.vue
  11. 276
      assets/vue/views/filemanager/Upload.vue
  12. 544
      assets/vue/views/personalfile/List.vue
  13. 8
      public/main/auth/inscription.php
  14. 4
      public/main/inc/lib/usermanager.lib.php
  15. 19
      src/CoreBundle/Component/Editor/CkEditor/CkEditor.php
  16. 23
      src/CoreBundle/Resources/views/Mailer/Legacy/content_registration_platform.html.twig
  17. 5
      src/CoreBundle/Resources/views/Mailer/Legacy/subject_registration_platform.html.twig

@ -790,6 +790,49 @@ form .field {
}
}
.filemanager-container {
.mdi-icon {
font-size: 48px;
}
.thumbnails {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.thumbnail-item {
width: 150px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
.thumbnail-icon {
font-size: 2rem;
}
.thumbnail-title {
margin-top: 10px;
font-size: 1rem;
cursor: pointer;
}
.thumbnail-actions {
margin-top: 10px;
display: flex;
justify-content: center;
gap: 5px;
}
.thumbnail-image {
width: 100px;
height: 100px;
object-fit: cover;
}
}
//@import 'primevue-md-light-indigo/theme.css';
//@import '~primevue/resources/primevue.min.css';
//@import '~primeflex/primeflex.css';

@ -32,3 +32,59 @@
}
}
}
.filemanager-container .mdi-icon {
@apply text-6xl;
}
.filemanager-container .thumbnails-container {
@apply flex justify-center;
}
.filemanager-container .thumbnails {
@apply flex flex-wrap gap-2.5 justify-center mb-12;
}
.filemanager-container .thumbnail-item {
@apply w-36 p-2 border border-gray-25 rounded-md text-center cursor-pointer;
}
.filemanager-container .thumbnail-item:hover {
@apply bg-gray-15;
}
.filemanager-container .thumbnail-icon {
@apply text-2xl w-24 h-24 object-cover flex items-center justify-center mx-auto;
}
.filemanager-container .thumbnail-title {
@apply mt-2 text-base break-words;
}
.filemanager-container .thumbnail-actions {
@apply mt-2 flex justify-center gap-1;
}
.filemanager-container .thumbnail-image {
@apply w-24 h-24 object-cover;
}
.context-menu {
@apply absolute bg-white shadow-lg z-50 rounded-md py-1 min-w-[150px] font-sans text-[14px];
}
.context-menu ul {
@apply list-none m-0 p-0;
}
.context-menu li {
@apply flex items-center px-4 py-2 cursor-pointer text-center transition duration-200 ease-in-out;
}
.context-menu li:hover {
@apply bg-gray-15 shadow-inner;
}
.context-menu li .mdi {
@apply mr-2;
}

@ -0,0 +1,36 @@
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
position: {
type: Object,
default: () => ({ x: 0, y: 0 }),
}
})
const emit = defineEmits(['close'])
const handleClickOutside = (event) => {
emit('close')
}
watch(() => props.visible, (newVal) => {
if (newVal) {
setTimeout(() => {
document.addEventListener('click', handleClickOutside)
}, 0)
} else {
document.removeEventListener('click', handleClickOutside)
}
})
</script>
<template>
<div class="context-menu" v-if="visible" :style="{ top: `${position.y}px`, left: `${position.x}px` }">
<slot />
</div>
</template>

@ -21,7 +21,7 @@
:url="tool.url"
class="course-tool__title"
>
{{ tool.tool.titleToShow }}
{{ $t(tool.tool.titleToShow) }}
</BaseAppLink>
<div class="course-tool__options">
@ -54,22 +54,13 @@
size="small"
/>
</a>
<!-- a
v-if="securityStore.isCurrentTeacher"
:href="goToSettingCourseTool(tool)"
>
<BaseIcon
icon="cog"
size="lg"
/>
</a -->
</div>
</div>
</template>
<script setup>
import { computed, inject } from "vue"
import { useI18n } from "vue-i18n"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import { useSecurityStore } from "../../store/securityStore"
import { usePlatformConfig } from "../../store/platformConfig"
@ -77,6 +68,8 @@ import { storeToRefs } from "pinia"
import { useCidReqStore } from "../../store/cidReq"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()
const securityStore = useSecurityStore()
const platformConfigStore = usePlatformConfig()
const cidReqStore = useCidReqStore()

@ -0,0 +1,223 @@
<template>
<div class="filemanager-container">
<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 class="btn btn--primary" icon="fa fa-folder-plus" label="New folder" @click="openNewDialog" />
<Button class="btn btn--primary" icon="fa fa-file-upload" label="Upload" @click="uploadDocumentHandler" />
<Button v-if="selectedFiles.length" class="btn btn--danger" icon="pi pi-trash" label="Delete" @click="confirmDeleteMultiple" />
<Button class="btn btn--primary" :icon="viewModeIcon" @click="toggleViewMode" />
<Button v-if="previousFolders.length" class="btn btn--primary" icon="pi pi-arrow-left" label="Back" @click="goBack" />
</div>
</div>
<div class="breadcrumbs">
<span v-for="(folder, index) in previousFolders" :key="index">
<span>{{ folder.title }}</span> /
</span>
<span>{{ currentFolderTitle }}</span>
</div>
</div>
<div v-if="viewMode === 'list'">
<DataTable
v-model:filters="filters"
v-model:selection="selectedFiles"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalFiles"
:value="files"
class="p-datatable-sm"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onFilesPage"
@sort="sortingFilesChanged"
>
<Column :header="$t('Title')" :sortable="true" field="resourceNode.title">
<template #body="slotProps">
<div>
<span
v-if="!slotProps.data.resourceNode.firstResourceFile" @click="handleClickFile(slotProps.data)">
{{ slotProps.data.resourceNode.title }} folder
</span>
<span v-else>
{{ slotProps.data.resourceNode.title }}
</span>
</div>
</template>
</Column>
<Column :header="$t('Size')" :sortable="true" field="resourceNode.firstResourceFile.size">
<template #body="slotProps">
{{ slotProps.data.resourceNode.firstResourceFile ? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size) : "" }}
</template>
</Column>
<Column :header="$t('Modified')" :sortable="true" field="resourceNode.updatedAt">
<template #body="slotProps">
{{ relativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button v-if="isAuthenticated" class="btn btn--danger" icon="pi pi-trash" @click="confirmDeleteItem(slotProps.data)" />
</div>
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
v-if="slotProps.data.resourceNode.firstResourceFile"
class="p-button-sm p-button p-mr-2"
label="Select"
@click="returnToEditor(slotProps.data)"
/>
</div>
</template>
</Column>
</DataTable>
</div>
<div v-else>
<div class="thumbnails">
<div v-for="file in files" :key="file.iid" class="thumbnail-item" @click="handleClickFile(file)" @contextmenu.prevent="showContextMenu($event, file)">
<div class="thumbnail-icon">
<template v-if="isImage(file)">
<img :src="getFileUrl(file)" :alt="file.resourceNode.title" :title="file.resourceNode.title" class="thumbnail-image" />
</template>
<template v-else>
<span :class="['mdi', getIcon(file)]" class="mdi-icon"></span>
</template>
</div>
<div class="thumbnail-title">{{ file.resourceNode.title }}</div>
</div>
</div>
<BaseContextMenu :visible="contextMenuVisible" :position="contextMenuPosition" @close="contextMenuVisible = false">
<ul>
<li @click="selectFile(contextMenuFile)">
<span class="mdi mdi-file-check-outline"></span>
Select
</li>
<li @click="confirmDeleteItem(contextMenuFile)">
<span class="mdi mdi-delete-outline"></span>
Delete
</li>
</ul>
</BaseContextMenu>
</div>
<Dialog v-model:visible="dialog" :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 />
<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="deleteDialog" :modal="true" :style="{ width: '450px' }" header="Confirm">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem"></i>
<span>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b>?</span>
</div>
<template #footer>
<Button class="p-button-text" icon="pi pi-times" label="No" @click="deleteDialog = 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"></i>
<span>{{ $t('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>
<Dialog v-model:visible="detailsDialogVisible" :header="selectedItem.title || 'Item Details'" :modal="true" :style="{ width: '50%' }">
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p><strong>URL:</strong> <a :href="selectedItem.contentUrl" target="_blank">Open File</a></p>
</div>
<template #footer>
<Button class="p-button-text" label="Close" @click="closeDetailsDialog" />
</template>
</Dialog>
</div>
</template>
<script setup>
import { useFileManager } from '../../composables/useFileManager';
import { useI18n } from 'vue-i18n';
import { useFormatDate } from '../../composables/formatDate'
import BaseContextMenu from '../basecomponents/BaseContextMenu.vue';
import prettyBytes from "pretty-bytes"
const { t } = useI18n();
const { relativeDatetime } = useFormatDate();
const {
files,
totalFiles,
isLoading,
selectedFiles,
dialog,
deleteDialog,
deleteMultipleDialog,
detailsDialogVisible,
selectedItem,
itemToDelete,
item,
submitted,
filters,
viewMode,
contextMenuVisible,
contextMenuPosition,
contextMenuFile,
previousFolders,
currentFolderTitle,
handleClickFile,
goBack,
returnToEditor,
toggleViewMode,
viewModeIcon,
isImage,
getFileUrl,
getIcon,
showContextMenu,
openNewDialog,
hideDialog,
saveItem,
confirmDeleteItem,
confirmDeleteMultiple,
deleteMultipleItems,
deleteItemButton,
onFilesPage,
sortingFilesChanged,
closeDetailsDialog,
uploadDocumentHandler,
onMountedCallback,
isAuthenticated,
selectFile
} = useFileManager('documents', '/api/documents', 'CourseDocumentsUploadFile', true);
onMountedCallback();
</script>

@ -0,0 +1,224 @@
<template>
<div class="filemanager-container">
<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 class="btn btn--primary" icon="fa fa-folder-plus" label="New folder" @click="openNewDialog" />
<Button class="btn btn--primary" icon="fa fa-file-upload" label="Upload" @click="uploadDocumentHandler" />
<Button v-if="selectedFiles.length" class="btn btn--danger" icon="pi pi-trash" label="Delete" @click="confirmDeleteMultiple" />
<Button class="btn btn--primary" :icon="viewModeIcon" @click="toggleViewMode" />
<Button v-if="previousFolders.length" class="btn btn--primary" icon="pi pi-arrow-left" label="Back" @click="goBack" />
</div>
</div>
<div class="breadcrumbs">
<span v-for="(folder, index) in previousFolders" :key="index">
<span>{{ folder.title }}</span> /
</span>
<span>{{ currentFolderTitle }}</span>
</div>
</div>
<div v-if="viewMode === 'list'">
<DataTable
v-model:filters="filters"
v-model:selection="selectedFiles"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalFiles"
:value="files"
class="p-datatable-sm"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onFilesPage"
@sort="sortingFilesChanged"
>
<Column :header="$t('Title')" :sortable="true" field="resourceNode.title">
<template #body="slotProps">
<div>
<span
v-if="!slotProps.data.resourceNode.firstResourceFile" @click="handleClickFile(slotProps.data)">
{{ slotProps.data.resourceNode.title }} folder
</span>
<span v-else>
{{ slotProps.data.resourceNode.title }}
</span>
</div>
</template>
</Column>
<Column :header="$t('Size')" :sortable="true" field="resourceNode.firstResourceFile.size">
<template #body="slotProps">
{{ slotProps.data.resourceNode.firstResourceFile ? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size) : ""
}}
</template>
</Column>
<Column :header="$t('Modified')" :sortable="true" field="resourceNode.updatedAt">
<template #body="slotProps">
{{ relativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button v-if="isAuthenticated" class="btn btn--danger" icon="pi pi-trash" @click="confirmDeleteItem(slotProps.data)" />
</div>
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
v-if="slotProps.data.resourceNode.firstResourceFile"
class="p-button-sm p-button p-mr-2"
label="Select"
@click="returnToEditor(slotProps.data)"
/>
</div>
</template>
</Column>
</DataTable>
</div>
<div v-else>
<div class="thumbnails">
<div v-for="file in files" :key="file.iid" class="thumbnail-item" @click="handleClickFile(file)" @contextmenu.prevent="showContextMenu($event, file)">
<div class="thumbnail-icon">
<template v-if="isImage(file)">
<img :src="getFileUrl(file)" :alt="file.resourceNode.title" :title="file.resourceNode.title" class="thumbnail-image" />
</template>
<template v-else>
<span :class="['mdi', getIcon(file)]" class="mdi-icon"></span>
</template>
</div>
<div class="thumbnail-title">{{ file.resourceNode.title }}</div>
</div>
</div>
<BaseContextMenu :visible="contextMenuVisible" :position="contextMenuPosition" @close="contextMenuVisible = false">
<ul>
<li @click="selectFile(contextMenuFile)">
<span class="mdi mdi-file-check-outline"></span>
Select
</li>
<li @click="confirmDeleteItem(contextMenuFile)">
<span class="mdi mdi-delete-outline"></span>
Delete
</li>
</ul>
</BaseContextMenu>
</div>
<Dialog v-model:visible="dialog" :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 />
<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="deleteDialog" :modal="true" :style="{ width: '450px' }" header="Confirm">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem"></i>
<span>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b>?</span>
</div>
<template #footer>
<Button class="p-button-text" icon="pi pi-times" label="No" @click="deleteDialog = 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"></i>
<span>{{ $t('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>
<Dialog v-model:visible="detailsDialogVisible" :header="selectedItem.title || 'Item Details'" :modal="true" :style="{ width: '50%' }">
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p><strong>URL:</strong> <a :href="selectedItem.contentUrl" target="_blank">Open File</a></p>
</div>
<template #footer>
<Button class="p-button-text" label="Close" @click="closeDetailsDialog" />
</template>
</Dialog>
</div>
</template>
<script setup>
import { useFileManager } from '../../composables/useFileManager';
import { useI18n } from 'vue-i18n';
import { useFormatDate } from '../../composables/formatDate'
import BaseContextMenu from '../basecomponents/BaseContextMenu.vue';
import prettyBytes from "pretty-bytes"
const { t } = useI18n();
const { relativeDatetime } = useFormatDate();
const {
files,
totalFiles,
isLoading,
selectedFiles,
dialog,
deleteDialog,
deleteMultipleDialog,
detailsDialogVisible,
selectedItem,
itemToDelete,
item,
submitted,
filters,
viewMode,
contextMenuVisible,
contextMenuPosition,
contextMenuFile,
previousFolders,
currentFolderTitle,
handleClickFile,
goBack,
returnToEditor,
toggleViewMode,
viewModeIcon,
isImage,
getFileUrl,
getIcon,
showContextMenu,
openNewDialog,
hideDialog,
saveItem,
confirmDeleteItem,
confirmDeleteMultiple,
deleteMultipleItems,
deleteItemButton,
onFilesPage,
sortingFilesChanged,
closeDetailsDialog,
uploadDocumentHandler,
onMountedCallback,
isAuthenticated,
selectFile
} = useFileManager('personalfile', '/api/personal_files', 'FileManagerUploadFile');
onMountedCallback();
</script>

@ -0,0 +1,388 @@
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useSecurityStore } from '../store/securityStore';
import { useCidReq } from './cidReq';
import { RESOURCE_LINK_PUBLISHED } from '../components/resource_links/visibility';
import { useCidReqStore } from "../store/cidReq"
import axios from "axios"
export function useFileManager(entity, apiEndpoint, uploadRoute, isCourseDocument = false) {
const route = useRoute();
const router = useRouter();
const store = useStore();
const { t } = useI18n();
const securityStore = useSecurityStore();
const { isAuthenticated, user } = storeToRefs(securityStore);
const cidReqStore = isCourseDocument ? useCidReqStore() : null;
const { course } = cidReqStore ? storeToRefs(cidReqStore) : { course: null };
const files = ref([]);
const totalFiles = ref(0);
const isLoading = ref(false);
const selectedFiles = ref([]);
const dialog = ref(false);
const deleteDialog = ref(false);
const deleteMultipleDialog = ref(false);
const detailsDialogVisible = ref(false);
const selectedItem = ref({});
const itemToDelete = ref(null);
const item = ref({});
const submitted = ref(false);
const filters = ref({ shared: 0, loadNode: 1 });
const viewMode = ref('thumbnails');
const contextMenuVisible = ref(false);
const contextMenuPosition = ref({ x: 0, y: 0 });
const contextMenuFile = ref(null);
const previousFolders = ref([]);
const currentFolderTitle = ref('Root');
const { cid, sid, gid } = useCidReq();
const flattenFilters = (filters) => {
return Object.keys(filters).reduce((acc, key) => {
acc[key] = filters[key];
return acc;
}, {});
};
const onUpdateOptions = async () => {
let flattenedFilters = flattenFilters({
...filters.value,
cid: route.query.cid || '',
sid: route.query.sid || '',
gid: route.query.gid || '',
type: route.query.type || '',
});
const params = {
...flattenedFilters,
page: 1,
itemsPerPage: 10,
sortBy: '',
sortDesc: false,
};
isLoading.value = true;
try {
const response = await fetch(`${apiEndpoint}?page=${params.page}&rows=${params.itemsPerPage}&sortBy=${params.sortBy}&sortDesc=${params.sortDesc}&shared=${params.shared}&loadNode=${params.loadNode}&resourceNode.parent=${params['resourceNode.parent']}&cid=${params.cid}&sid=${params.sid}&gid=${params.gid}&type=${params.type}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});
const data = await response.json();
if (data['hydra:member']) {
files.value = data['hydra:member'];
totalFiles.value = data['hydra:totalItems'];
} else {
console.error('Error: Data format is not correct', data);
}
} catch (error) {
console.error('Error fetching files:', error);
} finally {
isLoading.value = false;
}
};
const handleClickFile = (data) => {
if (data.resourceNode.firstResourceFile) {
returnToEditor(data);
} else {
previousFolders.value.push({
id: filters.value["resourceNode.parent"],
title: currentFolderTitle.value
});
filters.value["resourceNode.parent"] = data.resourceNode.id;
currentFolderTitle.value = data.resourceNode.title;
onUpdateOptions();
}
};
const goBack = () => {
if (previousFolders.value.length > 0) {
const previousFolder = previousFolders.value.pop();
filters.value["resourceNode.parent"] = previousFolder.id;
currentFolderTitle.value = previousFolder.title;
onUpdateOptions();
} else {
filters.value["resourceNode.parent"] = isCourseDocument ? course.value.resourceNode.id : user.value.resourceNode.id;
currentFolderTitle.value = 'Root';
onUpdateOptions();
}
};
const returnToEditor = (data) => {
const url = data.contentUrl;
window.parent.postMessage({ url: url }, '*');
if (parent.tinymce) {
parent.tinymce.activeEditor.windowManager.close();
}
function getUrlParam(paramName) {
const reParam = new RegExp('(?:[\\?&]|&amp;)' + paramName + '=([^&]+)', 'i');
const match = window.location.search.match(reParam);
return (match && match.length > 1) ? match[1] : '';
}
const funcNum = getUrlParam('CKEditorFuncNum');
if (window.opener.CKEDITOR) {
window.opener.CKEDITOR.tools.callFunction(funcNum, url);
window.close();
}
};
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'list' ? 'thumbnails' : 'list';
onUpdateOptions();
};
const viewModeIcon = computed(() => viewMode.value === 'list' ? 'pi pi-th-large' : 'pi pi-list');
const isImage = (file) => {
const fileExtensions = ['jpeg', 'jpg', 'png', 'gif'];
const extension = file.resourceNode.title.split('.').pop().toLowerCase();
return fileExtensions.includes(extension);
};
const getFileUrl = (file) => {
return file.contentUrl;
};
const getIcon = (file) => {
if (!file.resourceNode.firstResourceFile) {
return 'mdi-folder';
}
const fileTypeIcons = {
'pdf': 'mdi-file-pdf-box',
'doc': 'mdi-file-word-box',
'docx': 'mdi-file-word-box',
'xls': 'mdi-file-excel-box',
'xlsx': 'mdi-file-excel-box',
'zip': 'mdi-zip-box',
'jpeg': 'mdi-file-image-box',
'jpg': 'mdi-file-image-box',
'png': 'mdi-file-image-box',
'gif': 'mdi-file-image-box',
'default': 'mdi-file',
};
const extension = file.resourceNode.title.split('.').pop().toLowerCase();
return fileTypeIcons[extension] || fileTypeIcons['default'];
};
const showContextMenu = (event, file) => {
event.preventDefault();
contextMenuFile.value = file;
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
contextMenuVisible.value = true;
};
const openNewDialog = () => {
item.value = {};
submitted.value = false;
dialog.value = true;
};
const hideDialog = () => {
dialog.value = false;
submitted.value = false;
};
const saveItem = async () => {
submitted.value = true;
if (item.value.title.trim()) {
if (!item.value.id) {
item.value.filetype = 'folder';
item.value.parentResourceNodeId = filters.value["resourceNode.parent"];
item.value.resourceLinkList = JSON.stringify([{ gid, sid, cid, visibility: RESOURCE_LINK_PUBLISHED }]);
try {
await store.dispatch(`${entity}/createWithFormData`, item.value);
await onUpdateOptions();
} catch (error) {
console.error('Error creating folder:', error);
}
}
dialog.value = false;
item.value = {};
submitted.value = false;
}
};
const confirmDeleteItem = (item) => {
itemToDelete.value = { ...item };
deleteDialog.value = true;
};
const confirmDeleteMultiple = () => {
deleteMultipleDialog.value = true;
};
const deleteMultipleItems = async () => {
const ids = selectedFiles.value.map(file => file.id);
try {
await store.dispatch(`${entity}/delMultiple`, ids);
deleteMultipleDialog.value = false;
selectedFiles.value = [];
onUpdateOptions();
} catch (error) {
console.error('Error deleting multiple items:', error);
}
};
const deleteItemButton = async () => {
if (isCourseDocument) {
if (itemToDelete.value && itemToDelete.value.iid) {
try {
await axios.delete(`/api/documents/${itemToDelete.value.iid}`);
deleteDialog.value = false;
itemToDelete.value = { resourceNode: {} };
await onUpdateOptions();
} catch (error) {
console.error('Error deleting document:', error);
}
} else {
console.error('Document to delete is missing or invalid', itemToDelete.value);
}
} else {
if (itemToDelete.value && itemToDelete.value.id) {
try {
await store.dispatch(`${entity}/del`, itemToDelete.value);
deleteDialog.value = false;
itemToDelete.value = null;
onUpdateOptions();
} catch (error) {
console.error('An error occurred while deleting the item', error);
}
}
}
};
const onFilesPage = (event) => {
filters.value.itemsPerPage = event.rows;
filters.value.page = event.page + 1;
filters.value.sortBy = event.sortField;
filters.value.sortDesc = event.sortOrder === -1;
onUpdateOptions();
};
const sortingFilesChanged = (event) => {
filters.value.sortBy = event.sortField;
filters.value.sortDesc = event.sortOrder === -1;
onUpdateOptions();
};
const closeDetailsDialog = () => {
detailsDialogVisible.value = false;
};
const uploadDocumentHandler = async () => {
localStorage.setItem('previousFolders', JSON.stringify(previousFolders.value));
localStorage.setItem('currentFolderTitle', currentFolderTitle.value);
localStorage.setItem('isUploaded', 'true');
localStorage.setItem('uploadParentNodeId', filters.value['resourceNode.parent']);
await router.push({
name: uploadRoute,
query: {
...route.query,
parentResourceNodeId: filters.value['resourceNode.parent'],
parent: filters.value['resourceNode.parent'],
returnTo: route.name
},
});
};
const onMountedCallback = () => {
onMounted(() => {
const savedPreviousFolders = localStorage.getItem('previousFolders');
const savedCurrentFolderTitle = localStorage.getItem('currentFolderTitle');
const isUploaded = localStorage.getItem('isUploaded');
const uploadParentNodeId = localStorage.getItem('uploadParentNodeId');
if (isUploaded === 'true' && uploadParentNodeId) {
filters.value["resourceNode.parent"] = Number(uploadParentNodeId);
localStorage.removeItem('isUploaded');
localStorage.removeItem('uploadParentNodeId');
} else if (!filters.value["resourceNode.parent"] || filters.value["resourceNode.parent"] === 0) {
filters.value["resourceNode.parent"] = isCourseDocument ? course.value.resourceNode.id : user.value.resourceNode.id;
}
if (savedPreviousFolders) {
previousFolders.value = JSON.parse(savedPreviousFolders);
localStorage.removeItem('previousFolders');
}
if (savedCurrentFolderTitle) {
currentFolderTitle.value = savedCurrentFolderTitle;
localStorage.removeItem('currentFolderTitle');
}
onUpdateOptions();
});
};
const selectFile = (file) => {
returnToEditor(file);
contextMenuVisible.value = false;
};
const showHandler = (item) => {
selectedItem.value = item;
detailsDialogVisible.value = true;
};
const editHandler = (item) => {
item.value = { ...item };
dialog.value = true;
};
return {
files,
totalFiles,
isLoading,
selectedFiles,
dialog,
deleteDialog,
deleteMultipleDialog,
detailsDialogVisible,
selectedItem,
itemToDelete,
item,
submitted,
filters,
viewMode,
contextMenuVisible,
contextMenuPosition,
contextMenuFile,
previousFolders,
currentFolderTitle,
flattenFilters,
onUpdateOptions,
handleClickFile,
goBack,
returnToEditor,
toggleViewMode,
viewModeIcon,
isImage,
getFileUrl,
getIcon,
showContextMenu,
openNewDialog,
hideDialog,
saveItem,
confirmDeleteItem,
confirmDeleteMultiple,
deleteMultipleItems,
deleteItemButton,
onFilesPage,
sortingFilesChanged,
closeDetailsDialog,
uploadDocumentHandler,
onMountedCallback,
isAuthenticated,
selectFile,
showHandler,
editHandler
};
}

@ -15,5 +15,11 @@ export default {
component: () => import('../views/filemanager/Upload.vue'),
meta: { emptyLayout: true },
},
{
name: 'CourseDocumentsUploadFile',
path: '/course-upload',
meta: { emptyLayout: true },
component: () => import('../views/documents/DocumentsUpload.vue')
},
],
};

@ -12,7 +12,6 @@
<Dashboard
:plugins="['Webcam', 'ImageEditor']"
:props="{
//metaFields: [{id: 'name', name: 'Name', placeholder: 'file name'}],
proudlyDisplayPoweredByUppy: false,
width: '100%',
height: '350px',
@ -88,7 +87,7 @@ const isUncompressZipEnabled = ref(false)
const fileExistsOption = ref("rename")
const resourceNode = computed(() => store.getters["resourcenode/getResourceNode"])
const parentResourceNodeId = ref(Number(route.params.node))
const parentResourceNodeId = ref(Number(route.query.parentResourceNodeId || route.params.node))
const resourceLinkList = ref(
JSON.stringify([
{
@ -130,7 +129,21 @@ uppy.value = new Uppy()
onCreated(response.body)
})
.on('complete', () => {
router.back()
console.log('Upload complete, sending message...');
const parentNodeId = parentResourceNodeId.value;
localStorage.setItem('isUploaded', 'true');
localStorage.setItem('uploadParentNodeId', parentNodeId);
setTimeout(() => {
if (route.query.returnTo) {
router.push({
name: route.query.returnTo,
params: { node: parentNodeId },
query: { ...route.query, parentResourceNodeId: parentNodeId },
});
} else {
router.back();
}
}, 2000);
})
uppy.value.setMeta({
@ -164,11 +177,15 @@ watch(fileExistsOption, () => {
})
function back() {
if (!resourceNode.value) {
return
let queryParams = { cid, sid, gid, filetype, tab: route.query.tab }
if (route.query.tab) {
router.push({
name: 'FileManagerList',
params: { node: parentResourceNodeId.value },
query: queryParams
})
} else {
router.back()
}
let queryParams = { cid, sid, gid, filetype }
router.push({ name: "DocumentsList", params: { node: resourceNode.value.id }, query: queryParams })
}
</script>

@ -1,545 +1,68 @@
<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
class="btn btn--primary"
icon="fa fa-folder-plus"
label="New folder"
@click="openNew"
/>
<Button
class="btn btn--primary"
icon="fa fa-file-upload"
label="Upload"
@click="uploadDocumentHandler()"
/>
<Button
v-if="selectedItems.length"
class="btn btn--danger"
icon="pi pi-trash"
label="Delete"
@click="confirmDeleteMultiple"
/>
</div>
</div>
</div>
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
class="p-datatable-sm"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onPage($event)"
@sort="sortingChanged($event)"
>
<Column
:header="$t('Title')"
:sortable="true"
field="resourceNode.title"
>
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
<v-icon
v-if="slotProps.data.resourceLinkListFromEntity && slotProps.data.resourceLinkListFromEntity.length > 0"
icon="mdi-link"
/>
</div>
<div v-else>
<a
v-if="slotProps.data"
class="cursor-pointer"
@click="handleClick(slotProps.data)"
>
<v-icon icon="mdi-folder" />
{{ slotProps.data.resourceNode.title }}
</a>
</div>
</template>
</Column>
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
</Column>
<Column
:header="$t('Modified')"
:sortable="true"
field="resourceNode.updatedAt"
>
<template #body="slotProps">
{{ relativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
v-if="isAuthenticated"
class="btn btn--danger"
icon="pi pi-trash"
@click="confirmDeleteItem(slotProps.data)"
/>
</div>
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
class="p-button-sm p-button p-mr-2"
label="Select"
@click="returnToEditor(slotProps.data)"
/>
</div>
</template>
</Column>
</DataTable>
<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 v-if="!isLoading">
<div class="flex border-b border-gray-200">
<button
class="px-4 py-2 -mb-px font-semibold border-b-2"
:class="{
'border-blue-500 text-blue-600': activeTab === 'personalFiles',
'border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300': activeTab !== 'personalFiles'
}"
@click="changeTab('personalFiles')"
>
</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"
></i>
<span
>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b
>?</span
{{ t('Personal Files') }}
</button>
<button
v-if="isAllowedToEdit"
class="px-4 py-2 -mb-px font-semibold border-b-2"
:class="{
'border-blue-500 text-blue-600': activeTab === 'documents',
'border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300': activeTab !== 'documents'
}"
@click="changeTab('documents')"
>
{{ t('Documents') }}
</button>
</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">{{ $t("Are you sure you want to delete the selected items?") }}</span>
<div v-if="activeTab === 'personalFiles'" class="mt-4">
<PersonalFiles />
</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>
<Dialog
v-model:visible="detailsDialogVisible"
:header="selectedItem.title || 'Item Details'"
:modal="true"
:style="{ width: '50%' }"
>
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.resourceNode.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p>
<strong>URL:</strong>
<a
:href="selectedItem.contentUrl"
target="_blank"
>Open File</a
>
</p>
<div v-if="activeTab === 'documents' && isAllowedToEdit" class="mt-4">
<CourseDocuments />
</div>
<template #footer>
<Button
class="p-button-text"
label="Close"
@click="closeDetailsDialog"
/>
</template>
</Dialog>
</div>
</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 ResourceIcon from "../../components/documents/ResourceIcon.vue"
import ResourceFileLink from "../../components/documents/ResourceFileLink.vue"
import DataFilter from "../../components/DataFilter"
import isEmpty from "lodash/isEmpty"
import { RESOURCE_LINK_PUBLISHED } from "../../components/resource_links/visibility"
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PersonalFiles from "../../components/filemanager/PersonalFiles.vue"
import CourseDocuments from "../../components/filemanager/CourseDocuments.vue"
import { checkIsAllowedToEdit } from "../../composables/userPermissions"
import { useI18n } from "vue-i18n"
import { useFormatDate } from "../../composables/formatDate"
import prettyBytes from "pretty-bytes"
import { useSecurityStore } from "../../store/securityStore"
import { storeToRefs } from "pinia"
export default {
name: "FileManagerList",
servicePrefix: "FileManager",
components: {
ActionCell,
ResourceIcon,
ResourceFileLink,
DataFilter,
},
mixins: [ListMixin],
setup() {
const { t } = useI18n()
const { relativeDatetime } = useFormatDate()
const securityStore = useSecurityStore()
const { isAuthenticated, isAdmin, user } = storeToRefs(securityStore)
const data = {
sortBy: "title",
sortDesc: false,
columnsQua: [
{ align: "left", name: "resourceNode.title", label: t("Title"), field: "resourceNode.title", sortable: true },
{
align: "left",
name: "resourceNode.updatedAt",
label: t("Modified"),
field: "resourceNode.updatedAt",
sortable: true,
},
{
name: "resourceNode.firstResourceFile.size",
label: t("Size"),
field: "resourceNode.firstResourceFile.size",
sortable: true,
},
{ name: "action", label: t("Actions"), field: "action", sortable: false },
],
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],
selected: [],
isBusy: false,
options: [],
selectedItems: [],
// prime vue
deleteMultipleDialog: false,
item: {},
filters: { shared: 0, loadNode: 1 },
submitted: false,
prettyBytes,
relativeDatetime,
t,
currentUser: user,
isAdmin,
isAuthenticated,
}
const route = useRoute()
const router = useRouter()
return data
},
created() {
this.resetList = true
this.onUpdateOptions(this.options)
this.isFromEditor = window.location.search.includes("editor=tinymce")
},
computed: {
// From crud.js list function
...mapGetters("resourcenode", {
resourceNode: "getResourceNode",
}),
const activeTab = ref(route.query.tab || 'personalFiles')
const isAllowedToEdit = ref(false)
const isLoading = ref(true)
const { t } = useI18n()
...mapGetters("personalfile", {
items: "list",
}),
//...getters
// From ListMixin
...mapFields("personalfile", {
deletedResource: "deleted",
error: "error",
isLoading: "isLoading",
resetList: "resetList",
totalItems: "totalItems",
view: "view",
}),
},
data() {
return {
itemDialog: false,
detailsDialogVisible: false,
deleteItemDialog: false,
selectedItem: {},
itemToDelete: null,
isFromEditor: false,
}
},
methods: {
showHandler(item) {
this.selectedItem = item
this.detailsDialogVisible = true
},
closeDetailsDialog() {
this.detailsDialogVisible = false
},
// 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 {
let resourceNodeId = this.currentUser.resourceNode["id"]
if (!isEmpty(this.$route.params.node)) {
resourceNodeId = this.$route.params.node
}
this.item.filetype = "folder"
this.item.parentResourceNodeId = resourceNodeId
this.item.resourceLinkList = JSON.stringify([
{
gid: 0,
sid: 0,
cid: 0,
visibility: RESOURCE_LINK_PUBLISHED,
},
])
this.createWithFormData(this.item)
this.showMessage("Saved")
}
this.itemDialog = false
this.item = {}
}
},
editItem(item) {
this.item = { ...item }
this.itemDialog = true
},
confirmDeleteItem(item) {
console.log("confirmDeleteItem :::", item)
this.item = { ...item }
this.itemToDelete = { ...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
},
deleteItemButton() {
console.log("deleteItem", this.itemToDelete)
if (this.itemToDelete && this.itemToDelete.id) {
this.deleteItem(this.itemToDelete)
.then(() => {
this.$toast.add({
severity: "success",
summary: "Success",
detail: "Item deleted successfully",
life: 3000,
})
this.deleteItemDialog = false
this.itemToDelete = null
this.onUpdateOptions(this.options)
})
.catch((error) => {
console.error("Error deleting the item:", error)
this.$toast.add({
severity: "error",
summary: "Error",
detail: "An error occurred while deleting the item",
life: 3000,
})
})
} else {
console.error("No item to delete or item ID is missing")
}
},
onRowSelected(items) {
this.selected = items
},
selectAllRows() {
this.$refs.selectableTable.selectAllRows()
},
clearSelected() {
this.$refs.selectableTable.clearSelected()
},
returnToEditor(item) {
const url = item.contentUrl
// Tiny mce.
window.parent.postMessage(
{
url: url,
},
"*",
)
if (parent.tinymce) {
parent.tinymce.activeEditor.windowManager.close()
}
const changeTab = (tab) => {
activeTab.value = tab
router.replace({ query: { ...route.query, tab } })
}
// Ckeditor
function getUrlParam(paramName) {
var reParam = new RegExp("(?:[\?&]|&amp;)" + paramName + "=([^&]+)", "i")
var match = window.location.search.match(reParam)
return match && match.length > 1 ? match[1] : ""
}
watch(route, (newRoute) => {
if (newRoute.query.tab !== activeTab.value) {
activeTab.value = newRoute.query.tab || 'personalFiles'
}
})
var funcNum = getUrlParam("CKEditorFuncNum")
if (window.opener.CKEDITOR) {
window.opener.CKEDITOR.tools.callFunction(funcNum, url)
window.close()
}
},
async deleteSelected() {
console.log("deleteSelected")
this.deleteMultipleAction(this.selected)
this.onRequest({
pagination: this.pagination,
})
console.log("end -- deleteSelected")
},
...mapActions("personalfile", {
getPage: "fetchAll",
createWithFormData: "createWithFormData",
deleteItem: "del",
deleteMultipleAction: "delMultiple",
}),
...mapActions("resourcenode", {
findResourceNode: "findResourceNode",
}),
},
}
onMounted(async () => {
isAllowedToEdit.value = await checkIsAllowedToEdit()
isLoading.value = false
})
</script>

@ -1,131 +1,171 @@
<template>
<div>
<dashboard
:plugins="['Webcam', 'ImageEditor']"
:props="{
proudlyDisplayPoweredByUppy: false,
width: '100%',
}"
:uppy="uppy"
<BaseToolbar>
<BaseButton
:label="t('Back')"
icon="back"
type="black"
@click="back"
/>
</BaseToolbar>
<div class="flex flex-col justify-start">
<div class="mb-4">
<Dashboard
:plugins="['Webcam', 'ImageEditor']"
:props="{
proudlyDisplayPoweredByUppy: false,
width: '100%',
height: '350px',
}"
:uppy="uppy"
/>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters, useStore } from "vuex"
import { createHelpers } from "vuex-map-fields"
import UploadMixin from "../../mixins/UploadMixin"
import { computed, ref } from "vue"
import isEmpty from "lodash/isEmpty"
import "@uppy/core/dist/style.css"
import "@uppy/dashboard/dist/style.css"
import "@uppy/image-editor/dist/style.css"
import Uppy from "@uppy/core"
import Webcam from "@uppy/webcam"
import { Dashboard } from "@uppy/vue"
import { useRoute, useRouter } from "vue-router"
import { ENTRYPOINT } from "../../config/entrypoint"
import { useSecurityStore } from "../../store/securityStore"
import { storeToRefs } from "pinia"
const XHRUpload = require("@uppy/xhr-upload")
const ImageEditor = require("@uppy/image-editor")
const servicePrefix = "FileManager"
const { mapFields } = createHelpers({
getterType: "personalfile/getField",
mutationType: "personalfile/updateField",
<script setup>
import { ref, computed, watch } from 'vue'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import '@uppy/image-editor/dist/style.css'
import Uppy from '@uppy/core'
import Webcam from '@uppy/webcam'
import { Dashboard } from '@uppy/vue'
import { useRoute, useRouter } from 'vue-router'
import { RESOURCE_LINK_PUBLISHED } from '../../components/resource_links/visibility'
import { ENTRYPOINT } from '../../config/entrypoint'
import { useCidReq } from '../../composables/cidReq'
import { useUpload } from '../../composables/upload'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../components/basecomponents/BaseCheckbox.vue'
import BaseRadioButtons from '../../components/basecomponents/BaseRadioButtons.vue'
import BaseAdvancedSettingsButton from '../../components/basecomponents/BaseAdvancedSettingsButton.vue'
import BaseButton from '../../components/basecomponents/BaseButton.vue'
import BaseToolbar from '../../components/basecomponents/BaseToolbar.vue'
import { useStore } from 'vuex'
const XHRUpload = require('@uppy/xhr-upload')
const ImageEditor = require('@uppy/image-editor')
const store = useStore()
const route = useRoute()
const router = useRouter()
const { gid, sid, cid } = useCidReq()
const { onCreated } = useUpload()
const { t } = useI18n()
const filetype = route.query.filetype === 'certificate' ? 'certificate' : 'file'
const showAdvancedSettings = ref(false)
const isUncompressZipEnabled = ref(false)
const fileExistsOption = ref('rename')
const parentResourceNodeId = ref(Number(route.query.parentResourceNodeId || route.params.node))
const resourceLinkList = ref(
JSON.stringify([
{
gid,
sid,
cid,
visibility: RESOURCE_LINK_PUBLISHED,
},
])
)
const uppy = ref(
new Uppy()
.use(ImageEditor, {
cropperOptions: {
viewMode: 1,
background: false,
autoCropArea: 1,
responsive: true,
},
actions: {
revert: true,
rotate: true,
granularRotate: true,
flip: true,
zoomIn: true,
zoomOut: true,
cropSquare: true,
cropWidescreen: true,
cropWidescreenVertical: true,
},
})
.use(XHRUpload, {
endpoint: ENTRYPOINT + 'personal_files',
formData: true,
fieldName: 'uploadFile',
})
.on('upload-success', (item, response) => {
onCreated(response.body)
})
.on('complete', () => {
console.log('Upload complete, sending message...');
const parentNodeId = parentResourceNodeId.value;
localStorage.setItem('isUploaded', 'true');
localStorage.setItem('uploadParentNodeId', parentNodeId);
setTimeout(() => {
if (route.query.returnTo) {
router.push({
name: route.query.returnTo,
params: { node: parentNodeId },
query: { ...route.query, parentResourceNodeId: parentNodeId },
});
} else {
router.push({
name: 'FileManagerList',
params: { node: parentNodeId },
query: { ...route.query, parentResourceNodeId: parentNodeId },
});
}
}, 2000);
})
)
uppy.value.setMeta({
filetype,
parentResourceNodeId: parentResourceNodeId.value,
resourceLinkList: resourceLinkList.value,
isUncompressZipEnabled: isUncompressZipEnabled.value,
fileExistsOption: fileExistsOption.value,
})
export default {
name: "FileManagerUploadFile",
servicePrefix,
components: {
Dashboard,
},
setup() {
const parentResourceNodeId = ref(null)
const route = useRoute()
const router = useRouter();
const store = useStore()
const securityStore = useSecurityStore()
const { isAuthenticated, isAdmin, user } = storeToRefs(securityStore)
parentResourceNodeId.value = user.value.resourceNode["id"]
if (filetype === 'certificate') {
uppy.value.opts.restrictions.allowedFileTypes = ['.html']
} else {
uppy.value.use(Webcam)
}
if (route.params.node) {
parentResourceNodeId.value = Number(route.params.node)
}
watch(isUncompressZipEnabled, () => {
uppy.value.setOptions({
meta: {
isUncompressZipEnabled: isUncompressZipEnabled.value,
},
})
})
let uppy = ref()
uppy.value = new Uppy()
.use(Webcam)
.use(ImageEditor, {
cropperOptions: {
viewMode: 1,
background: false,
autoCropArea: 1,
responsive: true,
},
actions: {
revert: true,
rotate: true,
granularRotate: true,
flip: true,
zoomIn: true,
zoomOut: true,
cropSquare: true,
cropWidescreen: true,
cropWidescreenVertical: true,
},
})
.use(XHRUpload, {
endpoint: ENTRYPOINT + "personal_files",
formData: true,
fieldName: "uploadFile",
})
watch(fileExistsOption, () => {
uppy.value.setOptions({
meta: {
fileExistsOption: fileExistsOption.value,
},
})
})
uppy.value.setMeta({
filetype: "file",
parentResourceNodeId: parentResourceNodeId.value,
function back() {
let queryParams = { cid, sid, gid, filetype, tab: route.query.tab }
if (route.query.tab) {
router.push({
name: 'FileManagerList',
params: { node: parentResourceNodeId.value },
query: queryParams,
})
uppy.value.on("complete", (result) => {
router.push({ name: "FileManagerList" });
});
return {
uppy,
currentUser: user,
isAdmin,
isAuthenticated,
}
},
mixins: [UploadMixin],
data() {
return {
files: [],
parentResourceNodeId: 0,
}
},
computed: {
...mapFields(["error", "isLoading", "created", "violations"]),
},
created() {
let nodeId = this.$route.params.node
if (isEmpty(nodeId)) {
nodeId = this.currentUser.resourceNode["id"]
}
this.parentResourceNodeId = Number(nodeId)
},
methods: {
...mapActions("personalfile", ["uploadMany", "createFile"]),
},
} else {
router.push({
name: 'FileManagerList',
params: { node: 0 },
query: queryParams,
})
}
}
</script>

@ -1,77 +1,42 @@
<template>
<div
v-if="isAuthenticated"
class="q-card"
>
<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
class="btn btn--primary"
icon="fa fa-folder-plus"
label="New folder"
@click="openNew"
/>
<Button
class="btn btn--primary"
icon="fa fa-file-upload"
label="Upload"
@click="uploadDocumentHandler()"
/>
<Button
class="btn btn--success"
icon="fa fa-file-upload"
label="Shared"
@click="sharedDocumentHandler()"
/>
<Button
v-if="selectedItems.length"
class="btn btn--danger"
icon="pi pi-trash"
label="Delete"
@click="confirmDeleteMultiple"
/>
<Button class="btn btn--primary" icon="fa fa-folder-plus" label="New folder" @click="openNewDialog" />
<Button class="btn btn--primary" icon="fa fa-file-upload" label="Upload" @click="uploadDocumentHandler" />
<Button v-if="selectedFiles.length" class="btn btn--danger" icon="pi pi-trash" label="Delete" @click="confirmDeleteMultiple" />
<Button v-if="previousFolders.length" class="btn btn--primary" icon="pi pi-arrow-left" label="Back" @click="goBack" />
</div>
</div>
</div>
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
v-model:selection="selectedFiles"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
:total-records="totalFiles"
:value="files"
class="p-datatable-sm"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onPage($event)"
@sort="sortingChanged($event)"
@page="onFilesPage"
@sort="sortingFilesChanged"
>
<Column
:header="$t('Title')"
:sortable="true"
field="resourceNode.title"
>
<Column :header="$t('Title')" :sortable="true" field="resourceNode.title">
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
<v-icon
v-if="slotProps.data.resourceLinkListFromEntity && slotProps.data.resourceLinkListFromEntity.length > 0"
icon="mdi-link"
/>
<v-icon v-if="slotProps.data.resourceLinkListFromEntity && slotProps.data.resourceLinkListFromEntity.length > 0" icon="mdi-link" />
</div>
<div v-else>
<a
v-if="slotProps.data"
class="cursor-pointer"
@click="handleClick(slotProps.data)"
>
<a v-if="slotProps.data" class="cursor-pointer" @click="handleClickFile(slotProps.data)">
<v-icon icon="mdi-folder" />
{{ slotProps.data.resourceNode.title }}
</a>
@ -79,11 +44,7 @@
</template>
</Column>
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.firstResourceFile.size"
>
<Column :header="$t('Size')" :sortable="true" field="resourceNode.firstResourceFile.size">
<template #body="slotProps">
{{
slotProps.data.resourceNode.firstResourceFile
@ -93,11 +54,7 @@
</template>
</Column>
<Column
:header="$t('Modified')"
:sortable="true"
field="resourceNode.updatedAt"
>
<Column :header="$t('Modified')" :sortable="true" field="resourceNode.updatedAt">
<template #body="slotProps">
{{ relativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
@ -106,23 +63,7 @@
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
class="btn btn--primary"
icon="pi pi-info-circle"
@click="showHandler(slotProps.data)"
/>
<Button
v-if="isAuthenticated"
class="btn btn--primary p-mr-2"
icon="pi pi-share-alt"
@click="editHandler(slotProps.data)"
/>
<Button
v-if="isAuthenticated"
class="btn btn--danger"
icon="pi pi-trash"
@click="confirmDeleteItem(slotProps.data)"
/>
<Button v-if="isAuthenticated" class="btn btn--danger" icon="pi pi-trash" @click="confirmDeleteItem(slotProps.data)" />
</div>
</template>
</Column>
@ -130,433 +71,104 @@
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button
v-if="isFromEditor"
class="p-button-sm p-button p-mr-2"
label="Select"
@click="returnToEditor(slotProps.data)"
/>
<Button v-if="isFromEditor" class="p-button-sm p-button p-mr-2" label="Select" @click="returnToEditor(slotProps.data)" />
</div>
</template>
</Column>
</DataTable>
<Dialog
v-model:visible="itemDialog"
:header="$t('New folder')"
:modal="true"
:style="{ width: '450px' }"
class="p-fluid"
>
<Dialog v-model:visible="dialog" :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
>
<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"
/>
<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"
>
<Dialog v-model:visible="deleteDialog" :modal="true" :style="{ width: '450px' }" header="Confirm">
<div class="confirmation-content">
<i
class="pi pi-exclamation-triangle p-mr-3"
style="font-size: 2rem"
></i>
<span
>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b
>?</span
>
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem"></i>
<span>Are you sure you want to delete <b>{{ itemToDelete?.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"
/>
<Button class="p-button-text" icon="pi pi-times" label="No" @click="deleteDialog = 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"
>
<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"
/>
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem" />
<span v-if="item">{{ $t("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"
/>
<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>
<Dialog
v-model:visible="detailsDialogVisible"
:header="selectedItem.title || 'Item Details'"
:modal="true"
:style="{ width: '50%' }"
>
<Dialog v-model:visible="detailsDialogVisible" :header="selectedItem.title || 'Item Details'" :modal="true" :style="{ width: '50%' }">
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.resourceNode.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p>
<strong>URL:</strong>
<a
:href="selectedItem.contentUrl"
target="_blank"
>Open File</a
>
</p>
<p><strong>URL:</strong> <a :href="selectedItem.contentUrl" target="_blank">Open File</a></p>
</div>
<template #footer>
<Button
class="p-button-text"
label="Close"
@click="closeDetailsDialog"
/>
<Button class="p-button-text" label="Close" @click="closeDetailsDialog" />
</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 ResourceIcon from "../../components/documents/ResourceIcon.vue"
import ResourceFileLink from "../../components/documents/ResourceFileLink.vue"
import DataFilter from "../../components/DataFilter"
import isEmpty from "lodash/isEmpty"
import { RESOURCE_LINK_PUBLISHED } from "../../components/resource_links/visibility"
import { useI18n } from "vue-i18n"
import { useFormatDate } from "../../composables/formatDate"
<script setup>
import { useFileManager } from '../../composables/useFileManager';
import { useI18n } from 'vue-i18n';
import { useFormatDate } from '../../composables/formatDate'
import prettyBytes from "pretty-bytes"
import { useSecurityStore } from "../../store/securityStore"
import { storeToRefs } from "pinia"
export default {
name: "PersonalFileList",
servicePrefix: "PersonalFile",
components: {
ActionCell,
ResourceIcon,
ResourceFileLink,
DataFilter,
},
mixins: [ListMixin],
setup() {
const { t } = useI18n()
const securityStore = useSecurityStore()
const { relativeDatetime } = useFormatDate()
const { user, isAuthenticated, isAdmin } = storeToRefs(securityStore)
const data = {
sortBy: "title",
sortDesc: false,
columnsQua: [
{ align: "left", name: "resourceNode.title", label: t("Title"), field: "resourceNode.title", sortable: true },
{
align: "left",
name: "resourceNode.updatedAt",
label: t("Modified"),
field: "resourceNode.updatedAt",
sortable: true,
},
{
name: "resourceNode.firstResourceFile.size",
label: t("Size"),
field: "resourceNode.firstResourceFile.size",
sortable: true,
},
{ name: "action", label: t("Actions"), field: "action", sortable: false },
],
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],
selected: [],
isBusy: false,
options: [],
selectedItems: [],
// prime vue
deleteMultipleDialog: false,
item: {},
filters: { shared: 0, loadNode: 1 },
submitted: false,
prettyBytes,
relativeDatetime,
t,
currentUser: user,
isAuthenticated,
isAdmin,
}
return data
},
created() {
this.resetList = true
this.onUpdateOptions(this.options)
this.isFromEditor = window.location.search.includes("editor=tinymce")
},
computed: {
// From crud.js list function
...mapGetters("resourcenode", {
resourceNode: "getResourceNode",
}),
...mapGetters("personalfile", {
items: "list",
}),
//...getters
// From ListMixin
...mapFields("personalfile", {
deletedResource: "deleted",
error: "error",
isLoading: "isLoading",
resetList: "resetList",
totalItems: "totalItems",
view: "view",
}),
},
data() {
return {
itemDialog: false,
detailsDialogVisible: false,
deleteItemDialog: false,
selectedItem: {},
itemToDelete: null,
isFromEditor: false,
}
},
methods: {
showHandler(item) {
this.selectedItem = item
this.detailsDialogVisible = true
},
closeDetailsDialog() {
this.detailsDialogVisible = false
},
// 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 {
let resourceNodeId = this.currentUser.resourceNode["id"]
if (!isEmpty(this.$route.params.node)) {
resourceNodeId = this.$route.params.node
}
this.item.filetype = "folder"
this.item.parentResourceNodeId = resourceNodeId
this.item.resourceLinkList = JSON.stringify([
{
gid: 0,
sid: 0,
cid: 0,
visibility: RESOURCE_LINK_PUBLISHED,
},
])
this.createWithFormData(this.item)
this.showMessage("Saved")
}
this.itemDialog = false
this.item = {}
}
},
editItem(item) {
this.item = { ...item }
this.itemDialog = true
},
confirmDeleteItem(item) {
console.log("confirmDeleteItem :::", item)
this.item = { ...item }
this.itemToDelete = { ...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
},
deleteItemButton() {
console.log("deleteItem", this.itemToDelete)
if (this.itemToDelete && this.itemToDelete.id) {
this.deleteItem(this.itemToDelete)
.then(() => {
this.$toast.add({
severity: "success",
summary: "Success",
detail: "Item deleted successfully",
life: 3000,
})
this.deleteItemDialog = false
this.itemToDelete = null
this.onUpdateOptions(this.options)
})
.catch((error) => {
console.error("Error deleting the item:", error)
this.$toast.add({
severity: "error",
summary: "Error",
detail: "An error occurred while deleting the item",
life: 3000,
})
})
} else {
console.error("No item to delete or item ID is missing")
}
},
onRowSelected(items) {
this.selected = items
},
selectAllRows() {
this.$refs.selectableTable.selectAllRows()
},
clearSelected() {
this.$refs.selectableTable.clearSelected()
},
returnToEditor(item) {
const url = item.contentUrl
// Tiny mce.
window.parent.postMessage(
{
url: url,
},
"*",
)
if (parent.tinymce) {
parent.tinymce.activeEditor.windowManager.close()
}
// Ckeditor
function getUrlParam(paramName) {
var reParam = new RegExp("(?:[\?&]|&amp;)" + paramName + "=([^&]+)", "i")
var match = window.location.search.match(reParam)
return match && match.length > 1 ? match[1] : ""
}
import ResourceFileLink from "../../components/documents/ResourceFileLink.vue"
var funcNum = getUrlParam("CKEditorFuncNum")
if (window.opener.CKEDITOR) {
window.opener.CKEDITOR.tools.callFunction(funcNum, url)
window.close()
}
},
async deleteSelected() {
console.log("deleteSelected")
this.deleteMultipleAction(this.selected)
this.onRequest({
pagination: this.pagination,
})
console.log("end -- deleteSelected")
},
...mapActions("personalfile", {
getPage: "fetchAll",
createWithFormData: "createWithFormData",
deleteItem: "del",
deleteMultipleAction: "delMultiple",
}),
...mapActions("resourcenode", {
findResourceNode: "findResourceNode",
}),
},
}
const { t } = useI18n();
const { relativeDatetime } = useFormatDate();
const {
files,
totalFiles,
isLoading,
selectedFiles,
dialog,
deleteDialog,
deleteMultipleDialog,
detailsDialogVisible,
selectedItem,
itemToDelete,
item,
submitted,
filters,
handleClickFile,
returnToEditor,
openNewDialog,
hideDialog,
saveItem,
confirmDeleteItem,
confirmDeleteMultiple,
deleteMultipleItems,
deleteItemButton,
onFilesPage,
sortingFilesChanged,
closeDetailsDialog,
uploadDocumentHandler,
onMountedCallback,
isAuthenticated,
showHandler,
editHandler,
goBack,
previousFolders
} = useFileManager('personalfile', '/api/personal_files', 'FileManagerUploadFile');
onMountedCallback();
</script>

@ -1171,9 +1171,9 @@ if ($form->validate()) {
$recipient_name = api_get_person_name($values['firstname'], $values['lastname']);
$textAfterRegistration =
'<p>'.
get_lang('Dear').' '.
get_lang('Dear', $userEntity->getLocale()).' '.
stripslashes(Security::remove_XSS($recipient_name)).',<br /><br />'.
get_lang('Your personal settings have been registered')."</p>";
get_lang('Your personal settings have been registered', $userEntity->getLocale())."</p>";
$formData = [
'button' => Display::button(
@ -1205,11 +1205,11 @@ if ($form->validate()) {
} else {
if (!empty($values['email'])) {
$linkDiagnostic = api_get_path(WEB_PATH).'main/search/search.php';
$textAfterRegistration .= '<p>'.get_lang('An e-mail has been sent to remind you of your login and password').'</p>';
$textAfterRegistration .= '<p>'.get_lang('An e-mail has been sent to remind you of your login and password', $userEntity->getLocale()).'</p>';
$diagnosticPath = '<a href="'.$linkDiagnostic.'" class="custom-link">'.$linkDiagnostic.'</a>';
$textAfterRegistration .= '<p>';
$textAfterRegistration .= sprintf(
get_lang('Welcome, please go to diagnostic at %s.'),
get_lang('Welcome, please go to diagnostic at %s.', $userEntity->getLocale()),
$diagnosticPath
);
$textAfterRegistration .= '</p>';

@ -315,6 +315,7 @@ class UserManager
$userId = $user->getId();
if (!empty($userId)) {
$userLocale = $user->getLocale();
if ($isAdmin) {
self::addUserAsAdmin($user);
}
@ -377,7 +378,7 @@ class UserManager
PERSON_NAME_EMAIL_ADDRESS
);
$tpl = Container::getTwig();
$emailSubject = $tpl->render('@ChamiloCore/Mailer/Legacy/subject_registration_platform.html.twig');
$emailSubject = $tpl->render('@ChamiloCore/Mailer/Legacy/subject_registration_platform.html.twig', ['locale' => $userLocale]);
$sender_name = api_get_person_name(
api_get_setting('administratorName'),
api_get_setting('administratorSurname'),
@ -405,6 +406,7 @@ class UserManager
'mailWebPath' => $url,
'new_user' => $user,
'search_link' => $url,
'locale' => $userLocale,
];
// ofaj

@ -201,16 +201,21 @@ class CkEditor extends Editor
if ($onlyPersonalfiles) {
if (null !== $user) {
$cidReqQuery = '';
if (null !== $course) {
$parentResourceNodeId = $course->getResourceNode()->getId();
$cidReqQuery = '&'.api_get_cidreq().'&parentResourceNodeId='.$parentResourceNodeId;
}
$resourceNodeId = $user->getResourceNode()->getId();
$url = api_get_path(WEB_PATH).'resources/filemanager/personal_list/'.$resourceNodeId;
$url = api_get_path(WEB_PATH).'resources/filemanager/personal_list/'.$resourceNodeId.'?loadNode=1'.$cidReqQuery;
}
} else {
if (null !== $course) {
$resourceNodeId = $course->getResourceNode()->getId();
$url = api_get_path(WEB_PATH).'resources/document/'.$resourceNodeId.'/manager?'.api_get_cidreq().'&type=images';
$url = api_get_path(WEB_PATH).'resources/document/'.$resourceNodeId.'/manager?'.api_get_cidreq();
} elseif (null !== $user) {
$resourceNodeId = $user->getResourceNode()->getId();
$url = api_get_path(WEB_PATH).'resources/filemanager/personal_list/'.$resourceNodeId;
$url = api_get_path(WEB_PATH).'resources/filemanager/personal_list/'.$resourceNodeId.'?loadNode=1';
}
}
@ -225,16 +230,16 @@ class CkEditor extends Editor
let fileManagerUrl = "'.$url.'";
if (fileType === "image") {
fileManagerUrl += "?type=images";
fileManagerUrl += "&type=images";
} else if (fileType === "file") {
fileManagerUrl += "?type=files";
fileManagerUrl += "&type=files";
}
tinymce.activeEditor.windowManager.openUrl({
title: "File Manager",
url: fileManagerUrl,
width: 950,
height: 450
width: 980,
height: 600
});
}
';

@ -1,21 +1,24 @@
{% set user_locale = locale is defined ? locale : 'en' %}
{% trans_default_domain 'messages' %}
{% autoescape false %}
<p>{{ 'Dear'|trans }} {{ complete_name }},</p>
<p>{{ 'Welcome to this platform'|trans }}</p>
<p>{{ 'You are registered to'|trans }} {{ 'platform.site_name' | api_get_setting }} {{ 'with the following settings:'|trans }}</p>
<p>{{ 'Username'|trans }} : {{ login_name }}<br>
{{ 'Pass'|trans }} : {{ original_password }}</p>
<p>{{ 'For more details visit %s'|trans|format(search_link) }}</p>
<p>{{ 'In case of trouble, contact us.'|trans }}</p>
<p>{{ 'Dear'|trans({}, 'messages', user_locale) }} {{ complete_name }},</p>
<p>{{ 'Welcome to this platform'|trans({}, 'messages', user_locale) }}</p>
<p>{{ 'You are registered to'|trans({}, 'messages', user_locale) }} {{ 'platform.site_name' | api_get_setting }} {{ 'with the following settings:'|trans({}, 'messages', user_locale) }}</p>
<p>{{ 'Username'|trans({}, 'messages', user_locale) }} : {{ login_name }}<br>
{{ 'Pass'|trans({}, 'messages', user_locale) }} : {{ original_password }}</p>
<p>{{ 'For more details visit %s'|trans({'%s': search_link}, 'messages', user_locale) }}</p>
<p>{{ 'In case of trouble, contact us.'|trans({}, 'messages', user_locale) }}</p>
<p>{{ 'Sincerely'|trans }}</p>
<p>{{ 'Sincerely'|trans({}, 'messages', user_locale) }}</p>
<p>{{ 'admin.administrator_name' | api_get_setting }} {{ 'admin.administrator_surname' | api_get_setting }}<br />
{{ 'Manager'|trans }} {{ 'platform.site_name' | api_get_setting }}<br />
{{ 'Manager'|trans({}, 'messages', user_locale) }} {{ 'platform.site_name' | api_get_setting }}<br />
{% if 'admin.administrator_phone' | api_get_setting %}
{{ 'T. ' ~ 'admin.administrator_phone' | api_get_setting }}<br />
{% endif %}
{% if 'admin.administrator_email' | api_get_setting %}
{{ 'Email'|trans ~ ': ' ~ 'admin.administrator_email' | api_get_setting }}
{{ 'Email'|trans({}, 'messages', user_locale) ~ ': ' ~ 'admin.administrator_email' | api_get_setting }}
{% endif %}
</p>
{% endautoescape %}

@ -1 +1,4 @@
{{ '['~ 'platform.site_name' | api_get_setting ~ '] ' ~ 'Your Reg'|trans ~ ' ' ~ 'platform.site_name' | api_get_setting }}
{% set user_locale = locale is defined ? locale : 'en' %}
{% trans_default_domain 'messages' %}
{{ '['~ 'platform.site_name' | api_get_setting ~ '] ' ~ 'Your Reg'|trans({}, 'messages', user_locale) ~ ' ' ~ 'platform.site_name' | api_get_setting }}

Loading…
Cancel
Save