Skip to content

Commit

Permalink
22995 Change multi-file upload to new design (#738)
Browse files Browse the repository at this point in the history
* - app version = 5.11.23
- added helpful base class
- added public "open" method to file uploader
- deleted unneeded v-form
- refactored multi-file upload per new design
- updated unit tests

* - renamed "Upload File" -> "Upload Documents"

* - reformatted "enter your business information manually"

* - fix to open THIS file input, not the first one in the document

* - on error, show first paragraph as red
- update store when files are added or removed

---------

Co-authored-by: Severin Beauvais <[email protected]>
  • Loading branch information
severinbeauvais and Severin Beauvais authored Sep 27, 2024
1 parent eddcd76 commit ae7df17
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 96 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "business-create-ui",
"version": "5.11.22",
"version": "5.11.23",
"private": true,
"appName": "Create UI",
"sbcName": "SBC Common Components",
Expand Down
4 changes: 4 additions & 0 deletions src/assets/styles/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ p {
background-color: $gray1 !important;
}

.dk-gray-background {
background-color: $gray3 !important;
}

// Hides an element to all devices except screen readers
.sr-only {
position: absolute;
Expand Down
186 changes: 113 additions & 73 deletions src/components/ContinuationIn/ContinuationAuthorization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,63 @@
v-if="authorization"
id="continuation-authorization"
>
<!-- Authorization Date -->
<v-card
flat
class="py-8 px-6"
:class="{ 'invalid-section': getShowErrors && !authorizationDateValid }"
>
<v-form
ref="formRef"
lazy-validation
@submit.prevent
>
<!-- Authorization Date -->
<v-row no-gutters>
<v-col
cols="12"
sm="3"
>
<label>Authorization Date</label>
</v-col>
<v-row no-gutters>
<v-col
cols="12"
sm="3"
>
<label>Authorization Date</label>
</v-col>

<v-col
cols="12"
sm="9"
class="pl-8"
>
<DatePickerShared
id="authorization-date"
ref="authorizationDateRef"
title="Authorization Date"
:nudgeRight="40"
:nudgeTop="85"
hint="The date the authorization was issued"
:persistentHint="true"
:initialValue="authorization.date"
:inputRules="getShowErrors ? authorizationDateRules : []"
:minDate="minAuthorizationDate"
:maxDate="getCurrentDate"
@emitDateSync="authorization.date = $event"
/>
</v-col>
</v-row>
</v-form>
<v-col
cols="12"
sm="9"
>
<DatePickerShared
id="authorization-date"
ref="authorizationDateRef"
title="Authorization Date"
:nudgeRight="40"
:nudgeTop="85"
hint="The date the authorization was issued"
:persistentHint="true"
:initialValue="authorization.date"
:inputRules="getShowErrors ? authorizationDateRules : []"
:minDate="minAuthorizationDate"
:maxDate="getCurrentDate"
@emitDateSync="authorization.date = $event"
/>
</v-col>
</v-row>
</v-card>

<!-- Upload Documents -->
<v-card
flat
class="mt-6 py-8 px-6"
:class="{ 'invalid-section': getShowErrors && !authorizationFilesValid }"
>
<!-- Upload File -->
<v-row no-gutters>
<v-col
cols="12"
sm="3"
>
<label>Upload File</label>
<label>Upload Documents</label>
</v-col>

<v-col
cols="12"
sm="9"
>
<p>
Upload documents that support proof of authorization from your home jursidiction.
<p :class="{ 'error-text': getShowErrors && !authorizationFilesValid }">
Upload one or more documents that show proof of authorization to continue out of your
previous jursidiction.
</p>

<ul>
Expand All @@ -83,15 +77,41 @@
Maximum 5 files
</li>
</ul>

<v-btn
id="add-document-button"
outlined
color="primary"
class="btn-outlined-primary mt-4"
:disabled="numFiles >= 5"
@click="onClickAddDocumentButton()"
>
<v-icon>mdi-plus</v-icon>
<span>Add a Document</span>
</v-btn>

<p
v-if="customErrorMessage"
class="error-text mt-4 mb-0"
>
{{ customErrorMessage }}
</p>

<FileUploadPreview
ref="fileUploadPreview"
class="d-none mt-6"
:maxSize="MAX_FILE_SIZE"
:customErrorMessage.sync="customErrorMessage"
@fileValidity="onFileValidity($event)"
@fileSelected="onFileSelected($event)"
/>
</v-col>
</v-row>

<!-- file upload components -->
<v-row
v-for="(num, index) in numUploads"
:key="authorization.files[index]?.fileKey"
:class="(index === 0) ? 'mt-6' : 'mt-4'"
class="upload-file-row mb-n2"
v-for="(num, index) in numFiles"
:key="authorization.files[index].fileKey"
class="upload-file-row mt-5 mb-n2"
no-gutters
>
<v-col
Expand All @@ -104,19 +124,24 @@
<v-col
cols="12"
sm="9"
class="dk-gray-background rounded d-flex justify-space-between px-2 py-1"
>
<FileUploadPreview
inputFileLabel="Continuation authorization"
:maxSize="MAX_FILE_SIZE"
:pdfPageSize="PdfPageSize.LETTER_SIZE"
:hint="authorization.files[index]?.file ? 'File uploaded' : undefined"
:inputFile="authorization.files[index]?.file"
:showErrors="getShowErrors"
:customErrorMessage.sync="customErrorMessage[index]"
:isRequired="getShowErrors && (index === 0)"
@fileValidity="onFileValidity($event)"
@fileSelected="onFileSelected(index, $event)"
/>
<div class="document-details">
<v-icon>mdi-paperclip</v-icon>
<span class="pl-2">{{ authorization.files[index]?.file.name }}</span>
<span class="pl-2">({{ friendlyFileSize(authorization.files[index]?.file.size) }})</span>
</div>
<v-btn
class="remove-document-button"
text
color="primary"
@click="onFileSelected(null, index)"
>
<span>Remove</span>
<v-icon dense>
mdi-close
</v-icon>
</v-btn>
</v-col>
</v-row>

Expand Down Expand Up @@ -173,8 +198,8 @@ import { Action, Getter } from 'pinia-class'
import { StatusCodes } from 'http-status-codes'
import { useStore } from '@/store/store'
import { DocumentMixin } from '@/mixins'
import { ContinuationAuthorizationIF, ExistingBusinessInfoIF, FormIF, PresignedUrlIF } from '@/interfaces'
import { FilingStatus, PdfPageSize } from '@/enums'
import { ContinuationAuthorizationIF, ExistingBusinessInfoIF, PresignedUrlIF } from '@/interfaces'
import { FilingStatus } from '@/enums'
import { VuetifyRuleFunction } from '@/types'
import FileUploadPreview from '@/components/common/FileUploadPreview.vue'
import { DatePicker as DatePickerShared } from '@bcrs-shared-components/date-picker'
Expand All @@ -195,11 +220,9 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
// Refs
$refs!: {
authorizationDateRef: DatePickerShared,
formRef: FormIF
fileUploadPreview: FileUploadPreview
}
readonly PdfPageSize = PdfPageSize
@Getter(useStore) getContinuationAuthorization!: ContinuationAuthorizationIF
@Getter(useStore) getCurrentDate!: string
@Getter(useStore) getExistingBusinessInfo!: ExistingBusinessInfoIF
Expand All @@ -213,7 +236,7 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
authorization = null as ContinuationAuthorizationIF
authorizationDateValid = false
fileValidity = false
customErrorMessage = ['', '', '', '', ''] // max 5 files
customErrorMessage = ''
isFileAdded = false
/**
Expand Down Expand Up @@ -259,9 +282,8 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
return (this.isStatusChangeRequested && !!this.latestReviewComment)
}
/** The number of file upload components to display (max 5). */
get numUploads (): number {
return Math.min((this.authorization.files.length + 1), 5)
get numFiles (): number {
return (this.authorization?.files.length || 0)
}
/** Whether authorization files section is valid. */
Expand All @@ -270,7 +292,7 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
// AND this must a draft filing or if it's a Change Requested filing
// then a new file must have been added
return (
(this.authorization?.files.length >= 1) &&
(this.numFiles >= 1) &&
(!this.isStatusChangeRequested || this.isFileAdded)
)
}
Expand All @@ -281,6 +303,20 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
{ files: [], date: null } as ContinuationAuthorizationIF
}
/** When user has clicked the Add button, opens the file selection dialog. */
onClickAddDocumentButton (): void {
this.$refs.fileUploadPreview.clickFileInput()
}
/** Converts a file size to a string in MB, KB or bytes. */
friendlyFileSize (size: number): string {
const sizeKB = size / 1024
const sizeMB = sizeKB / 1024
if (sizeMB > 1) return `${sizeMB.toFixed(1)} MB`
if (sizeKB > 1) return `${sizeKB.toFixed(0)} KB`
return `${size} bytes`
}
/**
* Called when FileUploadPreview tells us whether a file is valid.
* This is called right before the File Selected event.
Expand All @@ -292,8 +328,10 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
/**
* Called when FileUploadPreview tells us about a new or cleared file.
* This is called right after the File Validity event.
* @param file the file to add or null to delete
* @param index the index of the file to delete
*/
async onFileSelected (index: number, file: File): Promise<void> {
async onFileSelected (file: File, index = 0): Promise<void> {
if (file) {
// verify that file is valid
if (!this.fileValidity) {
Expand All @@ -304,8 +342,7 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
// verify that file doesn't already exist
if (this.authorization.files.find(f => f.file.name === file.name)) {
// put file uploader into manual error mode by setting custom error message
this.customErrorMessage[index] = 'Duplicate file'
this.$forceUpdate() // force file upload component to react
this.customErrorMessage = 'Duplicate file'
return // don't add to list
}
Expand All @@ -317,8 +354,7 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
if (!res || res.status !== StatusCodes.OK) throw new Error()
} catch {
// put file uploader into manual error mode by setting custom error message
this.customErrorMessage[index] = this.UPLOAD_FAILED_MESSAGE
this.$forceUpdate() // force file upload component to react
this.customErrorMessage = this.UPLOAD_FAILED_MESSAGE
return // don't add to array
}
Expand Down Expand Up @@ -349,14 +385,14 @@ export default class ExtraproRegistration extends Mixins(DocumentMixin) {
if (this.getShowErrors) {
// wait for form to finish rendering
await this.$nextTick()
// validate the form and our custom component that doesn't support form validation
this.$refs.formRef.validate()
// validate date component
this.authorizationDateValid = this.$refs.authorizationDateRef.validateForm()
}
}
@Watch('authorizationFilesValid') // re-validate when the Authorization Files validity changes
@Watch('authorizationDateValid') // re-validate when the Authorization Date validity changes
@Watch('authorization.files') // update store when files are added or removed
@Emit('valid')
private onComponentValid (): boolean {
// sync local object to the store
Expand Down Expand Up @@ -433,4 +469,8 @@ ul {
padding-left: 0.25rem;
}
}
.document-details {
padding-top: 6px;
}
</style>
9 changes: 6 additions & 3 deletions src/components/ContinuationIn/ManualBusinessInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
<div
class="font-14 ml-3"
>
Not extraprovincially registered in B.C.?
&nbsp; &nbsp;
<a @click="onClick()">Enter your business information manually</a>
Not extraprovincially registered in B.C.?<br>
<a @click="onClick()">
<span class="font-weight-bold text-decoration-underline">
Enter your business information manually
</span>
</a>
</div>
</v-col>
</v-row>
Expand Down
8 changes: 8 additions & 0 deletions src/components/common/FileUploadPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
lazy-validation
>
<v-file-input
id="file-input"
v-model="fileUpload"
:label="label"
filled
Expand Down Expand Up @@ -85,6 +86,13 @@ export default class FileUploadPreview extends Mixins(DocumentMixin) {
await this.computeEmitFileValidity(this.fileUpload)
}
/** Programmatically opens the file selection dialog. Can be called externally. */
public clickFileInput (): void {
// find the find input element in THIS component (not globally)
const element = this.$el.querySelector('#file-input') as HTMLElement
element.click()
}
// Note: the validation is done this way as opposed to being all defined in the validation rules(fileUploadRules)
// above because Vuetify does not support async validation out of the box. This was needed to work
// around this limitation. vuelidate does support this but this would mean introducing a component that
Expand Down
Loading

0 comments on commit ae7df17

Please sign in to comment.