diff --git a/webapp/src/components/NameManager/NameManagerDialog.tsx b/webapp/src/components/NameManager/NameManagerDialog.tsx new file mode 100644 index 0000000..265fd15 --- /dev/null +++ b/webapp/src/components/NameManager/NameManagerDialog.tsx @@ -0,0 +1,171 @@ +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, Plus, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { getFullRangeToString } from "../util"; +import NamedRange from "./NamedRange"; + +interface NameManagerDialogProperties { + onClose: () => void; + open: boolean; + model: Model; +} + +function NameManagerDialog({ + onClose, + open, + model, +}: NameManagerDialogProperties) { + const [definedNamesLocal, setDefinedNamesLocal] = useState(); + const [showNewName, setShowNewName] = useState(false); + const [showOptions, setShowOptions] = useState(true); + + useEffect(() => { + // render definedNames from model + if (open) { + const definedNamesModel = model.getDefinedNameList(); + setDefinedNamesLocal(definedNamesModel); + } + setShowNewName(false); + setShowOptions(true); + }, [open, model]); + + const handleNewName = () => { + setShowNewName(true); + setShowOptions(false); + }; + + const handleDelete = () => { + // re-render modal + setDefinedNamesLocal(model.getDefinedNameList()); + }; + + const formatFormula = (): string => { + const worksheets = model.getWorksheetsProperties(); + const selectedView = model.getSelectedView(); + + return getFullRangeToString(selectedView, worksheets); + }; + + const toggleOptions = () => { + setShowOptions(!showOptions); + }; + + const toggleShowNewName = () => { + setShowNewName(false); + }; + + return ( + + + {t("name_manager_dialog.title")} + onClose()}> + + + + + + {t("name_manager_dialog.name")} + {t("name_manager_dialog.range")} + {t("name_manager_dialog.scope")} + + {definedNamesLocal?.map((definedName) => ( + + ))} + {showNewName && ( + + )} + + + + + + {t("name_manager_dialog.help")} + + + + + + ); +} + +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/NameManager/NamedRange.tsx b/webapp/src/components/NameManager/NamedRange.tsx new file mode 100644 index 0000000..76b591b --- /dev/null +++ b/webapp/src/components/NameManager/NamedRange.tsx @@ -0,0 +1,217 @@ +import type { Model, WorksheetProperties } from "@ironcalc/wasm"; +import { + Box, + Divider, + IconButton, + MenuItem, + TextField, + styled, +} from "@mui/material"; +import { t } from "i18next"; +import { Check, PencilLine, Trash2, X } from "lucide-react"; +import { useEffect, useState } from "react"; + +interface NamedRangeProperties { + model: Model; + worksheets: WorksheetProperties[]; + name?: string; + scope?: number; + formula: string; + onDelete?: () => void; + toggleShowNewName?: () => void; + toggleOptions: () => void; + showOptions?: boolean; +} + +function NamedRange({ + model, + worksheets, + name, + scope, + formula, + onDelete, + toggleShowNewName, + toggleOptions, + showOptions, +}: NamedRangeProperties) { + const [newName, setNewName] = useState(name || ""); + const [newScope, setNewScope] = useState(scope); + const [newFormula, setNewFormula] = useState(formula); + const [readOnly, setReadOnly] = useState(true); + const [showEditDelete, setShowEditDelete] = useState(false); + + // todo: add error messages for validations + const [nameError, setNameError] = useState(false); + const [formulaError, setFormulaError] = useState(false); + + useEffect(() => { + // set state for new name + const definedNamesModel = model.getDefinedNameList(); + if (!definedNamesModel.find((n) => n.name === newName)) { + setReadOnly(false); + setShowEditDelete(true); + } + }, [newName, model]); + + const handleSaveUpdate = () => { + const definedNamesModel = model.getDefinedNameList(); + + if (definedNamesModel.find((n) => n.name === name)) { + // update name + try { + model.updateDefinedName( + name || "", + scope, + newName, + newScope, + newFormula, + ); + } catch (error) { + console.log("DefinedName update failed", error); + } + } else { + // create name + try { + model.newDefinedName(newName, newScope, newFormula); + } catch (error) { + console.log("DefinedName save failed", error); + } + setReadOnly(true); + } + setShowEditDelete(false); + toggleOptions(); + }; + + const handleCancel = () => { + setReadOnly(true); + setShowEditDelete(false); + toggleOptions(); + setNewName(name || ""); + setNewScope(scope); + + // if it's newName remove it from modal + toggleShowNewName?.(); + }; + + const handleEdit = () => { + setReadOnly(false); + setShowEditDelete(true); + toggleOptions(); + }; + + const handleDelete = () => { + try { + model.deleteDefinedName(newName, newScope); + } catch (error) { + console.log("DefinedName delete failed", error); + } + onDelete?.(); // refresh modal + }; + + return ( + <> + + setNewName(event.target.value)} + onKeyDown={(event) => { + event.stopPropagation(); + }} + onClick={(event) => event.stopPropagation()} + /> + { + event.target.value === "global" + ? setNewScope(undefined) + : setNewScope(+event.target.value); + }} + > + + {t("name_manager_dialog.workbook")} + + {worksheets.map((option, index) => ( + + {option.name} + + ))} + + setNewFormula(event.target.value)} + onKeyDown={(event) => { + event.stopPropagation(); + }} + onClick={(event) => event.stopPropagation()} + /> + + {showEditDelete ? ( + // save cancel + <> + + + + + + + + ) : ( + // edit delete + <> + + + + + + + + )} + + + + ); +} + +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/NameManagerDialog.tsx b/webapp/src/components/NameManagerDialog.tsx deleted file mode 100644 index 08e524b..0000000 --- a/webapp/src/components/NameManagerDialog.tsx +++ /dev/null @@ -1,224 +0,0 @@ -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 deleted file mode 100644 index 9b3bc62..0000000 --- a/webapp/src/components/NamedRange.tsx +++ /dev/null @@ -1,158 +0,0 @@ -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 201f055..30d265c 100644 --- a/webapp/src/components/toolbar.tsx +++ b/webapp/src/components/toolbar.tsx @@ -36,7 +36,7 @@ import { DecimalPlacesIncreaseIcon, } from "../icons"; import { theme } from "../theme"; -import NameManagerDialog from "./NameManagerDialog"; +import NameManagerDialog from "./NameManager/NameManagerDialog"; import BorderPicker from "./borderPicker"; import ColorPicker from "./colorPicker"; import { TOOLBAR_HEIGHT } from "./constants"; diff --git a/webapp/src/locale/en_us.json b/webapp/src/locale/en_us.json index 25f8519..be49c37 100644 --- a/webapp/src/locale/en_us.json +++ b/webapp/src/locale/en_us.json @@ -81,8 +81,7 @@ "range": "Scope", "scope": "Range", "help": "Learn more about Named Ranges", - "save": "Save", - "cancel": "Cancel", + "new": "Add new", "workbook": "Workbook (Global)" } }