fix(uploads): show configured guest file types

Assisted-by: Claude Code
This commit is contained in:
Dodothereal
2026-07-17 21:35:53 +02:00
parent 2a0361a83b
commit 433fb9a989
12 changed files with 77 additions and 29 deletions
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import { Button } from '../common'; import { Button } from '../common';
import { api } from '../../config/api'; import { api } from '../../config/api';
import { usePublicSettings } from '../../hooks/usePublicSettings'; import { usePublicSettings } from '../../hooks/usePublicSettings';
import { extensionsToMimeTypes, extensionsToAcceptString } from '../../utils/fileTypes'; import { extensionsToMimeTypes, extensionsToAcceptString, getSupportedExtensions } from '../../utils/fileTypes';
interface UserPhotoUploadProps { interface UserPhotoUploadProps {
eventId: number; eventId: number;
@@ -61,6 +61,11 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
[publicSettings?.allowed_file_types] [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). // Shared filter pipeline for both <input> change and drag-and-drop (#504).
const addFiles = (incoming: File[]) => { const addFiles = (incoming: File[]) => {
const validFiles = incoming.filter((file) => { const validFiles = incoming.filter((file) => {
@@ -233,10 +238,11 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
{t('upload.clickToUpload')} {t('upload.clickToUpload')}
</p> </p>
<p className="text-xs text-muted-theme"> <p className="text-xs text-muted-theme">
{/* #613 — pass { limit } so `{{limit}}` interpolates {t('upload.fileRequirements', {
with the real number from settings instead of types: allowedExtensions.map(extension => extension.toUpperCase()).join(', '),
rendering literally. */} limit: maxFilesPerUpload,
{t('upload.fileRequirements', { limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })} sizeLimit: maxFileSizeMb,
})}
</p> </p>
<input <input
type="file" type="file"
+1 -1
View File
@@ -159,7 +159,7 @@
"noCategory": "Keine Kategorie", "noCategory": "Keine Kategorie",
"eventSpecific": "(Veranstaltungsspezifisch)", "eventSpecific": "(Veranstaltungsspezifisch)",
"clickToUpload": "Klicken zum Hochladen oder per Drag & Drop", "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", "selectedFiles": "Ausgewählte Dateien",
"uploading": "Wird hochgeladen...", "uploading": "Wird hochgeladen...",
"transferring": "Übertragung", "transferring": "Übertragung",
+1 -1
View File
@@ -159,7 +159,7 @@
"noCategory": "No category", "noCategory": "No category",
"eventSpecific": "(Event specific)", "eventSpecific": "(Event specific)",
"clickToUpload": "Click to upload or drag and drop", "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", "selectedFiles": "Selected files",
"uploading": "Uploading...", "uploading": "Uploading...",
"transferring": "Transferring", "transferring": "Transferring",
+1 -1
View File
@@ -126,7 +126,7 @@
"noCategory": "Sin categoría", "noCategory": "Sin categoría",
"eventSpecific": "(Específico del evento)", "eventSpecific": "(Específico del evento)",
"clickToUpload": "Haz clic para subir o arrastra y suelta", "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)", "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).", "unsupportedFiles": "Algunos archivos se omitieron porque el formato no es compatible (usa JPEG/PNG/WebP/MP4/MOV/WEBM).",
"selectedFiles": "Archivos seleccionados", "selectedFiles": "Archivos seleccionados",
+1 -1
View File
@@ -131,7 +131,7 @@
"noCategory": "Aucune catégorie", "noCategory": "Aucune catégorie",
"eventSpecific": "(Spécifique à l'événement)", "eventSpecific": "(Spécifique à l'événement)",
"clickToUpload": "Cliquez pour téléverser ou glissez-déposez", "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", "selectedFiles": "Fichiers sélectionnés",
"uploading": "Téléversement en cours...", "uploading": "Téléversement en cours...",
"transferring": "Transfert en cours", "transferring": "Transfert en cours",
+1 -1
View File
@@ -131,7 +131,7 @@
"noCategory": "Geen categorie", "noCategory": "Geen categorie",
"eventSpecific": "(Evenement-specifiek)", "eventSpecific": "(Evenement-specifiek)",
"clickToUpload": "Klik om te uploaden of sleep bestanden hierheen", "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", "selectedFiles": "Geselecteerde bestanden",
"uploading": "Uploaden...", "uploading": "Uploaden...",
"uploadComplete": "Upload voltooid!", "uploadComplete": "Upload voltooid!",
+1 -1
View File
@@ -131,7 +131,7 @@
"noCategory": "Sem categoria", "noCategory": "Sem categoria",
"eventSpecific": "(Específico do evento)", "eventSpecific": "(Específico do evento)",
"clickToUpload": "Clique para enviar ou arraste e solte", "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", "selectedFiles": "Arquivos selecionados",
"uploading": "Enviando...", "uploading": "Enviando...",
"uploadComplete": "Envio concluído!", "uploadComplete": "Envio concluído!",
+1 -1
View File
@@ -131,7 +131,7 @@
"noCategory": "Без категории", "noCategory": "Без категории",
"eventSpecific": "(Для конкретного события)", "eventSpecific": "(Для конкретного события)",
"clickToUpload": "Нажмите для загрузки или перетащите файлы", "clickToUpload": "Нажмите для загрузки или перетащите файлы",
"fileRequirements": "JPEG, PNG или WebP (макс. {{sizeLimit}} МБ на файл, {{limit}} файлов за загрузку)", "fileRequirements": "{{types}} (макс. {{sizeLimit}} МБ на файл, {{limit}} файлов за загрузку)",
"selectedFiles": "Выбранные файлы", "selectedFiles": "Выбранные файлы",
"uploading": "Загрузка...", "uploading": "Загрузка...",
"uploadComplete": "Загрузка завершена!", "uploadComplete": "Загрузка завершена!",
+1 -1
View File
@@ -131,7 +131,7 @@
"noCategory": "Brez kategorije", "noCategory": "Brez kategorije",
"eventSpecific": "(specifično za dogodek)", "eventSpecific": "(specifično za dogodek)",
"clickToUpload": "Kliknite za nalaganje ali povlecite in spustite", "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", "selectedFiles": "Izbrane datoteke",
"uploading": "Nalaganje...", "uploading": "Nalaganje...",
"transferring": "Prenašanje", "transferring": "Prenašanje",
@@ -82,6 +82,8 @@ export interface PublicSettings {
// modal can render the real number in `upload.fileRequirements` and refuse // modal can render the real number in `upload.fileRequirements` and refuse
// oversized batches client-side. Backend enforces the same value too. // oversized batches client-side. Backend enforces the same value too.
general_max_files_per_upload?: number; 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 field requirements
event_require_customer_name?: boolean; event_require_customer_name?: boolean;
event_require_customer_email?: 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',
]);
});
});
+19 -16
View File
@@ -16,27 +16,30 @@ const EXTENSION_TO_MIME: Record<string, string> = {
const DEFAULT_ALLOWED = 'jpg,jpeg,png,webp'; 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 * Convert a comma-separated extension string (e.g. "jpg,png,mp4") to an
* array of unique MIME types. * array of unique MIME types.
*/ */
export function extensionsToMimeTypes(extString?: string | null): string[] { export function extensionsToMimeTypes(extString?: string | null): string[] {
const input = extString?.trim() || DEFAULT_ALLOWED; return Array.from(new Set(
const mimeSet = new Set<string>(); getSupportedExtensions(extString).map(extension => EXTENSION_TO_MIME[extension])
));
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);
} }
/** /**