From 517221fac57d477a3c4a5ae3883d352e6ef8d866 Mon Sep 17 00:00:00 2001 From: francisco aloi Date: Mon, 9 Dec 2024 09:41:48 +0100 Subject: [PATCH] new components for name manager dialog and fields new text added to locale en_us added name manager to toolbar added model and worksheets as prop UPDATE: API for defined names NamedRange component changes NameManager dialog component changes new util formatting functions UPDATE: API for defined names new components for name manager dialog and fields new text added to locale en_us added name manager to toolbar added model and worksheets as prop UPDATE: API for defined names NamedRange component changes NameManager dialog component changes new util formatting functions UPDATE: API for defined names last changes corrected styling updates to namedRange and nameManagerDialog --- webapp/src/components/NameManagerDialog.tsx | 430 ++++++++++++++++++++ webapp/src/components/NamedRange.tsx | 168 ++++++++ webapp/src/components/toolbar.tsx | 25 ++ webapp/src/components/util.ts | 29 +- webapp/src/components/workbook.tsx | 5 + webapp/src/locale/en_us.json | 168 ++++---- 6 files changed, 744 insertions(+), 81 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..1e245f3 --- /dev/null +++ b/webapp/src/components/NameManagerDialog.tsx @@ -0,0 +1,430 @@ +import type { DefinedName, Model } from "@ironcalc/wasm"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + styled, + 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; + 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); + console.log("definedNamesModel EFFECT", 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, + ) => { + 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); + } + // should re-render modal + }; + + const handleUpdate = ( + name: string, + scope: number | undefined, + newName: string, + newScope: number | undefined, + newFormula: string, + ) => { + try { + // partially update? + props.model.updateDefinedName(name, scope, newName, newScope, newFormula); + } catch (error) { + console.log("DefinedName update failed", error); + } + }; + + const formatFormula = (): string => { + const worksheets = props.model.getWorksheetsProperties(); + const selectedView = props.model.getSelectedView(); + const formatFormula = (): string => { + const worksheets = props.model.getWorksheetsProperties(); + const selectedView = props.model.getSelectedView(); + + return getFullRangeToString(selectedView, worksheets); + }; + return getFullRangeToString(selectedView, worksheets); + }; + + return ( + + + Named Ranges + 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; +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 { getShortRangeToString } from "./util"; + +type NameManagerDialogProperties = { + onClose: () => void; + open: boolean; + model: Model; +}; + +function NameManagerDialog(props: NameManagerDialogProperties) { + const [definedNamesLocal, setDefinedNamesLocal] = useState(); + const [definedName, setDefinedName] = useState({ + name: "", + scope: 0, + formula: "", + }); + + // render named ranges from model + useEffect(() => { + const definedNamesModel = props.model.getDefinedNameList(); + setDefinedNamesLocal(definedNamesModel); + console.log("definedNamesModel EFFECT", definedNamesModel); + }); + + const handleSave = () => { + console.log("SAVE DIALOG", definedName); + // create newDefinedName and close + // props.model.newDefinedName( + // definedName.name, + // definedName.scope, + // definedName.formula, + // ); + props.onClose(); + }; + + const handleChange = (field: keyof DefinedName, value: string | number) => { + setDefinedName((prev: DefinedName) => ({ + ...prev, + [field]: value, + })); + }; + + const handleDelete = () => { + console.log("definedName marked for deletion"); + }; + + const formatFormula = (): string => { + const selectedView = props.model.getSelectedView(); + + return getShortRangeToString(selectedView); + }; + + return ( + + + Named Ranges + 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", + }, + "& .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, + 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..2455ad2 --- /dev/null +++ b/webapp/src/components/NamedRange.tsx @@ -0,0 +1,168 @@ +import type { DefinedName, Model, WorksheetProperties } from "@ironcalc/wasm"; +import { + Box, + Divider, + IconButton, + MenuItem, + TextField, + styled, + Box, + Divider, + IconButton, + MenuItem, + TextField, + styled, +} from "@mui/material"; +import { Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; + +type NamedRangeProperties = { + name?: string; + scope?: number; + formula: string; + model: Model; + worksheets: WorksheetProperties[]; + onChange: ( + field: keyof DefinedName, + value: string | number | undefined, + ) => void; + onDelete?: (name: string, scope: number | undefined) => void; + canDelete: boolean; + 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 || undefined); + 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 === "Workbook (Global)" + ? undefined + : event.target.value, + ) + } + > + Workbook (Global) + {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, + }, + "& .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, + }, + 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..8979f94 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,8 @@ import { DecimalPlacesIncreaseIcon, } from "../icons"; import { theme } from "../theme"; +import NameManagerDialog from "./NameManagerDialog"; +import NameManagerDialog from "./NameManagerDialog"; import BorderPicker from "./borderPicker"; import ColorPicker from "./colorPicker"; import { TOOLBAR_HEIGHT } from "./constants"; @@ -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..186f15b 100644 --- a/webapp/src/components/workbook.tsx +++ b/webapp/src/components/workbook.tsx @@ -115,6 +115,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { updateRangeStyle("num_fmt", numberFmt); }; + const onNamedRangesUpdate = () => { + // update named ranges in model + }; + const onCopyStyles = () => { const { sheet, @@ -569,6 +573,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { model.setShowGridLines(sheet, show); setRedrawId((id) => id + 1); }} + model={model} />