diff --git a/src/components/entity/upload.vue b/src/components/entity/upload.vue index 4fbf0ebd7..663ffcdac 100644 --- a/src/components/entity/upload.vue +++ b/src/components/entity/upload.vue @@ -21,7 +21,6 @@ except according to the terms contained in the LICENSE file. -
{{ file.name }}
+
+ + +
@@ -41,6 +46,7 @@ import { ref, watch } from 'vue'; import EntityUploadDataTemplate from './upload/data-template.vue'; import EntityUploadFileSelect from './upload/file-select.vue'; +import EntityUploadPopup from './upload/popup.vue'; import Modal from '../modal.vue'; import SentenceSeparator from '../sentence-separator.vue'; import Spinner from '../spinner.vue'; @@ -62,9 +68,11 @@ const { dataset } = useRequestData(); const file = ref(null); const selectFile = (value) => { file.value = value; }; -watch(() => props.state, (state) => { if (!state) file.value = null; }); +const clearFile = () => { file.value = null; }; +watch(() => props.state, (state) => { if (!state) clearFile(); }); const { request, awaitingResponse } = useRequest(); +const uploadProgress = ref(0); const upload = () => { request({ method: 'POST', @@ -72,14 +80,33 @@ const upload = () => { data: { source: { name: file.value.name, size: file.value.size }, entities: [] - } + }, + onUploadProgress: (event) => { uploadProgress.value = event.progress ?? 0; } }) // TODO. Emit the correct count. .then(() => { emit('success', 1); }) + .finally(() => { uploadProgress.value = 0; }) .catch(noop); }; + + { "en": { diff --git a/src/components/entity/upload/popup.vue b/src/components/entity/upload/popup.vue new file mode 100644 index 000000000..237107820 --- /dev/null +++ b/src/components/entity/upload/popup.vue @@ -0,0 +1,115 @@ + + + + + + + + +{ + "en": { + "rowCount": "{count} data row found | {count} data rows found", + "status": { + // This text is shown while a file is being uploaded to the server. + "sending": "Sending file… ({percentUploaded})", + // This text is shown after a file has been uploaded to the server, but + // before the server has finished processing it. + "processing": "Processing file…" + } + } +} + diff --git a/src/components/spinner.vue b/src/components/spinner.vue index d3bf43707..74ac45018 100644 --- a/src/components/spinner.vue +++ b/src/components/spinner.vue @@ -9,17 +9,23 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file. --> - - @@ -53,14 +59,14 @@ $spinner-width: 3px; transition-delay: 0.15s; } - select + & { + select + &, &.inline { display: inline-block; left: 0; - margin-left: 7px; position: relative; top: 0; vertical-align: text-top; } + select + & { margin-left: 7px; } } .spinner-glyph { height: $spinner-size; diff --git a/test/components/entity/upload.spec.js b/test/components/entity/upload.spec.js index e5b81093d..4b13cf6c2 100644 --- a/test/components/entity/upload.spec.js +++ b/test/components/entity/upload.spec.js @@ -1,4 +1,5 @@ import EntityUpload from '../../../src/components/entity/upload.vue'; +import EntityUploadPopup from '../../../src/components/entity/upload/popup.vue'; import OdataLoadingMessage from '../../../src/components/odata-loading-message.vue'; import testData from '../../data'; @@ -47,10 +48,12 @@ describe('EntityUpload', () => { testData.extendedDatasets.createPast(1); }); - it('shows the filename', async () => { + it('shows the pop-up', async () => { const modal = mountComponent(); await setFiles(modal.get('input'), [csv()]); - modal.get('#entity-upload-filename').text().should.equal('my_data.csv'); + const popup = modal.getComponent(EntityUploadPopup); + popup.props().filename.should.equal('my_data.csv'); + popup.props().count.should.equal(1); }); it('hides the drop zone', async () => { @@ -69,12 +72,22 @@ describe('EntityUpload', () => { button.attributes('aria-disabled').should.equal('false'); }); + it('resets after the clear button is clicked', async () => { + const modal = mountComponent(); + await setFiles(modal.get('input'), [csv()]); + await modal.get('#entity-upload-popup .close').trigger('click'); + modal.findComponent(EntityUploadPopup).exists().should.be.false(); + modal.get('#entity-upload-file-select').should.be.visible(); + const button = modal.get('.modal-actions .btn-primary'); + button.attributes('aria-disabled').should.equal('true'); + }); + it('resets after the modal is hidden', async () => { const modal = mountComponent(); await setFiles(modal.get('input'), [csv()]); await modal.setProps({ state: false }); await modal.setProps({ state: true }); - modal.find('#entity-upload-filename').exists().should.be.false(); + modal.findComponent(EntityUploadPopup).exists().should.be.false(); modal.get('#entity-upload-file-select').should.be.visible(); const button = modal.get('.modal-actions .btn-primary'); button.attributes('aria-disabled').should.equal('true'); diff --git a/test/components/entity/upload/popup.spec.js b/test/components/entity/upload/popup.spec.js new file mode 100644 index 000000000..99365981e --- /dev/null +++ b/test/components/entity/upload/popup.spec.js @@ -0,0 +1,71 @@ +import EntityUploadPopup from '../../../../src/components/entity/upload/popup.vue'; + +import { mergeMountOptions, mount } from '../../../util/lifecycle'; + +const mountComponent = (options = undefined) => + mount(EntityUploadPopup, mergeMountOptions(options, { + props: { filename: 'my_data.csv', count: 1, progress: 0 } + })); + +describe('EntityUploadPopup', () => { + it('shows the filename', async () => { + const div = mountComponent().get('#entity-upload-popup-heading div'); + div.text().should.equal('my_data.csv'); + await div.should.have.textTooltip(); + }); + + describe('clear button', () => { + it('emits a clear event if it is clicked', async () => { + const component = mountComponent(); + await component.get('.close').trigger('click'); + component.emitted().clear.should.eql([[]]); + }); + + it('is hidden if the awaitingResponse prop is true', () => { + const component = mountComponent({ + props: { awaitingResponse: true } + }); + component.get('.close').should.be.hidden(); + }); + }); + + it('shows the count', () => { + const component = mountComponent({ + props: { count: 1000 } + }); + const text = component.get('#entity-upload-popup-count').text(); + text.should.equal('1,000 data rows found'); + }); + + describe('request status', () => { + it('does not show a status if there is no request', () => { + const component = mountComponent({ + props: { awaitingResponse: false } + }); + component.get('#entity-upload-popup-status').should.be.hidden(); + }); + + it('shows the status during a request', () => { + const component = mountComponent({ + props: { awaitingResponse: true } + }); + component.get('#entity-upload-popup-status').should.be.visible(); + }); + + it('shows the upload progress', () => { + const component = mountComponent({ + props: { awaitingResponse: true, progress: 0.5 } + }); + const text = component.get('#entity-upload-popup-status').text(); + text.should.equal('Sending file… (50%)'); + }); + + it('changes the status once all data has been sent', () => { + const component = mountComponent({ + props: { awaitingResponse: true, progress: 1 } + }); + const text = component.get('#entity-upload-popup-status').text(); + text.should.equal('Processing file…'); + }); + }); +}); diff --git a/test/components/spinner.spec.js b/test/components/spinner.spec.js new file mode 100644 index 000000000..2efdf5c05 --- /dev/null +++ b/test/components/spinner.spec.js @@ -0,0 +1,19 @@ +import Spinner from '../../src/components/spinner.vue'; + +import { mount } from '../util/lifecycle'; + +describe('Spinner', () => { + it('adds the correct class if the state prop is true', () => { + const spinner = mount(Spinner, { + props: { state: true } + }); + spinner.classes('active').should.be.true(); + }); + + it('adds the correct class if the inline prop is true', () => { + const spinner = mount(Spinner, { + props: { inline: true } + }); + spinner.classes('inline').should.be.true(); + }); +}); diff --git a/transifex/strings_en.json b/transifex/strings_en.json index b33a872c0..aa178ea15 100644 --- a/transifex/strings_en.json +++ b/transifex/strings_en.json @@ -1982,6 +1982,21 @@ } } }, + "EntityUploadPopup": { + "rowCount": { + "string": "{count, plural, one {{count} data row found} other {{count} data rows found}}" + }, + "status": { + "sending": { + "string": "Sending file… ({percentUploaded})", + "developer_comment": "This text is shown while a file is being uploaded to the server." + }, + "processing": { + "string": "Processing file…", + "developer_comment": "This text is shown after a file has been uploaded to the server, but before the server has finished processing it." + } + } + }, "EntityVersionLink": { "submission": { "string": "Submission {instanceName}",