UPDATE: Webapp

This commit is contained in:
Nicolás Hatcher
2024-10-03 00:27:29 +02:00
parent ac0567e897
commit c99aea7b3d
18 changed files with 856 additions and 16 deletions

View File

@@ -205,6 +205,16 @@ impl UserModel {
self.model.to_bytes() self.model.to_bytes()
} }
/// Returns the workbook name
pub fn get_name(&self) -> String {
self.model.workbook.name.clone()
}
/// Sets the name of a workbook
pub fn set_name(&mut self, name: &str) {
self.model.workbook.name = name.to_string();
}
/// Undoes last change if any, places the change in the redo list and evaluates the model if needed /// Undoes last change if any, places the change in the redo list and evaluates the model if needed
/// ///
/// See also: /// See also:

View File

@@ -37,8 +37,8 @@ pub struct Model {
#[wasm_bindgen] #[wasm_bindgen]
impl Model { impl Model {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(locale: &str, timezone: &str) -> Result<Model, JsError> { pub fn new(name: &str, locale: &str, timezone: &str) -> Result<Model, JsError> {
let model = BaseModel::new_empty("workbook", locale, timezone).map_err(to_js_error)?; let model = BaseModel::new_empty(name, locale, timezone).map_err(to_js_error)?;
Ok(Model { model }) Ok(Model { model })
} }
@@ -482,4 +482,19 @@ impl Model {
.map_err(|e| to_js_error(e.to_string()))?; .map_err(|e| to_js_error(e.to_string()))?;
Ok(()) Ok(())
} }
#[wasm_bindgen(js_name = "toBytes")]
pub fn to_bytes(&self) -> Vec<u8> {
self.model.to_bytes()
}
#[wasm_bindgen(js_name = "getName")]
pub fn get_name(&self) -> String {
self.model.get_name()
}
#[wasm_bindgen(js_name = "setName")]
pub fn set_name(&mut self, name: &str) {
self.model.set_name(name);
}
} }

View File

@@ -1,7 +1,6 @@
#root { #root {
position: absolute; position: absolute;
inset: 0px; inset: 0px;
margin: 10px; margin: 0px;
border: 1px solid #aaa; border: none;
border-radius: 4px;
} }

View File

@@ -4,6 +4,14 @@ import "./i18n";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import init, { Model } from "@ironcalc/wasm"; import init, { Model } from "@ironcalc/wasm";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileBar } from "./AppComponents/FileBar";
import {
createNewModel,
loadModelFromStorageOrCreate,
saveModelToStorage,
saveSelectedModelInStorage,
selectModelFromStorage,
} from "./AppComponents/storage";
import { WorkbookState } from "./components/workbookState"; import { WorkbookState } from "./components/workbookState";
function App() { function App() {
@@ -17,20 +25,25 @@ function App() {
await init(); await init();
const queryString = window.location.search; const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString); const urlParams = new URLSearchParams(queryString);
const modelName = urlParams.get("model"); const modelHash = urlParams.get("model");
// If there is a model name ?model=example.ic we try to load it // If there is a model name ?model=modelHash we try to load it
// if there is not, or the loading failed we load an empty model // if there is not, or the loading failed we load an empty model
if (modelName) { if (modelHash) {
// Get a remote model
try { try {
const model_bytes = new Uint8Array( const model_bytes = new Uint8Array(
await (await fetch(`./${modelName}`)).arrayBuffer(), await (await fetch(`/api/model/${modelHash}`)).arrayBuffer(),
); );
setModel(Model.from_bytes(model_bytes)); const importedModel = Model.from_bytes(model_bytes);
localStorage.removeItem("selected");
setModel(importedModel);
} catch (e) { } catch (e) {
setModel(new Model("en", "UTC")); alert("Model not found, or failed to load");
} }
} else { } else {
setModel(new Model("en", "UTC")); // try to load from local storage
const newModel = loadModelFromStorageOrCreate();
setModel(newModel);
} }
setWorkbookState(new WorkbookState()); setWorkbookState(new WorkbookState());
} }
@@ -41,12 +54,53 @@ function App() {
return <Loading>Loading</Loading>; return <Loading>Loading</Loading>;
} }
// We try to save the model every second
setInterval(() => {
const queue = model.flushSendQueue();
if (queue.length !== 1) {
saveSelectedModelInStorage(model);
}
}, 1000);
// We could use context for model, but the problem is that it should initialized to null. // We could use context for model, but the problem is that it should initialized to null.
// Passing the property down makes sure it is always defined. // Passing the property down makes sure it is always defined.
return <Workbook model={model} workbookState={workbookState} />; return (
<Wrapper>
<FileBar
model={model}
onModelUpload={async (blob) => {
const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel);
setModel(newModel);
}}
newModel={() => {
setModel(createNewModel());
}}
setModel={(uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
}}
/>
<Workbook model={model} workbookState={workbookState} />
</Wrapper>
);
} }
const Wrapper = styled("div")`
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: absolute;
`;
const Loading = styled("div")` const Loading = styled("div")`
height: 100%; height: 100%;
display: flex; display: flex;

