mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-29 19:13:49 +00:00
refactor file drop widget
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection";
|
||||
import {createApp} from "vue";
|
||||
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue"
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
const i18n = _createI18n({});
|
||||
|
||||
const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElement): void => {
|
||||
console.log('app started', divElement);
|
||||
const input_stored_object: HTMLInputElement|null = divElement.querySelector("input[data-stored-object]");
|
||||
if (null === input_stored_object) {
|
||||
throw new Error('input to stored object not found');
|
||||
}
|
||||
|
||||
let existingDoc: StoredObject|null = null;
|
||||
if (input_stored_object.value !== "") {
|
||||
existingDoc = JSON.parse(input_stored_object.value);
|
||||
}
|
||||
const app_container = document.createElement("div");
|
||||
divElement.appendChild(app_container);
|
||||
|
||||
const app = createApp({
|
||||
template: '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
|
||||
data(vm) {
|
||||
return {
|
||||
existingDoc: existingDoc,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DropFileWidget,
|
||||
},
|
||||
methods: {
|
||||
addDocument: function(object: StoredObjectCreated): void {
|
||||
console.log('object added', object);
|
||||
this.$data.existingDoc = object;
|
||||
input_stored_object.value = JSON.stringify(object);
|
||||
},
|
||||
removeDocument: function(object: StoredObject): void {
|
||||
console.log('catch remove document', object);
|
||||
input_stored_object.value = "";
|
||||
this.$data.existingDoc = null;
|
||||
console.log('collectionEntry', collectionEntry);
|
||||
|
||||
if (null !== collectionEntry) {
|
||||
console.log('will remove collection');
|
||||
collectionEntry.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(app_container);
|
||||
}
|
||||
window.addEventListener('collection-add-entry', ((e: CustomEvent<CollectionEventPayload>) => {
|
||||
const detail = e.detail;
|
||||
const divElement: null|HTMLDivElement = detail.entry.querySelector('div[data-stored-object]');
|
||||
|
||||
if (null === divElement) {
|
||||
throw new Error('div[data-stored-object] not found');
|
||||
}
|
||||
|
||||
startApp(divElement, detail.entry);
|
||||
}) as EventListener);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll('div[data-stored-object]');
|
||||
|
||||
upload_inputs.forEach((input: HTMLDivElement): void => {
|
||||
// test for a parent to check if this is a collection entry
|
||||
let collectionEntry: null|HTMLLIElement = null;
|
||||
let parent = input.parentElement;
|
||||
console.log('parent', parent);
|
||||
if (null !== parent) {
|
||||
let grandParent = parent.parentElement;
|
||||
console.log('grandParent', grandParent);
|
||||
if (null !== grandParent) {
|
||||
if (grandParent.tagName.toLowerCase() === 'li' && grandParent.classList.contains('entry')) {
|
||||
collectionEntry = grandParent as HTMLLIElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
startApp(input, collectionEntry);
|
||||
})
|
||||
});
|
||||
|
||||
export {}
|
@@ -17,6 +17,20 @@ export interface StoredObject {
|
||||
type: string,
|
||||
uuid: string,
|
||||
status: StoredObjectStatus,
|
||||
_links?: {
|
||||
dav_link?: {
|
||||
href: string
|
||||
expiration: number
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export interface StoredObjectCreated {
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
type: string,
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
@@ -33,3 +47,18 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing information for performering a POST request to a swift object store
|
||||
*/
|
||||
export interface PostStoreObjectSignature {
|
||||
method: "POST",
|
||||
max_file_size: number,
|
||||
max_file_count: 1,
|
||||
expires: number,
|
||||
submit_delay: 180,
|
||||
redirect: string,
|
||||
prefix: string,
|
||||
url: string,
|
||||
signature: string,
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="'ready' === props.storedObject.status" class="btn-group">
|
||||
<div v-if="'ready' === props.storedObject.status || 'stored_object_created' === props.storedObject.status" class="btn-group">
|
||||
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, 'btn-sm': props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
@@ -35,14 +35,14 @@ import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectStatusChange,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
StoredObject, StoredObjectCreated,
|
||||
StoredObjectStatusChange,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject,
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
small?: boolean,
|
||||
canEdit?: boolean,
|
||||
canDownload?: boolean,
|
||||
@@ -99,6 +99,7 @@ const checkForReady = function(): void {
|
||||
if (
|
||||
'ready' === props.storedObject.status
|
||||
|| 'failure' === props.storedObject.status
|
||||
|| 'stored_object_created' === props.storedObject.status
|
||||
// stop reloading if the page stays opened for a long time
|
||||
|| tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
@@ -111,6 +112,11 @@ const checkForReady = function(): void {
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function(): Promise<void> {
|
||||
|
||||
if (props.storedObject.status === 'stored_object_created') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit('onStoredObjectStatusChange', new_status);
|
||||
|
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
import {encryptFile, uploadFile} from "../_components/helper";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
existingDoc?: StoredObjectCreated|StoredObject,
|
||||
}
|
||||
|
||||
const props = defineProps<DropFileConfig>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'addDocument', stored_object: StoredObjectCreated): void,
|
||||
}>();
|
||||
|
||||
const is_dragging: Ref<boolean> = ref(false);
|
||||
const uploading: Ref<boolean> = ref(false);
|
||||
|
||||
const has_existing_doc = computed<boolean>(() => {
|
||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||
});
|
||||
|
||||
const onDragOver = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
is_dragging.value = true;
|
||||
}
|
||||
|
||||
const onDragLeave = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
is_dragging.value = false;
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
console.log('on drop', e);
|
||||
e.preventDefault();
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
|
||||
if (null === files || undefined === files) {
|
||||
console.error("no files transferred", e.dataTransfer);
|
||||
return;
|
||||
}
|
||||
if (files.length === 0) {
|
||||
console.error("no files given");
|
||||
return;
|
||||
}
|
||||
|
||||
handleFile(files[0])
|
||||
}
|
||||
|
||||
const onZoneClick = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.addEventListener("change", onFileChange);
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
const onFileChange = async (event: Event): Promise<void> => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
console.log('event triggered', input);
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
console.log('file added', input.files[0]);
|
||||
const file = input.files[0];
|
||||
await handleFile(file);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw 'No file given';
|
||||
}
|
||||
|
||||
const handleFile = async (file: File): Promise<void> => {
|
||||
uploading.value = true;
|
||||
const type = file.type;
|
||||
const buffer = await file.arrayBuffer();
|
||||
const [encrypted, iv, jsonWebKey] = await encryptFile(buffer);
|
||||
const filename = await uploadFile(encrypted);
|
||||
|
||||
console.log(iv, jsonWebKey);
|
||||
|
||||
const storedObject: StoredObjectCreated = {
|
||||
filename: filename,
|
||||
iv,
|
||||
keyInfos: jsonWebKey,
|
||||
type: type,
|
||||
status: "stored_object_created",
|
||||
}
|
||||
|
||||
emit('addDocument', storedObject);
|
||||
uploading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drop-file">
|
||||
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<p v-if="has_existing_doc">
|
||||
<i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
</p>
|
||||
<!-- todo i18n -->
|
||||
<p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p>
|
||||
<p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p>
|
||||
</div>
|
||||
<div v-else class="waiting">
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.drop-file {
|
||||
width: 100%;
|
||||
|
||||
& > .area, & > .waiting {
|
||||
width: 100%;
|
||||
height: 8rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& > .area {
|
||||
border: 4px dashed #ccc;
|
||||
|
||||
&.dragging {
|
||||
border: 4px dashed blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.chill-collection ul.list-entry li.entry:nth-child(2n) {
|
||||
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean,
|
||||
existingDoc?: StoredObjectCreated|StoredObject,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DropFileConfig>(), {
|
||||
allowRemove: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'addDocument', stored_object: StoredObjectCreated): void,
|
||||
(e: 'removeDocument', stored_object: null): void
|
||||
}>();
|
||||
|
||||
const has_existing_doc = computed<boolean>(() => {
|
||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||
});
|
||||
|
||||
const dav_link_expiration = computed<number|undefined>(() => {
|
||||
if (props.existingDoc === undefined || props.existingDoc === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (props.existingDoc.status !== 'ready') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.existingDoc._links?.dav_link?.expiration;
|
||||
});
|
||||
|
||||
const dav_link_href = computed<string|undefined>(() => {
|
||||
if (props.existingDoc === undefined || props.existingDoc === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (props.existingDoc.status !== 'ready') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.existingDoc._links?.dav_link?.href;
|
||||
})
|
||||
|
||||
const onAddDocument = (s: StoredObjectCreated): void => {
|
||||
emit('addDocument', s);
|
||||
}
|
||||
|
||||
const onRemoveDocument = (e: Event): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
emit('removeDocument', null);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<drop-file :existingDoc="props.existingDoc" @addDocument="onAddDocument"></drop-file>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li v-if="has_existing_doc">
|
||||
<document-action-buttons-group
|
||||
:stored-object="props.existingDoc"
|
||||
:can-edit="props.existingDoc?.status === 'ready'"
|
||||
:can-download="true"
|
||||
:dav-link="dav_link_href"
|
||||
:dav-link-expiration="dav_link_expiration"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<button v-if="allowRemove" class="btn btn-delete" @click="onRemoveDocument($event)" ></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -10,10 +10,10 @@
|
||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {reactive} from "vue";
|
||||
import {StoredObject} from "../../types";
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
classes: { [key: string]: boolean},
|
||||
filename?: string,
|
||||
};
|
||||
|
@@ -13,10 +13,10 @@
|
||||
import {reactive, ref, nextTick, onMounted} from "vue";
|
||||
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {StoredObject} from "../../types";
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
|
||||
interface DownloadButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
classes: { [k: string]: boolean },
|
||||
filename?: string,
|
||||
}
|
||||
|
@@ -8,10 +8,10 @@
|
||||
<script lang="ts" setup>
|
||||
import WopiEditButton from "./WopiEditButton.vue";
|
||||
import {build_wopi_editor_link} from "./helpers";
|
||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
|
||||
interface WopiEditButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
returnPath?: string,
|
||||
classes: {[k: string] : boolean},
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
|
@@ -0,0 +1,60 @@
|
||||
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {PostStoreObjectSignature} from "../../types";
|
||||
|
||||
const algo = 'AES-CBC';
|
||||
|
||||
const URL_POST = '/asyncupload/temp_url/generate/post';
|
||||
|
||||
const keyDefinition = {
|
||||
name: algo,
|
||||
length: 256
|
||||
};
|
||||
|
||||
const createFilename = (): string => {
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('expires_delay', "180");
|
||||
params.append('submit_delay', "180");
|
||||
const asyncData: PostStoreObjectSignature = await makeFetch("GET", URL_POST + "?" + params.toString());
|
||||
const suffix = createFilename();
|
||||
const filename = asyncData.prefix + suffix;
|
||||
const formData = new FormData();
|
||||
formData.append("redirect", asyncData.redirect);
|
||||
formData.append("max_file_size", asyncData.max_file_size.toString());
|
||||
formData.append("max_file_count", asyncData.max_file_count.toString());
|
||||
formData.append("expires", asyncData.expires.toString());
|
||||
formData.append("signature", asyncData.signature);
|
||||
formData.append(filename, new Blob([uploadFile]), suffix);
|
||||
|
||||
const response = await window.fetch(asyncData.url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Error while sending file to store", response);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return Promise.resolve(filename);
|
||||
}
|
||||
|
||||
export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
|
||||
console.log('encrypt', originalFile);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]);
|
||||
const exportedKey = await window.crypto.subtle.exportKey('jwk', key);
|
||||
const encrypted = await window.crypto.subtle.encrypt({ name: algo, iv: iv}, key, originalFile);
|
||||
|
||||
return Promise.resolve([encrypted, iv, exportedKey]);
|
||||
};
|
Reference in New Issue
Block a user