Add missing actions to document tool

* Create composable to detect files from generic data about documents
* Add disable state for base button
* Create Base dialog components to reuse common visualization
* Create button toolbar
* Add new size for icons
pull/4726/head
Daniel Gayoso González 2 years ago
parent 1db210073b
commit a0dc565a5b
  1. 7
      assets/vue/components/basecomponents/BaseButton.vue
  2. 14
      assets/vue/components/basecomponents/BaseChart.vue
  3. 33
      assets/vue/components/basecomponents/BaseDialog.vue
  4. 61
      assets/vue/components/basecomponents/BaseDialogConfirmCancel.vue
  5. 4
      assets/vue/components/basecomponents/BaseIcon.vue
  6. 32
      assets/vue/components/basecomponents/ButtonToolbar.vue
  7. 35
      assets/vue/components/basecomponents/ChamiloIcons.js
  8. 39
      assets/vue/components/documents/DocumentEntry.vue
  9. 5
      assets/vue/components/documents/DocumentsLayout.vue
  10. 22
      assets/vue/composables/fileUtils.js
  11. 4
      assets/vue/router/documents.js
  12. 1
      assets/vue/views/course/CourseHome.vue
  13. 4
      assets/vue/views/documents/DocumentForHtmlEditor.vue
  14. 587
      assets/vue/views/documents/DocumentsList.vue
  15. 486
      assets/vue/views/documents/List.vue
  16. 2
      assets/vue/views/documents/ListQuasar.vue
  17. 5
      tailwind.config.js

@ -93,15 +93,16 @@ const buttonClass = computed(() => {
case "small":
result += "py-2 px-3.5 "
}
let commonDisabled = "disabled:bg-primary-bgdisabled disabled:border disabled:border-primary-borderdisabled disabled:text-fontdisabled"
switch (props.type) {
case "primary":
result += "border-primary hover:bg-primary text-primary hover:text-white ";
result += `border-primary hover:bg-primary text-primary hover:text-white ${commonDisabled} `;
break;
case "secondary":
result += "bg-secondary hover:bg-secondary-gradient text-white ";
result += "bg-secondary text-white hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled";
break;
case "danger":
result += "border-error hover:bg-error text-error hover:text-white ";
result += `border-error hover:bg-error text-error hover:text-white ${commonDisabled} `;
}
return result;
});

@ -0,0 +1,14 @@
<template>
<PrimeChart type="pie" :data="data" :options="{}" class="w-full md:w-30rem" />
</template>
<script setup>
import PrimeChart from 'primevue/chart'
defineProps({
data: {
type: Object,
required: true,
},
})
</script>

@ -0,0 +1,33 @@
<template>
<Dialog
:header="title"
:modal="true"
:style="{width: '450px'}"
:visible="isVisible"
class="p-fluid"
@update:visible="$emit('update:isVisible', $event)"
>
<slot></slot>
<template #footer>
<slot name="footer"></slot>
</template>
</Dialog>
</template>
<script setup>
import Dialog from 'primevue/dialog'
defineProps({
title: {
type: String,
required: true,
},
isVisible: {
type: Boolean,
required: true,
}
})
defineEmits(['update:isVisible'])
</script>

@ -0,0 +1,61 @@
<template>
<BaseDialog
:title="title"
:is-visible="isVisible"
@update:is-visible="$emit('update:isVisible', $event)"
>
<slot></slot>
<template #footer>
<BaseButton
:label="innerCancelLabel"
type="black"
icon="close"
@click="$emit('cancelClicked')"
/>
<BaseButton
:label="innerConfirmLabel"
type="secondary"
icon="confirm"
@click="$emit('confirmClicked')"
/>
</template>
</BaseDialog>
</template>
<script setup>
import BaseDialog from "./BaseDialog.vue";
import BaseButton from "./BaseButton.vue";
import {computed} from "vue";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
const props = defineProps({
title: {
type: String,
required: true,
},
isVisible: {
type: Boolean,
required: true,
},
confirmLabel: {
type: String,
default: '',
},
cancelLabel: {
type: String,
default: '',
},
})
defineEmits(['update:isVisible', 'confirmClicked', 'cancelClicked'])
const innerConfirmLabel = computed(() => {
return props.confirmLabel === "" ? t('Yes') : props.confirmLabel
})
const innerCancelLabel = computed(() => {
return props.cancelLabel === "" ? t('No') : props.cancelLabel
})
</script>

