269 lines
8.0 KiB
TypeScript
269 lines
8.0 KiB
TypeScript
import "./App.css";
|
|
import styled from "@emotion/styled";
|
|
// From IronCalc
|
|
import { IronCalc, IronCalcIcon, init, Model } from "@ironcalc/workbook";
|
|
import { Modal } from "@mui/material";
|
|
import { useEffect, useState } from "react";
|
|
import { FileBar } from "./components/FileBar";
|
|
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
|
|
import {
|
|
get_documentation_model,
|
|
get_model,
|
|
uploadFile,
|
|
} from "./components/rpc";
|
|
import {
|
|
createModelWithSafeTimezone,
|
|
createNewModel,
|
|
deleteModelByUuid,
|
|
deleteSelectedModel,
|
|
isStorageEmpty,
|
|
loadSelectedModelFromStorage,
|
|
saveModelToStorage,
|
|
saveSelectedModelInStorage,
|
|
selectModelFromStorage,
|
|
} from "./components/storage";
|
|
import TemplatesDialog from "./components/WelcomeDialog/TemplatesDialog";
|
|
import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog";
|
|
|
|
function App() {
|
|
const [model, setModel] = useState<Model | null>(null);
|
|
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
|
|
const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false);
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
const [localStorageId, setLocalStorageId] = useState<number>(1);
|
|
|
|
useEffect(() => {
|
|
async function start() {
|
|
await init();
|
|
const queryString = window.location.search;
|
|
const urlParams = new URLSearchParams(queryString);
|
|
const modelHash = urlParams.get("model");
|
|
const exampleFilename = urlParams.get("example");
|
|
// 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 (modelHash) {
|
|
// Get a remote model
|
|
try {
|
|
const model_bytes = await get_model(modelHash);
|
|
const importedModel = Model.from_bytes(model_bytes);
|
|
localStorage.removeItem("selected");
|
|
setModel(importedModel);
|
|
} catch (e) {
|
|
alert("Model not found, or failed to load");
|
|
}
|
|
} else if (exampleFilename) {
|
|
try {
|
|
const model_bytes = await get_documentation_model(exampleFilename);
|
|
const importedModel = Model.from_bytes(model_bytes);
|
|
localStorage.removeItem("selected");
|
|
setModel(importedModel);
|
|
} catch (e) {
|
|
alert("Example file not found, or failed to load");
|
|
}
|
|
} else {
|
|
// try to load from local storage
|
|
const newModel = loadSelectedModelFromStorage();
|
|
if (!newModel) {
|
|
setShowWelcomeDialog(true);
|
|
const createdModel = createModelWithSafeTimezone("template");
|
|
setModel(createdModel);
|
|
} else {
|
|
setModel(newModel);
|
|
}
|
|
}
|
|
}
|
|
start();
|
|
}, []);
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: localStorageId needed to detect name changes (model mutates internally)
|
|
useEffect(() => {
|
|
if (model) {
|
|
const workbookName = model.getName();
|
|
document.title = workbookName ? `${workbookName} - IronCalc` : "IronCalc";
|
|
} else {
|
|
document.title = "IronCalc";
|
|
}
|
|
}, [model, localStorageId]);
|
|
|
|
if (!model) {
|
|
return (
|
|
<Loading>
|
|
<IronCalcIcon style={{ width: 24, height: 24, marginBottom: 16 }} />
|
|
<div>Loading IronCalc</div>
|
|
</Loading>
|
|
);
|
|
}
|
|
|
|
// We try to save the model every second
|
|
setInterval(() => {
|
|
const queue = model.flushSendQueue();
|
|
if (queue.length !== 1) {
|
|
saveSelectedModelInStorage(model);
|
|
}
|
|
}, 1000);
|
|
|
|
// Handlers for model changes that also update our models state
|
|
const handleNewModel = () => {
|
|
const newModel = createNewModel();
|
|
setModel(newModel);
|
|
};
|
|
|
|
const handleSetModel = (uuid: string) => {
|
|
const newModel = selectModelFromStorage(uuid);
|
|
if (newModel) {
|
|
setModel(newModel);
|
|
}
|
|
};
|
|
|
|
const handleDeleteModel = () => {
|
|
const newModel = deleteSelectedModel();
|
|
if (newModel) {
|
|
setModel(newModel);
|
|
}
|
|
};
|
|
|
|
const handleDeleteModelByUuid = (uuid: string) => {
|
|
const newModel = deleteModelByUuid(uuid);
|
|
if (newModel) {
|
|
setModel(newModel);
|
|
}
|
|
};
|
|
|
|
// 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.
|
|
|
|
return (
|
|
<Wrapper>
|
|
<LeftDrawer
|
|
open={isDrawerOpen}
|
|
onClose={() => setIsDrawerOpen(false)}
|
|
newModel={handleNewModel}
|
|
setModel={handleSetModel}
|
|
onDelete={handleDeleteModelByUuid}
|
|
localStorageId={localStorageId}
|
|
/>
|
|
<MainContent isDrawerOpen={isDrawerOpen}>
|
|
{isDrawerOpen && (
|
|
<MobileOverlay onClick={() => setIsDrawerOpen(false)} />
|
|
)}
|
|
<FileBar
|
|
model={model}
|
|
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
|
|
const blob = await uploadFile(arrayBuffer, fileName);
|
|
|
|
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
const newModel = Model.from_bytes(bytes);
|
|
saveModelToStorage(newModel);
|
|
|
|
setModel(newModel);
|
|
}}
|
|
newModel={handleNewModel}
|
|
newModelFromTemplate={() => {
|
|
setTemplatesDialogOpen(true);
|
|
}}
|
|
setModel={handleSetModel}
|
|
onDelete={handleDeleteModel}
|
|
isDrawerOpen={isDrawerOpen}
|
|
setIsDrawerOpen={setIsDrawerOpen}
|
|
setLocalStorageId={setLocalStorageId}
|
|
/>
|
|
<IronCalc model={model} />
|
|
</MainContent>
|
|
{showWelcomeDialog && (
|
|
<WelcomeDialog
|
|
onClose={() => {
|
|
if (isStorageEmpty()) {
|
|
const createdModel = createNewModel();
|
|
setModel(createdModel);
|
|
}
|
|
setShowWelcomeDialog(false);
|
|
}}
|
|
onSelectTemplate={async (templateId) => {
|
|
switch (templateId) {
|
|
case "blank": {
|
|
const createdModel = createNewModel();
|
|
setModel(createdModel);
|
|
break;
|
|
}
|
|
default: {
|
|
const model_bytes = await get_documentation_model(templateId);
|
|
const importedModel = Model.from_bytes(model_bytes);
|
|
saveModelToStorage(importedModel);
|
|
setModel(importedModel);
|
|
break;
|
|
}
|
|
}
|
|
setShowWelcomeDialog(false);
|
|
}}
|
|
/>
|
|
)}
|
|
<Modal
|
|
open={isTemplatesDialogOpen}
|
|
onClose={() => setTemplatesDialogOpen(false)}
|
|
aria-labelledby="templates-dialog-title"
|
|
aria-describedby="templates-dialog-description"
|
|
>
|
|
<TemplatesDialog
|
|
onClose={() => setTemplatesDialogOpen(false)}
|
|
onSelectTemplate={async (fileName) => {
|
|
const model_bytes = await get_documentation_model(fileName);
|
|
const importedModel = Model.from_bytes(model_bytes);
|
|
saveModelToStorage(importedModel);
|
|
setModel(importedModel);
|
|
setTemplatesDialogOpen(false);
|
|
}}
|
|
/>
|
|
</Modal>
|
|
</Wrapper>
|
|
);
|
|
}
|
|
|
|
const Wrapper = styled("div")`
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const DRAWER_WIDTH = 264;
|
|
const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440;
|
|
|
|
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
|
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)};
|
|
width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")};
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
@media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) {
|
|
${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`}
|
|
}
|
|
`;
|
|
|
|
const MobileOverlay = styled("div")`
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
z-index: 1;
|
|
cursor: pointer;
|
|
|
|
@media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
const Loading = styled("div")`
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: "Inter";
|
|
font-size: 14px;
|
|
`;
|
|
|
|
export default App;
|