diff --git a/webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx b/webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx index ddbabb2..317cd61 100644 --- a/webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx +++ b/webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx @@ -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 = ({ @@ -32,14 +34,74 @@ const EditNamedRange: React.FC = ({ 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(undefined); + const [formulaError, setFormulaError] = useState( + 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 ( @@ -56,28 +118,29 @@ const EditNamedRange: React.FC = ({ {t("name_manager_dialog.range_name")} - setName(event.target.value)} - onKeyDown={(event) => { - event.stopPropagation(); - }} - onClick={(event) => event.stopPropagation()} - /> + + setName(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + /> + {nameError && {nameError}} + {t("name_manager_dialog.scope_label")} - + = ({ {t("name_manager_dialog.refers_to")} - setFormula(event.target.value)} - onKeyDown={(event) => { - event.stopPropagation(); - }} - onClick={(event) => event.stopPropagation()} - /> + + { + setFormula(e.target.value); + setFormulaError(undefined); + }} + onKeyDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + /> + {formulaError && ( + {formulaError} + )} + @@ -170,11 +240,17 @@ const EditNamedRange: React.FC = ({ } 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; diff --git a/webapp/IronCalc/src/components/RightDrawer/NamedRanges/NamedRanges.tsx b/webapp/IronCalc/src/components/RightDrawer/NamedRanges/NamedRanges.tsx index 890aec2..ae68deb 100644 --- a/webapp/IronCalc/src/components/RightDrawer/NamedRanges/NamedRanges.tsx +++ b/webapp/IronCalc/src/components/RightDrawer/NamedRanges/NamedRanges.tsx @@ -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 = ({ formula={formula} onSave={handleSave} onCancel={handleCancel} + definedNameList={definedNameList} + editingDefinedName={editingDefinedName} /> ); } - // Show list view const currentSelectedArea = selectedArea ? selectedArea() : null; return ( diff --git a/webapp/IronCalc/src/locale/en_us.json b/webapp/IronCalc/src/locale/en_us.json index be6677d..0c66c28 100644 --- a/webapp/IronCalc/src/locale/en_us.json +++ b/webapp/IronCalc/src/locale/en_us.json @@ -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",