feat: implement file size input component and update settings configuration

- Added a new FileSizeInput component to handle file size configurations in a user-friendly manner.
- Updated SettingsInput to integrate the FileSizeInput for maxFileSize and maxTotalStoragePerUser settings.
- Revised environment schema to remove MAX_FILESIZE validation, ensuring flexibility in configuration.
- Adjusted localization files to remove byte references in descriptions for clarity across multiple languages.
This commit is contained in:
Daniel Luiz Alves
2025-06-01 00:29:35 -03:00
parent 122781ca3d
commit d3b7fe04ed
20 changed files with 198 additions and 35 deletions

View File

@@ -39,7 +39,7 @@ const defaultConfigs = [
// Storage Configurations
{
key: "maxFileSize",
value: process.env.MAX_FILESIZE || "1073741824", // default 1GiB in bytes
value: "1073741824", // default 1GiB in bytes
type: "bigint",
group: "storage",
},

View File

@@ -11,7 +11,6 @@ const envSchema = z.object({
S3_REGION: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
MAX_FILESIZE: z.string().min(1),
});
export const env = envSchema.parse(process.env);

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "أقصى حجم للملف",
"description": "الحد الأقصى لحجم الملف المسموح به للرفع (بالبايت)"
"description": "الحد الأقصى لحجم الملف المسموح به للرفع "
},
"maxTotalStoragePerUser": {
"title": "أقصى تخزين لكل مستخدم",
"description": "الحد الإجمالي للتخزين لكل مستخدم (بالبايت)"
"description": "الحد الإجمالي للتخزين لكل مستخدم "
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Maximale Dateigröße",
"description": "Maximal erlaubte Dateigröße für Uploads (in Bytes)"
"description": "Maximal erlaubte Dateigröße für Uploads"
},
"maxTotalStoragePerUser": {
"title": "Maximaler Speicher pro Benutzer",
"description": "Gesamtspeicherlimit pro Benutzer (in Bytes)"
"description": "Gesamtspeicherlimit pro Benutzer"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Maximum File Size",
"description": "Maximum allowed file size for uploads (in bytes)"
"description": "Maximum allowed file size for uploads"
},
"maxTotalStoragePerUser": {
"title": "Maximum Storage Per User",
"description": "Total storage limit per user (in bytes)"
"description": "Total storage limit per user"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Tamaño máximo de archivo",
"description": "Tamaño máximo permitido para subir archivos (en bytes)"
"description": "Tamaño máximo permitido para subir archivos"
},
"maxTotalStoragePerUser": {
"title": "Almacenamiento máximo por usuario",
"description": "Límite total de almacenamiento por usuario (en bytes)"
"description": "Límite total de almacenamiento por usuario"
}
},
"buttons": {

View File

@@ -402,11 +402,11 @@
},
"maxFileSize": {
"title": "Taille Maximale de Fichier",
"description": "Taille maximale autorisée pour les fichiers (en octets)"
"description": "Taille maximale autorisée pour les fichiers"
},
"maxTotalStoragePerUser": {
"title": "Stockage Maximum par Utilisateur",
"description": "Limite totale de stockage par utilisateur (en octets)"
"description": "Limite totale de stockage par utilisateur"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "अधिकतम फाइल आकार",
"description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार (बाइट में)"
"description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार"
},
"maxTotalStoragePerUser": {
"title": "प्रति उपयोगकर्ता अधिकतम स्टोरेज",
"description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा (बाइट में)"
"description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Dimensione Massima File",
"description": "Dimensione massima consentita per i caricamenti (in byte)"
"description": "Dimensione massima consentita per i caricamenti"
},
"maxTotalStoragePerUser": {
"title": "Archiviazione Massima per Utente",
"description": "Limite totale di archiviazione per utente (in byte)"
"description": "Limite totale di archiviazione per utente"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "最大ファイルサイズ",
"description": "アップロード可能な最大ファイルサイズ(バイト単位)"
"description": "アップロード可能な最大ファイルサイズ"
},
"maxTotalStoragePerUser": {
"title": "ユーザーごとの最大ストレージ",
"description": "ユーザーごとの総ストレージ制限(バイト単位)"
"description": "ユーザーごとの総ストレージ制限"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "최대 파일 크기",
"description": "업로드 가능한 최대 파일 크기(바이트 단위)"
"description": "업로드 가능한 최대 파일 크기"
},
"maxTotalStoragePerUser": {
"title": "사용자당 최대 스토리지",
"description": "사용자당 총 스토리지 제한(바이트 단위)"
"description": "사용자당 총 스토리지 제한"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Maximum Bestandsgrootte",
"description": "Maximum toegestane bestandsgrootte voor uploads (in bytes)"
"description": "Maximum toegestane bestandsgrootte voor uploads"
},
"maxTotalStoragePerUser": {
"title": "Maximum Opslag Per Gebruiker",
"description": "Totale opslaglimiet per gebruiker (in bytes)"
"description": "Totale opslaglimiet per gebruiker"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Tamanho Máximo do Arquivo",
"description": "Tamanho máximo permitido para uploads (em bytes)"
"description": "Tamanho máximo permitido para uploads"
},
"maxTotalStoragePerUser": {
"title": "Armazenamento Máximo por Usuário",
"description": "Limite total de armazenamento por usuário (em bytes)"
"description": "Limite total de armazenamento por usuário"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Максимальный размер файла",
"description": "Максимальный размер файла для загрузки (в байтах)"
"description": "Максимальный размер файла для загрузки"
},
"maxTotalStoragePerUser": {
"title": "Максимальное хранилище для пользователя",
"description": "Общий лимит хранилища для каждого пользователя (в байтах)"
"description": "Общий лимит хранилища для каждого пользователя"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "Maksimum Dosya Boyutu",
"description": "Yüklemeye izin verilen maksimum dosya boyutu (bayt cinsinden)"
"description": "Yüklemeye izin verilen maksimum dosya boyutu"
},
"maxTotalStoragePerUser": {
"title": "Kullanıcı Başına Maksimum Depolama",
"description": "Kullanıcı başına toplam depolama sınırı (bayt cinsinden)"
"description": "Kullanıcı başına toplam depolama sınırı"
}
},
"buttons": {

View File

@@ -399,11 +399,11 @@
},
"maxFileSize": {
"title": "最大文件大小",
"description": "允许上传的最大文件大小(以字节为单位)"
"description": "允许上传的最大文件大小"
},
"maxTotalStoragePerUser": {
"title": "每个用户的最大存储量",
"description": "每个用户的总存储限制(以字节为单位)"
"description": "每个用户的总存储限制"
}
},
"buttons": {

View File

@@ -0,0 +1,142 @@
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
export interface FileSizeInputProps {
value: string; // valor em bytes
onChange: (value: string) => void;
disabled?: boolean;
error?: any;
}
type Unit = "MB" | "GB" | "TB";
const UNIT_MULTIPLIERS: Record<Unit, number> = {
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024,
};
function bytesToHumanReadable(bytes: string): { value: string; unit: Unit } {
const numBytes = parseInt(bytes, 10);
// Se for 0 ou inválido, retorna 0 GB
if (!numBytes || numBytes <= 0) {
return { value: "0", unit: "GB" };
}
// Verifica TB (com tolerância para valores próximos)
if (numBytes >= UNIT_MULTIPLIERS.TB) {
const tbValue = numBytes / UNIT_MULTIPLIERS.TB;
if (tbValue === Math.floor(tbValue)) {
return {
value: tbValue.toString(),
unit: "TB",
};
}
}
// Verifica GB (com tolerância para valores próximos)
if (numBytes >= UNIT_MULTIPLIERS.GB) {
const gbValue = numBytes / UNIT_MULTIPLIERS.GB;
if (gbValue === Math.floor(gbValue)) {
return {
value: gbValue.toString(),
unit: "GB",
};
}
}
// Verifica MB
if (numBytes >= UNIT_MULTIPLIERS.MB) {
const mbValue = numBytes / UNIT_MULTIPLIERS.MB;
return {
value: mbValue === Math.floor(mbValue) ? mbValue.toString() : mbValue.toFixed(2),
unit: "MB",
};
}
// Para valores menores que 1MB, converte para MB com decimais
const mbValue = numBytes / UNIT_MULTIPLIERS.MB;
return {
value: mbValue.toFixed(3),
unit: "MB",
};
}
function humanReadableToBytes(value: string, unit: Unit): string {
const numValue = parseFloat(value);
if (isNaN(numValue) || numValue <= 0) {
return "0";
}
return Math.floor(numValue * UNIT_MULTIPLIERS[unit]).toString();
}
export function FileSizeInput({ value, onChange, disabled = false, error }: FileSizeInputProps) {
const [displayValue, setDisplayValue] = useState("");
const [selectedUnit, setSelectedUnit] = useState<Unit>("GB");
// Inicializar os valores quando o componente monta ou o value muda
useEffect(() => {
if (value && value !== "0") {
const { value: humanValue, unit } = bytesToHumanReadable(value);
setDisplayValue(humanValue);
setSelectedUnit(unit);
} else {
setDisplayValue("");
setSelectedUnit("GB");
}
}, [value]);
const handleValueChange = (newValue: string) => {
// Só permitir números e ponto decimal
const sanitizedValue = newValue.replace(/[^0-9.]/g, "");
// Prevenir múltiplos pontos decimais
const parts = sanitizedValue.split(".");
const finalValue = parts.length > 2 ? parts[0] + "." + parts.slice(1).join("") : sanitizedValue;
setDisplayValue(finalValue);
if (finalValue === "" || finalValue === "0") {
onChange("0");
} else {
const bytesValue = humanReadableToBytes(finalValue, selectedUnit);
onChange(bytesValue);
}
};
const handleUnitChange = (newUnit: Unit) => {
setSelectedUnit(newUnit);
if (displayValue && displayValue !== "0") {
const bytesValue = humanReadableToBytes(displayValue, newUnit);
onChange(bytesValue);
}
};
return (
<div className="flex gap-2">
<Input
type="text"
value={displayValue}
onChange={(e) => handleValueChange(e.target.value)}
placeholder="0"
className="flex-1"
disabled={disabled}
aria-invalid={!!error}
/>
<select
value={selectedUnit}
onChange={(e) => handleUnitChange(e.target.value as Unit)}
className="w-20 rounded-md border border-input bg-transparent px-3 py-2 text-sm"
disabled={disabled}
>
<option value="MB">MB</option>
<option value="GB">GB</option>
<option value="TB">TB</option>
</select>
</div>
);
}

View File

@@ -52,6 +52,7 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
config={config}
error={form.formState.errors.configs?.[config.key]}
register={form.register}
setValue={form.setValue}
smtpEnabled={form.watch("configs.smtpEnabled")}
watch={form.watch}
/>

View File

@@ -1,20 +1,22 @@
import { useTranslations } from "next-intl";
import { UseFormRegister, UseFormWatch } from "react-hook-form";
import { UseFormRegister, UseFormSetValue, UseFormWatch } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { createFieldTitles } from "../constants";
import { Config } from "../types";
import { FileSizeInput } from "./file-size-input";
import { LogoInput } from "./logo-input";
export interface ConfigInputProps {
config: Config;
register: UseFormRegister<any>;
watch: UseFormWatch<any>;
setValue: UseFormSetValue<any>;
error?: any;
smtpEnabled?: string;
}
export function SettingsInput({ config, register, watch, error, smtpEnabled }: ConfigInputProps) {
export function SettingsInput({ config, register, watch, setValue, error, smtpEnabled }: ConfigInputProps) {
const t = useTranslations();
const FIELD_TITLES = createFieldTitles(t);
const isSmtpField = config.group === "email" && config.key !== "smtpEnabled";
@@ -31,9 +33,7 @@ export function SettingsInput({ config, register, watch, error, smtpEnabled }: C
isDisabled={isDisabled}
value={value}
onChange={(value) => {
register(`configs.${config.key}`).onChange({
target: { value },
});
setValue(`configs.${config.key}`, value, { shouldDirty: true });
}}
/>
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
@@ -41,6 +41,26 @@ export function SettingsInput({ config, register, watch, error, smtpEnabled }: C
);
}
// Special input for file size configurations
if (config.key === "maxFileSize" || config.key === "maxTotalStoragePerUser") {
const value = watch(`configs.${config.key}`);
return (
<div className="space-y-2">
<label className="block text-sm font-semibold">{friendlyLabel}</label>
<FileSizeInput
value={value || "0"}
onChange={(value) => {
setValue(`configs.${config.key}`, value, { shouldDirty: true });
}}
disabled={isDisabled}
error={error}
/>
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
</div>
);
}
switch (config.type) {
case "boolean":
return (

View File

@@ -24,6 +24,7 @@ export interface SettingsGroupProps {
export interface ConfigInputProps {
config: Config;
register: UseFormReturn<any>["register"];
setValue: UseFormReturn<any>["setValue"];
error?: any;
smtpEnabled?: string;
}