Skip to content

Commit

Permalink
Internal: Fix subfolder navigation, file display, and thumbnail consi…
Browse files Browse the repository at this point in the history
…stency in File Manager - refs BT#21647
  • Loading branch information
christianbeeznest committed Jul 24, 2024
1 parent 3461152 commit f4914ff
Show file tree
Hide file tree
Showing 12 changed files with 1,300 additions and 1,127 deletions.
43 changes: 43 additions & 0 deletions assets/css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
56 changes: 56 additions & 0 deletions assets/css/scss/_documents.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
36 changes: 36 additions & 0 deletions assets/vue/components/basecomponents/BaseContextMenu.vue
Original file line number Diff line number Diff line change
@@ -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>
223 changes: 223 additions & 0 deletions assets/vue/components/filemanager/CourseDocuments.vue
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit f4914ff

Please sign in to comment.