Skip to content

Commit 66be0ab

Browse files
committed
FIX: Minor cleanups
1 parent 2817e3e commit 66be0ab

File tree

5 files changed

+243
-201
lines changed

5 files changed

+243
-201
lines changed

webapp/IronCalc/src/components/Editor/util.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,40 @@ function getFormulaHTML(
197197
return { html, activeRanges };
198198
}
199199

200+
// Given a formula (without the equals sign) returns (sheetIndex, rowStart, columnStart, rowEnd, columnEnd)
201+
// if it represent a reference or range like `Sheet1!A1` or `Sheet3!D3:D10` in an existing sheet
202+
// If it is not a reference or range it returns null
203+
export function parseRangeInSheet(
204+
model: Model,
205+
formula: string,
206+
): [number, number, number, number, number] | null {
207+
// HACK: We are checking here the series of tokens in the range formula.
208+
// This is enough for our purposes but probably a more specific ranges in formula method would be better.
209+
const worksheets = model.getWorksheetsProperties();
210+
const tokens = getTokens(formula);
211+
const { token } = tokens[0];
212+
if (tokenIsRangeType(token)) {
213+
const {
214+
sheet: refSheet,
215+
left: { row: rowStart, column: columnStart },
216+
right: { row: rowEnd, column: columnEnd },
217+
} = token.Range;
218+
if (refSheet !== null) {
219+
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
220+
if (sheetIndex >= 0) {
221+
return [sheetIndex, rowStart, columnStart, rowEnd, columnEnd];
222+
}
223+
}
224+
} else if (tokenIsReferenceType(token)) {
225+
const { sheet: refSheet, row, column } = token.Reference;
226+
if (refSheet !== null) {
227+
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
228+
if (sheetIndex >= 0) {
229+
return [sheetIndex, row, column, row, column];
230+
}
231+
}
232+
}
233+
return null;
234+
}
235+
200236
export default getFormulaHTML;