View File

@@ -0,0 +1,108 @@
import styled from "@emotion/styled";
import type { Model } from "@ironcalc/wasm";
import { IronCalcLogo } from "./../icons";
import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton";
import { WorkbookTitle } from "./WorkbookTitle";
import { updateNameSelectedWorkbook } from "./storage";
export function FileBar(properties: {
model: Model;
newModel: () => void;
setModel: (key: string) => void;
onModelUpload: (blob: Blob) => void;
}) {
return (
<FileBarWrapper>
<IronCalcLogo style={{ width: "120px", marginLeft: "10px" }} />
<Divider />
<FileMenu
newModel={properties.newModel}
setModel={properties.setModel}
onModelUpload={properties.onModelUpload}
onDownload={() => {
const model = properties.model;
const arrayBuffer = model.toBytes();
const fileName = model.getName();
fetch("/api/download", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
body: arrayBuffer,
})
.then((response) => {
if (!response.ok) throw new Error("Network response was not ok");
return response.blob();
})
.then((blob) => {
// Create a link element and trigger a download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// Use the same filename or change as needed
a.download = `${fileName}.xlsx`;
document.body.appendChild(a);
a.click();
// Clean up
window.URL.revokeObjectURL(url);
a.remove();
})
.catch((error) => {
console.error("Error:", error);
});
}}
/>
<WorkbookTitle
name={properties.model.getName()}
onNameChange={(name) => {
properties.model.setName(name);
updateNameSelectedWorkbook(properties.model, name);
}}
/>
<ShareButton
onClick={() => {
const model = properties.model;
const arrayBuffer = model.toBytes();
const fileName = model.getName();
fetch("/api/share", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
body: arrayBuffer,
})
.then((response) => {
if (!response.ok) throw new Error("Network response was not ok");
return response.text();
})
.then((hash) => {
console.log(hash);
});
}}
/>
</FileBarWrapper>
);
}
const Divider = styled("div")`
margin: 10px;
height: 12px;
border-left: 1px solid #e0e0e0;
`;
const FileBarWrapper = styled("div")`
height: 60px;
width: 100%;
background: "#FFF";
display: flex;
line-height: 60px;
align-items: center;
border-bottom: 1px solid grey;
position: relative;
`;

View File

