mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
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:
@@ -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",
|
||||
},
|
||||
|
@@ -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);
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "أقصى حجم للملف",
|
||||
"description": "الحد الأقصى لحجم الملف المسموح به للرفع (بالبايت)"
|
||||
"description": "الحد الأقصى لحجم الملف المسموح به للرفع "
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "أقصى تخزين لكل مستخدم",
|
||||
"description": "الحد الإجمالي للتخزين لكل مستخدم (بالبايت)"
|
||||
"description": "الحد الإجمالي للتخزين لكل مستخدم "
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "अधिकतम फाइल आकार",
|
||||
"description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार (बाइट में)"
|
||||
"description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार"
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "प्रति उपयोगकर्ता अधिकतम स्टोरेज",
|
||||
"description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा (बाइट में)"
|
||||
"description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
@@ -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": {
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "最大ファイルサイズ",
|
||||
"description": "アップロード可能な最大ファイルサイズ(バイト単位)"
|
||||
"description": "アップロード可能な最大ファイルサイズ"
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "ユーザーごとの最大ストレージ",
|
||||
"description": "ユーザーごとの総ストレージ制限(バイト単位)"
|
||||
"description": "ユーザーごとの総ストレージ制限"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "최대 파일 크기",
|
||||
"description": "업로드 가능한 최대 파일 크기(바이트 단위)"
|
||||
"description": "업로드 가능한 최대 파일 크기"
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "사용자당 최대 스토리지",
|
||||
"description": "사용자당 총 스토리지 제한(바이트 단위)"
|
||||
"description": "사용자당 총 스토리지 제한"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "Максимальный размер файла",
|
||||
"description": "Максимальный размер файла для загрузки (в байтах)"
|
||||
"description": "Максимальный размер файла для загрузки"
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "Максимальное хранилище для пользователя",
|
||||
"description": "Общий лимит хранилища для каждого пользователя (в байтах)"
|
||||
"description": "Общий лимит хранилища для каждого пользователя"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
@@ -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": {
|
||||
|
@@ -399,11 +399,11 @@
|
||||
},
|
||||
"maxFileSize": {
|
||||
"title": "最大文件大小",
|
||||
"description": "允许上传的最大文件大小(以字节为单位)"
|
||||
"description": "允许上传的最大文件大小"
|
||||
},
|
||||
"maxTotalStoragePerUser": {
|
||||
"title": "每个用户的最大存储量",
|
||||
"description": "每个用户的总存储限制(以字节为单位)"
|
||||
"description": "每个用户的总存储限制"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
|
142
apps/web/src/app/settings/components/file-size-input.tsx
Normal file
142
apps/web/src/app/settings/components/file-size-input.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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 (
|
||||
|
@@ -24,6 +24,7 @@ export interface SettingsGroupProps {
|
||||
export interface ConfigInputProps {
|
||||
config: Config;
|
||||
register: UseFormReturn<any>["register"];
|
||||
setValue: UseFormReturn<any>["setValue"];
|
||||
error?: any;
|
||||
smtpEnabled?: string;
|
||||
}
|
||||
|
Reference in New Issue
Block a user