From f0fa8147df00ed7d96b34efcebc4613b01ff3c38 Mon Sep 17 00:00:00 2001 From: jasonchung1871 <101672465+jasonchung1871@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:02:15 -0700 Subject: [PATCH] Moved API calls from formio to FormViewer (#1396) * Moved API calls from formio to FormViewer API calls for file uploads have been moved from formio to the FormViewer. * Update Component.ts this should fix the issue of a missing storage for the file upload component. we don't actually use a storage but it's required to hide the message about a missing storage. --- .../src/components/designer/FormViewer.vue | 59 +++++- app/frontend/src/services/fileService.js | 10 +- app/frontend/src/store/form.js | 4 +- .../src/components/SimpleFile/Component.ts | 70 ++++--- components/src/index.ts | 3 - components/src/providers/index.ts | 5 - components/src/providers/storage/chefs.ts | 177 ------------------ components/src/providers/storage/index.ts | 5 - 8 files changed, 112 insertions(+), 221 deletions(-) delete mode 100644 components/src/providers/index.ts delete mode 100644 components/src/providers/storage/chefs.ts delete mode 100644 components/src/providers/storage/index.ts diff --git a/app/frontend/src/components/designer/FormViewer.vue b/app/frontend/src/components/designer/FormViewer.vue index 2595711a3..e245bbc28 100644 --- a/app/frontend/src/components/designer/FormViewer.vue +++ b/app/frontend/src/components/designer/FormViewer.vue @@ -8,14 +8,17 @@ import BaseDialog from '~/components/base/BaseDialog.vue'; import FormViewerActions from '~/components/designer/FormViewerActions.vue'; import FormViewerMultiUpload from '~/components/designer/FormViewerMultiUpload.vue'; import templateExtensions from '~/plugins/templateExtensions'; -import { formService, rbacService } from '~/services'; +import { fileService, formService, rbacService } from '~/services'; import { useAppStore } from '~/store/app'; import { useAuthStore } from '~/store/auth'; import { useFormStore } from '~/store/form'; import { useNotificationStore } from '~/store/notification'; import { isFormPublic } from '~/utils/permissionUtils'; -import { attachAttributesToLinks } from '~/utils/transformUtils'; +import { + attachAttributesToLinks, + getDisposition, +} from '~/utils/transformUtils'; import { FormPermissions, NotificationTypes } from '~/utils/constants'; export default { @@ -77,6 +80,7 @@ export default { bulkFile: false, confirmSubmit: false, currentForm: {}, + downloadTimeout: null, doYouWantToSaveTheDraft: false, forceNewTabLinks: true, form: {}, @@ -121,7 +125,7 @@ export default { 'tokenParsed', 'user', ]), - ...mapState(useFormStore, ['isRTL']), + ...mapState(useFormStore, ['downloadedFile', 'isRTL']), formScheduleExpireMessage() { return this.$t('trans.formViewer.formScheduleExpireMessage'); @@ -148,6 +152,9 @@ export default { simplefile: { config: this.config, chefsToken: this.getCurrentAuthHeader, + deleteFile: this.deleteFile, + getFile: this.getFile, + uploadFile: this.uploadFile, }, }, evalContext: { @@ -190,6 +197,7 @@ export default { }, beforeUnmount() { window.removeEventListener('beforeunload', this.beforeWindowUnload); + clearTimeout(this.downloadTimeout); }, beforeUpdate() { if (this.forceNewTabLinks) { @@ -197,6 +205,7 @@ export default { } }, methods: { + ...mapActions(useFormStore, ['downloadFile']), ...mapActions(useNotificationStore, ['addNotification']), isFormPublic: isFormPublic, getCurrentAuthHeader() { @@ -1079,6 +1088,50 @@ export default { e.returnValue = ''; } }, + async deleteFile(file) { + return fileService.deleteFile(file.id); + }, + async getFile(fileId, options = {}) { + await this.downloadFile(fileId, options); + if (this.downloadedFile && this.downloadedFile.headers) { + let data; + + if ( + this.downloadedFile.headers['content-type'].includes( + 'application/json' + ) + ) { + data = JSON.stringify(this.downloadedFile.data); + } else { + data = this.downloadedFile.data; + } + + if (typeof data === 'string') { + data = new Blob([data], { + type: this.downloadedFile.headers['content-type'], + }); + } + + // don't need to blob because it's already a blob + const url = window.URL.createObjectURL(data); + const a = document.createElement('a'); + a.href = url; + a.download = getDisposition( + this.downloadedFile.headers['content-disposition'] + ); + a.style.display = 'none'; + a.classList.add('hiddenDownloadTextElement'); + document.body.appendChild(a); + a.click(); + this.downloadTimeout = setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(a.href); + }); + } + }, + async uploadFile(file, config = {}) { + return fileService.uploadFile(file, config); + }, }, }; diff --git a/app/frontend/src/services/fileService.js b/app/frontend/src/services/fileService.js index e38383612..7ab99461b 100644 --- a/app/frontend/src/services/fileService.js +++ b/app/frontend/src/services/fileService.js @@ -2,7 +2,13 @@ import { appAxios } from '~/services/interceptors'; import { ApiRoutes } from '~/utils/constants'; export default { - async getFile(fileId) { - return appAxios().get(`${ApiRoutes.FILES}/${fileId}`); + async deleteFile(fileId) { + return appAxios().delete(`${ApiRoutes.FILES}/${fileId}`); + }, + async getFile(fileId, options = {}) { + return appAxios().get(`${ApiRoutes.FILES}/${fileId}`, options); + }, + async uploadFile(file, config = {}) { + return appAxios().post(`${ApiRoutes.FILES}`, file, config); }, }; diff --git a/app/frontend/src/store/form.js b/app/frontend/src/store/form.js index a30cb6b6f..27ee6ae43 100644 --- a/app/frontend/src/store/form.js +++ b/app/frontend/src/store/form.js @@ -826,10 +826,10 @@ export const useFormStore = defineStore('form', { if (!this.form || this.form.isDirty === isDirty) return; // don't do anything if not changing the val (or if form is blank for some reason) this.form.isDirty = isDirty; }, - async downloadFile(fileId) { + async downloadFile(fileId, options = {}) { try { this.downloadedFile = {}; - const response = await fileService.getFile(fileId); + const response = await fileService.getFile(fileId, options); this.downloadedFile.data = response.data; this.downloadedFile.headers = response.headers; } catch (error) { diff --git a/components/src/components/SimpleFile/Component.ts b/components/src/components/SimpleFile/Component.ts index 8b4852658..07100ae4a 100644 --- a/components/src/components/SimpleFile/Component.ts +++ b/components/src/components/SimpleFile/Component.ts @@ -75,12 +75,8 @@ export default class Component extends (ParentComponent as any) { deleteFile(fileInfo) { const { options = {} } = this.component; - const Provider = Formio.Providers.getProvider('storage', this.component.storage); - if (Provider) { - const provider = new Provider(this); - if (fileInfo && provider && typeof provider.deleteFile === 'function') { - provider.deleteFile(fileInfo, options) - } + if (fileInfo) { + options.deleteFile(fileInfo); } } @@ -89,9 +85,9 @@ export default class Component extends (ParentComponent as any) { if (!this.component.multiple) { files = Array.prototype.slice.call(files, 0, 1); } - if (this.component.storage && files && files.length) { + if (this.component && files && files.length) { // files is not really an array and does not have a forEach method, so fake it. - Array.prototype.forEach.call(files, (file) => { + Array.prototype.forEach.call(files, async (file) => { const fileName = uniqueName(file.name, this.component.fileNameTemplate, this.evalContext()); const fileUpload = { originalName: file.name, @@ -140,7 +136,7 @@ export default class Component extends (ParentComponent as any) { if (this.component.privateDownload) { file.private = true; } - const { storage, options = {} } = this.component; + const { options = {} } = this.component; const url = this.interpolate(this.component.url); let groupKey = null; let groupPermissions = null; @@ -162,19 +158,48 @@ export default class Component extends (ParentComponent as any) { }); const fileKey = this.component.fileKey || 'file'; - const groupResourceId = groupKey ? this.currentForm.submission.data[groupKey]._id : null; - fileService.uploadFile(storage, file, fileName, dir, (evt) => { - fileUpload.status = 'progress'; - // @ts-ignore - fileUpload.progress = parseInt(100.0 * evt.loaded / evt.total); - delete fileUpload.message; - this.redraw(); - }, url, options, fileKey, groupPermissions, groupResourceId) - .then((fileInfo) => { + + const blob = new Blob([file], { type: file.type }); + const fileFromBlob = new File([blob], file.name, { + type: file.type, + lastModified: file.lastModified, + }); + const formData = new FormData(); + const data = { + [fileKey]: fileFromBlob, + fileName, + dir, + }; + for (const key in data) { + formData.append(key, data[key]); + } + options.uploadFile(formData, { + onUploadProgress: (evt) => { + fileUpload.status = 'progress'; + // @ts-ignore + fileUpload.progress = parseInt(100.0 * evt.loaded / evt.total); + delete fileUpload.message; + this.redraw(); + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then((response) => { + response.data = response.data || {}; const index = this.statuses.indexOf(fileUpload); if (index !== -1) { this.statuses.splice(index, 1); } + let fileInfo = { + storage: 'chefs', + name: response.data.originalname, + originalName: '', + url: `${url}/${response.data.id}`, + size: response.data.size, + type: response.data.mimetype, + data: { id: response.data.id }, + }; fileInfo.originalName = file.name; if (!this.hasValue()) { this.dataValue = []; @@ -190,19 +215,16 @@ export default class Component extends (ParentComponent as any) { // @ts-ignore delete fileUpload.progress; this.redraw(); - }); + }) } }); } } getFile(fileInfo) { + const fileId = fileInfo?.data?.id ? fileInfo.data.id : fileInfo.id; const { options = {} } = this.component; - const { fileService } = this; - if (!fileService) { - return alert('File Service not provided'); - } - fileService.downloadFile(fileInfo, options) + options.getFile(fileId, { responseType: 'blob' }) .catch((response) => { // Is alert the best way to do this? // User is expecting an immediate notification due to attempting to download a file. diff --git a/components/src/index.ts b/components/src/index.ts index 06cc315bc..fb5d54b0e 100755 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -1,10 +1,7 @@ import './overrides/editform/utils'; import components from './components'; -// @ts-ignore -import providers from './providers'; export default { components, - providers, }; diff --git a/components/src/providers/index.ts b/components/src/providers/index.ts deleted file mode 100644 index 866184a95..000000000 --- a/components/src/providers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import storage from './storage'; - -export default { - storage, -}; diff --git a/components/src/providers/storage/chefs.ts b/components/src/providers/storage/chefs.ts deleted file mode 100644 index 8e41595c8..000000000 --- a/components/src/providers/storage/chefs.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* tslint:disable */ -import NativePromise from 'native-promise-only'; - -const chefs = function Provider(formio) { - const addHeaders = (xhr, options) => { - if (options) { - if (options.headers) { - Object.keys(options.headers).forEach(k => { - const v = options.headers[k]; - xhr.setRequestHeader(k, v); - }); - } - - // Allow manual setting of any supplied headers above, but need to get the latest - // token from the containing app to deal with expiries and override auth - if (options.chefsToken) { - xhr.setRequestHeader('Authorization', options.chefsToken()) - } - } - }; - - const xhrRequest = (url, name, query, data, options, onprogress) => { - return new NativePromise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - const json = (typeof data === 'string'); - const fd = new FormData(); - if (typeof onprogress === 'function') { - xhr.upload.onprogress = onprogress; - } - - if (!json) { - for (const key in data) { - fd.append(key, data[key]); - } - } - - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - // Need to test if xhr.response is decoded or not. - let respData = {}; - try { - respData = (typeof xhr.response === 'string') ? JSON.parse(xhr.response) : {}; - // @ts-ignore - respData = (respData && respData.data) ? respData.data : respData; - } - catch (err) { - respData = {}; - } - - // Get the url of the file. - // @ts-ignore - let respUrl = respData.hasOwnProperty('url') ? respData.url : `${xhr.responseURL}/${name}`; - - // If they provide relative url, then prepend the url. - if (respUrl && respUrl[0] === '/') { - respUrl = `${url}${respUrl}`; - } - resolve({ url: respUrl, data: respData }); - } - else { - reject(xhr.response || 'Unable to upload file'); - } - }; - - xhr.onerror = () => reject(xhr); - xhr.onabort = () => reject(xhr); - - let requestUrl = url + (url.indexOf('?') > -1 ? '&' : '?'); - for (const key in query) { - requestUrl += `${key}=${query[key]}&`; - } - if (requestUrl[requestUrl.length - 1] === '&') { - requestUrl = requestUrl.substr(0, requestUrl.length - 1); - } - - xhr.open('POST', requestUrl); - if (json) { - xhr.setRequestHeader('Content-Type', 'application/json'); - } - const token = formio.getToken(); - if (token) { - xhr.setRequestHeader('x-jwt-token', token); - } - - addHeaders(xhr, options); - - //Overrides previous request props - if (options) { - const parsedOptions = typeof options === 'string' ? JSON.parse(options) : options; - for (const prop in parsedOptions) { - xhr[prop] = parsedOptions[prop]; - } - } - xhr.send(json ? data : fd); - }); - }; - - return { - title: 'CHEFS', - name: 'chefs', - uploadFile(file, name, dir, progressCallback, url, options, fileKey) { - const uploadRequest = function (form) { - return xhrRequest(url, name, {}, { - [fileKey]: file, - name, - dir - }, options, progressCallback).then(response => { - response.data = response.data || {}; - return { - storage: 'chefs', - name: response.data.originalname, - url: `${url}/${response.data.id}`, - size: response.data.size, - type: response.data.mimetype, - data: { id: response.data.id } - }; - }); - }; - if (file.private && formio.formId) { - return formio.loadForm().then((form) => uploadRequest(form)); - } - else { - // @ts-ignore - return uploadRequest(); - } - }, - deleteFile(fileInfo, options) { - return new NativePromise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('DELETE', fileInfo.url, true); - addHeaders(xhr, options); - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - resolve('File deleted'); - } - else { - reject(xhr.response || 'Unable to delete file'); - } - }; - xhr.send(null); - }); - }, - downloadFile(file, options) { - return new NativePromise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', file.url, true); - addHeaders(xhr, options); - xhr.responseType = 'blob'; - xhr.onload = function (event) { - const blob = xhr.response; - let fileName; - const contentType = xhr.getResponseHeader('content-type'); - - // IE/EDGE doesn't send all response headers - if (xhr.getResponseHeader('content-disposition')) { - const contentDisposition = xhr.getResponseHeader('content-disposition'); - fileName = contentDisposition.substring(contentDisposition.indexOf('=') + 1); - } else { - fileName = 'unnamed.' + contentType.substring(contentType.indexOf('/') + 1); - } - - const url = window.URL.createObjectURL(blob); - let el = document.createElement('a'); - el.href = url; - el.download = fileName; - el.click(); - window.URL.revokeObjectURL(url); - el.remove(); - }; - xhr.send(); - }); - } - }; -}; - -chefs.title = 'CHEFS'; -export default chefs; diff --git a/components/src/providers/storage/index.ts b/components/src/providers/storage/index.ts deleted file mode 100644 index c5e0dd88d..000000000 --- a/components/src/providers/storage/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import chefs from './chefs'; - -export default { - chefs, -};