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 { import {
Box, Box,
FormControl, FormControl,
@@ -11,7 +11,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { t } from "i18next"; import { t } from "i18next";
import { Check, Tag } from "lucide-react"; import { Check, Tag } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import type React from "react"; import type React from "react";
import { theme } from "../../../theme"; import { theme } from "../../../theme";
import { Footer, NewButton } from "./NamedRanges"; import { Footer, NewButton } from "./NamedRanges";
@@ -23,6 +23,8 @@ interface EditNamedRangeProps {
formula: string; formula: string;
onSave: (name: string, scope: string, formula: string) => string | undefined; onSave: (name: string, scope: string, formula: string) => string | undefined;
onCancel: () => void; onCancel: () => void;
definedNameList?: DefinedName[];
editingDefinedName?: DefinedName | null;
} }
const EditNamedRange: React.FC<EditNamedRangeProps> = ({ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
@@ -32,14 +34,74 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
formula: initialFormula, formula: initialFormula,
onSave, onSave,
onCancel, 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 [scope, setScope] = useState(initialScope);
const [formula, setFormula] = useState(initialFormula); 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; 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 ( return (
<Container> <Container>
<ContentArea> <ContentArea>
@@ -56,6 +118,7 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<StyledLabel htmlFor="name"> <StyledLabel htmlFor="name">
{t("name_manager_dialog.range_name")} {t("name_manager_dialog.range_name")}
</StyledLabel> </StyledLabel>
<FormControl fullWidth size="small" error={!!nameError}>
<StyledTextField <StyledTextField
autoFocus={true} autoFocus={true}
id="name" id="name"
@@ -64,20 +127,20 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
margin="none" margin="none"
placeholder={t("name_manager_dialog.enter_range_name")} placeholder={t("name_manager_dialog.enter_range_name")}
fullWidth fullWidth
error={formulaError} error={!!nameError}
value={name} value={name}
onChange={(event) => setName(event.target.value)} onChange={(e) => setName(e.target.value)}
onKeyDown={(event) => { onKeyDown={(e) => e.stopPropagation()}
event.stopPropagation(); onClick={(e) => e.stopPropagation()}
}}
onClick={(event) => event.stopPropagation()}
/> />
{nameError && <StyledErrorText>{nameError}</StyledErrorText>}
</FormControl>
</FieldWrapper> </FieldWrapper>
<FieldWrapper> <FieldWrapper>
<StyledLabel htmlFor="scope"> <StyledLabel htmlFor="scope">
{t("name_manager_dialog.scope_label")} {t("name_manager_dialog.scope_label")}
</StyledLabel> </StyledLabel>
<FormControl fullWidth size="small" error={formulaError}> <FormControl fullWidth size="small">
<StyledSelect <StyledSelect
id="scope" id="scope"
value={scope} value={scope}
@@ -139,22 +202,29 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<StyledLabel htmlFor="formula"> <StyledLabel htmlFor="formula">
{t("name_manager_dialog.refers_to")} {t("name_manager_dialog.refers_to")}
</StyledLabel> </StyledLabel>
<FormControl fullWidth size="small" error={!!formulaError}>
<StyledTextField <StyledTextField
id="formula" id="formula"
variant="outlined" variant="outlined"
size="small" size="small"
margin="none" margin="none"
placeholder={t("name_manager_dialog.enter_formula")}
fullWidth fullWidth
multiline multiline
rows={3} rows={3}
error={formulaError} error={!!formulaError}
value={formula} value={formula}
onChange={(event) => setFormula(event.target.value)} onChange={(e) => {
onKeyDown={(event) => { setFormula(e.target.value);
event.stopPropagation(); setFormulaError(undefined);
}} }}
onClick={(event) => event.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/> />
{formulaError && (
<StyledErrorText>{formulaError}</StyledErrorText>
)}
</FormControl>
</FieldWrapper> </FieldWrapper>
</StyledBox> </StyledBox>
</ContentArea> </ContentArea>
@@ -170,11 +240,17 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
<NewButton <NewButton
variant="contained" variant="contained"
disableElevation disableElevation
disabled={hasAnyError}
startIcon={<Check size={16} />} startIcon={<Check size={16} />}
onClick={() => { onClick={() => {
const error = onSave(name, scope, formula); const error = onSave(name.trim(), scope, formula);
if (error) { 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", fontFamily: "Inter",
color: theme.palette.grey[500], color: theme.palette.grey[500],
margin: 0, margin: 0,
marginLeft: 0, marginTop: "6px",
marginRight: 0,
padding: 0, padding: 0,
lineHeight: 1.4, lineHeight: 1.4,
"&.MuiFormHelperText-root": { }));
marginTop: "6px",
marginLeft: 0, const StyledErrorText = styled(StyledHelperText)(() => ({
marginRight: 0, color: theme.palette.error.main,
},
})); }));
export default EditNamedRange; export default EditNamedRange;

View File

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

View File

@@ -120,9 +120,17 @@
"scope_label": "Scope", "scope_label": "Scope",
"scope_helper": "The scope of the named range determines where it is available.", "scope_helper": "The scope of the named range determines where it is available.",
"refers_to": "Refers to", "refers_to": "Refers to",
"enter_formula": "Enter formula",
"cancel": "Cancel", "cancel": "Cancel",
"apply": "Apply changes", "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": { "cell_context": {
"insert_row_above": "Insert 1 row above", "insert_row_above": "Insert 1 row above",