From 832ca02e16f7f49cdba126379420741f36cb229a Mon Sep 17 00:00:00 2001 From: francisco aloi Date: Sat, 28 Dec 2024 12:32:40 +0100 Subject: [PATCH] NamedRanges changes --- webapp/src/components/NameManagerDialog.tsx | 224 ++++++++++++++++++++ webapp/src/components/NamedRange.tsx | 158 ++++++++++++++ webapp/src/components/toolbar.tsx | 25 +++ webapp/src/components/util.ts | 29 ++- webapp/src/components/workbook.tsx | 1 + webapp/src/locale/en_us.json | 15 +- 6 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 webapp/src/components/NameManagerDialog.tsx create mode 100644 webapp/src/components/NamedRange.tsx diff --git a/webapp/src/components/NameManagerDialog.tsx b/webapp/src/components/NameManagerDialog.tsx new file mode 100644 index 0000000..08e524b --- /dev/null +++ b/webapp/src/components/NameManagerDialog.tsx @@ -0,0 +1,224 @@ +import type { DefinedName, Model } from "@ironcalc/wasm"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + styled, +} from "@mui/material"; +import { t } from "i18next"; +import { BookOpen, Check, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import NamedRange from "./NamedRange"; +import { getFullRangeToString } from "./util"; + +type NameManagerDialogProperties = { + onClose: () => void; + open: boolean; + model: Model; +}; + +function NameManagerDialog(props: NameManagerDialogProperties) { + const [definedNamesLocal, setDefinedNamesLocal] = useState(); + const [definedName, setDefinedName] = useState({ + name: "", + scope: undefined, + formula: "", + }); + + // render definedNames from model + useEffect(() => { + if (props.open) { + const definedNamesModel = props.model.getDefinedNameList(); + setDefinedNamesLocal(definedNamesModel); + } + }, [props.open]); + + const handleSave = () => { + try { + console.log("SAVE", definedName); + + props.model.newDefinedName( + definedName.name, + definedName.scope, + definedName.formula, + ); + } catch (error) { + console.log("DefinedName save failed", error); + } + props.onClose(); + }; + + const handleChange = ( + field: keyof DefinedName, + value: string | number | undefined, + ) => { + console.log("CHANGE", field, value); + + setDefinedName((prev: DefinedName) => ({ + ...prev, + [field]: value, + })); + }; + + const handleDelete = (name: string, scope: number | undefined) => { + try { + props.model.deleteDefinedName(name, scope); + } catch (error) { + console.log("DefinedName delete failed", error); + } + // re-render modal + setDefinedNamesLocal(props.model.getDefinedNameList()); + }; + + const handleUpdate = ( + name: string, + scope: number | undefined, + newName: string, + newScope: number | undefined, + newFormula: string, + ) => { + try { + // what about partial update? + props.model.updateDefinedName(name, scope, newName, newScope, newFormula); + } catch (error) { + console.log("DefinedName update failed", error); + } + // re-render modal + setDefinedNamesLocal(props.model.getDefinedNameList()); + }; + + const formatFormula = (): string => { + const worksheets = props.model.getWorksheetsProperties(); + const selectedView = props.model.getSelectedView(); + + return getFullRangeToString(selectedView, worksheets); + }; + + return ( + + + {t("name_manager_dialog.title")} + props.onClose()}> + + + + + + {t("name_manager_dialog.name")} + {t("name_manager_dialog.range")} + {t("name_manager_dialog.scope")} + + {definedNamesLocal?.map((definedName) => ( + + ))} + + + + + + + {t("name_manager_dialog.help")} + + + + {/* change hover color? */} + + + + + + ); +} + +const StyledDialog = styled(Dialog)(() => ({ + "& .MuiPaper-root": { + height: "380px", + minWidth: "620px", + }, +})); + +const StyledDialogTitle = styled(DialogTitle)` +padding: 12px 20px; +font-size: 14px; +font-weight: 600; +display: flex; +align-items: center; +justify-content: space-between; +`; + +const StyledDialogContent = styled(DialogContent)` +display: flex; +flex-direction: column; +gap: 12px; +padding: 20px 12px 20px 20px; +`; + +const StyledRangesHeader = styled(Stack)(({ theme }) => ({ + flexDirection: "row", + gap: "12px", + fontFamily: theme.typography.fontFamily, + fontSize: "12px", + fontWeight: "700", + color: theme.palette.info.main, +})); + +const StyledDialogActions = styled(DialogActions)` +padding: 12px 20px; +height: 40px; +display: flex; +align-items: center; +justify-content: space-between; +font-size: 12px; +color: #757575; +`; + +export default NameManagerDialog; diff --git a/webapp/src/components/NamedRange.tsx b/webapp/src/components/NamedRange.tsx new file mode 100644 index 0000000..9b3bc62 --- /dev/null +++ b/webapp/src/components/NamedRange.tsx @@ -0,0 +1,158 @@ +import type { DefinedName, Model, WorksheetProperties } from "@ironcalc/wasm"; +import { + Box, + Divider, + IconButton, + MenuItem, + TextField, + styled, +} from "@mui/material"; +import { t } from "i18next"; +import { Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; + +type NamedRangeProperties = { + model: Model; + worksheets: WorksheetProperties[]; + name?: string; + scope?: number; + formula: string; + canEdit: boolean; + canDelete: boolean; + onChange: ( + field: keyof DefinedName, + value: string | number | undefined, + ) => void; + onDelete?: (name: string, scope: number | undefined) => void; + onUpdate?: ( + name: string, + scope: number | undefined, + newName: string, + newScope: number | undefined, + newFormula: string, + ) => void; +}; + +function NamedRange(props: NamedRangeProperties) { + const [name, setName] = useState(props.name || ""); + const [scope, setScope] = useState(props.scope); + const [formula, setFormula] = useState(props.formula); + const [nameError, setNameError] = useState(false); + const [formulaError, setFormulaError] = useState(false); + + const handleChange = ( + field: keyof DefinedName, + value: string | number | undefined, + ) => { + if (field === "name") { + setName(value as string); + props.onChange("name", value); + } + if (field === "scope") { + setScope(value as number | undefined); + props.onChange("scope", value); + } + if (field === "formula") { + setFormula(value as string); + props.onChange("formula", value); + } + }; + + useEffect(() => { + // send initial formula value to parent + handleChange("formula", formula); + }, []); + + const handleDelete = () => { + props.onDelete?.(name, scope); + }; + + return ( + <> + + handleChange("name", event.target.value)} + onKeyDown={(event) => { + event.stopPropagation(); + }} + onClick={(event) => event.stopPropagation()} + /> + + handleChange( + "scope", + event.target.value === "global" ? undefined : event.target.value, + ) + } + > + + {t("name_manager_dialog.workbook")} + + {props.worksheets.map((option, index) => ( + + {option.name} + + ))} + + handleChange("formula", event.target.value)} + onKeyDown={(event) => { + event.stopPropagation(); + }} + onClick={(event) => event.stopPropagation()} + /> + + + + + + + ); +} + +const StyledBox = styled(Box)` +display: flex; +gap: 12px; +width: 577px; +`; + +const StyledTextField = styled(TextField)(() => ({ + "& .MuiInputBase-root": { + height: "28px", + margin: 0, + }, +})); + +const StyledIconButton = styled(IconButton)(({ theme }) => ({ + color: theme.palette.error.main, + "&.Mui-disabled": { + opacity: 0.6, + color: theme.palette.error.light, + }, +})); + +export default NamedRange; diff --git a/webapp/src/components/toolbar.tsx b/webapp/src/components/toolbar.tsx index 3cc94b3..201f055 100644 --- a/webapp/src/components/toolbar.tsx +++ b/webapp/src/components/toolbar.tsx @@ -1,6 +1,7 @@ import type { BorderOptions, HorizontalAlignment, + Model, VerticalAlignment, } from "@ironcalc/wasm"; import { styled } from "@mui/material/styles"; @@ -22,6 +23,7 @@ import { Percent, Redo2, Strikethrough, + Tags, Type, Underline, Undo2, @@ -34,6 +36,7 @@ import { DecimalPlacesIncreaseIcon, } from "../icons"; import { theme } from "../theme"; +import NameManagerDialog from "./NameManagerDialog"; import BorderPicker from "./borderPicker"; import ColorPicker from "./colorPicker"; import { TOOLBAR_HEIGHT } from "./constants"; @@ -60,6 +63,7 @@ type ToolbarProperties = { onFillColorPicked: (hex: string) => void; onNumberFormatPicked: (numberFmt: string) => void; onBorderChanged: (border: BorderOptions) => void; + onNamedRangesUpdate: () => void; fillColor: string; fontColor: string; bold: boolean; @@ -72,12 +76,14 @@ type ToolbarProperties = { numFmt: string; showGridLines: boolean; onToggleShowGridLines: (show: boolean) => void; + model: Model; }; function Toolbar(properties: ToolbarProperties) { const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false); const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false); const [borderPickerOpen, setBorderPickerOpen] = useState(false); + const [nameManagerDialogOpen, setNameManagerDialogOpen] = useState(false); const fontColorButton = useRef(null); const fillColorButton = useRef(null); @@ -340,6 +346,18 @@ function Toolbar(properties: ToolbarProperties) { > {properties.showGridLines ? : } + + { + setNameManagerDialogOpen(true); + }} + disabled={!canEdit} + title={t("toolbar.name_manager")} + > + + + { + setNameManagerDialogOpen(false); + }} + model={properties.model} + /> ); } diff --git a/webapp/src/components/util.ts b/webapp/src/components/util.ts index c8e5309..e826081 100644 --- a/webapp/src/components/util.ts +++ b/webapp/src/components/util.ts @@ -1,6 +1,10 @@ import type { Area, Cell } from "./types"; -import { columnNameFromNumber } from "@ironcalc/wasm"; +import { + type SelectedView, + type WorksheetProperties, + columnNameFromNumber, +} from "@ironcalc/wasm"; /** * Returns true if the keypress should start editing @@ -57,7 +61,24 @@ export function rangeToStr( if (rowStart === rowEnd && columnStart === columnEnd) { return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; } - return `${sheetName}${columnNameFromNumber( - columnStart, - )}${rowStart}:${columnNameFromNumber(columnEnd)}${rowEnd}`; + return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber( + columnEnd, + )}${rowEnd}`; +} + +export function getFullRangeToString( + selectedView: SelectedView, + worksheets: WorksheetProperties[], // solo pasar names +): string { + // order of values is confusing compared to rangeToStr range type, needs refactoring for consistency + const [rowStart, columnStart, rowEnd, columnEnd] = selectedView.range; + const sheetNames = worksheets.map((s) => s.name); + const sheetName = `${sheetNames[selectedView.sheet]}!`; + + if (rowStart === rowEnd && columnStart === columnEnd) { + return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; + } + return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber( + columnEnd, + )}${rowEnd}`; } diff --git a/webapp/src/components/workbook.tsx b/webapp/src/components/workbook.tsx index 56aab6a..484e927 100644 --- a/webapp/src/components/workbook.tsx +++ b/webapp/src/components/workbook.tsx @@ -569,6 +569,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { model.setShowGridLines(sheet, show); setRedrawId((id) => id + 1); }} + model={model} />