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 {
|
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,28 +118,29 @@ 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>
|
||||||
<StyledTextField
|
<FormControl fullWidth size="small" error={!!nameError}>
|
||||||
autoFocus={true}
|
<StyledTextField
|
||||||
id="name"
|
autoFocus={true}
|
||||||
variant="outlined"
|
id="name"
|
||||||
size="small"
|
variant="outlined"
|
||||||
margin="none"
|
size="small"
|
||||||
placeholder={t("name_manager_dialog.enter_range_name")}
|
margin="none"
|
||||||
fullWidth
|
placeholder={t("name_manager_dialog.enter_range_name")}
|
||||||
error={formulaError}
|
fullWidth
|
||||||
value={name}
|
error={!!nameError}
|
||||||
onChange={(event) => setName(event.target.value)}
|
value={name}
|
||||||
onKeyDown={(event) => {
|
onChange={(e) => setName(e.target.value)}
|
||||||
event.stopPropagation();
|
onKeyDown={(e) => e.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>
|
||||||
<StyledTextField
|
<FormControl fullWidth size="small" error={!!formulaError}>
|
||||||
id="formula"
|
<StyledTextField
|
||||||
variant="outlined"
|
id="formula"
|
||||||
size="small"
|
variant="outlined"
|
||||||
margin="none"
|
size="small"
|
||||||
fullWidth
|
margin="none"
|
||||||
multiline
|
placeholder={t("name_manager_dialog.enter_formula")}
|
||||||
rows={3}
|
fullWidth
|
||||||
error={formulaError}
|
multiline
|
||||||
value={formula}
|
rows={3}
|
||||||
onChange={(event) => setFormula(event.target.value)}
|
error={!!formulaError}
|
||||||
onKeyDown={(event) => {
|
value={formula}
|
||||||
event.stopPropagation();
|
onChange={(e) => {
|
||||||
}}
|
setFormula(e.target.value);
|
||||||
onClick={(event) => event.stopPropagation()}
|
setFormulaError(undefined);
|
||||||
/>
|
}}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user