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 // Storage Configurations
{ {
key: "maxFileSize", key: "maxFileSize",
value: process.env.MAX_FILESIZE || "1073741824", // default 1GiB in bytes value: "1073741824", // default 1GiB in bytes
type: "bigint", type: "bigint",
group: "storage", group: "storage",
}, },

View File

@@ -11,7 +11,6 @@ const envSchema = z.object({
S3_REGION: z.string().optional(), S3_REGION: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(), S3_BUCKET_NAME: z.string().optional(),
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"), 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); export const env = envSchema.parse(process.env);

View File

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

View File

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

View File

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

View File

@@ -399,11 +399,11 @@
}, },
"maxFileSize": { "maxFileSize": {
"title": "Tamaño máximo de archivo", "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": { "maxTotalStoragePerUser": {
"title": "Almacenamiento máximo por usuario", "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": { "buttons": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -399,11 +399,11 @@
}, },
"maxFileSize": { "maxFileSize": {
"title": "Tamanho Máximo do Arquivo", "title": "Tamanho Máximo do Arquivo",
"description": "Tamanho máximo permitido para uploads (em bytes)" "description": "Tamanho máximo permitido para uploads"
}, },
"maxTotalStoragePerUser": { "maxTotalStoragePerUser": {
"title": "Armazenamento Máximo por Usuário", "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": { "buttons": {

View File

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

View File

@@ -399,11 +399,11 @@
}, },
"maxFileSize": { "maxFileSize": {
"title": "Maksimum Dosya Boyutu", "title": "Maksimum Dosya Boyutu",
"description": "Yüklemeye izin verilen maksimum dosya boyutu (bayt cinsinden)" "description": "Yüklemeye izin verilen maksimum dosya boyutu"
}, },
"maxTotalStoragePerUser": { "maxTotalStoragePerUser": {
"title": "Kullanıcı Başına Maksimum Depolama", "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": { "buttons": {

View File

@@ -399,11 +399,11 @@
}, },
"maxFileSize": { "maxFileSize": {
"title": "最大文件大小", "title": "最大文件大小",
"description": "允许上传的最大文件大小(以字节为单位)" "description": "允许上传的最大文件大小"
}, },
"maxTotalStoragePerUser": { "maxTotalStoragePerUser": {
"title": "每个用户的最大存储量", "title": "每个用户的最大存储量",
"description": "每个用户的总存储限制(以字节为单位)" "description": "每个用户的总存储限制"
} }
}, },
"buttons": { "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} config={config}
error={form.formState.errors.configs?.[config.key]} error={form.formState.errors.configs?.[config.key]}
register={form.register} register={form.register}
setValue={form.setValue}
smtpEnabled={form.watch("configs.smtpEnabled")} smtpEnabled={form.watch("configs.smtpEnabled")}
watch={form.watch} watch={form.watch}
/> />

View File

@@ -1,20 +1,22 @@
import { useTranslations } from "next-intl"; 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 { Input } from "@/components/ui/input";
import { createFieldTitles } from "../constants"; import { createFieldTitles } from "../constants";
import { Config } from "../types"; import { Config } from "../types";
import { FileSizeInput } from "./file-size-input";
import { LogoInput } from "./logo-input"; import { LogoInput } from "./logo-input";
export interface ConfigInputProps { export interface ConfigInputProps {
config: Config; config: Config;
register: UseFormRegister<any>; register: UseFormRegister<any>;
watch: UseFormWatch<any>; watch: UseFormWatch<any>;
setValue: UseFormSetValue<any>;
error?: any; error?: any;
smtpEnabled?: string; 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 t = useTranslations();
const FIELD_TITLES = createFieldTitles(t); const FIELD_TITLES = createFieldTitles(t);
const isSmtpField = config.group === "email" && config.key !== "smtpEnabled"; const isSmtpField = config.group === "email" && config.key !== "smtpEnabled";
@@ -31,9 +33,7 @@ export function SettingsInput({ config, register, watch, error, smtpEnabled }: C
isDisabled={isDisabled} isDisabled={isDisabled}
value={value} value={value}
onChange={(value) => { onChange={(value) => {
register(`configs.${config.key}`).onChange({ setValue(`configs.${config.key}`, value, { shouldDirty: true });
target: { value },
});
}} }}
/> />
{error && <p className="text-danger text-xs mt-1">{error.message}</p>} {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) { switch (config.type) {
case "boolean": case "boolean":
return ( return (

View File

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