1- import type { DefinedName , WorksheetProperties } from "@ironcalc/wasm" ;
1+ import type { DefinedName , Model } from "@ironcalc/wasm" ;
22import {
33 Box ,
44 FormControl ,
@@ -10,43 +10,60 @@ import {
1010 TextField ,
1111} from "@mui/material" ;
1212import { t } from "i18next" ;
13- import { Check , Tag } from "lucide-react" ;
13+ import { Check , MousePointerClick , Tag } from "lucide-react" ;
1414import { useEffect , useState } from "react" ;
1515import { theme } from "../../../theme" ;
16+ import { getFullRangeToString } from "../../util" ;
1617import { Footer , NewButton } from "./NamedRanges" ;
1718
1819export interface SaveError {
19- nameError ? : string ;
20- formulaError ? : string ;
20+ nameError : string ;
21+ formulaError : string ;
2122}
2223
2324interface 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 - z A - Z _ ] [ a - z A - Z 0 - 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
264296const 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
313345const HeaderBoxText = styled ( "span" ) `
314346 max-width: 100%;
315347 text-overflow: ellipsis;
316348 overflow: hidden;
317349 white-space: nowrap;
318- `;
350+ ` ;
319351
320352const HeaderIcon = styled ( Box ) `
321353 width: 28px;
0 commit comments