webapp/IronCalc/src/components/RightDrawer/NamedRanges/EditNamedRange.tsx

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
1+
import type { DefinedName, Model } from "@ironcalc/wasm";
22
import {
33
Box,
44
FormControl,
@@ -10,43 +10,60 @@ import {
1010
TextField,
1111
} from "@mui/material";
1212
import { t } from "i18next";
13-
import { Check, Tag } from "lucide-react";
13+
import { Check, MousePointerClick, Tag } from "lucide-react";
1414
import { useEffect, useState } from "react";
1515
import { theme } from "../../../theme";
16+
import { getFullRangeToString } from "../../util";
1617
import { Footer, NewButton } from "./NamedRanges";
1718

1819
export interface SaveError {
19-
nameError?: string;
20-
formulaError?: string;
20+
nameError: string;
21+
formulaError: string;
2122
}
2223

2324
interface EditNamedRangeProps {
24-
worksheets: WorksheetProperties[];
2525
name: string;
2626
scope: string;
2727
formula: string;
28+
model: Model;
2829
onSave: (name: string, scope: string, formula: string) => SaveError;
2930
onCancel: () => void;
30-
definedNameList: DefinedName[];
3131
editingDefinedName: DefinedName | null;
3232
}
3333

34-
function EditNamedRange({
35-
worksheets,
34+
// HACK: We are using the text structure of the server error
35+
// to add an error here. This is wrong for several reasons:
36+
// 1. There is no i18n
37+
// 2. Server error messages could change with no warning
38+
export function formatOnSaveError(error: string): SaveError {
39+
if (error.startsWith("Name: ")) {
40+
return { formulaError: "", nameError: error.slice(6) };
41+
} else if (error.startsWith("Formula: ")) {
42+
return { formulaError: error.slice(9), nameError: "" };
43+
} else if (error.startsWith("Scope: ")) {
44+
return { formulaError: "", nameError: error.slice(7) };
45+
}
46+
// Fallback for other errors
47+
return { formulaError: error, nameError: "" };
48+
}
49+
50+
const EditNamedRange = ({
3651
name: initialName,
3752
scope: initialScope,
3853
formula: initialFormula,
3954
onSave,
4055
onCancel,
41-
definedNameList,
4256
editingDefinedName,
43-
}: EditNamedRangeProps) {
57+
model,
58+
}: EditNamedRangeProps) => {
4459
const getDefaultName = () => {
4560
if (initialName) return initialName;
4661
let counter = 1;
4762
let defaultName = `Range${counter}`;
63+
const worksheets = model.getWorksheetsProperties();
4864
const scopeIndex = worksheets.findIndex((s) => s.name === initialScope);
49-
const newScope = scopeIndex >= 0 ? scopeIndex : null;
65+
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
66+
const definedNameList = model.getDefinedNameList();
5067

5168
while (
5269
definedNameList.some(
@@ -69,38 +86,27 @@ function EditNamedRange({
6986

7087
// Validate name (format and duplicates)
7188
useEffect(() => {
72-
const trimmed = name.trim();
73-
let error = "";
74-
75-
if (!trimmed) {
76-
error = t("name_manager_dialog.errors.range_name_required");
77-
} else if (trimmed.includes(" ")) {
78-
error = t("name_manager_dialog.errors.name_cannot_contain_spaces");
79-
} else if (/^\d/.test(trimmed)) {
80-
error = t("name_manager_dialog.errors.name_cannot_start_with_number");
81-
} else if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) {
82-
error = t("name_manager_dialog.errors.name_invalid_characters");
83-
} else {
84-
// Check for duplicates only if format is valid
85-
const scopeIndex = worksheets.findIndex((s) => s.name === scope);
86-
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
87-
const existing = definedNameList.find(
88-
(dn) =>
89-
dn.name === trimmed &&
90-
dn.scope === newScope &&
91-
!(
92-
editingDefinedName?.name === dn.name &&
93-
editingDefinedName?.scope === dn.scope
94-
),
95-
);
96-
if (existing) {
97-
error = t("name_manager_dialog.errors.name_already_exists");
89+
const worksheets = model.getWorksheetsProperties();
90+
const scopeIndex = worksheets.findIndex((s) => s.name === scope);
91+
const newScope = scopeIndex >= 0 ? scopeIndex : null;
92+
try {
93+
model.isValidDefinedName(name, newScope, formula);
94+
} catch (e) {
95+
const message = (e as Error).message;
96+
if (editingDefinedName && message.includes("already exists")) {
97+
// Allow the same name if it's the one being edited
98+
setNameError("");
99+
setFormulaError("");
100+
return;
98101
}
102+
const { nameError, formulaError } = formatOnSaveError(message);
103+
setNameError(nameError);
104+
setFormulaError(formulaError);
105+
return;
99106
}
100-
101-
setNameError(error);
107+
setNameError("");
102108
setFormulaError("");
103-
}, [name, scope, definedNameList, editingDefinedName, worksheets]);
109+
}, [name, scope, formula, model, editingDefinedName]);
104110

105111
const hasAnyError = nameError !== "" || formulaError !== "";
106112

@@ -154,7 +160,9 @@ function EditNamedRange({
154160
return stringValue === "[Global]" ? (
155161
<>
156162
<MenuSpan>{t("name_manager_dialog.workbook")}</MenuSpan>
157-
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
163+
<MenuSpanGrey>{` ${t(
164+
"name_manager_dialog.global",
165+
)}`}</MenuSpanGrey>
158166
</>
159167
) : (
160168
stringValue
@@ -180,9 +188,11 @@ function EditNamedRange({
180188
<MenuSpan $selected={isSelected("[Global]")}>
181189
{t("name_manager_dialog.workbook")}
182190
</MenuSpan>
183-
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
191+
<MenuSpanGrey>{` ${t(
192+
"name_manager_dialog.global",
193+
)}`}</MenuSpanGrey>
184194
</StyledMenuItem>
185-
{worksheets.map((option) => (
195+
{model.getWorksheetsProperties().map((option) => (
186196
<StyledMenuItem key={option.name} value={option.name}>
187197
{isSelected(option.name) ? (
188198
<CheckIcon />
@@ -201,9 +211,25 @@ function EditNamedRange({
201211
</FormControl>
202212
</FieldWrapper>
203213
<FieldWrapper>
204-
<StyledLabel htmlFor="formula">
205-
{t("name_manager_dialog.refers_to")}
206-
</StyledLabel>
214+
<LineWrapper>
215+
<StyledLabel htmlFor="formula">
216+
{t("name_manager_dialog.refers_to")}
217+
</StyledLabel>
218+
<MousePointerClick
219+
size={16}
220+
onClick={() => {
221+
const worksheetNames = model
222+
.getWorksheetsProperties()
223+
.map((s) => s.name);
224+
const selectedView = model.getSelectedView();
225+
const formula = getFullRangeToString(
226+
selectedView,
227+
worksheetNames,
228+
);
229+
setFormula(formula);
230+
}}
231+
/>
232+
</LineWrapper>
207233
<FormControl fullWidth size="small" error={!!formulaError}>
208234
<StyledTextField
209235
id="formula"
@@ -259,7 +285,13 @@ function EditNamedRange({
259285
</Footer>
260286
</Container>
261287
);
262-
}
288+
};
289+
290+
const LineWrapper = styled("div")({
291+
display: "flex",
292+
alignItems: "center",
293+
gap: "8px",
294+
});
263295

264296
const Container = styled("div")({
265297
height: "100%",
@@ -308,14 +340,14 @@ const HeaderBox = styled(Box)`
308340
align-items: center;
309341
text-align: center;
310342
border-bottom: 1px solid ${theme.palette.grey["200"]};
311-
`;
343+
`;
312344

313345
const HeaderBoxText = styled("span")`
314346
max-width: 100%;
315347
text-overflow: ellipsis;
316348
overflow: hidden;
317349
white-space: nowrap;
318-
`;
350+
`;
319351

320352
const HeaderIcon = styled(Box)`
321353
width: 28px;

0 commit comments

Comments
 (0)