diff --git a/crm/api/__init__.py b/crm/api/__init__.py index 141da446a..58739de0d 100644 --- a/crm/api/__init__.py +++ b/crm/api/__init__.py @@ -3,6 +3,7 @@ from frappe.translate import get_all_translations from frappe.utils import validate_email_address, split_emails, cstr from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD +from frappe.core.api.file import get_max_file_size @frappe.whitelist(allow_guest=True) @@ -107,3 +108,20 @@ def invite_by_email(emails: str, role: str): for email in to_invite: frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True) + + +@frappe.whitelist() +def get_file_uploader_defaults(doctype: str): + max_number_of_files = None + make_attachments_public = False + if doctype: + meta = frappe.get_meta(doctype) + max_number_of_files = meta.get("max_attachments") + make_attachments_public = meta.get("make_attachments_public") + + return { + 'allowed_file_types': frappe.get_system_settings("allowed_file_extensions"), + 'max_file_size': get_max_file_size(), + 'max_number_of_files': max_number_of_files, + 'make_attachments_public': bool(make_attachments_public), + } \ No newline at end of file diff --git a/frontend/src/components/FilesUploader/FilesUploader.vue b/frontend/src/components/FilesUploader/FilesUploader.vue new file mode 100644 index 000000000..df20d1459 --- /dev/null +++ b/frontend/src/components/FilesUploader/FilesUploader.vue @@ -0,0 +1,152 @@ + + + diff --git a/frontend/src/components/FilesUploader/FilesUploaderArea.vue b/frontend/src/components/FilesUploader/FilesUploaderArea.vue new file mode 100644 index 000000000..5d301254d --- /dev/null +++ b/frontend/src/components/FilesUploader/FilesUploaderArea.vue @@ -0,0 +1,321 @@ + + diff --git a/frontend/src/components/FilesUploader/filesUploaderHandler.ts b/frontend/src/components/FilesUploader/filesUploaderHandler.ts new file mode 100644 index 000000000..59253b9de --- /dev/null +++ b/frontend/src/components/FilesUploader/filesUploaderHandler.ts @@ -0,0 +1,129 @@ +interface UploadOptions { + file?: File + private?: boolean + fileUrl?: string + folder?: string + doctype?: string + docname?: string + type?: string +} + +type EventListenerOption = 'start' | 'progress' | 'finish' | 'error' + +declare global { + interface Window { + csrf_token?: string + } +} + +class FilesUploadHandler { + listeners: { [event: string]: Function[] } + failed: boolean + + constructor() { + this.listeners = {} + this.failed = false + } + + on(event: EventListenerOption, handler: Function) { + this.listeners[event] = this.listeners[event] || [] + this.listeners[event].push(handler) + } + + trigger(event: string, data?: any) { + let handlers = this.listeners[event] || [] + handlers.forEach((handler) => { + handler.call(this, data) + }) + } + + upload(file: File | null, options: UploadOptions): Promise { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest() + xhr.upload.addEventListener('loadstart', () => { + this.trigger('start') + }) + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + this.trigger('progress', { + uploaded: e.loaded, + total: e.total, + }) + } + }) + xhr.upload.addEventListener('load', () => { + this.trigger('finish') + }) + xhr.addEventListener('error', () => { + this.trigger('error') + reject() + }) + xhr.onreadystatechange = () => { + if (xhr.readyState == XMLHttpRequest.DONE) { + let error: any = null + if (xhr.status === 200) { + let r: any = null + try { + r = JSON.parse(xhr.responseText) + } catch (e) { + r = xhr.responseText + } + let out = r.message || r + resolve(out) + } else if (xhr.status === 403) { + error = JSON.parse(xhr.responseText) + } else if (xhr.status === 413) { + this.failed = true + error = 'Size exceeds the maximum allowed file size.' + } else { + this.failed = true + try { + error = JSON.parse(xhr.responseText) + } catch (e) { + // pass + } + } + if (error && error.exc) { + console.error(JSON.parse(error.exc)[0]) + } + reject(error) + } + } + + xhr.open('POST', '/api/method/upload_file', true) + xhr.setRequestHeader('Accept', 'application/json') + + if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') { + xhr.setRequestHeader('X-Frappe-CSRF-Token', window.csrf_token) + } + + let formData = new FormData() + + if (options.file && file) { + formData.append('file', options.file, file.name) + } + formData.append('is_private', options.private || false ? '1' : '0') + formData.append('folder', options.folder || 'Home') + + if (options.fileUrl) { + formData.append('file_url', options.fileUrl) + } + + if (options.doctype) { + formData.append('doctype', options.doctype) + } + + if (options.docname) { + formData.append('docname', options.docname) + } + + if (options.type) { + formData.append('type', options.type) + } + + xhr.send(formData) + }) + } +} + +export default FilesUploadHandler diff --git a/frontend/src/components/Icons/FileAudioIcon.vue b/frontend/src/components/Icons/FileAudioIcon.vue new file mode 100644 index 000000000..c93d8ca5f --- /dev/null +++ b/frontend/src/components/Icons/FileAudioIcon.vue @@ -0,0 +1,20 @@ + diff --git a/frontend/src/components/Icons/FileVideoIcon.vue b/frontend/src/components/Icons/FileVideoIcon.vue new file mode 100644 index 000000000..36b4fb2a8 --- /dev/null +++ b/frontend/src/components/Icons/FileVideoIcon.vue @@ -0,0 +1,19 @@ +