Skip to content

Commit

Permalink
Merge pull request #978 from maykinmedia/feature/1982-error-validatio…
Browse files Browse the repository at this point in the history
…n-document-type-upload

[#1982] Add file-type error validation for document-upload
  • Loading branch information
stevenbal authored Feb 8, 2024
2 parents a5f2d32 + 8027356 commit fefb1f5
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 67 deletions.
2 changes: 1 addition & 1 deletion open-inwoner-design-tokens
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@
{% for category in categories %}
{% url 'products:category_detail' slug=category.slug as category_url %}
{% if category.icon %}
{% card src=category.icon.file.url alt=category.icon.name title=category.name href=category_url image_object_fit=image_object_fit %}
{% card src=category.icon.file.url alt=category.icon.name title=category.name href=category_url compact=True image_object_fit=image_object_fit %}
{% else %}
{% card src=category.image.file.url alt=category.image.name title=category.name href=category_url image_object_fit=image_object_fit %}
{% card src=category.image.file.url alt=category.image.name title=category.name href=category_url compact=True image_object_fit=image_object_fit %}
{% endif %}
{% endfor %}
{% endif %}

{% if subcategories %}
{% for subcategory in subcategories %}
{% category_card category=subcategory parent_category=parent_category image_object_fit=image_object_fit %}
{% category_card category=subcategory parent_category=parent_category compact=True image_object_fit=image_object_fit %}
{% endfor %}
{% endif %}

{% if products %}
{% for product in products %}
{% get_product_url product as product_url %}
{% product_card title=product.name description=product.summary url=product_url image=product.icon image_object_fit=image_object_fit %}
{% product_card title=product.name description=product.summary url=product_url image=product.icon compact=True image_object_fit=image_object_fit %}
{% endfor %}
{% endif %}

{% if plans %}
{% for plan in plans %}
{% description_card title=plan.title description=plan.goal|truncatechars:51 url=plan.get_absolute_url elypsis=True object=plan image_object_fit=image_object_fit %}
{% description_card title=plan.title description=plan.goal|truncatechars:51 url=plan.get_absolute_url elypsis=True object=plan compact=True image_object_fit=image_object_fit %}
{% endfor %}
{% endif %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</span>
{% endif %}

<div class="card__body--compact {% if direction %} card__body--direction-{{ direction }}{% endif %}{% if grid %} card__body--grid{% endif %}">
<div class="card__body{% if compact %}--compact{% endif %} {% if direction %} card__body--direction-{{ direction }}{% endif %}{% if grid %} card__body--grid{% endif %}">
{% if title %}
<p class="{% if compact %}h4{% else %}h3{% endif %}">
<span class="link link__text">{{ title }}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
{% load i18n solo_tags button_tags card_tags form_tags icon_tags %}

{% get_solo 'openzaak.OpenZaakConfig' as openzaak_config %}
{% load i18n button_tags card_tags form_tags icon_tags %}

<div class="form__control file-input" aria-live="polite">
{% render_card direction="vertical" %}
{% icon icon="upload" icon_position="before" outlined=True %}
<input class="file-input__input" id="{{ field.auto_id }}" name="file" type="file"{% if field.field.required %} required{% endif %}{% if multiple %} multiple{% endif %} data-max-size="{{ openzaak_config.max_upload_size }}">
<input class="file-input__input" id="{{ field.auto_id }}" name="file" type="file"{% if field.field.required %} required{% endif %}{% if multiple %} multiple{% endif %} data-max-size="{{ max_upload_size }}" data-file-types="{{ allowed_file_extensions }}">
<label class="button button--primary file-input__label-empty" for="{{ field.auto_id }}">
{% if multiple %}{% trans 'Sleep of selecteer bestanden' %}{% else %}{% trans 'Sleep of selecteer bestand' %}{% endif %}
</label>
Expand Down
1 change: 1 addition & 0 deletions src/open_inwoner/components/templatetags/card_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def card(href, title, **kwargs):
- direction: string | can be set to "horizontal" to show contents horizontally.
- inline: bool | Whether the card should be rendered inline.
- src: string | the src of the header image.
- compact: bool | Whether the card has uniform padding on all sides.
- stretch: bool | Whether to stretch the card vertically.
- tinted: bool | whether to use gray as background color.
- type: string (info) | Set to info for an info card.
Expand Down
132 changes: 80 additions & 52 deletions src/open_inwoner/js/components/form/FileInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export class FileInput extends Component {
return parseInt(this.getInput().dataset.maxSize)
}

/**
* Get configured limited file-types from 'data-file-types', return as clean string, and use in node.
* @returns {string} of file extensions.
*/
getUploadTypes() {
return this.getInput().dataset.fileTypes.replace(/["'\[\]]/g, '')
}

/**
* Returns the card (drop zone) associated with the file input.
* @return {HTMLDivElement}
Expand Down Expand Up @@ -156,33 +164,39 @@ export class FileInput extends Component {
}

/**
* Gets called when click event is received on the files list, it originates from a delete button, handle the deletion accordingly.
* Gets called when click event is received on the files list, it originates from a delete button, to handle the deletion accordingly.
* @param {PointerEvent} e
*/
onClick(e) {
e.preventDefault()
const { target } = e

// Do nothing if the click does not originate from a delete button.
if (
!target.classList.contains('link') &&
!target.parentElement.classList.contains('link')
) {
return
}
// Check if the click originates from a delete button
const isDeleteButton =
// Filter the file list.
target.classList.contains('link') ||
target.parentElement.classList.contains('link')

// Filter the file list.
const listItem = target.closest('.file-list__list-item')
const index = [...this.getFilesList().children].indexOf(listItem)
const input = this.getInput()
if (isDeleteButton) {
const listItem = target.closest('.file-list__list-item')

const files = [...input.files].filter((_, i) => i !== index)
// Ensure the list item is found
if (listItem) {
const index = Array.from(listItem.parentElement.children).indexOf(
listItem
)
const input = this.getInput()

this.addFiles(files, true)
this.files = files
// Use filter, not splice
const files = Array.from(input.files).filter((_, i) => i !== index)

// We need to render manually since we're not making state changes.
this.render()
this.addFiles(files, true)
this.files = files

// We need to render manually since we're not making state changes.
this.render()
}
}
}

/**
Expand Down Expand Up @@ -261,53 +275,67 @@ export class FileInput extends Component {
* @return {string}
*/
renderFileHTML(file) {
// renderFileHTML is a separate function, where the context of 'this' changes when it is called
const { name, size, type } = file
const ext = name.split('.').pop().toUpperCase()
const sizeMB = (size / (1024 * 1024)).toFixed(2)
const labelDelete = this.getFilesList().dataset.labelDelete || 'Delete'
const getFormNonFieldError = this.getFormNonFieldError()
const formSubmitButton = this.getFormSubmitButton()

// Only show errors notification if data-max-file-size is exceeded + add error class to file-list
const maxMegabytes = this.getLimit()
const uploadFileTypes = this.getUploadTypes().toUpperCase()

const sizeError = sizeMB > maxMegabytes
const typeError = !uploadFileTypes.split(', ').includes(ext)

const htmlStart = `
<li class="file-list__list-item">
<aside class="file">
<div class="file__container">
${
sizeMB > maxMegabytes
? '<div class="file__file error">'
: '<div class="file__file">'
}
<p class="file__symbol">
<span aria-hidden="true" class="material-icons-outlined">${
type.match('image') ? 'image' : 'description'
}</span>
</p>
<p class="p file__data">
<span class="file__name">${name} (${ext}, ${sizeMB}MB)</span>
</p>
<a class="link link--primary" href="#" role="button" aria-label="${labelDelete}">
<span aria-hidden="true" class="material-icons-outlined">delete</span>
</a>
</div>
</div>
</aside>
</li>`

if (sizeMB > maxMegabytes) {
<li class="file-list__list-item">
<aside class="file">
<div class="file__container">
<div class="file__file ${typeError || sizeError ? 'error' : ''}">
<p class="file__symbol">
<span aria-hidden="true" class="material-icons-outlined">${
type.match('image') ? 'image' : 'description'
}</span>
</p>
<p class="p file__data">
<span class="file__name">${name} (${ext}, ${sizeMB}MB)</span>
</p>
<a class="link link--primary" href="#document-upload" role="button" aria-label="${labelDelete}">
<span aria-hidden="true" class="material-icons-outlined">delete</span>
</a>
</div>
${
typeError && sizeError
? `
<p class="p p--small error">
<span aria-hidden="true" class="material-icons-outlined">warning_amber</span>
<span class="file-error__content">Dit bestand is te groot - en van een ongeldig type ("${ext}"). <br />Toegestaan zijn: ${uploadFileTypes}.</span>
</p>`
: typeError || sizeError
? `
<p class="p p--small error">
<span aria-hidden="true" class="material-icons-outlined">warning_amber</span>
${
typeError
? `<span class="file-error__content">Dit type bestand ("${ext}") is ongeldig. Geldige bestandstypen zijn: ${uploadFileTypes}.</span>`
: ''
}
${
sizeError
? '<span class="file-error__content">Dit bestand is te groot.</span>'
: ''
}
</p>`
: ''
}
</div>
</aside>
</li>`

if (typeError || sizeError) {
getFormNonFieldError.removeAttribute('hidden')
formSubmitButton.setAttribute('disabled', 'true')

return (
htmlStart +
`<p class="p p--small p--centered error">
<span aria-hidden="true" class="material-icons-outlined">warning_amber</span>
Dit bestand is te groot
</p>`
)
} else {
getFormNonFieldError.setAttribute('hidden', 'hidden')
formSubmitButton.removeAttribute('disabled')
Expand Down
16 changes: 14 additions & 2 deletions src/open_inwoner/scss/components/Form/FileInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
}
}

.p--centered.error {
.error {
color: var(--color-red-notification);
display: flex;
align-items: normal;
Expand All @@ -109,7 +109,19 @@

[class*='icon'] {
color: var(--color-red-notification);
font-size: var(--font-size-body-large);
}
}

.p--small {
margin-top: var(--spacing-small);
margin-bottom: var(--spacing-small);

&.error {
[class*='icon'] {
color: var(--color-red-notification);
font-size: var(--font-size-body);
margin-top: 2px;
}
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/open_inwoner/templates/pages/cases/document_form.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{% load i18n form_tags button_tags icon_tags %}
{% load i18n solo_tags form_tags button_tags icon_tags %}

{% get_solo 'openzaak.OpenZaakConfig' as openzaak_config %}

{% render_form id="document-upload" form=form method="POST" hxencoding="multipart/form-data" hxpost=hxpost_document_action hxtarget="#form_upload" submit_text=_("Bestand uploaden") extra_classes="case-detail-form" %}
{% csrf_token %}
{% input form.type no_label=True no_help=True class="label input" id="id_type" extra_classes="file-type__select" %}
{% file_input form.files %}
{% file_input form.files max_upload_size=openzaak_config.max_upload_size allowed_file_extensions=openzaak_config.allowed_file_extensions %}
{% form_actions primary_text=_("Upload documenten") enctype="multipart/form-data" %}
<div class="non-field-error" hidden>
<p class="p p--small p--centered">{% trans "Verwijder eerst bestanden die niet voldoen aan de voorwaarden" %}</p>
Expand Down

0 comments on commit fefb1f5

Please sign in to comment.