update: improve error handling
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
36beccd4ae
commit
c283fd7b60
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user