@@ -0,0 +1,120 @@
import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material";
import { useRef, useState } from "react";
import { UploadFileDialog } from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuuid } from "./storage";
export function FileMenu(props: {
newModel: () => void;
setModel: (key: string) => void;
onDownload: () => void;
onModelUpload: (blob: Blob) => void;
}) {
const [isMenuOpen, setMenuOpen] = useState(false);
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
const anchorElement = useRef<HTMLDivElement>(null);
const models = getModelsMetadata();
const uuids = Object.keys(models);
const selectedUuid = getSelectedUuuid();
const elements = [];
for (const uuid of uuids) {
elements.push(
<MenuItemWrapper
key={uuid}
onClick={() => {
props.setModel(uuid);
setMenuOpen(false);
}}
style={{ justifyContent: "flex-start" }}
>
<span style={{ width: "20px" }}>
{uuid === selectedUuid ? "•" : ""}
</span>
<MenuItemText>{models[uuid]}</MenuItemText>
</MenuItemWrapper>,
);
}
return (
<>
<FileMenuWrapper
onClick={(): void => setMenuOpen(true)}
ref={anchorElement}
>
File
</FileMenuWrapper>
<Menu
open={isMenuOpen}
onClose={(): void => setMenuOpen(false)}
anchorEl={anchorElement.current}
// anchorOrigin={properties.anchorOrigin}
>
<MenuItemWrapper onClick={props.newModel}>
<MenuItemText>New</MenuItemText>
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
setImportMenuOpen(true);
setMenuOpen(false);
}}
>
<MenuItemText>Import</MenuItemText>
</MenuItemWrapper>
<MenuItemWrapper>
<MenuItemText onClick={props.onDownload}>
Download (.xlsx)
</MenuItemText>
</MenuItemWrapper>
<MenuDivider />
{elements}
</Menu>
<Modal
open={isImportMenuOpen}
onClose={() => {
setImportMenuOpen(false);
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<>
<UploadFileDialog
onClose={() => setImportMenuOpen(false)}
onModelUpload={props.onModelUpload}
/>
</>
</Modal>
</>
);
}
const MenuDivider = styled("div")`
width: 80%;
margin: auto;
border-top: 1px solid #e0e0e0;
`;
const MenuItemText = styled("div")`
color: #000;
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
justify-content: space-between;
font-size: 14px;
width: 100%;
`;
const FileMenuWrapper = styled("div")`
display: flex;
align-items: center;
font-size: 12px;
font-family: Inter;
padding: 10px;
height: 20px;
border-radius: 4px;
margin: 10px;
&:hover {
background-color: #f2f2f2;
}
`;

View File

@@ -0,0 +1,28 @@
import { Share2 } from "lucide-react";
export function ShareButton(properties: { onClick: () => void }) {
const { onClick } = properties;
return (
<div
onClick={onClick}
onKeyDown={() => {}}
style={{
position: "absolute",
right: "0px",
cursor: "pointer",
color: "#FFFFFF",
background: "#F2994A",
padding: "0px 10px",
height: "36px",
lineHeight: "36px",
borderRadius: "4px",
marginRight: "10px",
display: "flex",
alignItems: "center",
}}
>
<Share2 style={{ width: "16px", height: "16px", marginRight: "10px" }} />
<span>Share</span>
</div>
);
}

View File

