Skip to content
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f52b9b3
perf: clipboard and move cells
AyushAgrawal-A2 Jun 12, 2025
5332ad3
use transferables for paste data transfer
AyushAgrawal-A2 Jun 13, 2025
aa98264
fix
AyushAgrawal-A2 Jun 13, 2025
14bb361
avoid clones
AyushAgrawal-A2 Jun 13, 2025
756ec31
Merge branch 'tables-perf' of github.com:quadratichq/quadratic into a…
AyushAgrawal-A2 Jun 13, 2025
d395a24
optimise data table formats to clipboard
AyushAgrawal-A2 Jun 13, 2025
ce6af3a
clippy
AyushAgrawal-A2 Jun 13, 2025
6fb9552
avoid display values in move ops
AyushAgrawal-A2 Jun 14, 2025
761102e
SetCellValues
AyushAgrawal-A2 Jun 14, 2025
c8ddfb2
fix and log
AyushAgrawal-A2 Jun 14, 2025
51cd666
try compression
AyushAgrawal-A2 Jun 14, 2025
8479912
cargo file
AyushAgrawal-A2 Jun 14, 2025
96480c2
Merge branch 'tables-perf' of github.com:quadratichq/quadratic into a…
AyushAgrawal-A2 Jun 14, 2025
fa8ea83
use zstd compression
AyushAgrawal-A2 Jun 14, 2025
a30bd07
fix set_cell_values_operations
AyushAgrawal-A2 Jun 14, 2025
6252f0b
fix formats perf for sorted + hidden tables
AyushAgrawal-A2 Jun 15, 2025
9fdb523
get wrap rows perf
AyushAgrawal-A2 Jun 15, 2025
6958a73
fix bugs
AyushAgrawal-A2 Jun 15, 2025
c1a92f8
clippy
AyushAgrawal-A2 Jun 15, 2025
6383708
apply data tables formats perf
AyushAgrawal-A2 Jun 15, 2025
8c4b492
fix flatten validations
AyushAgrawal-A2 Jun 15, 2025
381a4b7
serialize html with value and style
AyushAgrawal-A2 Jun 16, 2025
1bc1bfa
paste special
AyushAgrawal-A2 Jun 16, 2025
addc924
Merge branch 'tables-perf' of github.com:quadratichq/quadratic into a…
AyushAgrawal-A2 Jun 25, 2025
bd7d242
fix
AyushAgrawal-A2 Jun 25, 2025
ea0e2cf
redo format_ops fn
AyushAgrawal-A2 Jun 26, 2025
0ee25bc
add try catch
AyushAgrawal-A2 Jun 26, 2025
29a2fd5
Merge branch 'number-types-poc' of github.com:quadratichq/quadratic i…
AyushAgrawal-A2 Jun 26, 2025
55091eb
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jun 28, 2025
fb808fe
Merge branch 'number-types-poc' of github.com:quadratichq/quadratic i…
AyushAgrawal-A2 Jun 28, 2025
1c039c0
Merge branch 'number-types-poc' of github.com:quadratichq/quadratic i…
AyushAgrawal-A2 Jul 9, 2025
fdc17fb
Merge branch 'number-types-poc' of github.com:quadratichq/quadratic i…
AyushAgrawal-A2 Jul 9, 2025
67e4a6c
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 9, 2025
0265f00
fix test
AyushAgrawal-A2 Jul 9, 2025
eb80b07
fix e2e and remove cancel button
AyushAgrawal-A2 Jul 9, 2025
0be37a0
fix wrap and compute tracking for sorted / hidden states
AyushAgrawal-A2 Jul 10, 2025
4fd8d26
fix wrap in table on column resize
AyushAgrawal-A2 Jul 10, 2025
fa6e4c6
Merge branch 'qa' into ayush/2894
AyushAgrawal-A2 Jul 11, 2025
756d824
improve sorted table perf
AyushAgrawal-A2 Jul 15, 2025
ddeb243
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 15, 2025
64dda19
more sorted perf
AyushAgrawal-A2 Jul 15, 2025
5283a4e
fix sorted perf
AyushAgrawal-A2 Jul 15, 2025
5ac3362
fix perf and bugs
AyushAgrawal-A2 Jul 17, 2025
56f4185
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 17, 2025
a5f2ef9
remove expensive get_display_index_from_row_index
AyushAgrawal-A2 Jul 18, 2025
d90671c
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 18, 2025
ea908e0
optimize inserts in Contiguous2D
AyushAgrawal-A2 Jul 18, 2025
7a1cc5e
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 18, 2025
11f1f73
pr feedback
AyushAgrawal-A2 Jul 22, 2025
1a2a889
let chaining
AyushAgrawal-A2 Jul 22, 2025
6e0a815
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 22, 2025
0ead3aa
fix format summary behind tables
davidfig Jul 22, 2025
f41cee9
Merge pull request #3274 from quadratichq/fix-format-summary-in-tables
AyushAgrawal-A2 Jul 22, 2025
247cbb2
Merge branch 'qa' of github.com:quadratichq/quadratic into ayush/2894
AyushAgrawal-A2 Jul 23, 2025
b1c9b2f
dd feedback
AyushAgrawal-A2 Jul 23, 2025
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
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
"healthchecks",
"hljs",
"HLOOKUP",
"htmlescape",
"iloc",
"indexeddb",
"indexmap",
Expand Down
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ members = [
"quadratic-multiplayer",
"quadratic-rust-shared",
]
exclude = [
"poc/number_type",
]
exclude = ["poc/number_type"]

