commit
f861532b76
@ -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> |
||||
@ -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('(?:[\\?&]|&)' + 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 |
||||
}; |
||||
} |
||||
@ -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("(?:[\?&]|&)" + 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,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…
Reference in new issue