update: move all functionalities from dialog to drawer

This commit is contained in:
Daniel Gonzalez Albo
2025-11-07 02:06:53 +01:00
committed by Nicolás Hatcher Andrés
parent d8b3ba0dae
commit 4217c1455b
4 changed files with 458 additions and 8 deletions

View File

@@ -0,0 +1,216 @@
import type { WorksheetProperties } from "@ironcalc/wasm";
import { Box, IconButton, MenuItem, TextField, styled } from "@mui/material";
import { t } from "i18next";
import { Check, X } from "lucide-react";
import { useState } from "react";
import type React from "react";
import { theme } from "../../../theme";
interface EditNamedRangeProps {
worksheets: WorksheetProperties[];
name: string;
scope: string;
formula: string;
onSave: (name: string, scope: string, formula: string) => string | undefined;
onCancel: () => void;
}
const EditNamedRange: React.FC<EditNamedRangeProps> = ({
worksheets,
name: initialName,
scope: initialScope,
formula: initialFormula,
onSave,
onCancel,
}) => {
const [name, setName] = useState(initialName);
const [scope, setScope] = useState(initialScope);
const [formula, setFormula] = useState(initialFormula);
const [formulaError, setFormulaError] = useState(false);
return (
<Container>
<ContentArea>
<StyledBox>
<FieldWrapper>
<StyledLabel htmlFor="name">Range name</StyledLabel>
<StyledTextField
id="name"
variant="outlined"
size="small"
margin="none"
fullWidth
error={formulaError}
value={name}
onChange={(event) => setName(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
</FieldWrapper>
<FieldWrapper>
<StyledLabel htmlFor="scope">Scope</StyledLabel>
<StyledTextField
id="scope"
variant="outlined"
select
size="small"
margin="none"
fullWidth
error={formulaError}
value={scope}
onChange={(event) => {
setScope(event.target.value);
}}
>
<MenuItem value={"[global]"}>
<MenuSpan>{t("name_manager_dialog.workbook")}</MenuSpan>
<MenuSpanGrey>{` ${t("name_manager_dialog.global")}`}</MenuSpanGrey>
</MenuItem>
{worksheets.map((option) => (
<MenuItem key={option.name} value={option.name}>
<MenuSpan>{option.name}</MenuSpan>
</MenuItem>
))}
</StyledTextField>
</FieldWrapper>
<FieldWrapper>
<StyledLabel htmlFor="formula">Refers to</StyledLabel>
<StyledTextField
id="formula"
variant="outlined"
size="small"
margin="none"
fullWidth
error={formulaError}
value={formula}
onChange={(event) => setFormula(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
}}
onClick={(event) => event.stopPropagation()}
/>
</FieldWrapper>
</StyledBox>
</ContentArea>
<Footer>
<IconsWrapper>
<StyledIconButton
onClick={() => {
const error = onSave(name, scope, formula);
if (error) {
setFormulaError(true);
}
}}
title={t("name_manager_dialog.apply")}
>
<StyledCheck size={16} />
</StyledIconButton>
<StyledIconButton
onClick={onCancel}
title={t("name_manager_dialog.discard")}
>
<X size={16} />
</StyledIconButton>
</IconsWrapper>
</Footer>
</Container>
);
};
const Container = styled("div")({
height: "100%",
display: "flex",
flexDirection: "column",
});
const ContentArea = styled("div")({
flex: 1,
overflow: "auto",
});
const MenuSpan = styled("span")`
font-size: 12px;
font-family: "Inter";
`;
const MenuSpanGrey = styled("span")`
white-space: pre;
font-size: 12px;
font-family: "Inter";
color: ${theme.palette.grey[400]};
`;
const StyledBox = styled(Box)`
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: auto;
padding: 12px;
@media (max-width: 600px) {
padding: 12px;
}
`;
const StyledTextField = styled(TextField)(() => ({
"& .MuiInputBase-root": {
height: "36px",
width: "100%",
margin: 0,
fontFamily: "Inter",
fontSize: "12px",
},
"& .MuiInputBase-input": {
padding: "8px",
},
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
color: theme.palette.error.main,
borderRadius: "8px",
"&:hover": {
backgroundColor: theme.palette.grey["50"],
},
"&.Mui-disabled": {
opacity: 0.6,
color: theme.palette.error.light,
},
}));
const StyledCheck = styled(Check)(({ theme }) => ({
color: theme.palette.success.main,
}));
const Footer = styled("div")`
padding: 8px;
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 12px;
color: ${theme.palette.grey["600"]};
border-top: 1px solid ${theme.palette.grey["300"]};
`;
const IconsWrapper = styled(Box)({
display: "flex",
});
const FieldWrapper = styled(Box)`
display: flex;
flex-direction: column;
width: 100%;
gap: 4px;
`;
const StyledLabel = styled("label")`
font-size: 12px;
font-family: "Inter";
font-weight: 500;
color: ${theme.palette.text.primary};
display: block;
`;
export default EditNamedRange;

View File

@@ -1,20 +1,154 @@
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
import { Button, Tooltip, styled } from "@mui/material"; import { Button, Tooltip, styled } from "@mui/material";
import { t } from "i18next"; import { t } from "i18next";
import { BookOpen, Plus } from "lucide-react"; import { BookOpen, ChevronRight, Plus } from "lucide-react";
import type React from "react"; import { useState } from "react";
import { theme } from "../../../theme"; import { theme } from "../../../theme";
import EditNamedRange from "./EditNamedRange";
interface NamedRangesProps { interface NamedRangesProps {
title?: string; title?: string;
definedNameList?: DefinedName[];
worksheets?: WorksheetProperties[];
updateDefinedName?: (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => void;
newDefinedName?: (
name: string,
scope: number | undefined,
formula: string,
) => void;
selectedArea?: () => string;
} }
const NamedRanges: React.FC<NamedRangesProps> = ({ const NamedRanges: React.FC<NamedRangesProps> = ({
title = "Named Ranges", definedNameList = [],
worksheets = [],
updateDefinedName,
newDefinedName,
selectedArea,
}) => { }) => {
const [editingDefinedName, setEditingDefinedName] =
useState<DefinedName | null>(null);
const [isCreatingNew, setIsCreatingNew] = useState(false);
const handleListItemClick = (definedName: DefinedName) => {
setEditingDefinedName(definedName);
setIsCreatingNew(false);
};
const handleNewClick = () => {
setIsCreatingNew(true);
setEditingDefinedName(null);
};
const handleCancel = () => {
setEditingDefinedName(null);
setIsCreatingNew(false);
};
const handleSave = (
name: string,
scope: string,
formula: string,
): string | undefined => {
if (isCreatingNew) {
if (!newDefinedName) return undefined;
const scope_index = worksheets.findIndex((s) => s.name === scope);
const newScope = scope_index >= 0 ? scope_index : undefined;
try {
newDefinedName(name, newScope, formula);
setIsCreatingNew(false);
return undefined;
} catch (e) {
return `${e}`;
}
} else {
if (!editingDefinedName || !updateDefinedName) return undefined;
const scope_index = worksheets.findIndex((s) => s.name === scope);
const newScope = scope_index >= 0 ? scope_index : undefined;
try {
updateDefinedName(
editingDefinedName.name,
editingDefinedName.scope,
name,
newScope,
formula,
);
setEditingDefinedName(null);
return undefined;
} catch (e) {
return `${e}`;
}
}
};
// Show edit view if a named range is being edited or created
if (editingDefinedName || isCreatingNew) {
let name = "";
let scopeName = "[global]";
let formula = "";
if (editingDefinedName) {
name = editingDefinedName.name;
scopeName =
editingDefinedName.scope !== undefined
? worksheets[editingDefinedName.scope]?.name || "[unknown]"
: "[global]";
formula = editingDefinedName.formula;
} else if (isCreatingNew && selectedArea) {
formula = selectedArea();
}
return (
<Container>
<Content>
<EditNamedRange
worksheets={worksheets}
name={name}
scope={scopeName}
formula={formula}
onSave={handleSave}
onCancel={handleCancel}
/>
</Content>
</Container>
);
}
// Show list view
return ( return (
<Container> <Container>
<Content> <Content>
<h3>{title}</h3> {definedNameList.length > 0 && (
<ListContainer>
{definedNameList.map((definedName) => {
const scopeName =
definedName.scope !== undefined
? worksheets[definedName.scope]?.name || "[unknown]"
: "[global]";
return (
<ListItem
key={`${definedName.name}-${definedName.scope}`}
onClick={() => handleListItemClick(definedName)}
tabIndex={0}
>
<ListItemText>
<ScopeText>{scopeName}</ScopeText>
<ChevronRightStyled />
<NameText>{definedName.name}</NameText>
</ListItemText>
</ListItem>
);
})}
</ListContainer>
)}
</Content> </Content>
<Footer> <Footer>
<Tooltip <Tooltip
@@ -37,6 +171,7 @@ const NamedRanges: React.FC<NamedRangesProps> = ({
variant="contained" variant="contained"
disableElevation disableElevation
startIcon={<Plus size={16} />} startIcon={<Plus size={16} />}
onClick={handleNewClick}
> >
{t("name_manager_dialog.new")} {t("name_manager_dialog.new")}
</NewButton> </NewButton>
@@ -55,12 +190,54 @@ const Content = styled("div")({
flex: 1, flex: 1,
color: theme.palette.grey[700], color: theme.palette.grey[700],
lineHeight: "1.5", lineHeight: "1.5",
overflow: "auto",
});
"& p": { const ListContainer = styled("div")({
margin: "0 0 12px 0", display: "flex",
flexDirection: "column",
});
const ListItem = styled("div")({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 12px",
minHeight: "40px",
cursor: "pointer",
boxSizing: "border-box",
borderBottom: `1px solid ${theme.palette.grey[200]}`,
"&:hover": {
backgroundColor: theme.palette.grey[50],
}, },
}); });
const ListItemText = styled("div")({
fontSize: "12px",
color: theme.palette.common.black,
fontFamily: theme.typography.fontFamily,
flex: 1,
display: "flex",
alignItems: "center",
gap: "4px",
});
const ScopeText = styled("span")({
fontSize: "12px",
color: theme.palette.common.black,
});
const ChevronRightStyled = styled(ChevronRight)({
width: "12px",
height: "12px",
color: theme.palette.grey[500],
});
const NameText = styled("span")({
fontSize: "12px",
color: theme.palette.common.black,
});
const Footer = styled("div")` const Footer = styled("div")`
padding: 8px; padding: 8px;
display: flex; display: flex;
@@ -93,6 +270,7 @@ const HelpLink = styled("a")`
const NewButton = styled(Button)` const NewButton = styled(Button)`
text-transform: none; text-transform: none;
min-width: fit-content; min-width: fit-content;
font-size: 12px;
`; `;
export default NamedRanges; export default NamedRanges;

View File

@@ -1,3 +1,4 @@
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
import Breadcrumbs from "@mui/material/Breadcrumbs"; import Breadcrumbs from "@mui/material/Breadcrumbs";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
@@ -19,6 +20,21 @@ interface RightDrawerProps {
showCloseButton?: boolean; showCloseButton?: boolean;
backgroundColor?: string; backgroundColor?: string;
title?: string; title?: string;
definedNameList?: DefinedName[];
worksheets?: WorksheetProperties[];
updateDefinedName?: (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => void;
newDefinedName?: (
name: string,
scope: number | undefined,
formula: string,
) => void;
selectedArea?: () => string;
} }
const RightDrawer = ({ const RightDrawer = ({
@@ -28,6 +44,11 @@ const RightDrawer = ({
children, children,
showCloseButton = true, showCloseButton = true,
title = "Named Ranges", title = "Named Ranges",
definedNameList,
worksheets,
updateDefinedName,
newDefinedName,
selectedArea,
}: RightDrawerProps) => { }: RightDrawerProps) => {
if (!isOpen) return null; if (!isOpen) return null;
@@ -73,7 +94,14 @@ const RightDrawer = ({
{children} {children}
<Divider /> <Divider />
<DrawerContent> <DrawerContent>
<NamedRanges title={title} /> <NamedRanges
title={title}
definedNameList={definedNameList}
worksheets={worksheets}
updateDefinedName={updateDefinedName}
newDefinedName={newDefinedName}
selectedArea={selectedArea}
/>
</DrawerContent> </DrawerContent>
</DrawerContainer> </DrawerContainer>
); );

View File

@@ -768,7 +768,35 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}} }}
/> />
</WorksheetAreaLeft> </WorksheetAreaLeft>
<RightDrawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)} /> <RightDrawer
isOpen={isDrawerOpen}
onClose={() => setDrawerOpen(false)}
definedNameList={model.getDefinedNameList()}
worksheets={worksheets}
updateDefinedName={(
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => {
model.updateDefinedName(name, scope, newName, newScope, newFormula);
setRedrawId((id) => id + 1);
}}
newDefinedName={(
name: string,
scope: number | undefined,
formula: string,
) => {
model.newDefinedName(name, scope, formula);
setRedrawId((id) => id + 1);
}}
selectedArea={() => {
const worksheetNames = worksheets.map((s) => s.name);
const selectedView = model.getSelectedView();
return getFullRangeToString(selectedView, worksheetNames);
}}
/>
</Container> </Container>
); );
}; };