diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d4d84..a8a8239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +- Forms app mobile improvements +- Updated eslintrc and linted code. - Added redirect to dashboard error page if response from api has a response.error.code and it's not 200. - Removed hardcoded version validation diff --git a/package-lock.json b/package-lock.json index 54432a3..7f62643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "edpub-data-upload-utility": "file:../earthdata-pub-upload", "js-logger": "^1.6.1", "jsonwebtoken": "^9.0.0", + "moment": "^2.29.1", "vue": "^2.6.14", "vue-fixed-header": "^3.2.15", "vue-router": "^3.5.3", @@ -16432,6 +16433,14 @@ "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "dev": true }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/package.json b/package.json index 5a2b160..9f261e5 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "edpub-data-upload-utility": "file:../earthdata-pub-upload", "js-logger": "^1.6.1", "jsonwebtoken": "^9.0.0", + "moment": "^2.29.1", "vue": "^2.6.14", "vue-fixed-header": "^3.2.15", "vue-router": "^3.5.3", diff --git a/public/index.html b/public/index.html index 43eadc6..8439ef1 100644 --- a/public/index.html +++ b/public/index.html @@ -16,9 +16,7 @@ - - - + diff --git a/src/App.vue b/src/App.vue index 07b747d..05ba067 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,351 +1,542 @@ \ No newline at end of file diff --git a/src/assets/css/FormsQuestions.css b/src/assets/css/FormsQuestions.css index 76a70e8..7ce7ba0 100644 --- a/src/assets/css/FormsQuestions.css +++ b/src/assets/css/FormsQuestions.css @@ -33,6 +33,14 @@ margin-top:5px!important; margin-bottom:5px!important; } +.text-muted { + word-wrap: break-word!important; +} +@media (min-width: 1200px) { + #title { + word-wrap: break-word; + } +} .alert{ text-align: center!important; border-radius:unset!important; @@ -57,6 +65,7 @@ float:right; color: #DB1400 !important; font-size: 16px!important; + margin-top: 1rem; } .btable.form-control { display: inline-flex; diff --git a/src/assets/js/FormsQuestions.js b/src/assets/js/FormsQuestions.js index d6689b0..f828e9c 100644 --- a/src/assets/js/FormsQuestions.js +++ b/src/assets/js/FormsQuestions.js @@ -8,6 +8,7 @@ import { import FixedHeader from "vue-fixed-header"; import BEditableTable from 'bootstrap-vue-editable-table'; import mixin from "@/mixins/mixin.js"; +import localUpload from 'edpub-data-upload-utility'; // This FormsQuestions component gets the questions data for the selected daac and // sets the template properties, methods, and custom validation used. @@ -33,6 +34,31 @@ export default { alertMessage: '', dismissSecs: 7, dismissCountDown: 0, + uploadFile: null, + uploadQuestionId: "", + uploadStatusMsg: "Select a file", + uploadedFiles: [], + uploadFields: [ + { + key: 'file_name', + label: 'Filename' + }, { + key: 'size', + label: 'Size', + formatter: (value) => { + return this.calculateStorage(value) + } + }, { + key:'sha256Checksum', + label:'sha256Checksum', + }, { + key: 'lastModified', + label: 'Last Modified', + formatter: (value) => { + return this.shortDateShortTimeYearFirstJustValue(value); + } + } + ], timer: null, }; }, @@ -345,6 +371,7 @@ export default { this.fetchQuestions().then(() => { this.accessibilityHack(), this.loadAnswers() + this.getFileList(); }) }) this.timer = setInterval(() => { @@ -597,6 +624,11 @@ export default { if (has_all_directions) { return false; } + } else if (input.type == "file") { + // TODO Once we have the uploaded files split by type this will need to be updated + if (Array.isArray(this.uploadedFiles) && this.uploadedFiles.length) { + return false; + } } else { if ( typeof this.values[input.control_id] != "undefined" && @@ -943,6 +975,154 @@ export default { redoToPreviousState() { this.valueHistoryUndoIdx-- this.$set(this, 'values', JSON.parse(JSON.stringify(this.valueHistory[this.valueHistory.length - this.valueHistoryUndoIdx - 1]))) + }, + + resetUploads(alertMsg, statusMsg, controlId) { + // Display error message if there is an alert message + if (alertMsg != '' && alertMsg != null) { + this.alertVariant = 'danger'; + this.alertMessage = alertMsg; + this.showAlert(); + } + + // Update the Status Message + this.uploadStatusMsg = statusMsg; + + // Clear the upload text box + this.$refs[controlId][0].reset(); + + }, + + async listFileUploadsBySubmission(requestId) { + const url = `${process.env.VUE_APP_API_ROOT}/data/upload/list/${requestId}`; + try { + const response = await fetch(url, { + method: 'GET', + headers: { + "Content-Type": "application/json; charset=utf-8", + "Authorization": `Bearer ${localStorage.getItem('auth-token')}` + } + }).catch(function(e) { + return this.failedResponse(e) + }); + this.checkApiResponse(response) + if (response.statusText.match(/Forbidden/g)){ + return this.failedResponse() + } else { + return response.json(); // parses JSON response into native JavaScript objects + } + } catch(e) { + return this.failedResponse(e) + } + + }, + + async getFileList() { + const requestId = this.$store.state.global_params['requestId']; + if (requestId !== '' && requestId != undefined && requestId !== null) { + await(this.listFileUploadsBySubmission(requestId)) + .then((resp) => { + if (JSON.stringify(resp) === '{}' || JSON.stringify(resp) === '[]' || (resp.data && resp.data.length === 0)) { + return + } + let error = resp?.data?.error || resp?.error || resp?.data?.[0]?.error + if (error){ + if (!error.match(/not authorized/gi) && !error.match(/not implemented/gi)) { + const str = `An error has occurred while getting the list of files: ${error}.`; + // eslint-disable-next-line + console.log(str) + return + } else { + return + } + } + + const files = resp + + files.sort(function (a, b) { + var keyA = new Date(a.lastModified), + keyB = new Date(b.lastModified); + if (keyA > keyB) return -1; + if (keyA < keyB) return 1; + return 0; + }); + this.uploadedFiles = files; + }); + } + }, + + updateUploadStatusWithTimeout(msg, timeout) { + setTimeout(() => { + msg ? this.uploadStatusMsg = msg : null + }, timeout); + }, + + validateFile(file, controlId) { + let valid = false; + let msg = ''; + let statusMsg = 'Please select a different file.' + if (file.name.match(/\.([^.]+)$/) !== null) { + var ext = file.name.match(/\.([^.]+)$/)[1]; + if (ext.match(/exe/gi)) { + msg = 'exe is an invalid file type.'; + this.resetUploads(msg, statusMsg, controlId); + + } else { + valid = true + } + } else { + msg = 'The file must have an extension.'; + this.resetUploads(msg, statusMsg, controlId) + } + return valid; + }, + + async uploadFiles(event, controlId){ + const file = event.target.files[0] + this.uploadQuestionId = controlId; + let alertMsg = ''; + let statusMsg = ''; + + if (this.validateFile(file, controlId)) { + this.uploadStatusMsg = 'Uploading'; + + const upload = new localUpload(); + const requestId = this.$store.state.global_params['requestId']; + try { + let payload = { + fileObj: file, + authToken: localStorage.getItem('auth-token'), + } + if (requestId !== '' && requestId != undefined && requestId !== null) { + payload['apiEndpoint'] = `${process.env.VUE_APP_API_ROOT}/data/upload/getPostUrl`; + payload['submissionId'] = requestId + } + const resp = await upload.uploadFile(payload) + let error = resp?.data?.error || resp?.error || resp?.data?.[0]?.error + if (error) { + alertMsg = `An error has occured on uploadFile: ${error}.`; + statusMsg = `Select a file`; + // eslint-disable-next-line + console.log(`An error has occured on uploadFile: ${error}.`); + this.resetUploads(alertMsg, statusMsg, controlId); + } else { + alertMsg = ''; + statusMsg = 'Upload Complete'; + this.resetUploads(alertMsg, statusMsg, controlId); + this.updateUploadStatusWithTimeout('Select another file', 1000) + + if (requestId !== '' && requestId != undefined && requestId !== null){ + this.getFileList(); + } + } + } catch (error) { + // eslint-disable-next-line + console.log(`try catch error: ${error.stack}`); + alertMsg = `An error has occured on uploadFile`; + statusMsg = `Select a file` + this.resetUploads(alertMsg, statusMsg, controlId); + } + } } }, beforeUnmount() { diff --git a/src/components/FormsFooter.vue b/src/components/FormsFooter.vue index 500058c..343e6a0 100644 --- a/src/components/FormsFooter.vue +++ b/src/components/FormsFooter.vue @@ -7,8 +7,6 @@
  • FOIA
  • NASA Privacy Policy
  • USA.gov
  • -
  • Feedback
  • - diff --git a/src/components/FormsQuestions.vue b/src/components/FormsQuestions.vue index 278e176..066a68e 100644 --- a/src/components/FormsQuestions.vue +++ b/src/components/FormsQuestions.vue @@ -330,24 +330,47 @@ :multiple="Boolean(getAttribute('multiple', question.inputs[c_key]))"> + +
    {{ uploadQuestionId === input.control_id ? uploadStatusMsg : "Select a file"}} + +
    + + :multiple="Boolean(getAttribute('multiple', question.inputs[c_key]))" + @change="uploadFiles($event, input.control_id)"> +
    +
    +
    +
    + +
    - -
    Selected file: {{ values[input.control_id] ? values[input.control_id].name : '' }}
    - diff --git a/src/main.js b/src/main.js index 30626c7..22be20e 100644 --- a/src/main.js +++ b/src/main.js @@ -18,10 +18,10 @@ const isProduction = process.env.NODE_ENV === 'production'; const logOptions = { isEnabled: true, - logLevel : isProduction ? 'error' : 'debug', - stringifyArguments : false, - showLogLevel : true, - showMethodName : true, + logLevel: isProduction ? 'error' : 'debug', + stringifyArguments: false, + showLogLevel: true, + showMethodName: true, separator: '-', showConsoleColors: true }; @@ -95,11 +95,11 @@ new Vue({ router, store, - created(){ + created() { }, - mounted(){ - + mounted() { + }, render: h => h(App) diff --git a/src/mixins/mixin.js b/src/mixins/mixin.js index 3891d5f..465afc1 100644 --- a/src/mixins/mixin.js +++ b/src/mixins/mixin.js @@ -1,4 +1,5 @@ // This mixins file acts as a common js file and the functions are shared between components. +import moment from 'moment'; export default { props:{ }, @@ -857,6 +858,31 @@ export default { await this.postData(urlApi, {daac_id: daacId}) this.confirmExit(urlReturn) + }, + + // Copied from earthdata-pub-dashboard/app/src/js/utils/format.js + calculateStorage(n){ + const number = +n; + if (!n || Number.isNaN(number)) return '--'; + + if (number === 0) return n; + + if (number < 1e9) return `${(number / 1e6).toFixed(2)} MB`; + if (number < 1e12) return `${(number / 1e9).toFixed(2)} GB`; + if (number < 1e15) return `${(number / 1e12).toFixed(2)} TB`; + return `${(number / 1e15).toFixed(2)} PB`; + }, + + // Copied from earthdata-pub-dashboard/app/src/js/utils/format.js + shortDateShortTimeYearFirstJustValue(datestring) { + if (!datestring) { return '--'; } + let day, time; + if (datestring) { + const date = moment(datestring); + day = date.format('MMM D, YYYY'); + time = date.format('h:mm a'); + } + return `${day} ${time}`; } } }