Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions locales/de/LC_MESSAGES/volto.po
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@ msgstr ""
msgid "Thank you."
msgstr ""

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr "Dieses Feld ist erforderlich"

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
5 changes: 5 additions & 0 deletions locales/en/LC_MESSAGES/volto.po
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,11 @@ msgstr ""
msgid "Thank you."
msgstr ""

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr ""

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
5 changes: 5 additions & 0 deletions locales/es/LC_MESSAGES/volto.po
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,11 @@ msgstr "Texto..."
msgid "Thank you."
msgstr "Gracias."

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr "Este campo es obligatorio"

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
5 changes: 5 additions & 0 deletions locales/fr/LC_MESSAGES/volto.po
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,11 @@ msgstr ""
msgid "Thank you."
msgstr "Merci."

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr "Ce champ est obligatoire"

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
5 changes: 5 additions & 0 deletions locales/it/LC_MESSAGES/volto.po
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,11 @@ msgstr "Testo..."
msgid "Thank you."
msgstr "Grazie."

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr ""

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
7 changes: 6 additions & 1 deletion locales/volto.pot
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Plone\n"
"POT-Creation-Date: 2025-11-27T13:35:51.867Z\n"
"POT-Creation-Date: 2026-02-10T13:39:49.907Z\n"
"Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
"Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
"MIME-Version: 1.0\n"
Expand Down Expand Up @@ -673,6 +673,11 @@ msgstr ""
msgid "Thank you."
msgstr ""

#: overrideTranslations
# defaultMessage: This is a required field.
msgid "This field is required."
msgstr ""

#: components/ItaliaTheme/Blocks/Accordion/Edit
#: components/ItaliaTheme/Blocks/ContactsBlock/Edit
#: components/ItaliaTheme/Blocks/IconBlocks/Edit
Expand Down
111 changes: 90 additions & 21 deletions src/customizations/volto/components/manage/Widgets/FileWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
// CUSTOMIZATION:
// - 177-179: Added link to download uploaded file

/**
* FileWidget component.
* @module components/manage/Widgets/FileWidget
* @module components/manage/Widgets/FileWidget.jsx
* Volto version 19
* - Diff from 17.x.x: added aria-label and aria-required to the file input and the button, to improve accessibility. The aria-label includes information about whether the field is required or not.
* PR with Volto changes: https://github.com/plone/volto/pull/7494
*/

import React from 'react';
import PropTypes from 'prop-types';
import { Button, Image, Dimmer } from 'semantic-ui-react';
import { Button, Dimmer } from 'semantic-ui-react';
import { readAsDataURL } from 'promise-file-reader';
import { injectIntl } from 'react-intl';
import deleteSVG from '@plone/volto/icons/delete.svg';
import { Icon, FormFieldWrapper, UniversalLink } from '@plone/volto/components';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
import Image from '@plone/volto/components/theme/Image/Image';
import loadable from '@loadable/component';
import { flattenToAppURL, validateFileUploadSize } from '@plone/volto/helpers';
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
import { defineMessages, useIntl } from 'react-intl';
import { toast } from 'react-toastify';

const imageMimetypes = [
'image/png',
Expand Down Expand Up @@ -48,6 +53,19 @@ const messages = defineMessages({
id: 'Choose a file',
defaultMessage: 'Choose a file',
},
maxSizeError: {
id: 'The file you uploaded exceeded the maximum allowed size of {size} bytes',
defaultMessage:
'The file you uploaded exceeded the maximum allowed size of {size} bytes',
},
acceptError: {
id: 'File is not of the accepted type {accept}',
defaultMessage: 'File is not of the accepted type {accept}',
},
requiredField: {
id: 'This field is required.',
defaultMessage: 'This is a required field.',
},
});

/**
Expand Down Expand Up @@ -78,25 +96,58 @@ const FileWidget = (props) => {
const [fileType, setFileType] = React.useState(false);
const intl = useIntl();

const imgAttrs = React.useMemo(() => {
const data = {};
if (value?.download) {
data.item = {
'@id': value.download.substring(0, value.download.indexOf('/@@images')),
image: value,
};
} else if (value?.data) {
data.src = `data:${value['content-type']};${value.encoding},${value.data}`;
}
return data;
}, [value]);

React.useEffect(() => {
if (value && imageMimetypes.includes(value['content-type'])) {
setFileType(true);
}
}, [value]);

const imgsrc = value?.download
? `${flattenToAppURL(value?.download)}`
: null || value?.data
? `data:${value['content-type']};${value.encoding},${value.data}`
: null;

/**
* Drop handler
* @method onDrop
* @param {array} files File objects
* @returns {undefined}
*/
const onDrop = (files) => {
const onDrop = (files, rejectedFiles) => {
rejectedFiles.forEach((file) => {
file.errors.forEach((err) => {
if (err.code === 'file-too-large') {
toast.error(
<Toast
error
title={intl.formatMessage(messages.maxSizeError, {
size: props.size,
})}
/>,
);
}

if (err.code === 'file-invalid-type') {
toast.error(
<Toast
error
title={intl.formatMessage(messages.acceptError, {
accept: props.accept,
})}
/>,
);
}
});
});
if (files.length < 1) return;
const file = files[0];
if (!validateFileUploadSize(file, intl.formatMessage)) return;
readAsDataURL(file).then((data) => {
Expand All @@ -115,7 +166,7 @@ const FileWidget = (props) => {
if (imageMimetypes.includes(fields[1])) {
setFileType(true);
let imagePreview = document.getElementById(`field-${id}-image`);
imagePreview.src = reader.result;
if (imagePreview) imagePreview.src = reader.result;
} else {
setFileType(false);
}
Expand All @@ -125,16 +176,19 @@ const FileWidget = (props) => {

return (
<FormFieldWrapper {...props}>
<Dropzone onDrop={onDrop}>
<Dropzone
onDrop={onDrop}
{...(props.size ? { maxSize: props.size } : {})}
{...(props.accept ? { accept: props.accept } : {})}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div className="file-widget-dropzone" {...getRootProps()}>
{isDragActive && <Dimmer active></Dimmer>}
{fileType ? (
<Image
className="image-preview"
className="image-preview small ui image"
id={`field-${id}-image`}
size="small"
src={imgsrc}
{...imgAttrs}
/>
) : (
<div className="dropzone-placeholder">
Expand All @@ -154,15 +208,28 @@ const FileWidget = (props) => {
</div>
)}

<label className="label-file-widget-input">
<Button
className="label-file-widget-input"
tabIndex={-1}
aria-label={
props.required
? `${
value
? intl.formatMessage(messages.replaceFile)
: intl.formatMessage(messages.addNewFile)
}. (${intl.formatMessage(messages.requiredField)})`
: null
}
>
{value
? intl.formatMessage(messages.replaceFile)
: intl.formatMessage(messages.addNewFile)}
</label>
</Button>
<input
{...getInputProps({
type: 'file',
style: { display: 'none' },
'aria-required': props.required,
})}
id={`field-${id}`}
name={id}
Expand All @@ -172,6 +239,7 @@ const FileWidget = (props) => {
</div>
)}
</Dropzone>

<div className="field-file-name">
{value && (
<UniversalLink href={value.download} download={true}>
Expand All @@ -180,6 +248,7 @@ const FileWidget = (props) => {
)}
{value && (
<Button
type="button"
icon
basic
className="delete-button"
Expand Down
Loading