FIX: Minor cleanups
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
1391f196b5
commit
3bb49d1e8f
@@ -197,4 +197,40 @@ function getFormulaHTML(
|
|||||||
return { html, activeRanges };
|
return { html, activeRanges };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a formula (without the equals sign) returns (sheetIndex, rowStart, columnStart, rowEnd, columnEnd)
|
||||||
|
// if it represent a reference or range like `Sheet1!A1` or `Sheet3!D3:D10` in an existing sheet
|
||||||
|
// If it is not a reference or range it returns null
|
||||||
|
export function parseRangeInSheet(
|
||||||
|
model: Model,
|
||||||
|
formula: string,
|
||||||
|
): [number, number, number, number, number] | null {
|
||||||
|
// HACK: We are checking here the series of tokens in the range formula.
|
||||||
|
// This is enough for our purposes but probably a more specific ranges in formula method would be better.
|
||||||
|
const worksheets = model.getWorksheetsProperties();
|
||||||
|
const tokens = getTokens(formula);
|
||||||
|
const { token } = tokens[0];
|
||||||
|
if (tokenIsRangeType(token)) {
|
||||||
|
const {
|
||||||
|
sheet: refSheet,
|
||||||
|
left: { row: rowStart, column: columnStart },
|
||||||
|
right: { row: rowEnd, column: columnEnd },
|
||||||
|
} = token.Range;
|
||||||
|
if (refSheet !== null) {
|
||||||
|
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
|
||||||
|
if (sheetIndex >= 0) {
|
||||||
|
return [sheetIndex, rowStart, columnStart, rowEnd, columnEnd];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (tokenIsReferenceType(token)) {
|
||||||
|
const { sheet: refSheet, row, column } = token.Reference;
|
||||||
|
if (refSheet !== null) {
|
||||||
|
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
|
||||||
|
if (sheetIndex >= 0) {
|
||||||
|
return [sheetIndex, row, column, row, column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default getFormulaHTML;
|
export default getFormulaHTML;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
|
import type { DefinedName, Model } from "@ironcalc/wasm";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -10,43 +10,60 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { Check, Tag } from "lucide-react";
|
import { Check, MousePointerClick, Tag } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { theme } from "../../../theme";
|
import { theme } from "../../../theme";
|
||||||
|
import { getFullRangeToString } from "../../util";
|
||||||
import { Footer, NewButton } from "./NamedRanges";
|
import { Footer, NewButton } from "./NamedRanges";
|
||||||
|
|
||||||
export interface SaveError {
|
export interface SaveError {
|
||||||
nameError?: string;
|
nameError: string;
|
||||||
formulaError?: string;
|
formulaError: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditNamedRangeProps {
|
interface EditNamedRangeProps {
|
||||||
worksheets: WorksheetProperties[];
|
|
||||||
name: string;
|
name: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
formula: string;
|
formula: string;
|
||||||
|
model: Model;
|
||||||
onSave: (name: string, scope: string, formula: string) => SaveError;
|
onSave: (name: string, scope: string, formula: string) => SaveError;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
definedNameList: DefinedName[];
|
|
||||||
editingDefinedName: DefinedName | null;
|
editingDefinedName: DefinedName | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditNamedRange({
|
// HACK: We are using the text structure of the server error
|
||||||
worksheets,
|
// to add an error here. This is wrong for several reasons:
|
||||||
|
// 1. There is no i18n
|
||||||
|
// 2. Server error messages could change with no warning
|
||||||
|
export function formatOnSaveError(error: string): SaveError {
|
||||||
|
if (error.startsWith("Name: ")) {
|
||||||
|
return { formulaError: "", nameError: error.slice(6) };
|
||||||
|
} else if (error.startsWith("Formula: ")) {
|
||||||
|
return { formulaError: error.slice(9), nameError: "" };
|
||||||
|
} else if (error.startsWith("Scope: ")) {
|
||||||
|
return { formulaError: "", nameError: error.slice(7) };
|
||||||
|
}
|
||||||
|
// Fallback for other errors
|
||||||
|
return { formulaError: error, nameError: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditNamedRange = ({
|
||||||
name: initialName,
|
name: initialName,
|
||||||
scope: initialScope,
|
scope: initialScope,
|
||||||
formula: initialFormula,
|
formula: initialFormula,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
definedNameList,
|
|
||||||
editingDefinedName,
|
editingDefinedName,
|
||||||
}: EditNamedRangeProps) {
|
model,
|
||||||
|
}: EditNamedRangeProps) => {
|
||||||
const getDefaultName = () => {
|
const getDefaultName = () => {
|
||||||
if (initialName) return initialName;
|
if (initialName) return initialName;
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let defaultName = `Range${counter}`;
|
let defaultName = `Range${counter}`;
|
||||||
|
const worksheets = model.getWorksheetsProperties();
|
||||||
const scopeIndex = worksheets.findIndex((s) => s.name === initialScope);
|
const scopeIndex = worksheets.findIndex((s) => s.name === initialScope);
|
||||||
const newScope = scopeIndex >= 0 ? scopeIndex : null;
|
const newScope = scopeIndex >= 0 ? scopeIndex : undefined;
|
||||||
|
const definedNameList = model.getDefinedNameList();
|
||||||
|
|
||||||
while (
|
while (
|
||||||
definedNameList.some(
|
definedNameList.some(
|
||||||
@@ -69,38 +86,27 @@ function EditNamedRange({
|
|||||||
|
|
||||||
// Validate name (format and duplicates)
|
// Validate name (format and duplicates)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trimmed = name.trim();
|
const worksheets = model.getWorksheetsProperties();
|
||||||
let error = "";
|
const scopeIndex = worksheets.findIndex((s) => s.name === scope);
|
||||||
|
const newScope = scopeIndex >= 0 ? scopeIndex : null;
|
||||||
if (!trimmed) {
|
try {
|
||||||
error = t("name_manager_dialog.errors.range_name_required");
|
model.isValidDefinedName(name, newScope, formula);
|
||||||
} else if (trimmed.includes(" ")) {
|
} catch (e) {
|
||||||
error = t("name_manager_dialog.errors.name_cannot_contain_spaces");
|
const message = (e as Error).message;
|
||||||
} else if (/^\d/.test(trimmed)) {
|
if (editingDefinedName && message.includes("already exists")) {
|
||||||
error = t("name_manager_dialog.errors.name_cannot_start_with_number");
|
// Allow the same name if it's the one being edited
|
||||||
} else if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) {
|
setNameError("");
|
||||||
error = t("name_manager_dialog.errors.name_invalid_characters");
|
setFormulaError("");
|
||||||
} else {
|
return;
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
const { nameError, formulaError } = formatOnSaveError(message);
|
||||||
|
setNameError(nameError);
|
||||||
|
setFormulaError(formulaError);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setNameError("");
|
||||||
setNameError(error);
|
|
||||||
setFormulaError("");
|
setFormulaError("");
|
||||||
}, [name, scope, definedNameList, editingDefinedName, worksheets]);
|
}, [name, scope, formula, model, editingDefinedName]);
|
||||||
|
|
||||||
const hasAnyError = nameError !== "" || formulaError !== "";
|
const hasAnyError = nameError !== "" || formulaError !== "";
|
||||||
|
|
||||||
@@ -154,7 +160,9 @@ function EditNamedRange({
|
|||||||
return stringValue === "[Global]" ? (
|
return stringValue === "[Global]" ? (
|
||||||
<>
|
<>
|
||||||
<MenuSpan>{t("name_manager_dialog.workbook")}</MenuSpan>
|
<MenuSpan>{t("name_manager_dialog.workbook")}</MenuSpan>
|
||||||
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
|
<MenuSpanGrey>{` ${t(
|
||||||
|
"name_manager_dialog.global",
|
||||||
|
)}`}</MenuSpanGrey>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
stringValue
|
stringValue
|
||||||
@@ -180,9 +188,11 @@ function EditNamedRange({
|
|||||||
<MenuSpan $selected={isSelected("[Global]")}>
|
<MenuSpan $selected={isSelected("[Global]")}>
|
||||||
{t("name_manager_dialog.workbook")}
|
{t("name_manager_dialog.workbook")}
|
||||||
</MenuSpan>
|
</MenuSpan>
|
||||||
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
|
<MenuSpanGrey>{` ${t(
|
||||||
|
"name_manager_dialog.global",
|
||||||
|
)}`}</MenuSpanGrey>
|
||||||
</StyledMenuItem>
|
</StyledMenuItem>
|
||||||
{worksheets.map((option) => (
|
{model.getWorksheetsProperties().map((option) => (
|
||||||
<StyledMenuItem key={option.name} value={option.name}>
|
<StyledMenuItem key={option.name} value={option.name}>
|
||||||
{isSelected(option.name) ? (
|
{isSelected(option.name) ? (
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
@@ -201,9 +211,25 @@ function EditNamedRange({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
<FieldWrapper>
|
<FieldWrapper>
|
||||||
<StyledLabel htmlFor="formula">
|
<LineWrapper>
|
||||||
{t("name_manager_dialog.refers_to")}
|
<StyledLabel htmlFor="formula">
|
||||||
</StyledLabel>
|
{t("name_manager_dialog.refers_to")}
|
||||||
|
</StyledLabel>
|
||||||
|
<MousePointerClick
|
||||||
|
size={16}
|
||||||
|
onClick={() => {
|
||||||
|
const worksheetNames = model
|
||||||
|
.getWorksheetsProperties()
|
||||||
|
.map((s) => s.name);
|
||||||
|
const selectedView = model.getSelectedView();
|
||||||
|
const formula = getFullRangeToString(
|
||||||
|
selectedView,
|
||||||
|
worksheetNames,
|
||||||
|
);
|
||||||
|
setFormula(formula);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LineWrapper>
|
||||||
<FormControl fullWidth size="small" error={!!formulaError}>
|
<FormControl fullWidth size="small" error={!!formulaError}>
|
||||||
<StyledTextField
|
<StyledTextField
|
||||||
id="formula"
|
id="formula"
|
||||||
@@ -259,7 +285,13 @@ function EditNamedRange({
|
|||||||
</Footer>
|
</Footer>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const LineWrapper = styled("div")({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
});
|
||||||
|
|
||||||
const Container = styled("div")({
|
const Container = styled("div")({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@@ -308,14 +340,14 @@ const HeaderBox = styled(Box)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px solid ${theme.palette.grey["200"]};
|
border-bottom: 1px solid ${theme.palette.grey["200"]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const HeaderBoxText = styled("span")`
|
const HeaderBoxText = styled("span")`
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const HeaderIcon = styled(Box)`
|
const HeaderIcon = styled(Box)`
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
|
import type { DefinedName, Model } from "@ironcalc/wasm";
|
||||||
import { Button, styled, Tooltip } from "@mui/material";
|
import { Button, styled, Tooltip } from "@mui/material";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import {
|
import {
|
||||||
@@ -12,41 +12,29 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { theme } from "../../../theme";
|
import { theme } from "../../../theme";
|
||||||
import EditNamedRange, { type SaveError } from "./EditNamedRange";
|
import { parseRangeInSheet } from "../../Editor/util";
|
||||||
|
import EditNamedRange, {
|
||||||
|
formatOnSaveError,
|
||||||
|
type SaveError,
|
||||||
|
} from "./EditNamedRange";
|
||||||
|
|
||||||
const normalizeRangeString = (range: string): string => {
|
const normalizeRangeString = (range: string): string => {
|
||||||
return range.trim().replace(/['"]/g, "");
|
return range.trim().replace(/['"]/g, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NamedRangesProps {
|
interface NamedRangesProps {
|
||||||
title: string;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
definedNameList: DefinedName[];
|
model: Model;
|
||||||
worksheets: WorksheetProperties[];
|
|
||||||
updateDefinedName: (
|
|
||||||
name: string,
|
|
||||||
scope: number | null,
|
|
||||||
newName: string,
|
|
||||||
newScope: number | null,
|
|
||||||
newFormula: string,
|
|
||||||
) => void;
|
|
||||||
newDefinedName: (name: string, scope: number | null, formula: string) => void;
|
|
||||||
deleteDefinedName: (name: string, scope: number | null) => void;
|
|
||||||
getSelectedArea: () => string;
|
getSelectedArea: () => string;
|
||||||
onNameSelected: (name: string) => void;
|
onUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NamedRanges({
|
const NamedRanges = ({
|
||||||
title,
|
|
||||||
onClose,
|
onClose,
|
||||||
definedNameList,
|
|
||||||
worksheets,
|
|
||||||
updateDefinedName,
|
|
||||||
newDefinedName,
|
|
||||||
deleteDefinedName,
|
|
||||||
getSelectedArea,
|
getSelectedArea,
|
||||||
onNameSelected,
|
model,
|
||||||
}: NamedRangesProps) {
|
onUpdate,
|
||||||
|
}: NamedRangesProps) => {
|
||||||
const [editingDefinedName, setEditingDefinedName] =
|
const [editingDefinedName, setEditingDefinedName] =
|
||||||
useState<DefinedName | null>(null);
|
useState<DefinedName | null>(null);
|
||||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||||
@@ -71,26 +59,35 @@ function NamedRanges({
|
|||||||
scope: string,
|
scope: string,
|
||||||
formula: string,
|
formula: string,
|
||||||
): SaveError => {
|
): SaveError => {
|
||||||
|
const worksheets = model.getWorksheetsProperties();
|
||||||
if (isCreatingNew) {
|
if (isCreatingNew) {
|
||||||
if (!newDefinedName) return {};
|
|
||||||
|
|
||||||
const scope_index = worksheets.findIndex((s) => s.name === scope);
|
const scope_index = worksheets.findIndex((s) => s.name === scope);
|
||||||
const newScope = scope_index >= 0 ? scope_index : null;
|
const newScope = scope_index >= 0 ? scope_index : null;
|
||||||
try {
|
try {
|
||||||
newDefinedName(name, newScope, formula);
|
model.newDefinedName(name, newScope, formula);
|
||||||
setIsCreatingNew(false);
|
setIsCreatingNew(false);
|
||||||
return {};
|
onUpdate();
|
||||||
|
return {
|
||||||
|
formulaError: "",
|
||||||
|
nameError: "",
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Since name validation is done client-side, errors from model are formula errors
|
if (e instanceof Error) {
|
||||||
return { formulaError: `${e}` };
|
return formatOnSaveError(e.message);
|
||||||
|
}
|
||||||
|
return { formulaError: "", nameError: `${e}` };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!editingDefinedName) return {};
|
if (!editingDefinedName)
|
||||||
|
return {
|
||||||
|
formulaError: "",
|
||||||
|
nameError: "",
|
||||||
|
};
|
||||||
|
|
||||||
const scope_index = worksheets.findIndex((s) => s.name === scope);
|
const scope_index = worksheets.findIndex((s) => s.name === scope);
|
||||||
const newScope = scope_index >= 0 ? scope_index : null;
|
const newScope = scope_index >= 0 ? scope_index : null;
|
||||||
try {
|
try {
|
||||||
updateDefinedName(
|
model.updateDefinedName(
|
||||||
editingDefinedName.name,
|
editingDefinedName.name,
|
||||||
editingDefinedName.scope ?? null,
|
editingDefinedName.scope ?? null,
|
||||||
name,
|
name,
|
||||||
@@ -98,10 +95,13 @@ function NamedRanges({
|
|||||||
formula,
|
formula,
|
||||||
);
|
);
|
||||||
setEditingDefinedName(null);
|
setEditingDefinedName(null);
|
||||||
return {};
|
onUpdate();
|
||||||
|
return { formulaError: "", nameError: "" };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Since name validation is done client-side, errors from model are formula errors
|
if (e instanceof Error) {
|
||||||
return { formulaError: `${e}` };
|
return formatOnSaveError(e.message);
|
||||||
|
}
|
||||||
|
return { formulaError: "", nameError: `${e}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -114,12 +114,13 @@ function NamedRanges({
|
|||||||
|
|
||||||
if (editingDefinedName) {
|
if (editingDefinedName) {
|
||||||
name = editingDefinedName.name;
|
name = editingDefinedName.name;
|
||||||
|
const worksheets = model.getWorksheetsProperties();
|
||||||
scopeName =
|
scopeName =
|
||||||
editingDefinedName.scope != null
|
editingDefinedName.scope != null
|
||||||
? worksheets[editingDefinedName.scope]?.name || "[unknown]"
|
? worksheets[editingDefinedName.scope]?.name || "[unknown]"
|
||||||
: "[Global]";
|
: "[Global]";
|
||||||
formula = editingDefinedName.formula;
|
formula = editingDefinedName.formula;
|
||||||
} else if (isCreatingNew && getSelectedArea) {
|
} else if (isCreatingNew) {
|
||||||
formula = getSelectedArea();
|
formula = getSelectedArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +178,13 @@ function NamedRanges({
|
|||||||
</EditHeader>
|
</EditHeader>
|
||||||
<Content>
|
<Content>
|
||||||
<EditNamedRange
|
<EditNamedRange
|
||||||
worksheets={worksheets}
|
|
||||||
name={name}
|
name={name}
|
||||||
scope={scopeName}
|
scope={scopeName}
|
||||||
formula={formula}
|
formula={formula}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
definedNameList={definedNameList}
|
|
||||||
editingDefinedName={editingDefinedName}
|
editingDefinedName={editingDefinedName}
|
||||||
|
model={model}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -192,11 +192,22 @@ function NamedRanges({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSelectedArea = getSelectedArea();
|
const currentSelectedArea = getSelectedArea();
|
||||||
|
const definedNameList = model.getDefinedNameList();
|
||||||
|
const onNameSelected = (formula: string) => {
|
||||||
|
const range = parseRangeInSheet(model, formula);
|
||||||
|
if (range) {
|
||||||
|
const [sheetIndex, rowStart, columnStart, rowEnd, columnEnd] = range;
|
||||||
|
model.setSelectedSheet(sheetIndex);
|
||||||
|
model.setSelectedCell(rowStart, columnStart);
|
||||||
|
model.setSelectedRange(rowStart, columnStart, rowEnd, columnEnd);
|
||||||
|
}
|
||||||
|
onUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Header>
|
<Header>
|
||||||
<HeaderTitle>{title}</HeaderTitle>
|
<HeaderTitle>{t("name_manager_dialog.title")}</HeaderTitle>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={t("right_drawer.close")}
|
title={t("right_drawer.close")}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -239,6 +250,7 @@ function NamedRanges({
|
|||||||
) : (
|
) : (
|
||||||
<ListContainer>
|
<ListContainer>
|
||||||
{definedNameList.map((definedName) => {
|
{definedNameList.map((definedName) => {
|
||||||
|
const worksheets = model.getWorksheetsProperties();
|
||||||
const scopeName =
|
const scopeName =
|
||||||
definedName.scope != null
|
definedName.scope != null
|
||||||
? worksheets[definedName.scope]?.name || "[Unknown]"
|
? worksheets[definedName.scope]?.name || "[Unknown]"
|
||||||
@@ -252,7 +264,29 @@ function NamedRanges({
|
|||||||
key={`${definedName.name}-${definedName.scope}`}
|
key={`${definedName.name}-${definedName.scope}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
$isSelected={isSelected}
|
$isSelected={isSelected}
|
||||||
onClick={() => onNameSelected(definedName.formula)}
|
onClick={() => {
|
||||||
|
// select the area corresponding to the defined name
|
||||||
|
const formula = definedName.formula;
|
||||||
|
const range = parseRangeInSheet(model, formula);
|
||||||
|
if (range) {
|
||||||
|
const [
|
||||||
|
sheetIndex,
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
] = range;
|
||||||
|
model.setSelectedSheet(sheetIndex);
|
||||||
|
model.setSelectedCell(rowStart, columnStart);
|
||||||
|
model.setSelectedRange(
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onUpdate();
|
||||||
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -290,23 +324,21 @@ function NamedRanges({
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (deleteDefinedName) {
|
model.deleteDefinedName(
|
||||||
deleteDefinedName(
|
definedName.name,
|
||||||
definedName.name,
|
definedName.scope ?? null,
|
||||||
definedName.scope ?? null,
|
);
|
||||||
);
|
onUpdate();
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (deleteDefinedName) {
|
model.deleteDefinedName(
|
||||||
deleteDefinedName(
|
definedName.name,
|
||||||
definedName.name,
|
definedName.scope ?? null,
|
||||||
definedName.scope ?? null,
|
);
|
||||||
);
|
onUpdate();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label={t("name_manager_dialog.delete")}
|
aria-label={t("name_manager_dialog.delete")}
|
||||||
@@ -342,7 +374,7 @@ function NamedRanges({
|
|||||||
</Footer>
|
</Footer>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Container = styled("div")({
|
const Container = styled("div")({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@@ -362,26 +394,24 @@ const ListContainer = styled("div")({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ListItem = styled("div")<{ $isSelected?: boolean }>(
|
const ListItem = styled("div")<{ $isSelected: boolean }>(({ $isSelected }) => ({
|
||||||
({ $isSelected }) => ({
|
display: "flex",
|
||||||
display: "flex",
|
alignItems: "flex-start",
|
||||||
alignItems: "flex-start",
|
justifyContent: "space-between",
|
||||||
justifyContent: "space-between",
|
padding: "8px 12px",
|
||||||
padding: "8px 12px",
|
minHeight: "40px",
|
||||||
minHeight: "40px",
|
boxSizing: "border-box",
|
||||||
boxSizing: "border-box",
|
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
||||||
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
paddingLeft: $isSelected ? "20px" : "12px",
|
||||||
paddingLeft: $isSelected ? "20px" : "12px",
|
transition: "all 0.2s ease-in-out",
|
||||||
transition: "all 0.2s ease-in-out",
|
borderLeft: $isSelected
|
||||||
borderLeft: $isSelected
|
? `3px solid ${theme.palette.primary.main}`
|
||||||
? `3px solid ${theme.palette.primary.main}`
|
: "3px solid transparent",
|
||||||
: "3px solid transparent",
|
"&:hover": {
|
||||||
"&:hover": {
|
backgroundColor: theme.palette.grey[50],
|
||||||
backgroundColor: theme.palette.grey[50],
|
paddingLeft: "20px",
|
||||||
paddingLeft: "20px",
|
},
|
||||||
},
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const ListItemText = styled("div")({
|
const ListItemText = styled("div")({
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import type { MouseEvent as ReactMouseEvent } from "react";
|
import type { MouseEvent as ReactMouseEvent } from "react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
@@ -16,20 +16,9 @@ interface RightDrawerProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
width: number;
|
width: number;
|
||||||
onWidthChange: (width: number) => void;
|
onWidthChange: (width: number) => void;
|
||||||
title: string;
|
model: Model;
|
||||||
definedNameList: DefinedName[];
|
onUpdate: () => void;
|
||||||
worksheets: WorksheetProperties[];
|
|
||||||
updateDefinedName: (
|
|
||||||
name: string,
|
|
||||||
scope: number | null,
|
|
||||||
newName: string,
|
|
||||||
newScope: number | null,
|
|
||||||
newFormula: string,
|
|
||||||
) => void;
|
|
||||||
newDefinedName: (name: string, scope: number | null, formula: string) => void;
|
|
||||||
deleteDefinedName: (name: string, scope: number | null) => void;
|
|
||||||
getSelectedArea: () => string;
|
getSelectedArea: () => string;
|
||||||
onNameSelected: (name: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightDrawer = ({
|
const RightDrawer = ({
|
||||||
@@ -37,14 +26,9 @@ const RightDrawer = ({
|
|||||||
onClose,
|
onClose,
|
||||||
width,
|
width,
|
||||||
onWidthChange,
|
onWidthChange,
|
||||||
title,
|
|
||||||
definedNameList,
|
|
||||||
worksheets,
|
|
||||||
updateDefinedName,
|
|
||||||
newDefinedName,
|
|
||||||
deleteDefinedName,
|
|
||||||
getSelectedArea,
|
getSelectedArea,
|
||||||
onNameSelected,
|
model,
|
||||||
|
onUpdate,
|
||||||
}: RightDrawerProps) => {
|
}: RightDrawerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [drawerWidth, setDrawerWidth] = useState(width);
|
const [drawerWidth, setDrawerWidth] = useState(width);
|
||||||
@@ -57,7 +41,9 @@ const RightDrawer = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isResizing) return;
|
if (!isResizing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent text selection during resize
|
// Prevent text selection during resize
|
||||||
document.body.style.userSelect = "none";
|
document.body.style.userSelect = "none";
|
||||||
@@ -90,7 +76,9 @@ const RightDrawer = ({
|
|||||||
};
|
};
|
||||||
}, [isResizing, onWidthChange]);
|
}, [isResizing, onWidthChange]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerContainer $drawerWidth={drawerWidth}>
|
<DrawerContainer $drawerWidth={drawerWidth}>
|
||||||
@@ -103,15 +91,10 @@ const RightDrawer = ({
|
|||||||
<Divider />
|
<Divider />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<NamedRanges
|
<NamedRanges
|
||||||
title={title}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
definedNameList={definedNameList}
|
model={model}
|
||||||
worksheets={worksheets}
|
onUpdate={onUpdate}
|
||||||
updateDefinedName={updateDefinedName}
|
|
||||||
newDefinedName={newDefinedName}
|
|
||||||
deleteDefinedName={deleteDefinedName}
|
|
||||||
getSelectedArea={getSelectedArea}
|
getSelectedArea={getSelectedArea}
|
||||||
onNameSelected={onNameSelected}
|
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</DrawerContainer>
|
</DrawerContainer>
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import {
|
import type {
|
||||||
type BorderOptions,
|
BorderOptions,
|
||||||
type ClipboardCell,
|
ClipboardCell,
|
||||||
getTokens,
|
Model,
|
||||||
type Model,
|
WorksheetProperties,
|
||||||
type WorksheetProperties,
|
|
||||||
} from "@ironcalc/wasm";
|
} from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import { t } from "i18next";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
||||||
getNewClipboardId,
|
getNewClipboardId,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { TOOLBAR_HEIGHT } from "../constants";
|
import { TOOLBAR_HEIGHT } from "../constants";
|
||||||
import { tokenIsRangeType } from "../Editor/util";
|
|
||||||
import FormulaBar from "../FormulaBar/FormulaBar";
|
import FormulaBar from "../FormulaBar/FormulaBar";
|
||||||
import RightDrawer, { DEFAULT_DRAWER_WIDTH } from "../RightDrawer/RightDrawer";
|
import RightDrawer, { DEFAULT_DRAWER_WIDTH } from "../RightDrawer/RightDrawer";
|
||||||
import SheetTabBar from "../SheetTabBar";
|
import SheetTabBar from "../SheetTabBar";
|
||||||
@@ -745,53 +742,17 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
width={drawerWidth}
|
width={drawerWidth}
|
||||||
onWidthChange={setDrawerWidth}
|
onWidthChange={setDrawerWidth}
|
||||||
title={t("name_manager_dialog.title")}
|
model={model}
|
||||||
definedNameList={model.getDefinedNameList()}
|
onUpdate={() => {
|
||||||
worksheets={worksheets}
|
|
||||||
updateDefinedName={(
|
|
||||||
name: string,
|
|
||||||
scope: number | null,
|
|
||||||
newName: string,
|
|
||||||
newScope: number | null,
|
|
||||||
newFormula: string,
|
|
||||||
) => {
|
|
||||||
model.updateDefinedName(name, scope, newName, newScope, newFormula);
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
}}
|
|
||||||
newDefinedName={(
|
|
||||||
name: string,
|
|
||||||
scope: number | null,
|
|
||||||
formula: string,
|
|
||||||
) => {
|
|
||||||
model.newDefinedName(name, scope, formula);
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
}}
|
|
||||||
deleteDefinedName={(name: string, scope: number | null) => {
|
|
||||||
model.deleteDefinedName(name, scope);
|
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
getSelectedArea={() => {
|
getSelectedArea={() => {
|
||||||
const worksheetNames = worksheets.map((s) => s.name);
|
const worksheetNames = model
|
||||||
|
.getWorksheetsProperties()
|
||||||
|
.map((s) => s.name);
|
||||||
const selectedView = model.getSelectedView();
|
const selectedView = model.getSelectedView();
|
||||||
return getFullRangeToString(selectedView, worksheetNames);
|
return getFullRangeToString(selectedView, worksheetNames);
|
||||||
}}
|
}}
|
||||||
onNameSelected={(formula) => {
|
|
||||||
const tokens = getTokens(formula);
|
|
||||||
const { token } = tokens[0];
|
|
||||||
if (tokenIsRangeType(token)) {
|
|
||||||
const sheetName = worksheets[model.getSelectedSheet()].name;
|
|
||||||
const {
|
|
||||||
sheet: refSheet,
|
|
||||||
left: { row: rowStart, column: columnStart },
|
|
||||||
right: { row: rowEnd, column: columnEnd },
|
|
||||||
} = token.Range;
|
|
||||||
if (refSheet !== null && refSheet === sheetName) {
|
|
||||||
model.setSelectedCell(rowStart, columnStart);
|
|
||||||
model.setSelectedRange(rowStart, columnStart, rowEnd, columnEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user