Skip to content

Commit

Permalink
Merge pull request #157 from opendatakit/attachments-modal
Browse files Browse the repository at this point in the history
Add ability to select files from within upload files modal
  • Loading branch information
matthew-white authored Nov 1, 2018
2 parents c6fd4ee + c653fdb commit 4b12e08
Show file tree
Hide file tree
Showing 8 changed files with 500 additions and 395 deletions.
16 changes: 8 additions & 8 deletions lib/components/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,24 +176,24 @@ h1 {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

.no-text-decoration {
.text-no-decoration {
&, &:hover, &:focus, &.focus {
text-decoration: none;
}
}

@media print {
a.no-text-decoration:visited {
text-decoration: none;
a:hover, a:focus {
.underline-within-link {
text-decoration: underline;
}
}

.underline-on-hover-or-focus {
&:hover, &:focus {
text-decoration: underline;
@media print {
a.text-no-decoration:visited {
text-decoration: none;
}

@media print {
.underline-within-link {
text-decoration: underline;
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/components/field-key/row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ except according to the terms contained in the LICENSE file.
<td>{{ created }}</td>
<td>{{ lastUsed }}</td>
<td>
<a v-if="fieldKey.token != null" ref="popoverLink"
class="field-key-row-popover-link no-text-decoration" role="button"
<a v-if="fieldKey.token != null" ref="popoverLink" href="#"
class="field-key-row-popover-link text-no-decoration" role="button"
@click.prevent="showCode">
<span class="icon-qrcode"></span>
<span class="underline-on-hover-or-focus">See code</span>
<span class="underline-within-link">See code</span>
</a>
<template v-else>
Access revoked
Expand Down
94 changes: 56 additions & 38 deletions lib/components/form/attachment/list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ except according to the terms contained in the LICENSE file.
@confirm="uploadFiles" @cancel="cancelUploads"/>

<form-attachment-upload-files v-bind="uploadFilesModal"
@hide="hideModal('uploadFilesModal')"/>
@hide="hideModal('uploadFilesModal')" @choose="afterChoose"/>
<form-attachment-name-mismatch :state="nameMismatch.state"
:planned-uploads="plannedUploads" @hide="hideModal('nameMismatch')"
@confirm="uploadFiles" @cancel="cancelUploads"/>
Expand Down Expand Up @@ -152,6 +152,17 @@ export default {
}
},
methods: {
////////////////////////////////////////////////////////////////////////////
// File picker

afterChoose(files) {
this.uploadFilesModal.state = false;
this.matchFilesToAttachments(files);
},

////////////////////////////////////////////////////////////////////////////
// Drag and drop

// items is a DataTransferItemList, not an Array.
fileItemCount(maybeItems) {
// IE
Expand Down Expand Up @@ -180,6 +191,50 @@ export default {
this.countOfFilesOverDropZone = 0;
}
},
ondrop(jQueryEvent) {
this.countOfFilesOverDropZone = 0;
const { files } = jQueryEvent.originalEvent.dataTransfer;
if (this.dragoverAttachment != null) {
const upload = { attachment: this.dragoverAttachment, file: files[0] };
this.dragoverAttachment = null;
this.plannedUploads.push(upload);
if (upload.file.name === upload.attachment.name)
this.uploadFiles();
else
this.showModal('nameMismatch');
} else {
// The else case can be reached even if this.countOfFilesOverDropZone
// was 1, if the drop was not over a row.
this.matchFilesToAttachments(files);
}
},

////////////////////////////////////////////////////////////////////////////
// Core logic

matchFilesToAttachments(files) {
this.plannedUploads = [];
this.unmatchedFiles = [];
// files is a FileList, not an Array, hence the style of for-loop.
for (let i = 0; i < files.length; i += 1) {
const file = files[i];
const attachment = this.attachments.find(a => a.name === file.name);
if (attachment != null)
this.plannedUploads.push({ attachment, file });
else
this.unmatchedFiles.push(file);
}
// With the changes to this.plannedUploads and this.unmatchedFiles, the
// popup will show in the next tick.
},
// cancelUploads() cancels the uploads before they start, after files have
// been dropped. (It does not cancel an upload in progress.)
cancelUploads() {
// Checking `length` in order to avoid setting these properties
// unnecessarily, which could result in Vue calculations.
if (this.plannedUploads.length !== 0) this.plannedUploads = [];
if (this.unmatchedFiles.length !== 0) this.unmatchedFiles = [];
},
problemToAlert(problem) {
if (this.uploadStatus.total === 1) return null;
const uploaded = this.uploadStatus.total - this.uploadStatus.remaining;
Expand Down Expand Up @@ -229,43 +284,6 @@ export default {
.catch(() => {});
this.plannedUploads = [];
this.unmatchedFiles = [];
},
ondrop(jQueryEvent) {
this.countOfFilesOverDropZone = 0;
const { files } = jQueryEvent.originalEvent.dataTransfer;
if (this.dragoverAttachment != null) {
const upload = { attachment: this.dragoverAttachment, file: files[0] };
this.dragoverAttachment = null;
this.plannedUploads.push(upload);
if (upload.file.name === upload.attachment.name)
this.uploadFiles();
else
this.showModal('nameMismatch');
} else {
// The else case can be reached even if this.countOfFilesOverDropZone
// was 1, if the drop was not over a row.

// files is a FileList, not an Array, hence the style of for-loop.
for (let i = 0; i < files.length; i += 1) {
const file = files[i];
const attachment = this.attachments.find(a => a.name === file.name);
if (attachment != null)
this.plannedUploads.push({ attachment, file });
else
this.unmatchedFiles.push(file);
}

// With the changes to this.plannedUploads and this.unmatchedFiles, the
// popup will show in the next tick.
}
},
// cancelUploads() cancels the uploads before they start, after files have
// been dropped. (It does not cancel an upload in progress.)
cancelUploads() {
// Checking `length` in order to avoid setting these properties
// unnecessarily, which could result in Vue calculations.
if (this.plannedUploads.length !== 0) this.plannedUploads = [];
if (this.unmatchedFiles.length !== 0) this.unmatchedFiles = [];
}
}
};
Expand Down
6 changes: 5 additions & 1 deletion lib/components/form/attachment/popups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ except according to the terms contained in the LICENSE file.
</template>
</p>
<p>
<button type="button" class="btn btn-primary"
<button ref="confirmButton" type="button" class="btn btn-primary"
@click="$emit('confirm')">
Looks good, proceed
</button>
Expand Down Expand Up @@ -149,6 +149,10 @@ export default {
// in IE 10.
return `${Math.floor(100 * (loaded / total)).toLocaleString()}%`;
}
},
updated() {
if (this.plannedUploads.length !== 0 && !this.nameMismatch.state)
this.$refs.confirmButton.focus();
}
};
</script>
Expand Down
38 changes: 36 additions & 2 deletions lib/components/form/attachment/upload-files.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@ including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<modal :state="state" backdrop hideable @hide="$emit('hide')">
<modal id="form-attachment-upload-files" :state="state" backdrop hideable
@hide="$emit('hide')" @shown="focusLink">
<template slot="title">Upload Files</template>
<template slot="body">
<p class="modal-introduction">
To upload files, please drag and drop files onto the table on this page.
To upload files, you can <strong>drag and drop</strong> one or more
files onto the table on this page.
</p>
<p>
If you would rather select files from a prompt, ensure that their names
match the ones in the table and then
<input ref="input" type="file" class="hidden" multiple>
<a ref="link" href="#" class="text-no-decoration" role="button"
@click.prevent="clickInput">
<span class="icon-folder-open"></span>
<span class="underline-within-link">click here to choose</span></a>.
</p>
<div class="modal-actions">
<button type="button" class="btn btn-primary" @click="$emit('hide')">
Expand All @@ -33,6 +44,29 @@ export default {
type: Boolean,
default: false
}
},
mounted() {
// Using a jQuery event handler rather than a Vue one in order to facilitate
// testing: it is possible to mock a jQuery event but not a Vue event.
$(this.$refs.input).on('change.form-attachment-upload-files', (event) =>
this.$emit('choose', event.target.files));
},
beforeDestroy() {
$(this.$refs.input).off('.form-attachment-upload-files');
},
methods: {
focusLink() {
$(this.$refs.link).focus();
},
clickInput() {
$(this.$refs.input).click();
}
}
};
</script>

<style lang="sass">
#form-attachment-upload-files a[role="button"] {
margin-left: 5px;
}
</style>
2 changes: 2 additions & 0 deletions lib/components/form/new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export default {
}
},
mounted() {
// Using a jQuery event handler rather than a Vue one in order to facilitate
// testing: it is possible to mock a jQuery event but not a Vue event.
$(this.$refs.input)
.on('change.form-new', (event) => this.readFile(event.target.files));
},
Expand Down
2 changes: 1 addition & 1 deletion lib/components/form/submission/row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ except according to the terms contained in the LICENSE file.
:title="hasTitle(column) ? fieldValue(column) : null">
<template v-if="column.type === 'binary'">
<a v-if="fieldValue(column) !== ''" :href="fieldValue(column)"
class="form-submission-list-binary-link no-text-decoration" target="_blank"
class="form-submission-list-binary-link text-no-decoration" target="_blank"
title="File was submitted. Click to download.">
<span class="icon-check text-success"></span>
<span class="icon-download"></span>
Expand Down
Loading

0 comments on commit 4b12e08

Please sign in to comment.