/****************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 *****************************************************************************/ import type { ChangeEvent, FocusEvent } from "react"; import { FC, useEffect, useState } from 'react'; import type { SelectChangeEvent } from '@mui/material'; import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormHelperText, InputLabel, MenuItem, Select, TextField } from '@mui/material'; import { RefillStrategy, type Tier, TierType } from "../../../extension-registry-types"; import { handleError } from "../../../utils"; type DurationUnit = 'seconds' | 'minutes' | 'hours'; const DURATION_MULTIPLIERS: Record = { seconds: 1, minutes: 60, hours: 3600 }; function formatDuration(duration: number): [number, DurationUnit] { const hours = Math.floor(duration / 3600); if (hours > 0) { return [hours, "hours"]; } const minutes = Math.floor(duration / 60); if (minutes > 0) { return [minutes, "minutes"]; } return [duration, "seconds"]; } interface TierFormDialogProps { open: boolean; tier?: Tier; onClose: () => void; onSubmit: (formData: Tier) => Promise; } export const TierFormDialog: FC = ({ open, tier, onClose, onSubmit }) => { const [formData, setFormData] = useState({ name: '', description: '', capacity: 100, duration: 3600, refillStrategy: RefillStrategy.INTERVAL } as Tier); const [durationValue, setDurationValue] = useState(1); const [durationUnit, setDurationUnit] = useState('hours'); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState>({}); const [touched, setTouched] = useState>({}); const getDurationInSeconds = (): number => { return durationValue * DURATION_MULTIPLIERS[durationUnit]; }; useEffect(() => { if (tier) { setFormData(_ => ({ name: tier.name, description: tier.description || '', tierType: tier.tierType, capacity: tier.capacity, duration: tier.duration, refillStrategy: tier.refillStrategy } as Tier)); // Convert duration seconds to value/unit for display const [value, unit] = formatDuration(tier.duration); setDurationValue(value); setDurationUnit(unit); } else { setFormData(prev => ({ ...prev, name: '', description: '', tierType: TierType.NON_FREE, capacity: 100, duration: 3600, refillStrategy: RefillStrategy.INTERVAL })); setDurationValue(1); setDurationUnit('hours'); } setErrors({}); setTouched({}); }, [open, tier]); const clearFieldError = (fieldName: string) => { if (errors[fieldName]) { setErrors(prev => { const newErrors = { ...prev }; delete newErrors[fieldName]; return newErrors; }); } }; const handleChange = (e: ChangeEvent | SelectChangeEvent) => { const { name, value } = e.target as any; clearFieldError(name); setFormData((prev: Tier) => ({ ...prev, [name]: name === 'capacity' || name === 'duration' ? Number.parseInt(value as string, 10) : value } as Tier)); }; const handleBlur = (e: FocusEvent) => { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); validateField(name); }; const fieldValidators: Record string | undefined> = { name: () => { if (formData.name === undefined) { return "Tier name is required"; } else if (formData.name.trim() !== formData.name) { return "Tier name must not contain trailing whitespace"; } else { return undefined; } }, tierType: () => formData.tierType ? undefined : 'Tier type is required', capacity: () => formData.capacity <= 0 ? 'Capacity must be greater than 0' : undefined, duration: () => durationValue <= 0 ? 'Duration must be greater than 0' : undefined, refillStrategy: () => formData.refillStrategy ? undefined : 'Refill strategy is required', }; const validateField = (fieldName: string): string | undefined => { const validator = fieldValidators[fieldName]; const error = validator?.(); if (error) { setErrors(prev => ({ ...prev, [fieldName]: error })); } return error; }; const validateForm = (): boolean => { // Mark all fields as touched on submit setTouched({ name: true, tierType: true, capacity: true, duration: true, refillStrategy: true, }); const newErrors: Record = {}; for (const key of Object.keys(formData)) { const error = validateField(key); if (error !== undefined) { newErrors[key] = error; } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validateForm()) { return; } setLoading(true); try { const durationInSeconds = getDurationInSeconds(); await onSubmit({ ...formData, duration: durationInSeconds }); onClose(); } catch (err: any) { setErrors({ submit: handleError(err) }); } finally { setLoading(false); } }; const isEditMode = !!tier; const title = isEditMode ? 'Edit Tier' : 'Create New Tier'; return ( {title} {errors.submit && {errors.submit}} Tier Type {touched.tierType && errors.tierType && {errors.tierType}} { clearFieldError('duration'); setDurationValue(Math.max(1, Number.parseInt(e.target.value, 10) || 0)); }} onBlur={(e) => { setTouched(prev => ({ ...prev, duration: true })); validateField('duration'); }} inputProps={{ min: '1' }} disabled={loading} required={true} error={touched.duration && !!errors.duration} helperText={touched.duration && errors.duration} sx={{ flex: 1 }} /> Unit = {getDurationInSeconds().toLocaleString()} seconds Refill Strategy {touched.refillStrategy && errors.refillStrategy && {errors.refillStrategy}} ); };