@ -26,6 +26,7 @@ const props = defineProps({
return false
}
return [
"big",
"normal",
"small",
].includes(value);
@ -36,6 +37,9 @@ const props = defineProps({
const iconClass = computed(() => {
let iconClass = chamiloIconToClass[props.icon] + " ";
switch (props.size) {
case "big":
iconClass += "text-3xl/4 ";
break;
case "normal":
iconClass += "text-xl/4 ";
break;

@ -0,0 +1,32 @@
<template>
<Toolbar :class="toolbarClass">
<template #start>
<div class="flex flex-wrap">
<slot></slot>
</div>
</template>
</Toolbar>
</template>
<script setup>
import Toolbar from "primevue/toolbar";
import {computed} from "vue";
const props = defineProps({
showTopBorder: {
type: Boolean,
default: false,
}
})
const toolbarClass = computed(() => {
if (props.showTopBorder) {
return 'pt-5 border-t border-b';
}
return '';
})
</script>
<style scoped>
</style>

@ -3,15 +3,20 @@
*
* Transform name of icons according to https://github.com/chamilo/chamilo-lms/wiki/Graphical-design-guide#default-icons-terminology
* to the classes needed for represent every icon
*/
*/
export const chamiloIconToClass = {
"edit": "mdi mdi-pencil",
"delete": "mdi mdi-delete",
"back": "mdi mdi-arrow-left-bold-box",
"close": "mdi mdi-close",
"confirm": "mdi mdi-check",
"select-all": "mdi mdi-select-group",
"unselect-all": "mdi mdi-select-remove",
"camera": "mdi mdi-camera",
"record-generic": "mdi mdi-microphone",
"record-add": "mdi mdi-microphone-plus",
"download": "mdi mdi-download-box",
"hammer-wrench": "",
"download": "",
"download-box": "",
"upload": "",
"arrow-left-bold-box": "",
"account-multiple-plus": "",
"cursor-move": "",
"chevron-left": "",
@ -21,7 +26,7 @@ export const chamiloIconToClass = {
"arrow-right-bold": "",
"magnify-plus-outline": "",
"archive-arrow-up": "",
"alert": "",
"alert": "mdi mdi-alert",
"checkbox-marked": "",
"pencil-off": "",
"eye-on": "mdi mdi-eye",
@ -40,11 +45,8 @@ export const chamiloIconToClass = {
"package": "",
"text-box-plus": "",
"rocket-launch": "",
"file-pdf-box": "",
"content-save": "",
"send": "",
"file-plus": "",
"cloud-upload": "",
"dots-vertical": "",
"information": "mdi mdi-information",
"account-key": "",
@ -55,15 +57,22 @@ export const chamiloIconToClass = {
"file-video": "mdi mdi-file-video",
"file-pdf": "mdi mdi-file-pdf-box",
"file-text": "mdi mdi-file-document",
"file-add": "mdi mdi-file-plus",
"file-upload": "mdi mdi-file-upload",
"file-cloud-add": "mdi mdi-cloud-plus",
"folder-generic": "mdi mdi-folder",
"folder-multiple-plus": "mdi mdi-folder-multiple-plus",
"folder-plus": "mdi mdi-folder-plus",
"folder-open": "mdi mdi-folder-open",
"drawing": "mdi mdi-drawing",
"view-gallery": "mdi mdi-view-gallery",
"usage": "mdi mdi-chart-donut",
};
export const validator = (value) => {
if (typeof (value) !== "string") {
return false;
}
if (typeof (value) !== "string") {
return false;
}
return Object.keys(chamiloIconToClass).includes(value);
return Object.keys(chamiloIconToClass).includes(value);
};

@ -1,12 +1,12 @@
<template>
<div v-if="data && data.resourceNode && data.resourceNode.resourceFile">
<div v-if="isFile">
<a
data-fancybox="gallery"
class="flex align-center"
:href="data.contentUrl"
:data-type="dataType"
>
<ResourceIcon class="mr-2" :resource-data="data" />
<ResourceIcon class="mr-2" :resource-data="data"/>
{{ data.title }}
</a>
</div>
@ -15,8 +15,8 @@
class="flex align-center"
:to="{
name: 'DocumentsList',
params: { node: data.resourceNode.id },
query: folderParams,
params: {node: props.data.resourceNode.id},
query: cidQuery,
}"
>
<ResourceIcon class="mr-2" :resource-data="data"/>
@ -26,28 +26,37 @@
</template>
<script setup>
import ResourceIcon from "./ResourceIcon.vue";
import {computed} from "vue";
import ResourceIcon from "./ResourceIcon.vue"
import {computed} from "vue"
import {useCidReq} from '../../composables/cidReq'
import {useFileUtils} from "../../composables/fileUtils";
const props = defineProps({
data: {
type: Object,
required: true,
},
});
})
const cidQuery = useCidReq()
const {isFile: utilsIsFile, isImage, isVideo} = useFileUtils()
const dataType = computed(() => {
let resourceFile = props.data.resourceNode.resourceFile;
if (resourceFile === null) {
return '';
if (!utilsIsFile(props.data)) {
return ''
}
if (resourceFile.image) {
return 'image';
if (isImage(props.data)) {
return 'image'
}
if (resourceFile.video) {
return 'video';
if (isVideo(props.data)) {
return 'video'
}
return 'iframe';
});
})
const isFile = computed(() => {
return props.data && utilsIsFile(props.data)
})
</script>

@ -2,8 +2,5 @@
<router-view></router-view>
</template>
<script>
export default {
name: 'DocumentsLayout'
}
<script setup >
</script>

@ -0,0 +1,22 @@
export function useFileUtils() {
const isImage = (fileData) => {
return isFile(fileData) && fileData.resourceNode.resourceFile.image
}
const isVideo = (fileData) => {
return isFile(fileData) && fileData.resourceNode.resourceFile.video
}
const isFile = (fileData) => {
return fileData.resourceNode && fileData.resourceNode.resourceFile
}
return {
isFile,
isImage,
isVideo,
}
}

@ -2,13 +2,13 @@ export default {
path: '/resources/document/:node/',
meta: { requiresAuth: true, showBreadcrumb: true },
name: 'documents',
component: () => import('../components/documents/Layout.vue'),
component: () => import('../components/documents/DocumentsLayout.vue'),
redirect: { name: 'DocumentsList' },
children: [
{
name: 'DocumentsList',
path: '',
component: () => import('../views/documents/List.vue')
component: () => import('../views/documents/DocumentsList.vue')
},
{
name: 'DocumentsCreate',

@ -34,6 +34,7 @@
<BaseButton
v-if="showUpdateIntroductionButton"
:label="t('Edit introduction')"
type="black"
icon="edit"
@click="updateIntro(intro)"
/>

@ -152,11 +152,11 @@ export default {
};
},
created() {
console.log('created - vue/views/documents/List.vue');
console.log('created - vue/views/documents/DocumentsList.vue');
this.filters['loadNode'] = 1;
},
mounted() {
console.log('mounted - vue/views/documents/List.vue');
console.log('mounted - vue/views/documents/DocumentsList.vue');
this.filters['loadNode'] = 1;
this.onUpdateOptions(this.options);
},

@ -0,0 +1,587 @@
<template>
<ButtonToolbar v-if="isAuthenticated && isCurrentTeacher">
<BaseButton
v-if="showBackButtonIfNotRootFolder"
:label="t('Back')"
type="black"
class="mr-2 mb-2"
icon="back"
@click="back"
/>
<BaseButton
:label="t('New document')"
type="black"
class="mr-2 mb-2"
icon="file-add"
@click="goToNewDocument"
/>
<BaseButton
:disabled="true"
:label="t('New drawing')"
class="mr-2 mb-2"
type="black"
icon="drawing"
/>
<BaseButton
:disabled="true"
:label="t('Record audio')"
class="mr-2 mb-2"
type="black"
icon="record-add"
/>
<BaseButton
:label="t('Upload')"
type="black"
class="mr-2 mb-2"
icon="file-upload"
@click="goToUploadFile"
/>
<BaseButton
:label="t('New folder')"
type="black"
class="mr-2 mb-2"
icon="folder-plus"
@click="openNew"
/>
<BaseButton
:disabled="true"
:label="t('New cloud file')"
class="mr-2 mb-2"
type="black"
icon="file-cloud-add"
/>
<BaseButton
:disabled="!hasImageInDocumentEntries"
:label="t('Slideshow')"
class="mr-2 mb-2"
type="black"
icon="view-gallery"
@click="showSlideShowWithFirstImage"
/>
<BaseButton
:label="t('Usage')"
type="black"
class="mr-2 mb-2"
icon="usage"
@click="showUsageDialog"
/>
<BaseButton
:disabled="true"
:label="t('Download all')"
type="black"
class="mr-2 mb-2"
icon="download"
/>
</ButtonToolbar>
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="options.itemsPerPage"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
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"
striped-rows
class="mb-5"
@page="onPage($event)"
@sort="sortingChanged($event)"
>
<Column
v-if="isCurrentTeacher"
:exportable="false"
selection-mode="multiple"
/>
<Column
:header="t('Title')"
:sortable="true"
field="resourceNode.title"
>
<template #body="slotProps">
<DocumentEntry
v-if="slotProps.data"
:data="slotProps.data"
/>
</template>
</Column>
<Column
:header="t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile ? $filters.prettyBytes(slotProps.data.resourceNode.resourceFile.size) :
''
}}
</template>
</Column>
<Column
:header="t('Modified')"
:sortable="true"
field="resourceNode.updatedAt"
>
<template #body="slotProps">
{{ useRelativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
</Column>
<Column
:exportable="false"
>
<template #body="slotProps">
<div class="flex flex-row justify-end gap-2">
<BaseButton
type="black"
icon="information"
size="small"
@click="btnShowInformationOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="black"
:icon="RESOURCE_LINK_PUBLISHED === slotProps.data.resourceLinkListFromEntity[0].visibility ? 'eye-on' : (RESOURCE_LINK_DRAFT === slotProps.data.resourceLinkListFromEntity[0].visibility ? 'eye-off' : '')"
size="small"
@click="btnChangeVisibilityOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="black"
icon="edit"
size="small"
@click="btnEditOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="danger"
icon="delete"
size="small"
@click="confirmDeleteItem(slotProps.data)"
/>
</div>
</template>
</Column>
</DataTable>
<ButtonToolbar
v-if="isAuthenticated && isCurrentTeacher"
show-top-border
>
<BaseButton
:label="t('Select all')"
class="mr-2 mb-2"
type="black"
icon="select-all"
@click="selectAll"
/>
<BaseButton
:label="t('Unselect all')"
class="mr-2 mb-2"
type="black"
icon="unselect-all"
@click="unselectAll"
/>
<BaseButton
:disabled="!selectedItems || !selectedItems.length"
:label="t('Delete selected')"
class="mr-2 mb-2"
type="danger"
icon="delete"
@click="showDeleteMultipleDialog"
/>
</ButtonToolbar>
<BaseDialogConfirmCancel
v-model:is-visible="isNewFolderDialogVisible"
:title="t('New folder')"
:confirm-label="t('Save')"
:cancel-label="t('Cancel')"
@confirm-clicked="createNewFolder"
@cancel-clicked="hideNewFolderDialog"
>
<div class="p-float-label">
<InputText
id="title"
v-model.trim="item.title"
:class="{ 'p-invalid': submitted && !item.title }"
autocomplete="off"
autofocus
name="name"
required="true"
/>
<label
v-t="'Name'"
for="name"
/>
</div>
<small
v-if="submitted && !item.title"
v-t="'Title is required'"
class="p-error"
/>
</BaseDialogConfirmCancel>
<BaseDialogConfirmCancel
v-model:is-visible="isDeleteItemDialogVisible"
:title="t('Confirm')"
@confirm-clicked="deleteSingleItem"
@cancel-clicked="isDeleteItemDialogVisible = false"
>
<div class="confirmation-content">
<BaseIcon icon="alert" size="big" class="mr-2" />
<span v-if="item">{{ t('Are you sure you want to delete') }} <b>{{ item.title }}</b>?</span>
</div>
</BaseDialogConfirmCancel>
<BaseDialogConfirmCancel
v-model:is-visible="isDeleteMultipleDialogVisible"
:title="t('Confirm')"
@confirm-clicked="deleteMultipleItems"
@cancel-clicked="isDeleteMultipleDialogVisible = false"
>
<div class="confirmation-content">
<BaseIcon icon="alert" size="big" class="mr-2" />
<span v-if="item">{{ t('Are you sure you want to delete the selected items?') }}</span>
</div>
</BaseDialogConfirmCancel>
<BaseDialog
v-model:is-visible="isFileUsageDialogVisible"
:title="t('Space available')"
>
<p>This feature is in development, this is a mockup with placeholder data!</p>
<BaseChart
:data="usageData"
/>
</BaseDialog>
</template>
<script setup>
import {useStore} from 'vuex'
import {RESOURCE_LINK_DRAFT, RESOURCE_LINK_PUBLISHED} from '../../components/resource_links/visibility'
import {isEmpty} from 'lodash'
import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {computed, onMounted, ref, watch} from 'vue'
import {useCidReq} from '../../composables/cidReq'
import {useDatatableList} from '../../composables/datatableList'
import {useRelativeDatetime} from '../../composables/formatDate'
import axios from 'axios'
import {useToast} from 'primevue/usetoast';
import DocumentEntry from "../../components/documents/DocumentEntry.vue";
import BaseButton from "../../components/basecomponents/BaseButton.vue";
import ButtonToolbar from "../../components/basecomponents/ButtonToolbar.vue";
import BaseIcon from "../../components/basecomponents/BaseIcon.vue";
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue";
import {useFileUtils} from "../../composables/fileUtils";
import BaseDialog from "../../components/basecomponents/BaseDialog.vue";
import BaseChart from "../../components/basecomponents/BaseChart.vue";
const store = useStore()
const route = useRoute()
const router = useRouter()
const {t} = useI18n()
const {filters, options, onUpdateOptions, deleteItem} = useDatatableList('Documents')
const toast = useToast();
const {cid, sid, gid} = useCidReq()
const {isImage} = useFileUtils()
store.dispatch('course/findCourse', {id: `/api/courses/${cid}`})
if (sid) {
store.dispatch('session/findSession', {id: `/api/sessions/${sid}`})
}
const item = ref({})
const usageData = ref({})
const isNewFolderDialogVisible = ref(false)
const isDeleteItemDialogVisible = ref(false)
const isDeleteMultipleDialogVisible = ref(false)
const isFileUsageDialogVisible = ref(false)
const submitted = ref(false)
filters.value.loadNode = 1
const selectedItems = ref([])
const isAuthenticated = computed(() => store.getters['security/isAuthenticated'])
const isCurrentTeacher = computed(() => store.getters['security/isCurrentTeacher'])
const items = computed(() => store.getters['documents/getRecents'])
const isLoading = computed(() => store.getters['documents/isLoading'])
const totalItems = computed(() => store.getters['documents/getTotalItems'])
const resourceNode = computed(() => store.getters['resourcenode/getResourceNode'])
const hasImageInDocumentEntries = computed(() => {
return items.value.find(i => isImage(i)) !== undefined
})
onMounted(() => {
filters.value.loadNode = 1
// Set resource node.
let nodeId = route.params.node
if (isEmpty(nodeId)) {
nodeId = route.query.node
}
store.dispatch('resourcenode/findResourceNode', {id: `/api/resource_nodes/${nodeId}`});
onUpdateOptions(options.value)
});
watch(
() => route.params,
() => {
const nodeId = route.params.node
const finderParams = {id: `/api/resource_nodes/${nodeId}`, cid, sid, gid};
store.dispatch('resourcenode/findResourceNode', finderParams);
if ('DocumentsList' === route.name) {
onUpdateOptions(options.value);
}
}
);
const showBackButtonIfNotRootFolder = computed(() => {
if (!resourceNode.value) {
return false;
}
return resourceNode.value.resourceType.name !== 'courses';
})
function back() {
if (!resourceNode.value) {
return;
}
let parent = resourceNode.value.parent;
if (parent) {
let queryParams = {cid, sid, gid}
router.push({name: 'DocumentsList', params: {node: parent.id}, query: queryParams});
}
}
function openNew() {
item.value = {}
submitted.value = false
isNewFolderDialogVisible.value = true
}
function hideNewFolderDialog() {
isNewFolderDialogVisible.value = false
submitted.value = false
}
function createNewFolder() {
submitted.value = true
if (item.value.title?.trim()) {
if (!item.value.id) {
item.value.filetype = 'folder'
item.value.parentResourceNodeId = route.params.node
item.value.resourceLinkList = JSON.stringify([{
gid,
sid,
cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
}])
store.dispatch('documents/createWithFormData', item.value)
.then(() => {
toast.add({
severity: 'success',
detail: t('Saved'),
life: 3500,
})
onUpdateOptions(options.value)
})
}
isNewFolderDialogVisible.value = false
item.value = {}
}
}
function selectAll() {
selectedItems.value = items.value;
}
function showDeleteMultipleDialog() {
isDeleteMultipleDialogVisible.value = true
}
function confirmDeleteItem (itemToDelete) {
item.value = itemToDelete
isDeleteItemDialogVisible.value = true
}
function deleteMultipleItems () {
store.dispatch('documents/delMultiple', selectedItems.value)
.then(() => {
isDeleteMultipleDialogVisible.value = false
unselectAll()
})
onUpdateOptions(options.value)
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
}
function unselectAll () {
selectedItems.value = [];
}
function deleteSingleItem() {
deleteItem(item)
item.value = {}
isDeleteItemDialogVisible.value = false
}
function onPage (event) {
options.value = {
itemsPerPage: event.rows,
page: event.page + 1,
sortBy: event.sortField,
sortDesc: event.sortOrder === -1
}
onUpdateOptions(options.value)
}
function sortingChanged (event) {
options.value.sortBy = event.sortField
options.value.sortDesc = event.sortOrder === -1
onUpdateOptions(options.value)
}
function goToNewDocument () {
router.push({
name: 'DocumentsCreateFile',
query: route.query,
})
}
function goToUploadFile () {
router.push({
name: 'DocumentsUploadFile',
query: route.query
})
}
function btnShowInformationOnClick (item) {
const folderParams = route.query;
if (item) {
folderParams.id = item['@id'];
}
router.push({
name: 'DocumentsShow',
params: folderParams,
query: folderParams
});
}
function btnChangeVisibilityOnClick (item) {
const folderParams = route.query;
folderParams.id = item['@id'];
axios
.put(item['@id'] + '/toggle_visibility')
.then(response => {
item.resourceLinkListFromEntity = response.data.resourceLinkListFromEntity;
})
;
}
function btnEditOnClick (item) {
const folderParams = route.query;
folderParams.id = item['@id'];
if ('folder' === item.filetype || isEmpty(item.filetype)) {
router.push({
name: 'DocumentsUpdate',
params: {id: item['@id']},
query: folderParams,
});
return;
}
if ('file' === item.filetype) {
folderParams.getFile = true;
if (item.resourceNode.resourceFile
&& item.resourceNode.resourceFile.mimeType
&& 'text/html' === item.resourceNode.resourceFile.mimeType
) {
//folderParams.getFile = true;
}
router.push({
name: 'DocumentsUpdateFile',
params: {id: item['@id']},
query: folderParams
});
}
}
function showSlideShowWithFirstImage() {
let item = items.value.find(i => isImage(i))
if (item === undefined) { return }
// Right now Vue prime datatable does not offer a method to click on a row in a table
// https://primevue.org/datatable/#api.datatable.methods
// so we click on the dom element that has the href on the item
document.querySelector(`a[href='${item.contentUrl}']`).click()
// start slideshow trusting the button to play is present
document.querySelector('button[class="fancybox-button fancybox-button--play"]').click()
}
function showUsageDialog() {
// TODO retrieve usage data from server
usageData.value = {
datasets: [{
data: [83, 14, 5],
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)'
],
}],
labels: ['Course', 'Teacher', 'Available space'],
}
isFileUsageDialogVisible.value = true
}
</script>

@ -1,486 +0,0 @@
<template>
<Toolbar v-if="isAuthenticated && isCurrentTeacher">
<template #start>
<Button
:label="t('New folder')"
class="p-button-plain p-button-outlined"
icon="mdi mdi-folder-plus"
@click="openNew"
/>
<Button
:label="t('New document')"
class="p-button-plain p-button-outlined"
icon="mdi mdi-file-plus"
@click="goToNewDocument"
/>
<Button
:label="t('Upload')"
class="p-button-plain p-button-outlined"
icon="mdi mdi-file-upload"
@click="goToUploadFile"
/>
<!--
<Button label="{{ $t('Download') }}" class="btn btn--primary" @click="downloadDocumentHandler()" :disabled="!selectedItems || !selectedItems.length">
<v-icon icon="mdi-file-download"/>
{{ $t('Download') }}
</Button>
-->
<Button
:disabled="!selectedItems || !selectedItems.length"
:label="t('Delete selected')"
class="p-button-danger p-button-outlined"
icon="mdi mdi-delete"
@click="confirmDeleteMultiple"
/>
</template>
</Toolbar>
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="options.itemsPerPage"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
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"
striped-rows
@page="onPage($event)"
@sort="sortingChanged($event)"
>
<Column
v-if="isCurrentTeacher"
:exportable="false"
selection-mode="multiple"
/>
<Column
:header="t('Title')"
:sortable="true"
field="resourceNode.title"
>
<template #body="slotProps">
<DocumentEntry
v-if="slotProps.data"
:data="slotProps.data"
/>
</template>
</Column>
<Column
:header="t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile ? $filters.prettyBytes(slotProps.data.resourceNode.resourceFile.size) : ''
}}
</template>
</Column>
<Column
:header="t('Modified')"
:sortable="true"
field="resourceNode.updatedAt"
>
<template #body="slotProps">
{{ useRelativeDatetime(slotProps.data.resourceNode.updatedAt) }}
</template>
</Column>
<Column
:exportable="false"
>
<template #body="slotProps">
<div class="flex flex-row justify-end gap-2">
<BaseButton
type="black"
icon="information"
size="small"
@click="btnShowInformationOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="black"
:icon="RESOURCE_LINK_PUBLISHED === slotProps.data.resourceLinkListFromEntity[0].visibility ? 'eye-on' : (RESOURCE_LINK_DRAFT === slotProps.data.resourceLinkListFromEntity[0].visibility ? 'eye-off' : '')"
size="small"
@click="btnChangeVisibilityOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="black"
icon="edit"
size="small"
@click="btnEditOnClick(slotProps.data)"
/>
<BaseButton
v-if="isAuthenticated && isCurrentTeacher"
type="danger"
icon="delete"
size="small"
@click="confirmDeleteItem(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-float-label">
<InputText
id="title"
v-model.trim="item.title"
:class="{ 'p-invalid': submitted && !item.title }"
autocomplete="off"
autofocus
name="name"
required="true"
/>
<label
v-t="'Name'"
for="name"
/>
</div>
<small
v-if="submitted && !item.title"
v-t="'Title is required'"
class="p-error"
/>
<template #footer>
<Button
class="p-button-outlined p-button-plain"
icon="pi pi-times"
label="Cancel"
@click="hideDialog"
/>
<Button
class="p-button-secondary"
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"
/>
<span v-if="item">Are you sure you want to delete <b>{{ item.title }}</b>?</span>
</div>
<template #footer>
<Button
:label="t('No')"
class="p-button-outlined p-button-plain"
icon="pi pi-times"
@click="deleteItemDialog = false"
/>
<Button
:label="t('Yes')"
class="p-button-secondary"
icon="pi pi-check"
@click="btnCofirmSingleDeleteOnClick"
/>
</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>
<template #footer>
<Button
class="p-button-outlined p-button-plain"
icon="pi pi-times"
label="No"
@click="deleteMultipleDialog = false"
/>
<Button
class="p-button-secondary"
icon="pi pi-check"
label="Yes"
@click="deleteMultipleItems"
/>
</template>
</Dialog>
</template>
<script setup>
import { useStore } from 'vuex'
import { RESOURCE_LINK_DRAFT, RESOURCE_LINK_PUBLISHED } from '../../components/resource_links/visibility'
import { isEmpty } from 'lodash'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import Toolbar from 'primevue/toolbar'
import Dialog from 'primevue/dialog'
import { computed, onMounted, ref, watch } from 'vue'
import { useCidReq } from '../../composables/cidReq'
import { useDatatableList } from '../../composables/datatableList'
import { useRelativeDatetime } from '../../composables/formatDate'
import axios from 'axios'
import { useToast } from 'primevue/usetoast';
import DocumentEntry from "../../components/documents/DocumentEntry.vue";
import BaseButton from "../../components/basecomponents/BaseButton.vue";
const store = useStore()
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const { filters, options, onUpdateOptions, deleteItem } = useDatatableList('Documents')
const toast = useToast();
const { cid, sid, gid } = useCidReq()
store.dispatch('course/findCourse', { id: `/api/courses/${cid}` })
if (sid) {
store.dispatch('session/findSession', { id: `/api/sessions/${sid}` })
}
const item = ref({})
const itemDialog = ref(false)
const deleteItemDialog = ref(false)
const deleteMultipleDialog = ref(false)
const submitted = ref(false)
filters.value.loadNode = 1
const selectedItems = ref([])
const isAuthenticated = computed(() => store.getters['security/isAuthenticated'])
const isCurrentTeacher = computed(() => store.getters['security/isCurrentTeacher'])
const items = computed(() => store.getters['documents/getRecents'])
const isLoading = computed(() => store.getters['documents/isLoading'])
const totalItems = computed(() => store.getters['documents/getTotalItems'])
onMounted(() => {
filters.value.loadNode = 1
// Set resource node.
let nodeId = route.params.node
if (isEmpty(nodeId)) {
nodeId = route.query.node
}
store.dispatch('resourcenode/findResourceNode', { id: `/api/resource_nodes/${nodeId}` });
onUpdateOptions(options.value)
});
watch(
() => route.params,
() => {
const nodeId = route.params.node
const finderParams = { id: `/api/resource_nodes/${nodeId}`, cid, sid, gid };
store.dispatch('resourcenode/findResourceNode', finderParams);
if ('DocumentsList' === route.name) {
onUpdateOptions(options.value);
}
}
);
function openNew () {
item.value = {}
submitted.value = false
itemDialog.value = true
}
function hideDialog () {
itemDialog.value = false
submitted.value = false
}
function saveItem () {
submitted.value = true
if (item.value.title?.trim()) {
if (!item.value.id) {
item.value.filetype = 'folder'
item.value.parentResourceNodeId = route.params.node
item.value.resourceLinkList = JSON.stringify([{
gid,
sid,
cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
}])
store.dispatch('documents/createWithFormData', item.value)
.then(() => {
toast.add({
severity: 'success',
detail: t('Saved'),
life: 3500,
})
onUpdateOptions(options.value)
})
}
itemDialog.value = false
item.value = {}
}
}
function confirmDeleteMultiple () {
deleteMultipleDialog.value = true
}
function confirmDeleteItem (itemToDelete) {
item.value = itemToDelete
deleteItemDialog.value = true
}
function deleteMultipleItems () {
store.dispatch('documents/delMultiple', selectedItems.value)
.then(() => {
deleteMultipleDialog.value = false
selectedItems.value = []
})
onUpdateOptions(options.value)
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
}
function btnCofirmSingleDeleteOnClick () {
deleteItem(item)
item.value = {}
deleteItemDialog.value = false
}
function onPage (event) {
options.value = {
itemsPerPage: event.rows,
page: event.page + 1,
sortBy: event.sortField,
sortDesc: event.sortOrder === -1
}
onUpdateOptions(options.value)
}
function sortingChanged (event) {
options.value.sortBy = event.sortField
options.value.sortDesc = event.sortOrder === -1
onUpdateOptions(options.value)
}
function goToNewDocument () {
router.push({
name: 'DocumentsCreateFile',
query: route.query,
})
}
function goToUploadFile () {
router.push({
name: 'DocumentsUploadFile',
query: route.query
})
}
function btnShowInformationOnClick (item) {
const folderParams = route.query;
if (item) {
folderParams.id = item['@id'];
}
router.push({
name: 'DocumentsShow',
params: folderParams,
query: folderParams
});
}
function btnChangeVisibilityOnClick (item) {
const folderParams = route.query;
folderParams.id = item['@id'];
axios
.put(item['@id'] + '/toggle_visibility')
.then(response => {
item.resourceLinkListFromEntity = response.data.resourceLinkListFromEntity;
})
;
}
function btnEditOnClick (item) {
const folderParams = route.query;
folderParams.id = item['@id'];
if ('folder' === item.filetype || isEmpty(item.filetype)) {
router.push({
name: 'DocumentsUpdate',
params: { id: item['@id'] },
query: folderParams,
});
return;
}
if ('file' === item.filetype) {
folderParams.getFile = true;
if (item.resourceNode.resourceFile
&& item.resourceNode.resourceFile.mimeType
&& 'text/html' === item.resourceNode.resourceFile.mimeType
) {
//folderParams.getFile = true;
}
router.push({
name: 'DocumentsUpdateFile',
params: { id: item['@id'] },
query: folderParams
});
}
}
</script>

@ -121,7 +121,7 @@ export default {
};
},
created() {
//console.log('created assets/vue/views/documents/List.vue');
//console.log('created assets/vue/views/documents/DocumentsList.vue');
this.moment = moment;
const route = useRoute()
let nodeId = route.params['node'];

@ -12,10 +12,13 @@ module.exports = {
primary: {
DEFAULT: "#2e75a3",
gradient: "#9cc2da",
bgdisabled: '#fafafa',
borderdisabled: '#e4e9eD',
},
secondary: {
DEFAULT: "#f37e2f",
gradient: "#e06410",
bgdisabled: '#e4e9ed',
},
gray: {
5: "#fcfcfc",
@ -44,6 +47,8 @@ module.exports = {
black: colors.black,
transparent: colors.transparent,
current: colors.current,
fontdisabled: '#a2a6b0',
},
extend: {
fontFamily: {

Loading…
Cancel
Save