@@ -0,0 +1,263 @@
import styled from "@emotion/styled";
import { BookOpen, FileUp } from "lucide-react";
import { type DragEvent, useRef, useState } from "react";
export function UploadFileDialog(properties: {
onClose: () => void;
onModelUpload: (blob: Blob) => void;
}) {
const [hover, setHover] = useState(false);
const [message, setMessage] = useState("Drop file here");
const fileInputRef = useRef<HTMLInputElement>(null);
const { onClose, onModelUpload } = properties;
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
console.log("Enter");
setHover(true);
};
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "copy";
// setHover(true);
};
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
console.log("Leave");
setHover(false);
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
console.log("Dropping");
const dt = event.dataTransfer;
const items = dt.items;
if (items) {
// Use DataTransferItemList to access the file(s)
for (let i = 0; i < items.length; i++) {
// If dropped items aren't files, skip them
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) {
handleFileUpload(file);
}
}
}
} else {
const files = dt.files;
for (let i = 0; i < files.length; i++) {
handleFileUpload(files[i]);
}
}
};
const handleFileUpload = (file: File) => {
setMessage(`Uploading ${file.name}...`);
// Read the file as ArrayBuffer
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
// Fetch request to upload the file
fetch("/api/upload", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${file.name}"`,
},
body: arrayBuffer,
})
.then((response) => {
if (!response.ok) throw new Error("Network response was not ok");
return response.blob();
})
.then((blob) => {
setMessage(`File ${file.name} uploaded successfully!`);
onModelUpload(blob);
})
.catch((error) => {
setMessage(`Error uploading file: ${error.message}`);
});
};
reader.readAsArrayBuffer(file);
};
return (
<UploadDialog>
<UploadTitle>
<span style={{ flexGrow: 2, marginLeft: 12 }}>Import a .xlsx File</span>
<Cross
style={{ marginRight: 12 }}
onClick={onClose}
onKeyDown={() => {}}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross>
</UploadTitle>
<DropZone
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
// onDragLeave={handleDragLeave}
onDragExit={handleDragLeave}
onDrop={handleDrop}
>
{!hover ? (
<>
<div style={{ flexGrow: 2 }} />
<div>
<FileUp
style={{
width: 16,
color: "#EFAA6D",
backgroundColor: "#F2994A1A",
padding: "2px 4px",
borderRadius: 4,
}}
/>
</div>
<div style={{ fontSize: 12 }}>
<span style={{ color: "#333333" }}>
Drag and drop a file here or{" "}
</span>
<input
ref={fileInputRef}
type="file"
multiple
accept="*"
style={{ display: "none" }}
onChange={(event) => {
const files = event.target.files;
if (files) {
for (const file of files) {
handleFileUpload(file);
}
}
}}
/>
<DocLink
onClick={() => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
}}
>
click to browse
</DocLink>
</div>
<div style={{ flexGrow: 2 }} />
</>
) : (
<>
<div style={{ flexGrow: 2 }} />
<div>Drop file here</div>
<div style={{ flexGrow: 2 }} />
</>
)}
</DropZone>
<UploadFooter>
<BookOpen
style={{ width: 16, height: 16, marginLeft: 12, marginRight: 8 }}
/>
<span>Learn more about importing files into IronCalc</span>
</UploadFooter>
</UploadDialog>
);
}
const Cross = styled("div")`
&:hover {
background-color: #f5f5f5;
}
border-radius: 4px;
height: 16px;
width: 16px;
`;
const DocLink = styled("span")`
color: #f2994a;
text-decoration: underline;
`;
const UploadFooter = styled("div")`
height: 40px;
border-top: 1px solid #e0e0e0;
font-size: 12px;
font-weight: 400;
color: #757575;
display: flex;
align-items: center;
`;
const UploadTitle = styled("div")`
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
height: 40px;
font-size: 14px;
font-weight: 500;
`;
const UploadDialog = styled("div")`
display: flex;
flex-direction: column;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 455px;
height: 285px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0px 1px 3px 0px #0000001a;
font-family: Inter;
`;
const DropZone = styled("div")`
flex-grow: 2;
border-radius: 10px;
text-align: center;
margin: 12px;
color: #aaa;
font-family: Arial, sans-serif;
cursor: pointer;
background-color: #faebd7;
border: 1px dashed #f2994a;
background: linear-gradient(
180deg,
rgba(242, 153, 74, 0.08) 0%,
rgba(242, 153, 74, 0) 100%
);
display: flex;
flex-direction: column;
vertical-align: center;
`;

View File

