From 9bbae9c03fba3606d7775a6d82a94a5fd0a46798 Mon Sep 17 00:00:00 2001 From: Jiro Ghianni Date: Mon, 22 Jan 2024 14:05:16 +0100 Subject: [PATCH 1/2] [#1979] Added error validation for document-upload --- .../templates/components/Form/FileInput.html | 2 +- .../js/components/form/FileInput.js | 112 +++++++++++------- .../scss/components/Form/FileInput.scss | 3 +- 3 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/open_inwoner/components/templates/components/Form/FileInput.html b/src/open_inwoner/components/templates/components/Form/FileInput.html index a2ea0e1d15..94f79fb65d 100644 --- a/src/open_inwoner/components/templates/components/Form/FileInput.html +++ b/src/open_inwoner/components/templates/components/Form/FileInput.html @@ -5,7 +5,7 @@
{% render_card direction="vertical" %} {% icon icon="upload" icon_position="before" outlined=True %} - + diff --git a/src/open_inwoner/js/components/form/FileInput.js b/src/open_inwoner/js/components/form/FileInput.js index a451c0fe44..5d3d73df00 100644 --- a/src/open_inwoner/js/components/form/FileInput.js +++ b/src/open_inwoner/js/components/form/FileInput.js @@ -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} @@ -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() + } + } } /** @@ -271,39 +285,55 @@ export class FileInput extends Component { // 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 + // Show fil-type error if allowed types DO contain the extension and returns true + const typeError = !uploadFileTypes.includes(ext) const htmlStart = ` -
  • - +
  • ` - if (sizeMB > maxMegabytes) { + // If uploaded field does NOT contain allowed extension + if (typeError) { + getFormNonFieldError.removeAttribute('hidden') + formSubmitButton.setAttribute('disabled', 'true') + + return ( + htmlStart + + `

    + + Dit type bestand (${ext}) is ongeldig. Geldige bestandstypen zijn: ${uploadFileTypes}. +

    ` + ) + } + if (sizeError) { getFormNonFieldError.removeAttribute('hidden') formSubmitButton.setAttribute('disabled', 'true') return ( htmlStart + - `

    + `

    Dit bestand is te groot

    ` diff --git a/src/open_inwoner/scss/components/Form/FileInput.scss b/src/open_inwoner/scss/components/Form/FileInput.scss index 48be1a8528..c3b47eabb8 100644 --- a/src/open_inwoner/scss/components/Form/FileInput.scss +++ b/src/open_inwoner/scss/components/Form/FileInput.scss @@ -100,7 +100,7 @@ } } - .p--centered.error { + .error { color: var(--color-red-notification); display: flex; align-items: normal; @@ -109,7 +109,6 @@ [class*='icon'] { color: var(--color-red-notification); - font-size: var(--font-size-body-large); } } } From 802735688192c5f1e179d770d0e1284399334455 Mon Sep 17 00:00:00 2001 From: Jiro Ghianni Date: Mon, 5 Feb 2024 08:18:12 +0100 Subject: [PATCH 2/2] [#1982] Fixed delete-function in case of multiple upload errors --- open-inwoner-design-tokens | 2 +- .../components/Card/CardContainer.html | 10 ++-- .../templates/components/Card/RenderCard.html | 2 +- .../templates/components/Form/FileInput.html | 6 +- .../components/templatetags/card_tags.py | 1 + .../js/components/form/FileInput.js | 60 +++++++++---------- .../scss/components/Form/FileInput.scss | 13 ++++ .../templates/pages/cases/document_form.html | 6 +- 8 files changed, 56 insertions(+), 44 deletions(-) diff --git a/open-inwoner-design-tokens b/open-inwoner-design-tokens index 060acec632..b9c2e3ece8 160000 --- a/open-inwoner-design-tokens +++ b/open-inwoner-design-tokens @@ -1 +1 @@ -Subproject commit 060acec632b9f1f63b1a7a7324d599508930a2a9 +Subproject commit b9c2e3ece8f13ae94184734bc3ce825df67d7844 diff --git a/src/open_inwoner/components/templates/components/Card/CardContainer.html b/src/open_inwoner/components/templates/components/Card/CardContainer.html index a776e8d94a..97b77f043c 100644 --- a/src/open_inwoner/components/templates/components/Card/CardContainer.html +++ b/src/open_inwoner/components/templates/components/Card/CardContainer.html @@ -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 %} diff --git a/src/open_inwoner/components/templates/components/Card/RenderCard.html b/src/open_inwoner/components/templates/components/Card/RenderCard.html index e1f54d06ea..728a4ae107 100644 --- a/src/open_inwoner/components/templates/components/Card/RenderCard.html +++ b/src/open_inwoner/components/templates/components/Card/RenderCard.html @@ -10,7 +10,7 @@ {% endif %} -
    +
    {% if title %}

    {{ title }} diff --git a/src/open_inwoner/components/templates/components/Form/FileInput.html b/src/open_inwoner/components/templates/components/Form/FileInput.html index 94f79fb65d..5b281b0796 100644 --- a/src/open_inwoner/components/templates/components/Form/FileInput.html +++ b/src/open_inwoner/components/templates/components/Form/FileInput.html @@ -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 %}

    {% render_card direction="vertical" %} {% icon icon="upload" icon_position="before" outlined=True %} - + diff --git a/src/open_inwoner/components/templatetags/card_tags.py b/src/open_inwoner/components/templatetags/card_tags.py index 3ddfe5fd20..843f1d5924 100644 --- a/src/open_inwoner/components/templatetags/card_tags.py +++ b/src/open_inwoner/components/templatetags/card_tags.py @@ -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. diff --git a/src/open_inwoner/js/components/form/FileInput.js b/src/open_inwoner/js/components/form/FileInput.js index 5d3d73df00..198ea85930 100644 --- a/src/open_inwoner/js/components/form/FileInput.js +++ b/src/open_inwoner/js/components/form/FileInput.js @@ -20,7 +20,7 @@ export class FileInput extends Component { * @returns {string} of file extensions. */ getUploadTypes() { - return this.getInput().dataset.fileTypes.replace(/["|'\[\]]/g, '') + return this.getInput().dataset.fileTypes.replace(/["'\[\]]/g, '') } /** @@ -275,7 +275,6 @@ 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) @@ -283,21 +282,17 @@ export class FileInput extends Component { 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 - // Show fil-type error if allowed types DO contain the extension and returns true - const typeError = !uploadFileTypes.includes(ext) + const typeError = !uploadFileTypes.split(', ').includes(ext) const htmlStart = `
  • ` - // If uploaded field does NOT contain allowed extension - if (typeError) { - getFormNonFieldError.removeAttribute('hidden') - formSubmitButton.setAttribute('disabled', 'true') - - return ( - htmlStart + - `

    - - Dit type bestand (${ext}) is ongeldig. Geldige bestandstypen zijn: ${uploadFileTypes}. -

    ` - ) - } - if (sizeError) { + if (typeError || sizeError) { getFormNonFieldError.removeAttribute('hidden') formSubmitButton.setAttribute('disabled', 'true') - - return ( - htmlStart + - `

    - - Dit bestand is te groot -

    ` - ) } else { getFormNonFieldError.setAttribute('hidden', 'hidden') formSubmitButton.removeAttribute('disabled') diff --git a/src/open_inwoner/scss/components/Form/FileInput.scss b/src/open_inwoner/scss/components/Form/FileInput.scss index c3b47eabb8..6b39676418 100644 --- a/src/open_inwoner/scss/components/Form/FileInput.scss +++ b/src/open_inwoner/scss/components/Form/FileInput.scss @@ -111,5 +111,18 @@ color: var(--color-red-notification); } } + + .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; + } + } + } } } diff --git a/src/open_inwoner/templates/pages/cases/document_form.html b/src/open_inwoner/templates/pages/cases/document_form.html index c2f8c8a4cb..ff487d5591 100644 --- a/src/open_inwoner/templates/pages/cases/document_form.html +++ b/src/open_inwoner/templates/pages/cases/document_form.html @@ -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" %}