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 { 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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -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": "Загрузка завершена!",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user