fix(uploads): show configured guest file types
Assisted-by: Claude Code
This commit is contained in:
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
||||
import { Button } from '../common';
|
||||
import { api } from '../../config/api';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { extensionsToMimeTypes, extensionsToAcceptString } from '../../utils/fileTypes';
|
||||
import { extensionsToMimeTypes, extensionsToAcceptString, getSupportedExtensions } from '../../utils/fileTypes';
|
||||
|
||||
interface UserPhotoUploadProps {
|
||||
eventId: number;
|
||||
@@ -61,6 +61,11 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
[publicSettings?.allowed_file_types]
|
||||
);
|
||||
|
||||
const allowedExtensions = useMemo(
|
||||
() => getSupportedExtensions(publicSettings?.allowed_file_types),
|
||||
[publicSettings?.allowed_file_types]
|
||||
);
|
||||
|
||||
// Shared filter pipeline for both <input> change and drag-and-drop (#504).
|
||||
const addFiles = (incoming: File[]) => {
|
||||
const validFiles = incoming.filter((file) => {
|
||||
@@ -233,10 +238,11 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
{t('upload.clickToUpload')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-theme">
|
||||
{/* #613 — pass { limit } so `{{limit}}` interpolates
|
||||
with the real number from settings instead of
|
||||
rendering literally. */}
|
||||
{t('upload.fileRequirements', { limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })}
|
||||
{t('upload.fileRequirements', {
|
||||
types: allowedExtensions.map(extension => extension.toUpperCase()).join(', '),
|
||||
limit: maxFilesPerUpload,
|
||||
sizeLimit: maxFileSizeMb,
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"noCategory": "Keine Kategorie",
|
||||
"eventSpecific": "(Veranstaltungsspezifisch)",
|
||||
"clickToUpload": "Klicken zum Hochladen oder per Drag & Drop",
|
||||
"fileRequirements": "JPEG, PNG oder WebP (max. {{sizeLimit}}MB pro Datei, {{limit}} Dateien pro Upload)",
|
||||
"fileRequirements": "{{types}} (max. {{sizeLimit}}MB pro Datei, {{limit}} Dateien pro Upload)",
|
||||
"selectedFiles": "Ausgewählte Dateien",
|
||||
"uploading": "Wird hochgeladen...",
|
||||
"transferring": "Übertragung",
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"noCategory": "No category",
|
||||
"eventSpecific": "(Event specific)",
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"fileRequirements": "JPEG, PNG or WebP (max {{sizeLimit}}MB per file, {{limit}} files per upload)",
|
||||
"fileRequirements": "{{types}} (max {{sizeLimit}}MB per file, {{limit}} files per upload)",
|
||||
"selectedFiles": "Selected files",
|
||||
"uploading": "Uploading...",
|
||||
"transferring": "Transferring",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"noCategory": "Sin categoría",
|
||||
"eventSpecific": "(Específico del evento)",
|
||||
"clickToUpload": "Haz clic para subir o arrastra y suelta",
|
||||
"fileRequirements": "JPEG, PNG o WebP (máx. {{sizeLimit}}MB por archivo, {{limit}} archivos por subida)",
|
||||
"fileRequirements": "{{types}} (máx. {{sizeLimit}}MB por archivo, {{limit}} archivos por subida)",
|
||||
"fileRequirementsMedia": "Imágenes JPEG, PNG o WebP y videos MP4/MOV/WEBM (máx. 50MB por archivo, {{limit}} archivos por subida)",
|
||||
"unsupportedFiles": "Algunos archivos se omitieron porque el formato no es compatible (usa JPEG/PNG/WebP/MP4/MOV/WEBM).",
|
||||
"selectedFiles": "Archivos seleccionados",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"noCategory": "Aucune catégorie",
|
||||
"eventSpecific": "(Spécifique à l'événement)",
|
||||
"clickToUpload": "Cliquez pour téléverser ou glissez-déposez",
|
||||
"fileRequirements": "JPEG, PNG ou WebP (max {{sizeLimit}} Mo par fichier, {{limit}} fichiers par téléversement)",
|
||||
"fileRequirements": "{{types}} (max {{sizeLimit}} Mo par fichier, {{limit}} fichiers par téléversement)",
|
||||
"selectedFiles": "Fichiers sélectionnés",
|
||||
"uploading": "Téléversement en cours...",
|
||||
"transferring": "Transfert en cours",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"noCategory": "Geen categorie",
|
||||
"eventSpecific": "(Evenement-specifiek)",
|
||||
"clickToUpload": "Klik om te uploaden of sleep bestanden hierheen",
|
||||
"fileRequirements": "JPEG, PNG of WebP (max. {{sizeLimit}} MB per bestand, {{limit}} bestanden per upload)",
|
||||
"fileRequirements": "{{types}} (max. {{sizeLimit}} MB per bestand, {{limit}} bestanden per upload)",
|
||||
"selectedFiles": "Geselecteerde bestanden",
|
||||
"uploading": "Uploaden...",
|
||||
"uploadComplete": "Upload voltooid!",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"noCategory": "Sem categoria",
|
||||
"eventSpecific": "(Específico do evento)",
|
||||
"clickToUpload": "Clique para enviar ou arraste e solte",
|
||||
"fileRequirements": "JPEG, PNG ou WebP (máx. {{sizeLimit}}MB por arquivo, {{limit}} arquivos por envio)",
|
||||
"fileRequirements": "{{types}} (máx. {{sizeLimit}}MB por arquivo, {{limit}} arquivos por envio)",
|
||||
"selectedFiles": "Arquivos selecionados",
|
||||
"uploading": "Enviando...",
|
||||
"uploadComplete": "Envio concluído!",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"noCategory": "Без категории",
|
||||
"eventSpecific": "(Для конкретного события)",
|
||||
"clickToUpload": "Нажмите для загрузки или перетащите файлы",
|
||||
"fileRequirements": "JPEG, PNG или WebP (макс. {{sizeLimit}} МБ на файл, {{limit}} файлов за загрузку)",
|
||||
"fileRequirements": "{{types}} (макс. {{sizeLimit}} МБ на файл, {{limit}} файлов за загрузку)",
|
||||
"selectedFiles": "Выбранные файлы",
|
||||
"uploading": "Загрузка...",
|
||||
"uploadComplete": "Загрузка завершена!",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"noCategory": "Brez kategorije",
|
||||
"eventSpecific": "(specifično za dogodek)",
|
||||
"clickToUpload": "Kliknite za nalaganje ali povlecite in spustite",
|
||||
"fileRequirements": "JPEG, PNG ali WebP (največ {{sizeLimit}} MB na datoteko, {{limit}} datotek na nalaganje)",
|
||||
"fileRequirements": "{{types}} (največ {{sizeLimit}} MB na datoteko, {{limit}} datotek na nalaganje)",
|
||||
"selectedFiles": "Izbrane datoteke",
|
||||
"uploading": "Nalaganje...",
|
||||
"transferring": "Prenašanje",
|
||||
|
||||
@@ -82,6 +82,8 @@ export interface PublicSettings {
|
||||
// modal can render the real number in `upload.fileRequirements` and refuse
|
||||
// oversized batches client-side. Backend enforces the same value too.
|
||||
general_max_files_per_upload?: number;
|
||||
// Per-file size limit surfaced for the guest upload guard and requirements hint.
|
||||
general_max_file_size_mb?: number;
|
||||
// Event field requirements
|
||||
event_require_customer_name?: boolean;
|
||||
event_require_customer_email?: boolean;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
extensionsToAcceptString,
|
||||
extensionsToMimeTypes,
|
||||
getSupportedExtensions,
|
||||
} from '../fileTypes';
|
||||
|
||||
describe('file type settings', () => {
|
||||
it('keeps configured supported extensions for the upload hint', () => {
|
||||
expect(getSupportedExtensions('jpg, mov, mp4, .webp')).toEqual([
|
||||
'jpg',
|
||||
'mov',
|
||||
'mp4',
|
||||
'webp',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the same configured types for validation and file selection', () => {
|
||||
expect(extensionsToMimeTypes('jpg,jpeg,mov,mp4')).toEqual([
|
||||
'image/jpeg',
|
||||
'video/quicktime',
|
||||
'video/mp4',
|
||||
]);
|
||||
expect(extensionsToAcceptString('jpg,jpeg,mov,mp4')).toBe(
|
||||
'image/jpeg,video/quicktime,video/mp4'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the default formats when no configured types are supported', () => {
|
||||
expect(getSupportedExtensions('dng,unknown')).toEqual([
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'webp',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,27 +16,30 @@ const EXTENSION_TO_MIME: Record<string, string> = {
|
||||
|
||||
const DEFAULT_ALLOWED = 'jpg,jpeg,png,webp';
|
||||
|
||||
/**
|
||||
* Return the configured supported extensions, excluding values the upload
|
||||
* pipeline cannot validate. Falls back to the default list when none match.
|
||||
*/
|
||||
export function getSupportedExtensions(extString?: string | null): string[] {
|
||||
const input = extString?.trim() || DEFAULT_ALLOWED;
|
||||
const extensions = input
|
||||
.split(',')
|
||||
.map(ext => ext.trim().toLowerCase().replace(/^\./, ''))
|
||||
.filter(ext => Boolean(EXTENSION_TO_MIME[ext]));
|
||||
|
||||
return extensions.length > 0
|
||||
? Array.from(new Set(extensions))
|
||||
: getSupportedExtensions(DEFAULT_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a comma-separated extension string (e.g. "jpg,png,mp4") to an
|
||||
* array of unique MIME types.
|
||||
*/
|
||||
export function extensionsToMimeTypes(extString?: string | null): string[] {
|
||||
const input = extString?.trim() || DEFAULT_ALLOWED;
|
||||
const mimeSet = new Set<string>();
|
||||
|
||||
input.split(',').forEach(ext => {
|
||||
const cleaned = ext.trim().toLowerCase().replace(/^\./, '');
|
||||
const mime = EXTENSION_TO_MIME[cleaned];
|
||||
if (mime) {
|
||||
mimeSet.add(mime);
|
||||
}
|
||||
});
|
||||
|
||||
if (mimeSet.size === 0) {
|
||||
return extensionsToMimeTypes(DEFAULT_ALLOWED);
|
||||
}
|
||||
|
||||
return Array.from(mimeSet);
|
||||
return Array.from(new Set(
|
||||
getSupportedExtensions(extString).map(extension => EXTENSION_TO_MIME[extension])
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user