Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sheets Prototype #466

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/frontend/apps/impress/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const nextConfig = {
// Modify the file loader rule to ignore *.svg, since we have it handled now.
fileLoaderRule.exclude = /\.svg$/i;

config.resolve.alias['@ironcalc/wasm'] = "@/features/docs/doc-editor/components/ironcalc/wasm";

return config;
},
};
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/apps/impress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
"@blocknote/core": "*",
"@blocknote/mantine": "*",
"@blocknote/react": "*",
"@emotion/styled": "11.11.5",
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.14.0",
"@mui/material": "5.15.21",
"@openfun/cunningham-react": "2.9.4",
"@sentry/nextjs": "8.40.0",
"@tanstack/react-query": "5.61.3",
Expand All @@ -28,10 +30,12 @@
"i18next-browser-languagedetector": "8.0.0",
"idb": "8.0.0",
"lodash": "4.17.21",
"lucide-react": "0.427.0",
"luxon": "3.5.0",
"next": "15.0.3",
"react": "*",
"react-aria-components": "1.5.0",
"react-colorful": "5.6.1",
"react-dom": "*",
"react-i18next": "15.1.1",
"react-select": "5.8.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useResponsiveStore } from '@/stores';
import { useHeadingStore } from '../stores';

import { BlockNoteEditor } from './BlockNoteEditor';
import { IronCalcEditor } from './IronCalcEditor';
import { IconOpenPanelEditor, PanelEditor } from './PanelEditor';

interface DocEditorProps {
Expand All @@ -30,6 +31,8 @@ export const DocEditor = ({ doc }: DocEditorProps) => {

const isVersion = versionId && typeof versionId === 'string';

const isSpreadsheet = true; //doc.content_type === 'spreadsheet';

const { colorsTokens } = useCunninghamTheme();

const { providers } = useDocStore();
Expand Down Expand Up @@ -65,12 +68,14 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
$position="relative"
>
<Card
$padding={isMobile ? 'small' : 'big'}
$padding={isSpreadsheet ? 'none' : isMobile ? 'small' : 'big'}
$css="flex:1;"
$overflow="auto"
$position="relative"
>
{isVersion ? (
{isSpreadsheet ? (
<IronCalcEditor doc={doc} storeId={doc.id} provider={provider} />
) : isVersion ? (
<DocVersionEditor doc={doc} versionId={versionId} />
) : (
<BlockNoteEditor doc={doc} storeId={doc.id} provider={provider} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
import init, { Model } from "@ironcalc/wasm";
import { HocuspocusProvider } from '@hocuspocus/provider';
import { Doc } from '@/features/docs/doc-management';
import { WorkbookState } from "./ironcalc/components/workbookState";
import { base64ToBytes, bytesToBase64 } from "./ironcalc/AppComponents/util";
const IronCalcWorkbook = dynamic(
() => import("./ironcalc/components/workbook"),
{ ssr: false }
);

interface IronCalcEditorProps {
doc: Doc;
provider: HocuspocusProvider;
storeId: string;
}

export function IronCalcEditor({doc, storeId, provider}: IronCalcEditorProps) {
const [model, setModel] = useState<Model | null>(null);
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(null);

const isVersion = doc.id !== storeId;
const readOnly = !doc.abilities.partial_update || isVersion;

// Listen for model changes
useEffect(() => {
if (!model || readOnly) return;

const interval = setInterval(() => {
const queue = model.flushSendQueue();
if (queue.length !== 1) {
// Convert model to base64 string
const modelContent = bytesToBase64(model.toBytes());

// TODO: Save to server
console.log("Doc modified. new base64: ", modelContent);
}
}, 1000);

return () => clearInterval(interval);
}, [model, doc.id, readOnly]);

useEffect(() => {
init().then(() => {
setWorkbookState(new WorkbookState());

// TODO: Load existing content from server
if (doc.content && false) {
try {
const bytes = base64ToBytes(doc.content);
return setModel(Model.from_bytes(bytes));
} catch (e) {
console.error('Failed to load existing content:', e);
}
}

// If no content or failed to load, create new model
setModel(new Model("Workbook1", "en", "UTC"));
});
}, [doc.content]);

if (!model || !workbookState) {
return <div>Loading...</div>;
}

return <div className="ironcalc-workbook" style={{ height: '100%' }}>
<IronCalcWorkbook model={model} workbookState={workbookState} />
</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#root {
position: absolute;
inset: 0px;
margin: 0px;
border: none;
}
html,
body {
overscroll-behavior: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import "./App.css";
import Workbook from "./components/workbook";
import "./i18n";
import styled from "@emotion/styled";
import init, { Model } from "@ironcalc/wasm";
import { useEffect, useState } from "react";
import { FileBar } from "./AppComponents/FileBar";
import {
get_documentation_model,
get_model,
uploadFile,
} from "./AppComponents/rpc";
import {
createNewModel,
deleteSelectedModel,
loadModelFromStorageOrCreate,
saveModelToStorage,
saveSelectedModelInStorage,
selectModelFromStorage,
} from "./AppComponents/storage";
import { WorkbookState } from "./components/workbookState";
import { IronCalcIcon } from "./icons";

function App() {
const [model, setModel] = useState<Model | null>(null);
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
null,
);

useEffect(() => {
async function start() {
await init();
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const modelHash = urlParams.get("model");
const filename = urlParams.get("filename");
// 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");
}
}
if (filename) {
try {
const model_bytes = await get_documentation_model(filename);
const importedModel = Model.from_bytes(model_bytes);
localStorage.removeItem("selected");
setModel(importedModel);
} catch (e) {
alert("Model not found, or failed to load");
}
} else {
// try to load from local storage
const newModel = loadModelFromStorageOrCreate();
setModel(newModel);
}
setWorkbookState(new WorkbookState());
}
start();
}, []);

if (!model || !workbookState) {
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);

// 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>
<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={() => {
setModel(createNewModel());
}}
setModel={(uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
}}
onDelete={() => {
const newModel = deleteSelectedModel();
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")`
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 14px;
`;

export default App;
Loading