@@ -0,0 +1,100 @@
import styled from "@emotion/styled";
import { type ChangeEvent, useEffect, useRef, useState } from "react";
export function WorkbookTitle(props: {
name: string;
onNameChange: (name: string) => void;
}) {
const [width, setWidth] = useState(0);
const [value, setValue] = useState(props.name);
const mirrorDivRef = useRef<HTMLDivElement>(null);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setValue(event.target.value);
if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth);
}
};
useEffect(() => {
if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth);
}
}, []);
useEffect(() => {
setValue(props.name);
}, [props.name]);
return (
<div
style={{
position: "absolute",
left: "50%",
textAlign: "center",
transform: "translateX(-50%)",
// height: "60px",
// lineHeight: "60px",
padding: "8px",
fontSize: "14px",
fontWeight: "700",
fontFamily: "Inter",
width,
}}
>
<TitleWrapper
value={value}
rows={1}
onChange={handleChange}
onBlur={(event) => {
props.onNameChange(event.target.value);
}}
style={{ width: width }}
spellCheck="false"
>
{value}
</TitleWrapper>
<div
ref={mirrorDivRef}
style={{
position: "absolute",
top: "-9999px",
left: "-9999px",
whiteSpace: "pre-wrap",
textWrap: "nowrap",
visibility: "hidden",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
padding: "inherit",
border: "inherit",
}}
>
{value}
</div>
</div>
);
}
const TitleWrapper = styled("textarea")`
vertical-align: middle;
text-align: center;
height: 20px;
line-height: 20px;
border-radius: 4px;
padding: inherit;
overflow: hidden;
outline: none;
resize: none;
text-wrap: nowrap;
border: none;
&:hover {
background-color: #f2f2f2;
}
&:focus {
border: 1px solid grey;
}
font-weight: inherit;
font-family: inherit;
font-size: inherit;
`;

View File

@@ -0,0 +1,112 @@
import { Model } from "@ironcalc/wasm";
import { base64ToBytes, bytesToBase64 } from "./util";
const MAX_WORKBOOKS = 50;
type ModelsMetadata = Record<string, string>;
export function updateNameSelectedWorkbook(model: Model, newName: string) {
const uuid = localStorage.getItem("selected");
if (uuid) {
const modelsJson = localStorage.getItem("models");
if (modelsJson) {
try {
const models = JSON.parse(modelsJson);
models[uuid] = newName;
localStorage.setItem("models", JSON.stringify(models));
} catch (e) {
console.warn("Failed saving new name");
}
}
const modeBytes = model.toBytes();
localStorage.setItem(uuid, bytesToBase64(modeBytes));
}
}
export function getModelsMetadata(): ModelsMetadata {
let modelsJson = localStorage.getItem("models");
if (!modelsJson) {
modelsJson = "{}";
}
return JSON.parse(modelsJson);
}
// Pick a different name Workbook{N} where N = 1, 2, 3
function getNewName(existingNames: string[]): string {
const baseName = "Workbook";
let index = 1;
while (index < MAX_WORKBOOKS) {
const name = `${baseName}${index}`;
index += 1;
if (!existingNames.includes(name)) {
return name;
}
}
// FIXME: Too many workbooks?
return "Workbook-Infinity";
}
export function createNewModel(): Model {
const models = getModelsMetadata();
const name = getNewName(Object.values(models));
const model = new Model(name, "en", "UTC");
const uuid = crypto.randomUUID();
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
models[uuid] = name;
localStorage.setItem("models", JSON.stringify(models));
return model;
}
export function loadModelFromStorageOrCreate(): Model {
const uuid = localStorage.getItem("selected");
if (uuid) {
// We try to load the selected model
const modelBytesString = localStorage.getItem(uuid);
if (modelBytesString) {
return Model.from_bytes(base64ToBytes(modelBytesString));
}
// If it doesn't exist we create one at that uuid
const newModel = new Model("Workbook1", "en", "UTC");
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(newModel.toBytes()));
return newModel;
}
// If there was no selected model we create a new one
return createNewModel();
}
export function saveSelectedModelInStorage(model: Model) {
const uuid = localStorage.getItem("selected");
if (uuid) {
const modeBytes = model.toBytes();
localStorage.setItem(uuid, bytesToBase64(modeBytes));
}
}
export function saveModelToStorage(model: Model) {
const uuid = crypto.randomUUID();
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
let modelsJson = localStorage.getItem("models");
if (!modelsJson) {
modelsJson = "{}";
}
const models = JSON.parse(modelsJson);
models[uuid] = model.getName();
localStorage.setItem("models", JSON.stringify(models));
}
export function selectModelFromStorage(uuid: string): Model | undefined {
localStorage.setItem("selected", uuid);
const modelBytesString = localStorage.getItem(uuid);
if (modelBytesString) {
return Model.from_bytes(base64ToBytes(modelBytesString));
}
}
export function getSelectedUuuid(): string | null {
return localStorage.getItem("selected");
}