[workspace.package]
authors = ["Quadratic"]
Expand Down
183 changes: 119 additions & 64 deletions quadratic-client/src/app/grid/actions/clipboard/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { debugTimeCheck, debugTimeReset } from '@/app/gridGL/helpers/debugPerfor
import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp';
import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings';
import { copyAsPNG } from '@/app/gridGL/pixiApp/copyAsPNG';
import type { PasteSpecial } from '@/app/quadratic-core-types';
import type { JsClipboard, PasteSpecial } from '@/app/quadratic-core-types';
import { toUint8Array } from '@/app/shared/utils/Uint8Array';
import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore';
import type { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider';
import * as Sentry from '@sentry/react';
Expand All @@ -27,21 +28,33 @@ const canvasIsTarget = () => {
};

export const copyToClipboardEvent = async (e: ClipboardEvent) => {
if (!canvasIsTarget()) return;
e.preventDefault();
debugTimeReset();
await toClipboardCopy();
pixiApp.copy.changeCopyRanges();
debugTimeCheck('copy to clipboard');
try {
if (!canvasIsTarget()) return;
e.preventDefault();
debugTimeReset();
await toClipboardCopy();
pixiApp.copy.changeCopyRanges();
debugTimeCheck('copy to clipboard');
} catch (error) {
console.error(error);
pixiAppSettings.addGlobalSnackbar?.('Failed to copy to clipboard.', { severity: 'error' });
Sentry.captureException(error);
}
};

export const cutToClipboardEvent = async (e: ClipboardEvent) => {
if (!canvasIsTarget()) return;
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;
e.preventDefault();
debugTimeReset();
await toClipboardCut();
debugTimeCheck('[Clipboard] cut to clipboard');
try {
if (!canvasIsTarget()) return;
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;
e.preventDefault();
debugTimeReset();
await toClipboardCut();
debugTimeCheck('[Clipboard] cut to clipboard');
} catch (error) {
console.error(error);
pixiAppSettings.addGlobalSnackbar?.('Failed to cut to clipboard.', { severity: 'error' });
Sentry.captureException(error);
}
};

export const pasteFromClipboardEvent = (e: ClipboardEvent) => {
Expand All @@ -53,21 +66,29 @@ export const pasteFromClipboardEvent = (e: ClipboardEvent) => {
return;
}
e.preventDefault();
let html: string | undefined;
let plainText: string | undefined;

if (e.clipboardData.types.includes('text/html')) {
html = e.clipboardData.getData('text/html');
}
let plainText = '';
if (e.clipboardData.types.includes('text/plain')) {
plainText = e.clipboardData.getData('text/plain');
}

let html = '';
if (e.clipboardData.types.includes('text/html')) {
html = e.clipboardData.getData('text/html');
}

if (plainText || html) {
quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
const jsClipboard: JsClipboard = {
plainText,
html,
};
const jsClipboardUint8Array = toUint8Array(jsClipboard);
plainText = '';
html = '';

quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
jsClipboard: jsClipboardUint8Array,
special: 'None',
cursor: sheets.getCursorPosition(),
});
Expand Down Expand Up @@ -156,17 +177,29 @@ const toClipboardCopy = async () => {
};

export const cutToClipboard = async () => {
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;
debugTimeReset();
await toClipboardCut();
debugTimeCheck('cut to clipboard (fallback)');
try {
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;
debugTimeReset();
await toClipboardCut();
debugTimeCheck('cut to clipboard (fallback)');
} catch (error) {
console.error(error);
pixiAppSettings.addGlobalSnackbar?.('Failed to cut to clipboard.', { severity: 'error' });
Sentry.captureException(error);
}
};

export const copyToClipboard = async () => {
debugTimeReset();
await toClipboardCopy();
pixiApp.copy.changeCopyRanges();
debugTimeCheck('copy to clipboard');
try {
debugTimeReset();
await toClipboardCopy();
pixiApp.copy.changeCopyRanges();
debugTimeCheck('copy to clipboard');
} catch (error) {
console.error(error);
pixiAppSettings.addGlobalSnackbar?.('Failed to copy to clipboard.', { severity: 'error' });
Sentry.captureException(error);
}
};

export const copySelectionToPNG = async (addGlobalSnackbar: GlobalSnackbar['addGlobalSnackbar']) => {
Expand Down Expand Up @@ -201,47 +234,69 @@ export const copySelectionToPNG = async (addGlobalSnackbar: GlobalSnackbar['addG
};

export const pasteFromClipboard = async (special: PasteSpecial = 'None') => {
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;
try {
if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return;

if (fullClipboardSupport()) {
const clipboardData = await navigator.clipboard.read();

// get text/plain if available
const plainTextItem = clipboardData.find((item) => item.types.includes('text/plain'));
let plainText: string | undefined;
if (plainTextItem) {
const item = await plainTextItem.getType('text/plain');
plainText = await item.text();
}
if (fullClipboardSupport()) {
const clipboardData = await navigator.clipboard.read();

// get text/plain if available
let plainText = '';
const plainTextItem = clipboardData.find((item) => item.types.includes('text/plain'));
if (plainTextItem) {
const item = await plainTextItem.getType('text/plain');
plainText = await item.text();
}

// gets text/html if available
let html: string | undefined;
const htmlItem = clipboardData.find((item) => item.types.includes('text/html'));
if (htmlItem) {
const item = await htmlItem.getType('text/html');
html = await item.text();
// gets text/html if available
let html = '';
const htmlItem = clipboardData.find((item) => item.types.includes('text/html'));
if (htmlItem) {
const item = await htmlItem.getType('text/html');
html = await item.text();
}

if (plainText || html) {
const jsClipboard: JsClipboard = {
plainText,
html,
};
const jsClipboardUint8Array = toUint8Array(jsClipboard);
plainText = '';
html = '';

quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
jsClipboard: jsClipboardUint8Array,
special,
cursor: sheets.getCursorPosition(),
});
}
}
quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
plainText,
html,
special,
cursor: sheets.getCursorPosition(),
});
}

// handles firefox using localStorage :(
else {
const html = (await localforage.getItem(clipboardLocalStorageKey)) as string;
if (html) {
quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
plainText: undefined,
html,
special,
cursor: sheets.getCursorPosition(),
});
// handles firefox using localStorage :(
else {
let html = (await localforage.getItem(clipboardLocalStorageKey)) as string;
if (html) {
const jsClipboard: JsClipboard = {
plainText: '',
html,
};
const jsClipboardUint8Array = toUint8Array(jsClipboard);
html = '';

quadraticCore.pasteFromClipboard({
selection: sheets.sheet.cursor.save(),
jsClipboard: jsClipboardUint8Array,
special,
cursor: sheets.getCursorPosition(),
});
}
}
} catch (error) {
console.error(error);
pixiAppSettings.addGlobalSnackbar?.('Failed to paste from clipboard.', { severity: 'error' });
Sentry.captureException(error);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const MINIMUM_VIEWPORT_SCALE = 0.01;
const MAXIMUM_VIEWPORT_SCALE = 10;
const WHEEL_ZOOM_PERCENT = 1.5;

export const WAIT_TO_SNAP_TIME = 200;
const WAIT_TO_SNAP_TIME = 200;
const SNAPPING_TIME = 50;

type SnapState = 'waiting' | 'snapping' | undefined;
Expand Down
16 changes: 3 additions & 13 deletions quadratic-client/src/app/ui/components/ImportProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { filesImportProgressAtom } from '@/dashboard/atoms/filesImportProgressAtom';
import { Button } from '@/shared/shadcn/ui/button';
import { Progress } from '@/shared/shadcn/ui/progress';
import { memo } from 'react';
import { useRecoilValue } from 'recoil';

export const ImportProgress = () => {
export const ImportProgress = memo(() => {
const { importing, files, currentFileIndex } = useRecoilValue(filesImportProgressAtom);
if (!importing || currentFileIndex === undefined) return;

const fileNo = currentFileIndex + 1;
const totalFiles = files.length;
const name = files[currentFileIndex].name;
const progress = files[currentFileIndex].progress;
const abortController = files[currentFileIndex].abortController;

return (
<div className="absolute bottom-4 left-4 z-50 w-96 select-none rounded border border-border bg-background pb-2 pl-4 pr-4 pt-2 tracking-tight shadow-lg">
Expand All @@ -25,18 +24,9 @@ export const ImportProgress = () => {
{name}
</div>
</div>

<Button
variant="outline"
size="sm"
disabled={abortController === undefined}
onClick={() => abortController?.abort()}
>
Cancel
</Button>
</div>

<Progress key={fileNo} value={progress} />
</div>
);
};
});
29 changes: 11 additions & 18 deletions quadratic-client/src/app/ui/menus/BottomBar/TopLeftPosition.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import { events } from '@/app/events/events';
import { sheets } from '@/app/grid/controller/Sheets';
import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp';
import { WAIT_TO_SNAP_TIME } from '@/app/gridGL/pixiApp/viewport/Viewport';
import { xyToA1 } from '@/app/quadratic-core/quadratic_core';
import { useEffect, useState } from 'react';

export const TopLeftPosition = () => {
const [topLeftCoordinate, SetTopLeftCoordinate] = useState('');

useEffect(() => {
let timeoutId: NodeJS.Timeout | undefined;
const updateTopLeftCoordinate = () => {
// need to wait for the animation to complete before querying the viewport location
timeoutId = setTimeout(() => {
timeoutId = undefined;
const topLeft = pixiApp.viewport.getVisibleBounds();
const gridHeadings = pixiApp.headings.headingSize;
const topLeftCoordinate = sheets.sheet.getColumnRowFromScreen(
topLeft.x + gridHeadings.width,
topLeft.y + gridHeadings.height
);
SetTopLeftCoordinate(xyToA1(topLeftCoordinate.column, topLeftCoordinate.row));
}, WAIT_TO_SNAP_TIME);
const topLeft = pixiApp.viewport.getVisibleBounds();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

const gridHeadings = pixiApp.headings.headingSize;
const topLeftCoordinate = sheets.sheet.getColumnRowFromScreen(
topLeft.x + gridHeadings.width,
topLeft.y + gridHeadings.height
);
SetTopLeftCoordinate(xyToA1(topLeftCoordinate.column, topLeftCoordinate.row));
};

updateTopLeftCoordinate();
events.on('viewportChanged', updateTopLeftCoordinate);

events.on('viewportReadyAfterUpdate', updateTopLeftCoordinate);
return () => {
events.off('viewportChanged', updateTopLeftCoordinate);
if (timeoutId) {
clearTimeout(timeoutId);
}
events.off('viewportReadyAfterUpdate', updateTopLeftCoordinate);
};
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,7 @@ export interface CoreClientCutToClipboard {
export interface ClientCorePasteFromClipboard {
type: 'clientCorePasteFromClipboard';
selection: string;
plainText: string | undefined;
html: string | undefined;
jsClipboard: Uint8Array;
special: string;
cursor: string;
}
Expand Down
23 changes: 12 additions & 11 deletions quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,17 +869,18 @@ class QuadraticCore {
});
}

pasteFromClipboard(options: {
selection: string;
plainText: string | undefined;
html: string | undefined;
special: PasteSpecial;
cursor: string;
}) {
this.send({
type: 'clientCorePasteFromClipboard',
...options,
});
pasteFromClipboard(options: { selection: string; jsClipboard: Uint8Array; special: PasteSpecial; cursor: string }) {
const { selection, jsClipboard, special, cursor } = options;
this.send(
{
type: 'clientCorePasteFromClipboard',
selection,
jsClipboard,
special,
cursor,
},
jsClipboard.buffer
);
}

//#endregion
Expand Down
Loading
Loading