update: improve error handling

This commit is contained in:
Daniel Gonzalez Albo
2025-11-09 16:59:59 +01:00
committed by Nicolás Hatcher Andrés
parent 36beccd4ae
commit c283fd7b60
3 changed files with 131 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
import type { WorksheetProperties } from "@ironcalc/wasm";
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
import {
Box,
FormControl,
@@ -11,7 +11,7 @@ import {
} from "@mui/material";
import { t } from "i18next";
import { Check, Tag } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import type React from "react";
import { theme } from "../../../theme";
import { Footer, NewButton } from "./NamedRanges";
@@ -23,6 +23,8 @@ interface EditNamedRangeProps {
formula: string;
onSave: (name: string, scope: string, formula: string) => string | undefined;
onCancel: () => void;
definedNameList?: DefinedName[];
editingDefinedName?: DefinedName | null;
}
const EditNamedRange: React.FC<EditNamedRangeProps> = ({
@@ -32,14 +34,74 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
formula: initialFormula,
onSave,
onCancel,
definedNameList = [],
editingDefinedName = null,
}) => {
const [name, setName] = useState(initialName);
// Generate default name if empty
const getDefaultName = () => {
if (initialName) return initialName;
let counter = 1;
let defaultName = `Range${counter}`;
const scopeIndex = worksheets.findIndex((s) => s.name === initialScope);
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
while (
definedNameList.some(
(dn) => dn.name === defaultName && dn.scope === newScope,
)
) {
counter++;
defaultName = `Range${counter}`;
}
return defaultName;
};
const [name, setName] = useState(getDefaultName());
const [scope, setScope] = useState(initialScope);
const [formula, setFormula] = useState(initialFormula);
const [formulaError, setFormulaError] = useState(false);
const [nameError, setNameError] = useState<string | undefined>(undefined);
const [formulaError, setFormulaError] = useState<string | undefined>(
undefined,
);
const isSelected = (value: string) => scope === value;
// Validate name (format and duplicates)
useEffect(() => {
const trimmed = name.trim();
let error: string | undefined;
if (!trimmed) {
error = t("name_manager_dialog.errors.range_name_required");
} else if (trimmed.includes(" ")) {
error = t("name_manager_dialog.errors.name_cannot_contain_spaces");
} else if (/^\d/.test(trimmed)) {
error = t("name_manager_dialog.errors.name_cannot_start_with_number");
} else if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) {
error = t("name_manager_dialog.errors.name_invalid_characters");
} else {
// Check for duplicates only if format is valid
const scopeIndex = worksheets.findIndex((s) => s.name === scope);
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
const existing = definedNameList.find(
(dn) =>
dn.name === trimmed &&
dn.scope === newScope &&
!(
editingDefinedName?.name === dn.name &&
editingDefinedName?.scope === dn.scope
),
);
if (existing) {
error = t("name_manager_dialog.errors.name_already_exists");
}
}
setNameError(error);
}, [name, scope, definedNameList, editingDefinedName, worksheets]);
const hasAnyError = nameError !== undefined || formulaError !== undefined;
return (
<Container>
<ContentArea>
@@ -56,6 +118,7 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<StyledLabel htmlFor="name">
{t("name_manager_dialog.range_name")}
</StyledLabel>
<FormControl fullWidth size="small" error={!!nameError}>
<StyledTextField
autoFocus={true}
id="name"
@@ -64,20 +127,20 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
margin="none"
placeholder={t("name_manager_dialog.enter_range_name")}
fullWidth
error={formulaError}
error={!!nameError}
value={name}
onChange={(event) => setName(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/>
{nameError && <StyledErrorText>{nameError}</StyledErrorText>}
</FormControl>
</FieldWrapper>
<FieldWrapper>
<StyledLabel htmlFor="scope">
{t("name_manager_dialog.scope_label")}
</StyledLabel>
<FormControl fullWidth size="small" error={formulaError}>
<FormControl fullWidth size="small">
<StyledSelect
id="scope"
value={scope}
@@ -139,22 +202,29 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<StyledLabel htmlFor="formula">
{t("name_manager_dialog.refers_to")}
</StyledLabel>
<FormControl fullWidth size="small" error={!!formulaError}>
<StyledTextField
id="formula"
variant="outlined"
size="small"
margin="none"
placeholder={t("name_manager_dialog.enter_formula")}
fullWidth
multiline
rows={3}
error={formulaError}
error={!!formulaError}
value={formula}
onChange={(event) => setFormula(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
onChange={(e) => {
setFormula(e.target.value);
setFormulaError(undefined);
}}
onClick={(event) => event.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/>
{formulaError && (
<StyledErrorText>{formulaError}</StyledErrorText>
)}
</FormControl>
</FieldWrapper>
</StyledBox>
</ContentArea>
@@ -170,11 +240,17 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<NewButton
variant="contained"
disableElevation
disabled={hasAnyError}
startIcon={<Check size={16} />}
onClick={() => {
const error = onSave(name, scope, formula);
const error = onSave(name.trim(), scope, formula);
if (error) {
setFormulaError(true);
const isFormulaError = /formula|reference|cell/i.test(error);
if (isFormulaError) {
setFormulaError(error);
} else {
setNameError(error);
}
}
}}
>
@@ -343,15 +419,13 @@ const StyledHelperText = styled(FormHelperText)(() => ({
fontFamily: "Inter",
color: theme.palette.grey[500],
margin: 0,
marginLeft: 0,
marginRight: 0,
marginTop: "6px",
padding: 0,
lineHeight: 1.4,
"&.MuiFormHelperText-root": {
marginTop: "6px",
marginLeft: 0,
marginRight: 0,
},
}));
const StyledErrorText = styled(StyledHelperText)(() => ({
color: theme.palette.error.main,
}));
export default EditNamedRange;

View File

@@ -6,7 +6,6 @@ import { useState } from "react";
import { theme } from "../../../theme";
import EditNamedRange from "./EditNamedRange";
// Normalize range strings for comparison (remove quotes, handle case, etc.)
const normalizeRangeString = (range: string): string => {
return range.trim().replace(/['"]/g, "");
};
@@ -177,13 +176,14 @@ const NamedRanges: React.FC<NamedRangesProps> = ({
formula={formula}
onSave={handleSave}
onCancel={handleCancel}
definedNameList={definedNameList}
editingDefinedName={editingDefinedName}
/>
</Content>
</Container>
);
}
// Show list view
const currentSelectedArea = selectedArea ? selectedArea() : null;
return (

View File

@@ -120,9 +120,17 @@
"scope_label": "Scope",
"scope_helper": "The scope of the named range determines where it is available.",
"refers_to": "Refers to",
"enter_formula": "Enter formula",
"cancel": "Cancel",
"apply": "Apply changes",
"discard": "Discard changes"
"discard": "Discard changes",
"errors": {
"range_name_required": "Range name is required",
"name_cannot_contain_spaces": "Name cannot contain spaces",
"name_cannot_start_with_number": "Name cannot start with a number",
"name_invalid_characters": "Name contains invalid characters. Use only letters, numbers, underscores, and periods. Must start with a letter or underscore.",
"name_already_exists": "This name already exists in the selected scope"
}
},
"cell_context": {
"insert_row_above": "Insert 1 row above",