View File

@@ -0,0 +1,18 @@
export function base64ToBytes(base64: string): Uint8Array {
// const binString = atob(base64);
// return Uint8Array.from(binString, (m) => m.codePointAt(0));
return new Uint8Array(
atob(base64)
.split("")
.map((c) => c.charCodeAt(0)),
);
}
export function bytesToBase64(bytes: Uint8Array): string {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte),
).join("");
// btoa(String.fromCharCode(...bytes));
return btoa(binString);
}

View File

@@ -1304,6 +1304,7 @@ export default class WorksheetCanvas {
ctx.setLineDash([]); ctx.setLineDash([]);
} }
renderSheet(): void { renderSheet(): void {
console.time("renderSheet"); console.time("renderSheet");
this._renderSheet(); this._renderSheet();

View File

@@ -7,7 +7,7 @@ import { expect, test } from "vitest";
test("simple calculation", async () => { test("simple calculation", async () => {
const buffer = await readFile("node_modules/@ironcalc/wasm/wasm_bg.wasm"); const buffer = await readFile("node_modules/@ironcalc/wasm/wasm_bg.wasm");
initSync(buffer); initSync(buffer);
const model = new Model("en", "UTC"); const model = new Model("workbook", "en", "UTC");
model.setUserInput(0, 1, 1, "=21*2"); model.setUserInput(0, 1, 1, "=21*2");
expect(model.getFormattedCellValue(0, 1, 1)).toBe("42"); expect(model.getFormattedCellValue(0, 1, 1)).toBe("42");
}); });

View File

@@ -53,7 +53,7 @@ export function rangeToStr(
referenceName: string, referenceName: string,
): string { ): string {
const { sheet, rowStart, rowEnd, columnStart, columnEnd } = range; const { sheet, rowStart, rowEnd, columnStart, columnEnd } = range;
const sheetName = sheet === referenceSheet ? "" : `${referenceName}!`; const sheetName = sheet === referenceSheet ? "" : `'${referenceName}'!`;
if (rowStart === rowEnd && columnStart === columnEnd) { if (rowStart === rowEnd && columnStart === columnEnd) {
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
} }

View File

@@ -426,6 +426,7 @@ const Container = styled("div")`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
position: relative;
font-family: ${({ theme }) => theme.typography.fontFamily}; font-family: ${({ theme }) => theme.typography.fontFamily};
&:focus { &:focus {

View File

@@ -20,6 +20,8 @@ import InsertColumnRightIcon from "./insert-column-right.svg?react";
import InsertRowAboveIcon from "./insert-row-above.svg?react"; import InsertRowAboveIcon from "./insert-row-above.svg?react";
import InsertRowBelow from "./insert-row-below.svg?react"; import InsertRowBelow from "./insert-row-below.svg?react";
import IronCalcLogo from "./orange+black.svg?react";
import Fx from "./fx.svg?react"; import Fx from "./fx.svg?react";
export { export {
@@ -42,5 +44,6 @@ export {
InsertColumnRightIcon, InsertColumnRightIcon,
InsertRowAboveIcon, InsertRowAboveIcon,
InsertRowBelow, InsertRowBelow,
IronCalcLogo,
Fx, Fx,
}; };

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,8 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import "./index.css"; import "./index.css";
import ThemeProvider from "@mui/material/styles/ThemeProvider"; import ThemeProvider from "@mui/material/styles/ThemeProvider";
import React from "react";
import { theme } from "./theme.ts"; import { theme } from "./theme.ts";
// biome-ignore lint: we know the 'root' element exists. // biome-ignore lint: we know the 'root' element exists.