Editor: Integrate image upload with backend controller and fileManager option - refs #3795
parent
4bd237374e
commit
370f04dbaf
@ -0,0 +1,161 @@ |
||||
<template> |
||||
<div class="base-tiny-editor"> |
||||
<label v-if="title" :for="editorId">{{ title }}</label> |
||||
<TinyEditor |
||||
:id="editorId" |
||||
:model-value="modelValue" |
||||
:init="editorConfig" |
||||
:required="required" |
||||
@update:model-value="updateValue" |
||||
@input="updateValue" |
||||
/> |
||||
<p v-if="helpText" class="help-text">{{ helpText }}</p> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref, computed, watch } from 'vue' |
||||
import TinyEditor from '@tinymce/tinymce-vue' |
||||
import { useRoute, useRouter } from "vue-router" |
||||
import { useCidReqStore } from "../../store/cidReq" |
||||
import { storeToRefs } from "pinia" |
||||
import { useStore } from "vuex" |
||||
|
||||
const props = defineProps({ |
||||
editorId: String, |
||||
modelValue: String, |
||||
required: Boolean, |
||||
editorConfig: Object, |
||||
title: String, |
||||
helpText: String, |
||||
mode: { type: String, default: 'personal_files' }, |
||||
useFileManager: { type: Boolean, default: false } |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
const router = useRouter() |
||||
const route = useRoute() |
||||
const parentResourceNodeId = ref(0) |
||||
|
||||
const store = useStore() |
||||
const user = computed(() => store.getters["security/getUser"]) |
||||
|
||||
// Set the parent node ID based on the user's resource node ID or route parameter |
||||
parentResourceNodeId.value = user.value.resourceNode["id"] |
||||
if (route.params.node) { |
||||
parentResourceNodeId.value = Number(route.params.node) |
||||
} |
||||
|
||||
const updateValue = (value) => { |
||||
emit('update:modelValue', value) |
||||
} |
||||
|
||||
const defaultEditorConfig = { |
||||
skin_url: '/build/libs/tinymce/skins/ui/oxide', |
||||
content_css: '/build/libs/tinymce/skins/content/default/content.css', |
||||
branding: false, |
||||
relative_urls: false, |
||||
height: 280, |
||||
toolbar_mode: 'sliding', |
||||
autosave_ask_before_unload: true, |
||||
plugins: [ |
||||
'advlist autolink lists link image charmap print preview anchor', |
||||
'searchreplace visualblocks code fullscreen', |
||||
'insertdatetime media table paste wordcount emoticons', |
||||
], |
||||
toolbar: 'undo redo | bold italic underline strikethrough | ...', |
||||
file_picker_callback: filePickerCallback |
||||
} |
||||
|
||||
const editorConfig = computed(() => ({ |
||||
...defaultEditorConfig, |
||||
...props.editorConfig |
||||
})) |
||||
|
||||
function filePickerCallback(callback, value, meta) { |
||||
if (!props.useFileManager) { |
||||
const input = document.createElement('input'); |
||||
input.setAttribute('type', 'file'); |
||||
input.style.display = 'none'; |
||||
|
||||
input.onchange = () => { |
||||
const file = input.files[0]; |
||||
const title = file.name; |
||||
const comment = ''; |
||||
const fileType = 'file'; |
||||
const resourceLinkList = []; |
||||
|
||||
const formData = new FormData(); |
||||
formData.append('uploadFile', file); |
||||
formData.append('title', title); |
||||
formData.append('comment', comment); |
||||
formData.append('parentResourceNodeId', parentResourceNodeId.value); |
||||
formData.append('filetype', fileType); |
||||
formData.append('resourceLinkList', resourceLinkList); |
||||
|
||||
fetch('/file-manager/upload-image', { |
||||
method: 'POST', |
||||
body: formData, |
||||
}) |
||||
.then(response => response.json()) |
||||
.then(data => { |
||||
if (data.location) { |
||||
callback(data.location); |
||||
} else { |
||||
console.error('Failed to upload file'); |
||||
} |
||||
}) |
||||
.catch(error => console.error('Error uploading file:', error)) |
||||
.finally(() => document.body.removeChild(input)); |
||||
}; |
||||
|
||||
document.body.appendChild(input); |
||||
input.click(); |
||||
return |
||||
} |
||||
|
||||
let url; |
||||
if (props.mode === 'personal_files') { |
||||
url = '/resources/filemanager/personal_list/1477' |
||||
} else if (props.mode === 'documents') { |
||||
const cidReqStore = useCidReqStore(); |
||||
const { course, session } = storeToRefs(cidReqStore); |
||||
|
||||
let nodeId = course.value && course.value.resourceNode ? course.value.resourceNode.id : null; |
||||
|
||||
if (!nodeId) { |
||||
console.error('Resource node ID is not available.'); |
||||
return; |
||||
} |
||||
|
||||
let folderParams = Object.entries(route.query).map(([key, value]) => `${key}=${value}`).join('&'); |
||||
url = router.resolve({ name: "DocumentForHtmlEditor", params: { id: nodeId }, query: route.query }).href; |
||||
} |
||||
|
||||
if (meta.filetype === 'image') { |
||||
url += "&type=images"; |
||||
} else { |
||||
url += "&type=files"; |
||||
} |
||||
|
||||
window.addEventListener("message", function (event) { |
||||
var data = event.data; |
||||
if (data.url) { |
||||
url = data.url; |
||||
callback(url); |
||||
} |
||||
}); |
||||
|
||||
tinymce.activeEditor.windowManager.openUrl({ |
||||
url: url, |
||||
title: "File manager", |
||||
onMessage: (api, message) => { |
||||
if (message.mceAction === 'fileSelected') { |
||||
const fileUrl = message.content; |
||||
callback(fileUrl); |
||||
api.close(); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
</script> |
||||
@ -0,0 +1,21 @@ |
||||
<template> |
||||
<router-view></router-view> |
||||
</template> |
||||
<script setup> |
||||
import { useStore } from "vuex" |
||||
import { useRoute } from "vue-router" |
||||
import { onMounted, provide, readonly, ref, watch } from "vue" |
||||
import { useSocialInfo } from "../../composables/useSocialInfo" |
||||
|
||||
const store = useStore() |
||||
const route = useRoute() |
||||
|
||||
const { user, isCurrentUser, groupInfo, isGroup, loadUser } = useSocialInfo() |
||||
|
||||
provide("social-user", user) |
||||
provide("is-current-user", isCurrentUser) |
||||
provide("group-info", groupInfo) |
||||
provide("is-group", isGroup) |
||||
|
||||
onMounted(loadUser) |
||||
</script> |
||||
@ -0,0 +1,19 @@ |
||||
export default { |
||||
path: '/resources/filemanager', |
||||
meta: { requiresAuth: true }, |
||||
component: () => import('../components/filemanager/Layout.vue'), |
||||
children: [ |
||||
{ |
||||
path: 'personal_list/:node?', |
||||
name: 'FileManagerList', |
||||
component: () => import('../views/filemanager/List.vue'), |
||||
meta: { emptyLayout: true }, |
||||
}, |
||||
{ |
||||
name: 'FileManagerUploadFile', |
||||
path: 'upload', |
||||
component: () => import('../views/filemanager/Upload.vue'), |
||||
meta: { emptyLayout: true }, |
||||
}, |
||||
], |
||||
}; |
||||
@ -0,0 +1,496 @@ |
||||
<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" |
||||
:globalFilterFields="['resourceNode.title', 'resourceNode.updatedAt']" |
||||
:lazy="true" |
||||
:loading="isLoading" |
||||
:paginator="true" |
||||
:rows="10" |
||||
:rowsPerPageOptions="[5, 10, 20, 50]" |
||||
:totalRecords="totalItems" |
||||
:value="items" |
||||
class="p-datatable-sm" |
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}" |
||||
dataKey="iid" |
||||
filterDisplay="menu" |
||||
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown" |
||||
responsiveLayout="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.resourceFile"> |
||||
<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.resourceFile.size" |
||||
> |
||||
<template #body="slotProps"> |
||||
{{ |
||||
slotProps.data.resourceNode.resourceFile |
||||
? prettyBytes(slotProps.data.resourceNode.resourceFile.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> |
||||
|
||||
<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> |
||||
</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> |
||||
<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.resourceFile.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> |
||||
|
||||
</template> |
||||
|
||||
<script> |
||||
import { mapActions, mapGetters } from "vuex" |
||||
import { mapFields } from "vuex-map-fields" |
||||
import ListMixin from "../../mixins/ListMixin" |
||||
import ActionCell from "../../components/ActionCell.vue" |
||||
import ResourceIcon from "../../components/documents/ResourceIcon.vue" |
||||
import ResourceFileLink from "../../components/documents/ResourceFileLink.vue" |
||||
import DataFilter from "../../components/DataFilter" |
||||
import isEmpty from "lodash/isEmpty" |
||||
import { RESOURCE_LINK_PUBLISHED } from "../../components/resource_links/visibility" |
||||
import { useI18n } from "vue-i18n" |
||||
import { useFormatDate } from "../../composables/formatDate" |
||||
import prettyBytes from "pretty-bytes" |
||||
|
||||
export default { |
||||
name: "FileManagerList", |
||||
servicePrefix: "FileManager", |
||||
components: { |
||||
ActionCell, |
||||
ResourceIcon, |
||||
ResourceFileLink, |
||||
DataFilter, |
||||
}, |
||||
mixins: [ListMixin], |
||||
setup() { |
||||
const { t } = useI18n() |
||||
const { relativeDatetime } = useFormatDate() |
||||
|
||||
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.resourceFile.size", |
||||
label: t("Size"), |
||||
field: "resourceNode.resourceFile.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.resourceFile.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, |
||||
} |
||||
|
||||
return data |
||||
}, |
||||
created() { |
||||
this.resetList = true |
||||
this.onUpdateOptions(this.options) |
||||
this.isFromEditor = window.location.search.includes('editor=tinymce'); |
||||
}, |
||||
computed: { |
||||
// From crud.js list function |
||||
...mapGetters("resourcenode", { |
||||
resourceNode: "getResourceNode", |
||||
}), |
||||
...mapGetters({ |
||||
isAuthenticated: "security/isAuthenticated", |
||||
isAdmin: "security/isAdmin", |
||||
currentUser: "security/getUser", |
||||
}), |
||||
|
||||
...mapGetters("personalfile", { |
||||
items: "list", |
||||
}), |
||||
|
||||
//...getters |
||||
|
||||
// From ListMixin |
||||
...mapFields("personalfile", { |
||||
deletedResource: "deleted", |
||||
error: "error", |
||||
isLoading: "isLoading", |
||||
resetList: "resetList", |
||||
totalItems: "totalItems", |
||||
view: "view", |
||||
}), |
||||
}, |
||||
data() { |
||||
return { |
||||
itemDialog: false, |
||||
detailsDialogVisible: false, |
||||
deleteItemDialog: false, |
||||
selectedItem: {}, |
||||
itemToDelete: null, |
||||
isFromEditor: false, |
||||
}; |
||||
}, |
||||
methods: { |
||||
showHandler(item) { |
||||
this.selectedItem = item; |
||||
this.detailsDialogVisible = true; |
||||
}, |
||||
closeDetailsDialog() { |
||||
this.detailsDialogVisible = false; |
||||
}, |
||||
// prime |
||||
onPage(event) { |
||||
this.options.itemsPerPage = event.rows |
||||
this.options.page = event.page + 1 |
||||
this.options.sortBy = event.sortField |
||||
this.options.sortDesc = event.sortOrder === -1 |
||||
|
||||
this.onUpdateOptions(this.options) |
||||
}, |
||||
sortingChanged(event) { |
||||
console.log("sortingChanged") |
||||
console.log(event) |
||||
this.options.sortBy = event.sortField |
||||
this.options.sortDesc = event.sortOrder === -1 |
||||
|
||||
this.onUpdateOptions(this.options) |
||||
// ctx.sortBy ==> Field key for sorting by (or null for no sorting) |
||||
// ctx.sortDesc ==> true if sorting descending, false otherwise |
||||
}, |
||||
|
||||
openNew() { |
||||
this.item = {} |
||||
this.submitted = false |
||||
this.itemDialog = true |
||||
}, |
||||
hideDialog() { |
||||
this.itemDialog = false |
||||
this.submitted = false |
||||
}, |
||||
saveItem() { |
||||
this.submitted = true |
||||
|
||||
if (this.item.title.trim()) { |
||||
if (this.item.id) { |
||||
} else { |
||||
let resourceNodeId = this.currentUser.resourceNode["id"] |
||||
if (!isEmpty(this.$route.params.node)) { |
||||
resourceNodeId = this.$route.params.node |
||||
} |
||||
this.item.filetype = "folder" |
||||
this.item.parentResourceNodeId = resourceNodeId |
||||
this.item.resourceLinkList = JSON.stringify([ |
||||
{ |
||||
gid: 0, |
||||
sid: 0, |
||||
cid: 0, |
||||
visibility: RESOURCE_LINK_PUBLISHED, |
||||
}, |
||||
]) |
||||
|
||||
this.createWithFormData(this.item) |
||||
this.showMessage("Saved") |
||||
} |
||||
|
||||
this.itemDialog = false |
||||
this.item = {} |
||||
} |
||||
}, |
||||
editItem(item) { |
||||
this.item = { ...item } |
||||
this.itemDialog = true |
||||
}, |
||||
confirmDeleteItem(item) { |
||||
console.log('confirmDeleteItem :::', item) |
||||
this.item = { ...item } |
||||
this.itemToDelete = { ...item } |
||||
this.deleteItemDialog = true |
||||
}, |
||||
confirmDeleteMultiple() { |
||||
this.deleteMultipleDialog = true |
||||
}, |
||||
deleteMultipleItems() { |
||||
console.log("deleteMultipleItems") |
||||
console.log(this.selectedItems) |
||||
this.deleteMultipleAction(this.selectedItems) |
||||
this.onRequest({ |
||||
pagination: this.pagination, |
||||
}) |
||||
this.deleteMultipleDialog = false |
||||
this.selectedItems = null |
||||
}, |
||||
deleteItemButton() { |
||||
console.log("deleteItem", this.itemToDelete); |
||||
if (this.itemToDelete && this.itemToDelete.id) { |
||||
this.deleteItem(this.itemToDelete) |
||||
.then(() => { |
||||
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Item deleted successfully', life: 3000 }); |
||||
this.deleteItemDialog = false; |
||||
this.itemToDelete = null; |
||||
this.onUpdateOptions(this.options); |
||||
}) |
||||
.catch(error => { |
||||
console.error("Error deleting the item:", error); |
||||
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'An error occurred while deleting the item', life: 3000 }); |
||||
}); |
||||
} else { |
||||
console.error("No item to delete or item ID is missing"); |
||||
} |
||||
}, |
||||
onRowSelected(items) { |
||||
this.selected = items |
||||
}, |
||||
selectAllRows() { |
||||
this.$refs.selectableTable.selectAllRows() |
||||
}, |
||||
clearSelected() { |
||||
this.$refs.selectableTable.clearSelected() |
||||
}, |
||||
returnToEditor(item) { |
||||
const url = item.contentUrl |
||||
|
||||
// Tiny mce. |
||||
window.parent.postMessage( |
||||
{ |
||||
url: url, |
||||
}, |
||||
"*", |
||||
) |
||||
|
||||
if (parent.tinymce) { |
||||
parent.tinymce.activeEditor.windowManager.close() |
||||
} |
||||
|
||||
// Ckeditor |
||||
function getUrlParam(paramName) { |
||||
var reParam = new RegExp("(?:[\?&]|&)" + paramName + "=([^&]+)", "i") |
||||
var match = window.location.search.match(reParam) |
||||
return match && match.length > 1 ? match[1] : "" |
||||
} |
||||
|
||||
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", |
||||
}), |
||||
}, |
||||
} |
||||
</script> |
||||
@ -0,0 +1,129 @@ |
||||
<template> |
||||
<div> |
||||
<dashboard |
||||
:plugins="['Webcam', 'ImageEditor']" |
||||
:props="{ |
||||
proudlyDisplayPoweredByUppy: false, |
||||
width: '100%', |
||||
}" |
||||
:uppy="uppy" |
||||
/> |
||||
</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" |
||||
|
||||
const XHRUpload = require("@uppy/xhr-upload") |
||||
|
||||
const ImageEditor = require("@uppy/image-editor") |
||||
|
||||
const servicePrefix = "FileManager" |
||||
|
||||
const { mapFields } = createHelpers({ |
||||
getterType: "personalfile/getField", |
||||
mutationType: "personalfile/updateField", |
||||
}) |
||||
|
||||
export default { |
||||
name: "FileManagerUploadFile", |
||||
servicePrefix, |
||||
components: { |
||||
Dashboard, |
||||
}, |
||||
setup() { |
||||
const parentResourceNodeId = ref(null) |
||||
const route = useRoute() |
||||
const router = useRouter(); |
||||
|
||||
const store = useStore() |
||||
const user = computed(() => store.getters["security/getUser"]) |
||||
|
||||
parentResourceNodeId.value = user.value.resourceNode["id"] |
||||
|
||||
if (route.params.node) { |
||||
parentResourceNodeId.value = Number(route.params.node) |
||||
} |
||||
|
||||
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", |
||||
}) |
||||
|
||||
uppy.value.setMeta({ |
||||
filetype: "file", |
||||
parentResourceNodeId: parentResourceNodeId.value, |
||||
}) |
||||
|
||||
uppy.value.on("complete", (result) => { |
||||
router.push({ name: "FileManagerList" }); |
||||
}); |
||||
|
||||
return { |
||||
uppy, |
||||
} |
||||
}, |
||||
mixins: [UploadMixin], |
||||
data() { |
||||
return { |
||||
files: [], |
||||
parentResourceNodeId: 0, |
||||
} |
||||
}, |
||||
computed: { |
||||
...mapFields(["error", "isLoading", "created", "violations"]), |
||||
...mapGetters({ |
||||
isAuthenticated: "security/isAuthenticated", |
||||
isAdmin: "security/isAdmin", |
||||
currentUser: "security/getUser", |
||||
}), |
||||
}, |
||||
created() { |
||||
let nodeId = this.$route.params.node |
||||
if (isEmpty(nodeId)) { |
||||
nodeId = this.currentUser.resourceNode["id"] |
||||
} |
||||
this.parentResourceNodeId = Number(nodeId) |
||||
}, |
||||
methods: { |
||||
...mapActions("personalfile", ["uploadMany", "createFile"]), |
||||
}, |
||||
} |
||||
</script> |
||||
@ -0,0 +1,24 @@ |
||||
<template> |
||||
<div> |
||||
<h1>Editor Demo</h1> |
||||
<BaseTinyEditor |
||||
editor-id="demoEditor" |
||||
v-model="editorContent" |
||||
:mode="editorMode" |
||||
:use-file-manager="useFileManager" |
||||
title="Demo Editor" |
||||
help-text="Edit your content here in demo mode." |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref } from 'vue' |
||||
import BaseTinyEditor from "../../components/basecomponents/BaseTinyEditor.vue" |
||||
|
||||
const editorContent = ref('') |
||||
// Here you decide the mode, 'personal_files' or 'documents' |
||||
const editorMode = ref('personal_files') |
||||
// Decide if you want to use the file manager or not |
||||
const useFileManager = ref(true) |
||||
</script> |
||||
@ -0,0 +1,113 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Controller; |
||||
|
||||
use Chamilo\CoreBundle\Controller\Api\BaseResourceFileAction; |
||||
use Chamilo\CoreBundle\Entity\PersonalFile; |
||||
use Chamilo\CoreBundle\Repository\Node\PersonalFileRepository; |
||||
use Doctrine\ORM\EntityManager; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use Exception; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\Annotation\Route; |
||||
use Symfony\Component\HttpFoundation\JsonResponse; |
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse; |
||||
|
||||
#[Route('/file-manager')] |
||||
class FileManagerController extends AbstractController |
||||
{ |
||||
private BaseResourceFileAction $baseResourceFileAction; |
||||
private PersonalFileRepository $personalFileRepository; |
||||
private EntityManagerInterface $entityManager; |
||||
|
||||
public function __construct( |
||||
BaseResourceFileAction $baseResourceFileAction, |
||||
PersonalFileRepository $personalFileRepository, |
||||
EntityManager $entityManager |
||||
) { |
||||
$this->baseResourceFileAction = $baseResourceFileAction; |
||||
$this->personalFileRepository = $personalFileRepository; |
||||
$this->entityManager = $entityManager; |
||||
} |
||||
|
||||
#[Route('/list', name: 'file_manager_list', methods: ['GET'])] |
||||
public function list(): JsonResponse |
||||
{ |
||||
// Implement logic to list files and folders |
||||
// This could be a call to your service or logic to retrieve files/folders |
||||
return $this->json(['files' => []]); |
||||
} |
||||
|
||||
#[Route('/upload', name: 'file_manager_upload', methods: ['POST'])] |
||||
public function upload(Request $request): JsonResponse |
||||
{ |
||||
// Implement logic to upload files |
||||
// This part will handle receiving and storing uploaded files |
||||
return $this->json(['message' => 'File(s) uploaded successfully']); |
||||
} |
||||
|
||||
/** |
||||
* @throws Exception |
||||
*/ |
||||
#[Route('/upload-image', name: 'file_manager_upload_image', methods: ['POST'])] |
||||
public function uploadImage(Request $request): JsonResponse |
||||
{ |
||||
$resource = new PersonalFile(); |
||||
|
||||
$result = $this->baseResourceFileAction->handleCreateFileRequest( |
||||
$resource, |
||||
$this->personalFileRepository, |
||||
$request, |
||||
$this->entityManager, |
||||
'overwrite' |
||||
); |
||||
|
||||
$this->entityManager->persist($resource); |
||||
$this->entityManager->flush(); |
||||
|
||||
if (!$result) { |
||||
return $this->json(['error' => 'File upload failed'], Response::HTTP_BAD_REQUEST); |
||||
} |
||||
|
||||
return $this->json([ |
||||
'message' => 'File uploaded successfully', |
||||
'data' => $result, |
||||
'location' => $this->personalFileRepository->getResourceFileUrl($resource), |
||||
]); |
||||
} |
||||
|
||||
#[Route('/create-folder', name: 'file_manager_create_folder', methods: ['POST'])] |
||||
public function createFolder(Request $request): JsonResponse |
||||
{ |
||||
// Implement logic to create new folders |
||||
return $this->json(['message' => 'Folder created successfully']); |
||||
} |
||||
|
||||
#[Route('/rename', name: 'file_manager_rename', methods: ['POST'])] |
||||
public function rename(Request $request): JsonResponse |
||||
{ |
||||
// Implement logic to rename files/folders |
||||
return $this->json(['message' => 'File/folder renamed successfully']); |
||||
} |
||||
|
||||
#[Route('/delete', name: 'file_manager_delete', methods: ['DELETE'])] |
||||
public function delete(Request $request): JsonResponse |
||||
{ |
||||
// Implement logic to delete files/folders |
||||
return $this->json(['message' => 'File/folder deleted successfully']); |
||||
} |
||||
|
||||
#[Route('/download/{filename}', name: 'file_manager_download', methods: ['GET'])] |
||||
public function download(string $filename): Response |
||||
{ |
||||
// Implement logic to download files |
||||
// Replace 'path/to/your/files' with the actual path where the files are stored |
||||
$filePath = 'path/to/your/files/' . $filename; |
||||
|
||||
return new BinaryFileResponse($filePath); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue