From b69be09490c1c60675e5050a2232e631516e9799 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 15 Oct 2024 21:28:37 +0530 Subject: [PATCH 01/62] fix: inline editor arrow key interactions (edit & enter states) --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/editActionsSpec.ts | 45 +++++++++++++--- .../src/app/atoms/inlineEditorAtom.ts | 4 ++ quadratic-client/src/app/events/events.ts | 3 +- .../HTMLGrid/inlineEditor/InlineEditor.tsx | 4 +- .../inlineEditor/inlineEditorHandler.ts | 20 +++++-- .../inlineEditor/inlineEditorKeyboard.ts | 47 ++++++++++++---- .../inlineEditor/inlineEditorMonaco.ts | 6 ++- .../interaction/keyboard/keyboardCell.ts | 53 +++++++++++++++---- .../gridGL/interaction/pointer/PointerDown.ts | 7 ++- .../interaction/pointer/doubleClickCell.ts | 8 +-- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 5 +- quadratic-client/src/app/keyboard/defaults.ts | 8 ++- 13 files changed, 167 insertions(+), 44 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 63713707a6..8a263ee1e0 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -122,6 +122,7 @@ export enum Action { MoveCursorRightWithSelection = 'move_cursor_right_with_selection', MoveCursorLeftWithSelection = 'move_cursor_left_with_selection', EditCell = 'edit_cell', + ToggleArrowMode = 'toggle_arrow_mode', DeleteCell = 'delete_cell', ShowCellTypeMenu = 'show_cell_type_menu', CloseInlineEditor = 'close_inline_editor', diff --git a/quadratic-client/src/app/actions/editActionsSpec.ts b/quadratic-client/src/app/actions/editActionsSpec.ts index 8fc33cb604..b4e7edf49c 100644 --- a/quadratic-client/src/app/actions/editActionsSpec.ts +++ b/quadratic-client/src/app/actions/editActionsSpec.ts @@ -10,6 +10,7 @@ import { } from '@/app/grid/actions/clipboard/clipboard'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { downloadFile } from '@/app/helpers/downloadFileInBrowser'; @@ -42,6 +43,7 @@ type EditActionSpec = Pick< | Action.FillRight | Action.FillDown | Action.EditCell + | Action.ToggleArrowMode | Action.DeleteCell | Action.CloseInlineEditor | Action.SaveInlineEditor @@ -162,19 +164,46 @@ export const editActionsSpec: EditActionSpec = { label: 'Edit cell', run: () => { if (!inlineEditorHandler.isEditingFormula()) { - const cursor = sheets.sheet.cursor; - const cursorPosition = cursor.cursorPosition; - const column = cursorPosition.x; - const row = cursorPosition.y; - quadraticCore.getCodeCell(sheets.sheet.id, column, row).then((code) => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { if (code) { - doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + }); + } else { + quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { + doubleClickCell({ column: x, row: y, cell }); + }); + } + }); + return true; + } + }, + }, + [Action.ToggleArrowMode]: { + label: 'Toggle arrow mode', + run: () => { + if (!inlineEditorHandler.isEditingFormula()) { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { + if (code) { + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + arrowMode: ArrowMode.NavigateText, + }); } else { - quadraticCore.getEditCell(sheets.sheet.id, column, row).then((cell) => { - doubleClickCell({ column, row, cell }); + quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { + doubleClickCell({ column: x, row: y, cell, arrowMode: ArrowMode.NavigateText }); }); } }); + return true; } }, }, diff --git a/quadratic-client/src/app/atoms/inlineEditorAtom.ts b/quadratic-client/src/app/atoms/inlineEditorAtom.ts index eb17a32cef..53dd83bb9d 100644 --- a/quadratic-client/src/app/atoms/inlineEditorAtom.ts +++ b/quadratic-client/src/app/atoms/inlineEditorAtom.ts @@ -1,3 +1,4 @@ +import { ArrowMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { atom } from 'recoil'; @@ -7,6 +8,7 @@ export interface InlineEditorState { left: number; top: number; lineHeight: number; + insertCellRef: boolean; } export const defaultInlineEditor: InlineEditorState = { @@ -15,6 +17,7 @@ export const defaultInlineEditor: InlineEditorState = { left: 0, top: 0, lineHeight: 19, + insertCellRef: true, }; export const inlineEditorAtom = atom({ @@ -26,6 +29,7 @@ export const inlineEditorAtom = atom({ if (newValue.visible) { inlineEditorMonaco.focus(); } + inlineEditorKeyboard.arrowMode = newValue.insertCellRef ? ArrowMode.InsertCellRef : ArrowMode.NavigateText; }); }, ], diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 95971e56de..b4c8e006cf 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,5 +1,6 @@ import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { SheetPosTS } from '@/app/gridGL/types/size'; import { JsBordersSheet, @@ -50,7 +51,7 @@ interface EventTypes { setCursor: (cursor?: string, selection?: Selection) => void; cursorPosition: () => void; generateThumbnail: () => void; - changeInput: (input: boolean, initialValue?: string) => void; + changeInput: (input: boolean, initialValue?: string, arrowMode?: ArrowMode) => void; headingSize: (width: number, height: number) => void; gridSettings: () => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index 01217d8593..e8114b3dcc 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -26,7 +26,7 @@ export const InlineEditor = () => { // fix its positioning problem. There's probably a workaround, but it was too // much work. - const { visible, formula, left, top } = useRecoilValue(inlineEditorAtom); + const { visible, formula, left, top, insertCellRef } = useRecoilValue(inlineEditorAtom); return (
{ }} onClick={(e) => inlineEditorHandler.openCodeEditor(e)} > - + ) : null} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index 1fd528a21e..e8c5a06f76 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -6,7 +6,7 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { inlineEditorFormula } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula'; -import { inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { ArrowMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; @@ -156,7 +156,7 @@ class InlineEditorHandler { }; // Handler for the changeInput event. - private changeInput = async (input: boolean, initialValue?: string) => { + private changeInput = async (input: boolean, initialValue?: string, arrowMode?: ArrowMode) => { if (!input && !this.open) return; if (initialValue) { @@ -182,7 +182,7 @@ class InlineEditorHandler { let changeToFormula = false; if (initialValue) { value = initialValue; - this.changeToFormula(value[0] === '='); + changeToFormula = value[0] === '='; } else { const formula = await quadraticCore.getCodeCell(this.location.sheetId, this.location.x, this.location.y); if (formula?.language === 'Formula') { @@ -190,8 +190,22 @@ class InlineEditorHandler { changeToFormula = true; } else { value = (await quadraticCore.getEditCell(this.location.sheetId, this.location.x, this.location.y)) || ''; + changeToFormula = false; } } + + if (arrowMode === undefined) { + if (changeToFormula) { + arrowMode = value.length > 1 ? ArrowMode.NavigateText : ArrowMode.InsertCellRef; + } else { + arrowMode = value ? ArrowMode.NavigateText : ArrowMode.InsertCellRef; + } + } + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + insertCellRef: arrowMode === ArrowMode.InsertCellRef, + })); + this.formatSummary = await quadraticCore.getCellFormatSummary( this.location.sheetId, this.location.x, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index 9d5e1876d5..df69a76c18 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -15,17 +15,23 @@ import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +export enum ArrowMode { + InsertCellRef, + NavigateText, +} + class InlineEditorKeyboard { escapeBackspacePressed = false; + arrowMode: ArrowMode = ArrowMode.InsertCellRef; private handleArrowHorizontal = async (isRight: boolean, e: KeyboardEvent) => { - const target = isRight ? inlineEditorMonaco.getLastColumn() : 2; + // formula if (inlineEditorHandler.isEditingFormula()) { if (inlineEditorHandler.cursorIsMoving) { e.stopPropagation(); e.preventDefault(); keyboardPosition(e); - } else { + } else if (this.arrowMode === ArrowMode.InsertCellRef) { const column = inlineEditorMonaco.getCursorColumn(); e.stopPropagation(); e.preventDefault(); @@ -39,9 +45,10 @@ class InlineEditorKeyboard { inlineEditorHandler.close(isRight ? 1 : -1, 0, false); } } - } else { - const column = inlineEditorMonaco.getCursorColumn(); - if (column === target) { + } + // text + else { + if (this.arrowMode === ArrowMode.InsertCellRef) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -60,12 +67,13 @@ class InlineEditorKeyboard { return; } + // formula if (inlineEditorHandler.isEditingFormula()) { e.stopPropagation(); e.preventDefault(); if (inlineEditorHandler.cursorIsMoving) { keyboardPosition(e); - } else { + } else if (this.arrowMode === ArrowMode.InsertCellRef) { // If we're not moving and the formula doesn't want a cell reference, // close the editor. We can't just use "is the formula syntactically // valid" because many formulas are syntactically valid even though @@ -87,11 +95,15 @@ class InlineEditorKeyboard { return; } } - } else { - e.stopPropagation(); - e.preventDefault(); - if (!(await this.handleValidationError())) { - inlineEditorHandler.close(0, isDown ? 1 : -1, false); + } + // text + else { + if (this.arrowMode === ArrowMode.InsertCellRef) { + e.stopPropagation(); + e.preventDefault(); + if (!(await this.handleValidationError())) { + inlineEditorHandler.close(0, isDown ? 1 : -1, false); + } } } }; @@ -113,6 +125,13 @@ class InlineEditorKeyboard { return false; } + toggleArrowMode = () => { + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + insertCellRef: !prev.insertCellRef, + })); + }; + // Keyboard event for inline editor (via either Monaco's keyDown event or, // when on a different sheet, via window's keyDown listener). keyDown = async (e: KeyboardEvent) => { @@ -167,6 +186,12 @@ class InlineEditorKeyboard { } } + // toggle arrow mode + else if (matchShortcut(Action.ToggleArrowMode, e)) { + e.stopPropagation(); + this.toggleArrowMode(); + } + // Tab key else if (matchShortcut(Action.SaveInlineEditorMoveRight, e)) { if (inlineEditorMonaco.autocompleteSuggestionShowing) { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts index 857f411bda..bc05694aaa 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts @@ -2,6 +2,7 @@ import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { CURSOR_THICKNESS } from '@/app/gridGL/UI/Cursor'; import { CellAlign, CellVerticalAlign, CellWrap } from '@/app/quadratic-core-types'; import { provideCompletionItems, provideHover } from '@/app/quadratic-rust-client/quadratic_rust_client'; @@ -450,7 +451,10 @@ class InlineEditorMonaco { inlineEditorKeyboard.keyDown(e.browserEvent); }); this.editor.onDidChangeCursorPosition(inlineEditorHandler.updateMonacoCursorPosition); - this.editor.onMouseDown(() => inlineEditorKeyboard.resetKeyboardPosition()); + this.editor.onMouseDown(() => { + inlineEditorKeyboard.resetKeyboardPosition(); + pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, insertCellRef: false })); + }); this.editor.onDidChangeModelContent(() => inlineEditorEvents.emit('valueChanged', this.get())); } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts index 628feeb51f..cfc9c742ce 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts @@ -6,6 +6,7 @@ import { openCodeEditor } from '@/app/grid/actions/openCodeEditor'; import { sheets } from '@/app/grid/controller/Sheets'; import { SheetCursor } from '@/app/grid/sheet/SheetCursor'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { isAllowedFirstChar } from '@/app/gridGL/interaction/keyboard/keyboardCellChars'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -76,14 +77,41 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { // Edit cell if (matchShortcut(Action.EditCell, event)) { if (!inlineEditorHandler.isEditingFormula()) { - const column = cursorPosition.x; - const row = cursorPosition.y; - quadraticCore.getCodeCell(sheets.sheet.id, column, row).then((code) => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { if (code) { - doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + }); } else { - quadraticCore.getEditCell(sheets.sheet.id, column, row).then((cell) => { - doubleClickCell({ column, row, cell }); + quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { + doubleClickCell({ column: x, row: y, cell }); + }); + } + }); + return true; + } + } + + // Edit cell - navigate text + if (matchShortcut(Action.ToggleArrowMode, event)) { + if (!inlineEditorHandler.isEditingFormula()) { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { + if (code) { + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + arrowMode: ArrowMode.NavigateText, + }); + } else { + quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { + doubleClickCell({ column: x, row: y, cell, arrowMode: ArrowMode.NavigateText }); }); } }); @@ -131,11 +159,16 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { } if (isAllowedFirstChar(event.key)) { - const cursorPosition = cursor.cursorPosition; - quadraticCore.getCodeCell(sheets.sheet.id, cursorPosition.x, cursorPosition.y).then((code) => { + const { x, y } = cursor.cursorPosition; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { // open code cell unless this is the actual code cell. In this case we can overwrite it - if (code && (Number(code.x) !== cursorPosition.x || Number(code.y) !== cursorPosition.y)) { - doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); + if (code && (Number(code.x) !== x || Number(code.y) !== y)) { + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + }); } else { pixiAppSettings.changeInput(true, event.key); } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index f38c3117f8..198326b300 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -85,7 +85,12 @@ export class PointerDown { event.preventDefault(); const code = await quadraticCore.getCodeCell(sheet.id, column, row); if (code) { - doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + }); } else { const cell = await quadraticCore.getEditCell(sheets.sheet.id, column, row); doubleClickCell({ column, row, cell }); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts index 09a73aafa8..51118e8141 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts @@ -1,6 +1,7 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { CodeCellLanguage } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; @@ -11,8 +12,9 @@ export async function doubleClickCell(options: { row: number; language?: CodeCellLanguage; cell?: string; + arrowMode?: ArrowMode; }) { - const { language, cell, column, row } = options; + const { language, cell, column, row, arrowMode } = options; if (inlineEditorHandler.isEditingFormula()) return; if (multiplayer.cellIsBeingEdited(column, row, sheets.sheet.id)) return; @@ -47,7 +49,7 @@ export async function doubleClickCell(options: { sheets.sheet.cursor.changePosition({ cursorPosition: { x: column, y: row } }); } - pixiAppSettings.changeInput(true, cell); + pixiAppSettings.changeInput(true, cell, arrowMode); } else { pixiAppSettings.setCodeEditorState({ ...pixiAppSettings.codeEditorState, @@ -78,6 +80,6 @@ export async function doubleClickCell(options: { annotationState: `calendar${value.kind === 'date time' ? '-time' : ''}`, }); } - pixiAppSettings.changeInput(true, cell); + pixiAppSettings.changeInput(true, cell, arrowMode); } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index e9cc48067c..681395940a 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -6,6 +6,7 @@ import { defaultInlineEditor, InlineEditorState } from '@/app/atoms/inlineEditor import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; @@ -177,7 +178,7 @@ class PixiAppSettings { } } - changeInput(input: boolean, initialValue?: string) { + changeInput(input: boolean, initialValue?: string, arrowMode?: ArrowMode) { if (input === false) { multiplayer.sendEndCellEdit(); } @@ -204,7 +205,7 @@ class PixiAppSettings { this.setDirty({ cursor: true }); // this is used by CellInput to control visibility - events.emit('changeInput', input, initialValue); + events.emit('changeInput', input, initialValue, arrowMode); } get input() { diff --git a/quadratic-client/src/app/keyboard/defaults.ts b/quadratic-client/src/app/keyboard/defaults.ts index 44087411f7..138ae57550 100644 --- a/quadratic-client/src/app/keyboard/defaults.ts +++ b/quadratic-client/src/app/keyboard/defaults.ts @@ -306,8 +306,12 @@ export const defaultShortcuts: ActionShortcut = { windows: [[WindowsModifiers.Shift, Keys.Tab]], }, [Action.EditCell]: { - mac: [[Keys.Enter], [MacModifiers.Shift, Keys.Enter], [Keys.F2]], - windows: [[Keys.Enter], [WindowsModifiers.Shift, Keys.Enter], [Keys.F2]], + mac: [[Keys.Enter], [MacModifiers.Shift, Keys.Enter]], + windows: [[Keys.Enter], [WindowsModifiers.Shift, Keys.Enter]], + }, + [Action.ToggleArrowMode]: { + mac: [[Keys.F2]], + windows: [[Keys.F2]], }, [Action.DeleteCell]: { mac: [[Keys.Backspace], [Keys.Delete]], From 63d315f63c297fdf65dbf2e4bbf52c6c79dd29ff Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 17 Oct 2024 16:44:08 +0530 Subject: [PATCH 02/62] fix bugs --- .../src/app/actions/editActionsSpec.ts | 7 +++++- .../src/app/atoms/inlineEditorAtom.ts | 6 ++--- .../HTMLGrid/inlineEditor/InlineEditor.tsx | 4 ++-- .../inlineEditor/inlineEditorHandler.ts | 6 ++--- .../inlineEditor/inlineEditorKeyboard.ts | 22 +++++++++++++------ .../interaction/keyboard/keyboardCell.ts | 9 ++++++-- .../gridGL/interaction/pointer/PointerDown.ts | 3 ++- 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/quadratic-client/src/app/actions/editActionsSpec.ts b/quadratic-client/src/app/actions/editActionsSpec.ts index b4e7edf49c..2b2cecfec5 100644 --- a/quadratic-client/src/app/actions/editActionsSpec.ts +++ b/quadratic-client/src/app/actions/editActionsSpec.ts @@ -175,7 +175,12 @@ export const editActionsSpec: EditActionSpec = { }); } else { quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { - doubleClickCell({ column: x, row: y, cell }); + doubleClickCell({ + column: x, + row: y, + cell, + arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell, + }); }); } }); diff --git a/quadratic-client/src/app/atoms/inlineEditorAtom.ts b/quadratic-client/src/app/atoms/inlineEditorAtom.ts index 53dd83bb9d..a912d82bd7 100644 --- a/quadratic-client/src/app/atoms/inlineEditorAtom.ts +++ b/quadratic-client/src/app/atoms/inlineEditorAtom.ts @@ -8,7 +8,7 @@ export interface InlineEditorState { left: number; top: number; lineHeight: number; - insertCellRef: boolean; + navigateText: boolean; } export const defaultInlineEditor: InlineEditorState = { @@ -17,7 +17,7 @@ export const defaultInlineEditor: InlineEditorState = { left: 0, top: 0, lineHeight: 19, - insertCellRef: true, + navigateText: false, }; export const inlineEditorAtom = atom({ @@ -29,7 +29,7 @@ export const inlineEditorAtom = atom({ if (newValue.visible) { inlineEditorMonaco.focus(); } - inlineEditorKeyboard.arrowMode = newValue.insertCellRef ? ArrowMode.InsertCellRef : ArrowMode.NavigateText; + inlineEditorKeyboard.arrowMode = newValue.navigateText ? ArrowMode.NavigateText : ArrowMode.SelectCell; }); }, ], diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index e8114b3dcc..2e98b22685 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -26,7 +26,7 @@ export const InlineEditor = () => { // fix its positioning problem. There's probably a workaround, but it was too // much work. - const { visible, formula, left, top, insertCellRef } = useRecoilValue(inlineEditorAtom); + const { visible, formula, left, top, navigateText } = useRecoilValue(inlineEditorAtom); return (
{ }} onClick={(e) => inlineEditorHandler.openCodeEditor(e)} > - + ) : null} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index e8c5a06f76..c01a38a016 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -196,14 +196,14 @@ class InlineEditorHandler { if (arrowMode === undefined) { if (changeToFormula) { - arrowMode = value.length > 1 ? ArrowMode.NavigateText : ArrowMode.InsertCellRef; + arrowMode = value.length > 1 ? ArrowMode.NavigateText : ArrowMode.SelectCell; } else { - arrowMode = value ? ArrowMode.NavigateText : ArrowMode.InsertCellRef; + arrowMode = value ? ArrowMode.NavigateText : ArrowMode.SelectCell; } } pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, - insertCellRef: arrowMode === ArrowMode.InsertCellRef, + navigateText: arrowMode === ArrowMode.NavigateText, })); this.formatSummary = await quadraticCore.getCellFormatSummary( diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index df69a76c18..a79ad6340b 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -16,13 +16,13 @@ import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; export enum ArrowMode { - InsertCellRef, + SelectCell, NavigateText, } class InlineEditorKeyboard { escapeBackspacePressed = false; - arrowMode: ArrowMode = ArrowMode.InsertCellRef; + arrowMode: ArrowMode = ArrowMode.SelectCell; private handleArrowHorizontal = async (isRight: boolean, e: KeyboardEvent) => { // formula @@ -31,7 +31,7 @@ class InlineEditorKeyboard { e.stopPropagation(); e.preventDefault(); keyboardPosition(e); - } else if (this.arrowMode === ArrowMode.InsertCellRef) { + } else if (this.arrowMode === ArrowMode.SelectCell) { const column = inlineEditorMonaco.getCursorColumn(); e.stopPropagation(); e.preventDefault(); @@ -48,7 +48,7 @@ class InlineEditorKeyboard { } // text else { - if (this.arrowMode === ArrowMode.InsertCellRef) { + if (this.arrowMode === ArrowMode.SelectCell) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -73,7 +73,7 @@ class InlineEditorKeyboard { e.preventDefault(); if (inlineEditorHandler.cursorIsMoving) { keyboardPosition(e); - } else if (this.arrowMode === ArrowMode.InsertCellRef) { + } else if (this.arrowMode === ArrowMode.SelectCell) { // If we're not moving and the formula doesn't want a cell reference, // close the editor. We can't just use "is the formula syntactically // valid" because many formulas are syntactically valid even though @@ -98,7 +98,7 @@ class InlineEditorKeyboard { } // text else { - if (this.arrowMode === ArrowMode.InsertCellRef) { + if (this.arrowMode === ArrowMode.SelectCell) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -128,7 +128,7 @@ class InlineEditorKeyboard { toggleArrowMode = () => { pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, - insertCellRef: !prev.insertCellRef, + navigateText: !prev.navigateText, })); }; @@ -152,6 +152,14 @@ class InlineEditorKeyboard { this.escapeBackspacePressed = false; } + const position = inlineEditorMonaco.getPosition(); + if (e.code === 'Equal' && position.lineNumber === 1 && position.column === 1) { + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + navigateText: false, + })); + } + // Escape key if (matchShortcut(Action.CloseInlineEditor, e)) { e.stopPropagation(); diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts index cfc9c742ce..5daee20fd5 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts @@ -88,7 +88,12 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { }); } else { quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { - doubleClickCell({ column: x, row: y, cell }); + doubleClickCell({ + column: x, + row: y, + cell, + arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell, + }); }); } }); @@ -170,7 +175,7 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { cell: '', }); } else { - pixiAppSettings.changeInput(true, event.key); + pixiAppSettings.changeInput(true, event.key, ArrowMode.SelectCell); } }); return true; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index 24ab91d0b8..002f8ddf84 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -1,6 +1,7 @@ import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { isLinux } from '@/shared/utils/isLinux'; import { isMac } from '@/shared/utils/isMac'; @@ -101,7 +102,7 @@ export class PointerDown { }); } else { const cell = await quadraticCore.getEditCell(sheets.sheet.id, column, row); - doubleClickCell({ column, row, cell }); + doubleClickCell({ column, row, cell, arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell }); } this.active = false; return; From 9fafe8ecb691f5e57f15be071cedd7ca30ddd124 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 17 Oct 2024 21:53:51 +0530 Subject: [PATCH 03/62] bug --- .../src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts index bc05694aaa..b816ae31cb 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts @@ -453,7 +453,7 @@ class InlineEditorMonaco { this.editor.onDidChangeCursorPosition(inlineEditorHandler.updateMonacoCursorPosition); this.editor.onMouseDown(() => { inlineEditorKeyboard.resetKeyboardPosition(); - pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, insertCellRef: false })); + pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, navigateText: true })); }); this.editor.onDidChangeModelContent(() => inlineEditorEvents.emit('valueChanged', this.get())); } From 7be1442fe7c6b8f33f433c1fd70791cad435f856 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 18 Oct 2024 21:46:32 +0530 Subject: [PATCH 04/62] inline cursor mode ui --- .../src/app/atoms/inlineEditorAtom.ts | 7 +- .../HTMLGrid/inlineEditor/InlineEditor.tsx | 34 +++++----- .../inlineEditor/inlineEditorHandler.ts | 7 +- quadratic-client/src/app/gridGL/UI/Cursor.ts | 67 ++++++++++++++++--- .../src/shared/components/Icons.tsx | 4 ++ 5 files changed, 89 insertions(+), 30 deletions(-) diff --git a/quadratic-client/src/app/atoms/inlineEditorAtom.ts b/quadratic-client/src/app/atoms/inlineEditorAtom.ts index a912d82bd7..7d27007dda 100644 --- a/quadratic-client/src/app/atoms/inlineEditorAtom.ts +++ b/quadratic-client/src/app/atoms/inlineEditorAtom.ts @@ -1,5 +1,7 @@ import { ArrowMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { LINE_HEIGHT } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { atom } from 'recoil'; export interface InlineEditorState { @@ -7,7 +9,7 @@ export interface InlineEditorState { formula: boolean; left: number; top: number; - lineHeight: number; + height: number; navigateText: boolean; } @@ -16,7 +18,7 @@ export const defaultInlineEditor: InlineEditorState = { formula: false, left: 0, top: 0, - lineHeight: 19, + height: LINE_HEIGHT, navigateText: false, }; @@ -30,6 +32,7 @@ export const inlineEditorAtom = atom({ inlineEditorMonaco.focus(); } inlineEditorKeyboard.arrowMode = newValue.navigateText ? ArrowMode.NavigateText : ArrowMode.SelectCell; + pixiApp.cursor.dirty = true; }); }, ], diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index 2e98b22685..503bec169e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -3,11 +3,13 @@ //! in inlineEditorHandler.ts. import { inlineEditorAtom } from '@/app/atoms/inlineEditorAtom'; +import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { CURSOR_THICKNESS } from '@/app/gridGL/UI/Cursor'; import { colors } from '@/app/theme/colors'; +import { SubtitlesIcon } from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; -import { SubtitlesOutlined } from '@mui/icons-material'; -import { Tooltip } from '@mui/material'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; import { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import './inlineEditorStyles.scss'; @@ -21,12 +23,12 @@ export const InlineEditor = () => { } }, []); - // Note: I switched to using material's Tooltip because radix-ui's Tooltip did - // not keep position during viewport changes. Even forcing a remount did not - // fix its positioning problem. There's probably a workaround, but it was too - // much work. - - const { visible, formula, left, top, navigateText } = useRecoilValue(inlineEditorAtom); + let { visible, formula, left, top, height } = useRecoilValue(inlineEditorAtom); + height += CURSOR_THICKNESS * 1.5; + const inlineShowing = inlineEditorHandler.getShowing(); + if (inlineShowing) { + height = Math.max(height, sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y).height); + } return (
{
{visible && formula ? ( - + - + ) : null}
); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index c01a38a016..832cff5cf2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -357,8 +357,8 @@ class InlineEditorHandler { pixiAppSettings.setInlineEditorState((prev) => ({ ...prev, left: this.x, - top: this.y + OPEN_SANS_FIX.y / 3, - lineHeight: this.height, + top: this.y, + height: this.height, })); pixiApp.cursor.dirty = true; @@ -501,8 +501,7 @@ class InlineEditorHandler { }; // Handler for the click for the expand code editor button. - openCodeEditor = (e: React.MouseEvent) => { - e.stopPropagation(); + openCodeEditor = () => { if (!pixiAppSettings.setCodeEditorState) { throw new Error('Expected setCodeEditorState to be defined in openCodeEditor'); } diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index bbf9ceb88e..1da4c55595 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -16,6 +16,7 @@ export const FILL_ALPHA = 0.1; const INDICATOR_SIZE = 8; const INDICATOR_PADDING = 1; const HIDE_INDICATORS_BELOW_SCALE = 0.1; +const INLINE_NAVIGATE_TEXT_INDICATOR_SIZE = 6; export type CursorCell = { x: number; y: number; width: number; height: number }; const CURSOR_CELL_DEFAULT_VALUE: CursorCell = { x: 0, y: 0, width: 0, height: 0 }; @@ -97,8 +98,8 @@ export class Cursor extends Container { if (inlineShowing) { x = inlineEditorHandler.x - CURSOR_THICKNESS; y = inlineEditorHandler.y - CURSOR_THICKNESS; - width = inlineEditorHandler.width + CURSOR_THICKNESS * 2; - height = inlineEditorHandler.height + CURSOR_THICKNESS * 2; + width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * 2, width); + height = Math.max(inlineEditorHandler.height + CURSOR_THICKNESS * 2, height); } else { // we have to wait until react renders #cell-edit to properly calculate the width setTimeout(() => (this.dirty = true), 0); @@ -205,11 +206,27 @@ export class Cursor extends Container { const inlineShowing = inlineEditorHandler.getShowing(); if (inlineEditorHandler.formula && inlineShowing && sheets.sheet.id === inlineShowing.sheetId) { color = colors.cellColorUserFormula; - offsets = sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y); - offsets.x = inlineEditorHandler.x - CURSOR_THICKNESS * 0.5; - offsets.y = inlineEditorHandler.y - CURSOR_THICKNESS * 0.5; - offsets.width = inlineEditorHandler.width + CURSOR_THICKNESS; - offsets.height = inlineEditorHandler.height + CURSOR_THICKNESS; + const { width, height } = sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y); + offsets = { + x: inlineEditorHandler.x - CURSOR_THICKNESS * 0.5, + y: inlineEditorHandler.y - CURSOR_THICKNESS * 0.5, + width: Math.max(inlineEditorHandler.width + CURSOR_THICKNESS, width), + height: Math.max(inlineEditorHandler.height + CURSOR_THICKNESS, height), + }; + + this.graphics.lineStyle({ + width: CURSOR_THICKNESS * 1.5, + color, + alpha: CURSOR_INPUT_ALPHA, + alignment: 1, + }); + this.graphics.drawRect(offsets.x, offsets.y, offsets.width, offsets.height); + + const indicatorHalfSize = INLINE_NAVIGATE_TEXT_INDICATOR_SIZE / 2; + this.graphics.moveTo(offsets.x + offsets.width + indicatorHalfSize, offsets.y); + this.graphics.lineTo(offsets.x + offsets.width + indicatorHalfSize + 20, offsets.y); + this.graphics.lineTo(offsets.x + offsets.width + indicatorHalfSize + 20, offsets.y + offsets.height); + this.graphics.lineTo(offsets.x + offsets.width + indicatorHalfSize, offsets.y + offsets.height); } else { const { codeEditorState } = pixiAppSettings; const codeCell = codeEditorState.codeCell; @@ -226,15 +243,45 @@ export class Cursor extends Container { ? colors.cellColorUserJavascript : colors.independence; } + if (!color || !offsets) return; this.graphics.lineStyle({ - width: CURSOR_THICKNESS * 1, + width: CURSOR_THICKNESS, color, - alignment: 0.5, + alignment: 0, }); this.graphics.drawRect(offsets.x, offsets.y, offsets.width, offsets.height); } + private drawInlineNavigateTextModeIndicator() { + const inlineShowing = inlineEditorHandler.getShowing(); + if (!inlineShowing) return; + + const { visible, navigateText, formula } = pixiAppSettings.inlineEditorState; + if (!visible || !navigateText) return; + + const sheet = sheets.sheet; + const cell = sheet.cursor.cursorPosition; + let { x, y, width, height } = sheet.getCellOffsets(cell.x, cell.y); + width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * (formula ? 1 : 2), width); + height = Math.max(inlineEditorHandler.height + CURSOR_THICKNESS * (formula ? 1 : 2), height); + const color = formula ? colors.cellColorUserFormula : colors.cursorCell; + const indicatorSize = INLINE_NAVIGATE_TEXT_INDICATOR_SIZE; + const halfSize = indicatorSize / 2; + const corners = [ + { x: x - halfSize, y: y - halfSize }, + { x: x + width - halfSize, y: y - halfSize }, + { x: x - halfSize, y: y + height - halfSize }, + { x: x + width - halfSize, y: y + height - halfSize }, + ]; + this.graphics.lineStyle(0); + this.graphics.beginFill(color); + corners.forEach((corner) => { + this.graphics.drawRect(corner.x, corner.y, indicatorSize, indicatorSize); + }); + this.graphics.endFill(); + } + // Besides the dirty flag, we also need to update the cursor when the viewport // is dirty and columnRow is set because the columnRow selection is drawn to // visible bounds on the screen, not to the selection size. @@ -252,6 +299,8 @@ export class Cursor extends Container { } this.drawCodeCursor(); + this.drawInlineNavigateTextModeIndicator(); + if (!pixiAppSettings.input.show) { this.drawMultiCursor(); const columnRow = sheets.sheet.cursor.columnRow; diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 5b28239d40..1403a8e8fb 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -395,6 +395,10 @@ export const StopCircleIcon: IconComponent = (props) => { return stop_circle; }; +export const SubtitlesIcon: IconComponent = (props) => { + return subtitles; +}; + export const VerticalAlignBottomIcon: IconComponent = (props) => { return vertical_align_bottom; }; From b079e74487da9107143f3230416d9641026a4cc3 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 18 Oct 2024 12:40:23 -0600 Subject: [PATCH 05/62] tweak appearance --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 1da4c55595..bd1ef290e9 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -269,10 +269,10 @@ export class Cursor extends Container { const indicatorSize = INLINE_NAVIGATE_TEXT_INDICATOR_SIZE; const halfSize = indicatorSize / 2; const corners = [ - { x: x - halfSize, y: y - halfSize }, - { x: x + width - halfSize, y: y - halfSize }, - { x: x - halfSize, y: y + height - halfSize }, - { x: x + width - halfSize, y: y + height - halfSize }, + { x: x - halfSize + 1, y: y - halfSize + 1 }, + { x: x + width - halfSize - 1, y: y - halfSize + 1 }, + { x: x - halfSize + 1, y: y + height - halfSize - 1 }, + { x: x + width - halfSize - 1, y: y + height - halfSize - 1 }, ]; this.graphics.lineStyle(0); this.graphics.beginFill(color); From f5016209fb6c3bb98eb7d9a966c7d8955c685df0 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 18 Oct 2024 12:52:36 -0600 Subject: [PATCH 06/62] Update BottomBar.tsx --- .../src/app/ui/menus/BottomBar/BottomBar.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx b/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx index 52d1269d93..1c8bb7d160 100644 --- a/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx +++ b/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx @@ -1,10 +1,14 @@ +import { inlineEditorAtom } from '@/app/atoms/inlineEditorAtom'; import { VERSION } from '@/shared/constants/appConstants'; +import { useRecoilValue } from 'recoil'; import { debugShowFPS } from '../../../debugFlags'; import BottomBarItem from './BottomBarItem'; import { SelectionSummary } from './SelectionSummary'; import SyncState from './SyncState'; export const BottomBar = () => { + const inlineEditorState = useRecoilValue(inlineEditorAtom); + return (
{ @@ -20,6 +24,14 @@ export const BottomBar = () => { )} */} + {inlineEditorState.visible && ( + + + {inlineEditorState.navigateText ? 'Edit' : 'Enter'} (F2) + + + )} + {debugShowFPS && (
Date: Sat, 19 Oct 2024 00:32:07 +0530 Subject: [PATCH 07/62] fix cursor flash at (0,0) --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 1da4c55595..3bbec7f426 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -96,8 +96,6 @@ export class Cursor extends Container { const inlineShowing = inlineEditorHandler.getShowing(); if (showInput) { if (inlineShowing) { - x = inlineEditorHandler.x - CURSOR_THICKNESS; - y = inlineEditorHandler.y - CURSOR_THICKNESS; width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * 2, width); height = Math.max(inlineEditorHandler.height + CURSOR_THICKNESS * 2, height); } else { From d7d7ae5ac3d0936626567ca388747763c1ebb4b2 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 19 Oct 2024 00:40:34 +0530 Subject: [PATCH 08/62] fix insert cell highlight showing corners --- quadratic-client/src/app/gridGL/UI/Cursor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 1a6ba9ab38..989e044416 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -258,9 +258,7 @@ export class Cursor extends Container { const { visible, navigateText, formula } = pixiAppSettings.inlineEditorState; if (!visible || !navigateText) return; - const sheet = sheets.sheet; - const cell = sheet.cursor.cursorPosition; - let { x, y, width, height } = sheet.getCellOffsets(cell.x, cell.y); + let { x, y, width, height } = sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y); width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * (formula ? 1 : 2), width); height = Math.max(inlineEditorHandler.height + CURSOR_THICKNESS * (formula ? 1 : 2), height); const color = formula ? colors.cellColorUserFormula : colors.cursorCell; From e7b312ba970070b04e724f7e48427e02d719eb68 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 19 Oct 2024 09:45:58 +0530 Subject: [PATCH 09/62] DK feedback --- .../src/app/actions/editActionsSpec.ts | 8 +++--- .../src/app/atoms/inlineEditorAtom.ts | 8 +++--- quadratic-client/src/app/events/events.ts | 4 +-- .../HTMLGrid/inlineEditor/InlineEditor.tsx | 4 +-- .../inlineEditor/inlineEditorHandler.ts | 12 ++++---- .../inlineEditor/inlineEditorKeyboard.ts | 20 ++++++------- .../inlineEditor/inlineEditorMonaco.ts | 2 +- quadratic-client/src/app/gridGL/UI/Cursor.ts | 8 +++--- .../interaction/keyboard/keyboardCell.ts | 10 +++---- .../gridGL/interaction/pointer/PointerDown.ts | 4 +-- .../interaction/pointer/doubleClickCell.ts | 10 +++---- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 6 ++-- quadratic-client/src/app/ui/icons/index.tsx | 28 +++++++++++++++++++ .../src/app/ui/menus/BottomBar/BottomBar.tsx | 2 +- .../src/shared/components/Icons.tsx | 4 --- 15 files changed, 77 insertions(+), 53 deletions(-) diff --git a/quadratic-client/src/app/actions/editActionsSpec.ts b/quadratic-client/src/app/actions/editActionsSpec.ts index 2b2cecfec5..283dddf3d5 100644 --- a/quadratic-client/src/app/actions/editActionsSpec.ts +++ b/quadratic-client/src/app/actions/editActionsSpec.ts @@ -10,7 +10,7 @@ import { } from '@/app/grid/actions/clipboard/clipboard'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { downloadFile } from '@/app/helpers/downloadFileInBrowser'; @@ -179,7 +179,7 @@ export const editActionsSpec: EditActionSpec = { column: x, row: y, cell, - arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell, + cursorMode: cell ? CursorMode.Edit : CursorMode.Enter, }); }); } @@ -200,11 +200,11 @@ export const editActionsSpec: EditActionSpec = { row: Number(code.y), language: code.language, cell: '', - arrowMode: ArrowMode.NavigateText, + cursorMode: CursorMode.Edit, }); } else { quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { - doubleClickCell({ column: x, row: y, cell, arrowMode: ArrowMode.NavigateText }); + doubleClickCell({ column: x, row: y, cell, cursorMode: CursorMode.Edit }); }); } }); diff --git a/quadratic-client/src/app/atoms/inlineEditorAtom.ts b/quadratic-client/src/app/atoms/inlineEditorAtom.ts index 7d27007dda..526371575c 100644 --- a/quadratic-client/src/app/atoms/inlineEditorAtom.ts +++ b/quadratic-client/src/app/atoms/inlineEditorAtom.ts @@ -1,4 +1,4 @@ -import { ArrowMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { LINE_HEIGHT } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; @@ -10,7 +10,7 @@ export interface InlineEditorState { left: number; top: number; height: number; - navigateText: boolean; + editMode: boolean; } export const defaultInlineEditor: InlineEditorState = { @@ -19,7 +19,7 @@ export const defaultInlineEditor: InlineEditorState = { left: 0, top: 0, height: LINE_HEIGHT, - navigateText: false, + editMode: false, }; export const inlineEditorAtom = atom({ @@ -31,7 +31,7 @@ export const inlineEditorAtom = atom({ if (newValue.visible) { inlineEditorMonaco.focus(); } - inlineEditorKeyboard.arrowMode = newValue.navigateText ? ArrowMode.NavigateText : ArrowMode.SelectCell; + inlineEditorKeyboard.cursorMode = newValue.editMode ? CursorMode.Edit : CursorMode.Enter; pixiApp.cursor.dirty = true; }); }, diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index b4c8e006cf..e27b779c76 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,6 +1,6 @@ import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { SheetPosTS } from '@/app/gridGL/types/size'; import { JsBordersSheet, @@ -51,7 +51,7 @@ interface EventTypes { setCursor: (cursor?: string, selection?: Selection) => void; cursorPosition: () => void; generateThumbnail: () => void; - changeInput: (input: boolean, initialValue?: string, arrowMode?: ArrowMode) => void; + changeInput: (input: boolean, initialValue?: string, cursorMode?: CursorMode) => void; headingSize: (width: number, height: number) => void; gridSettings: () => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index 503bec169e..4d3a7851e1 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -7,7 +7,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { CURSOR_THICKNESS } from '@/app/gridGL/UI/Cursor'; import { colors } from '@/app/theme/colors'; -import { SubtitlesIcon } from '@/shared/components/Icons'; +import { SidebarRightIcon } from '@/app/ui/icons'; import { Button } from '@/shared/shadcn/ui/button'; import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; import { useCallback } from 'react'; @@ -65,7 +65,7 @@ export const InlineEditor = () => { inlineEditorHandler.openCodeEditor(); }} > - + ) : null} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index 832cff5cf2..2e33411e79 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -6,7 +6,7 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { inlineEditorFormula } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula'; -import { ArrowMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; @@ -156,7 +156,7 @@ class InlineEditorHandler { }; // Handler for the changeInput event. - private changeInput = async (input: boolean, initialValue?: string, arrowMode?: ArrowMode) => { + private changeInput = async (input: boolean, initialValue?: string, cursorMode?: CursorMode) => { if (!input && !this.open) return; if (initialValue) { @@ -194,16 +194,16 @@ class InlineEditorHandler { } } - if (arrowMode === undefined) { + if (cursorMode === undefined) { if (changeToFormula) { - arrowMode = value.length > 1 ? ArrowMode.NavigateText : ArrowMode.SelectCell; + cursorMode = value.length > 1 ? CursorMode.Edit : CursorMode.Enter; } else { - arrowMode = value ? ArrowMode.NavigateText : ArrowMode.SelectCell; + cursorMode = value ? CursorMode.Edit : CursorMode.Enter; } } pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, - navigateText: arrowMode === ArrowMode.NavigateText, + editMode: cursorMode === CursorMode.Edit, })); this.formatSummary = await quadraticCore.getCellFormatSummary( diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index a79ad6340b..af4b2242d3 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -15,14 +15,14 @@ import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -export enum ArrowMode { - SelectCell, - NavigateText, +export enum CursorMode { + Enter, + Edit, } class InlineEditorKeyboard { escapeBackspacePressed = false; - arrowMode: ArrowMode = ArrowMode.SelectCell; + cursorMode: CursorMode = CursorMode.Enter; private handleArrowHorizontal = async (isRight: boolean, e: KeyboardEvent) => { // formula @@ -31,7 +31,7 @@ class InlineEditorKeyboard { e.stopPropagation(); e.preventDefault(); keyboardPosition(e); - } else if (this.arrowMode === ArrowMode.SelectCell) { + } else if (this.cursorMode === CursorMode.Enter) { const column = inlineEditorMonaco.getCursorColumn(); e.stopPropagation(); e.preventDefault(); @@ -48,7 +48,7 @@ class InlineEditorKeyboard { } // text else { - if (this.arrowMode === ArrowMode.SelectCell) { + if (this.cursorMode === CursorMode.Enter) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -73,7 +73,7 @@ class InlineEditorKeyboard { e.preventDefault(); if (inlineEditorHandler.cursorIsMoving) { keyboardPosition(e); - } else if (this.arrowMode === ArrowMode.SelectCell) { + } else if (this.cursorMode === CursorMode.Enter) { // If we're not moving and the formula doesn't want a cell reference, // close the editor. We can't just use "is the formula syntactically // valid" because many formulas are syntactically valid even though @@ -98,7 +98,7 @@ class InlineEditorKeyboard { } // text else { - if (this.arrowMode === ArrowMode.SelectCell) { + if (this.cursorMode === CursorMode.Enter) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -128,7 +128,7 @@ class InlineEditorKeyboard { toggleArrowMode = () => { pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, - navigateText: !prev.navigateText, + editMode: !prev.editMode, })); }; @@ -156,7 +156,7 @@ class InlineEditorKeyboard { if (e.code === 'Equal' && position.lineNumber === 1 && position.column === 1) { pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, - navigateText: false, + editMode: false, })); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts index b816ae31cb..e78b374f50 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts @@ -453,7 +453,7 @@ class InlineEditorMonaco { this.editor.onDidChangeCursorPosition(inlineEditorHandler.updateMonacoCursorPosition); this.editor.onMouseDown(() => { inlineEditorKeyboard.resetKeyboardPosition(); - pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, navigateText: true })); + pixiAppSettings.setInlineEditorState?.((prev) => ({ ...prev, editMode: true })); }); this.editor.onDidChangeModelContent(() => inlineEditorEvents.emit('valueChanged', this.get())); } diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 989e044416..ef93021a0d 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -251,12 +251,12 @@ export class Cursor extends Container { this.graphics.drawRect(offsets.x, offsets.y, offsets.width, offsets.height); } - private drawInlineNavigateTextModeIndicator() { + private drawInlineCursorModeIndicator() { const inlineShowing = inlineEditorHandler.getShowing(); if (!inlineShowing) return; - const { visible, navigateText, formula } = pixiAppSettings.inlineEditorState; - if (!visible || !navigateText) return; + const { visible, editMode, formula } = pixiAppSettings.inlineEditorState; + if (!visible || editMode) return; let { x, y, width, height } = sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y); width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * (formula ? 1 : 2), width); @@ -295,7 +295,7 @@ export class Cursor extends Container { } this.drawCodeCursor(); - this.drawInlineNavigateTextModeIndicator(); + this.drawInlineCursorModeIndicator(); if (!pixiAppSettings.input.show) { this.drawMultiCursor(); diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts index 5daee20fd5..940a738c25 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts @@ -6,7 +6,7 @@ import { openCodeEditor } from '@/app/grid/actions/openCodeEditor'; import { sheets } from '@/app/grid/controller/Sheets'; import { SheetCursor } from '@/app/grid/sheet/SheetCursor'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { isAllowedFirstChar } from '@/app/gridGL/interaction/keyboard/keyboardCellChars'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -92,7 +92,7 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { column: x, row: y, cell, - arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell, + cursorMode: cell ? CursorMode.Edit : CursorMode.Enter, }); }); } @@ -112,11 +112,11 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { row: Number(code.y), language: code.language, cell: '', - arrowMode: ArrowMode.NavigateText, + cursorMode: CursorMode.Edit, }); } else { quadraticCore.getEditCell(sheets.sheet.id, x, y).then((cell) => { - doubleClickCell({ column: x, row: y, cell, arrowMode: ArrowMode.NavigateText }); + doubleClickCell({ column: x, row: y, cell, cursorMode: CursorMode.Edit }); }); } }); @@ -175,7 +175,7 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { cell: '', }); } else { - pixiAppSettings.changeInput(true, event.key, ArrowMode.SelectCell); + pixiAppSettings.changeInput(true, event.key, CursorMode.Enter); } }); return true; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index 002f8ddf84..b6ce67207f 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -1,7 +1,7 @@ import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { isLinux } from '@/shared/utils/isLinux'; import { isMac } from '@/shared/utils/isMac'; @@ -102,7 +102,7 @@ export class PointerDown { }); } else { const cell = await quadraticCore.getEditCell(sheets.sheet.id, column, row); - doubleClickCell({ column, row, cell, arrowMode: cell ? ArrowMode.NavigateText : ArrowMode.SelectCell }); + doubleClickCell({ column, row, cell, cursorMode: cell ? CursorMode.Edit : CursorMode.Enter }); } this.active = false; return; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts index 51118e8141..62713f266d 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts @@ -1,7 +1,7 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { CodeCellLanguage } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; @@ -12,9 +12,9 @@ export async function doubleClickCell(options: { row: number; language?: CodeCellLanguage; cell?: string; - arrowMode?: ArrowMode; + cursorMode?: CursorMode; }) { - const { language, cell, column, row, arrowMode } = options; + const { language, cell, column, row, cursorMode } = options; if (inlineEditorHandler.isEditingFormula()) return; if (multiplayer.cellIsBeingEdited(column, row, sheets.sheet.id)) return; @@ -49,7 +49,7 @@ export async function doubleClickCell(options: { sheets.sheet.cursor.changePosition({ cursorPosition: { x: column, y: row } }); } - pixiAppSettings.changeInput(true, cell, arrowMode); + pixiAppSettings.changeInput(true, cell, cursorMode); } else { pixiAppSettings.setCodeEditorState({ ...pixiAppSettings.codeEditorState, @@ -80,6 +80,6 @@ export async function doubleClickCell(options: { annotationState: `calendar${value.kind === 'date time' ? '-time' : ''}`, }); } - pixiAppSettings.changeInput(true, cell, arrowMode); + pixiAppSettings.changeInput(true, cell, cursorMode); } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 681395940a..4ed5565cf3 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -6,7 +6,7 @@ import { defaultInlineEditor, InlineEditorState } from '@/app/atoms/inlineEditor import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { ArrowMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; @@ -178,7 +178,7 @@ class PixiAppSettings { } } - changeInput(input: boolean, initialValue?: string, arrowMode?: ArrowMode) { + changeInput(input: boolean, initialValue?: string, cursorMode?: CursorMode) { if (input === false) { multiplayer.sendEndCellEdit(); } @@ -205,7 +205,7 @@ class PixiAppSettings { this.setDirty({ cursor: true }); // this is used by CellInput to control visibility - events.emit('changeInput', input, initialValue, arrowMode); + events.emit('changeInput', input, initialValue, cursorMode); } get input() { diff --git a/quadratic-client/src/app/ui/icons/index.tsx b/quadratic-client/src/app/ui/icons/index.tsx index 1d9a66187d..c9d562f5e3 100644 --- a/quadratic-client/src/app/ui/icons/index.tsx +++ b/quadratic-client/src/app/ui/icons/index.tsx @@ -211,3 +211,31 @@ export const BoxIcon = (props: SvgIconProps) => ( ); + +export const SidebarRightIcon = (props: SvgIconProps) => ( + + + + + + +); diff --git a/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx b/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx index 1c8bb7d160..90c90a2a19 100644 --- a/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx +++ b/quadratic-client/src/app/ui/menus/BottomBar/BottomBar.tsx @@ -27,7 +27,7 @@ export const BottomBar = () => { {inlineEditorState.visible && ( - {inlineEditorState.navigateText ? 'Edit' : 'Enter'} (F2) + {inlineEditorState.editMode ? 'Edit' : 'Enter'} (F2) )} diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 1403a8e8fb..5b28239d40 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -395,10 +395,6 @@ export const StopCircleIcon: IconComponent = (props) => { return stop_circle; }; -export const SubtitlesIcon: IconComponent = (props) => { - return subtitles; -}; - export const VerticalAlignBottomIcon: IconComponent = (props) => { return vertical_align_bottom; }; From 2d192d24cd09db7b05da9fb837543041a24b7c33 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 19 Oct 2024 11:35:13 +0530 Subject: [PATCH 10/62] fix bug --- .../app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts index a7d5eef4f9..e850e2689a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts @@ -151,7 +151,9 @@ class InlineEditorFormula { // or by keyboard input) or whether they want to switch to a different cell. wantsCellRef() { const lastCharacter = inlineEditorMonaco.getNonWhitespaceCharBeforeCursor(); - return ['', ',', '+', '-', '*', '/', '%', '=', '<', '>', '&', '.', '(', '{'].includes(lastCharacter); + return ( + !!lastCharacter && ['', ',', '+', '-', '*', '/', '%', '=', '<', '>', '&', '.', '(', '{'].includes(lastCharacter) + ); } // Returns whether we are editing a formula only if it is valid (used for From 7334456ee918ca09df47c7c9c210672b64c48c9f Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 30 Oct 2024 09:49:26 -0600 Subject: [PATCH 11/62] initial commit --- .../src/dashboard/components/FileDragDrop.tsx | 44 +-- .../src/dashboard/components/FilesList.tsx | 8 + .../dashboard/components/NewFileButton.tsx | 269 +++++++++++++++++- 3 files changed, 297 insertions(+), 24 deletions(-) diff --git a/quadratic-client/src/dashboard/components/FileDragDrop.tsx b/quadratic-client/src/dashboard/components/FileDragDrop.tsx index f6154e91c7..3001c250d2 100644 --- a/quadratic-client/src/dashboard/components/FileDragDrop.tsx +++ b/quadratic-client/src/dashboard/components/FileDragDrop.tsx @@ -44,29 +44,37 @@ export function FileDragDrop({ className }: FileDragDropProps) { if (!fileDragDropModal.show) return null; return ( -
+ <>
-
- Drop file here +
+
+ Drop file here - - Start a new spreadsheet by importing a CSV, Parquet, Excel or Grid file(s) - + + Start a new spreadsheet by importing a CSV, Parquet, Excel or Grid file(s) + +
-
+ + ); } diff --git a/quadratic-client/src/dashboard/components/FilesList.tsx b/quadratic-client/src/dashboard/components/FilesList.tsx index c2bcdad683..db8927afcb 100644 --- a/quadratic-client/src/dashboard/components/FilesList.tsx +++ b/quadratic-client/src/dashboard/components/FilesList.tsx @@ -3,6 +3,7 @@ import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { FileDragDrop } from '@/dashboard/components/FileDragDrop'; import { DRAWER_WIDTH } from '@/routes/_dashboard'; import { Action as FilesAction } from '@/routes/api.files.$uuid'; +import { AddIcon } from '@/shared/components/Icons'; import { ShareFileDialog } from '@/shared/components/ShareDialog'; import useLocalStorage from '@/shared/hooks/useLocalStorage'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; @@ -123,6 +124,13 @@ export function FilesList({ /> +
  • + +
  • {filesToRender.map((file, i) => ( { - setNewFileDialogState({ show: true, isPrivate }); - }} +
    + + + + + + + + + + Blank + An empty spreadsheet + + + setFileDragDropState({ show: true, isPrivate })}> + + + + Imported data + .csv, .xlsx, .pqt, .grid files + + + + + + + + API data + HTTP requests from code + + + + + + + + + + Example data + Research, analysis, and modeling demos + + + + + + + + Quadratic (production) + Postgres connection + + + + + + Customer data + MySQL connection + + + + See all connections... + + + + + + +
    + ); +} + +function DialogContentNewFileFromApi({ children }: any) { + const [isLoading, setIsLoading] = useState(false); + const [json, setJson] = useState({ + id: 'R7UfaahVfFd', + joke: 'My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.', + status: 200, + }); + const [url, setUrl] = useState('https://icanhazdadjoke.com'); + const [activeLanguage, setActiveLanguage] = useState('Javascript'); + + // TODO: needs better error handling + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + setIsLoading(true); + fetch(url, { headers: { Accept: 'application/json' } }) + .then((res) => res.json()) + .then((newJson) => { + setJson(newJson); + setIsLoading(false); + }); + }; + + return ( + + + Fetch data from an API + + Example showing how to fetch data and put it on a spreadsheet. Create a file then you can tweak the code any + way you want including custom headers, authentication, and more. + + +
    +
    +
    + {['Python', 'Javascript'].map((language) => ( + + ))} +
    +
    + setUrl(e.target.value)} /> + +
    +
    + +
    +
    +            {activeLanguage === 'Javascript'
    +              ? `const res = await fetch(
    +  "${url}",
    +  { headers: { Accept: 'application/json' }
    +});
    +const json = await res.json();
    +return [
    +  Object.keys(json),
    +  Object.values(json),
    +];`
    +              : `import requests
    +import pandas as pd
    +response = requests.get('${url}')
    +df = pd.DataFrame(response.json())
    +df
    +`}
    +          
    +
    +
    +
    + + + + + + + + + + + + + + + + + {Object.keys(json).map((key) => ( + + ))} + + + + + + + {Object.values(json).map((value) => ( + + ))} + + + + + + + + + + + + + + +
    ABCDEFG
    1{key}
    2{typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
    3
    +
    + + + + + + +
    + ); +} + +function TD({ + children, + as = 'td', + isFirstCol = false, + className, +}: { + children?: React.ReactNode; + as?: 'td' | 'th'; + isFirstCol?: boolean; + className?: string; +}) { + const Component = as; + + return ( + - New file - + {children} + ); } From eecda97b198a6f3f7192552066ba8c82d017ce15 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 30 Oct 2024 11:39:33 -0600 Subject: [PATCH 12/62] tweaks --- .../src/dashboard/components/FilesList.tsx | 22 ++++--- .../dashboard/components/NewFileButton.tsx | 63 ++++++++++++++----- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/quadratic-client/src/dashboard/components/FilesList.tsx b/quadratic-client/src/dashboard/components/FilesList.tsx index db8927afcb..51710c89f4 100644 --- a/quadratic-client/src/dashboard/components/FilesList.tsx +++ b/quadratic-client/src/dashboard/components/FilesList.tsx @@ -5,12 +5,13 @@ import { DRAWER_WIDTH } from '@/routes/_dashboard'; import { Action as FilesAction } from '@/routes/api.files.$uuid'; import { AddIcon } from '@/shared/components/Icons'; import { ShareFileDialog } from '@/shared/components/ShareDialog'; +import { ROUTES } from '@/shared/constants/routes'; import useLocalStorage from '@/shared/hooks/useLocalStorage'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { FilePermission, PublicLinkAccess } from 'quadratic-shared/typesAndSchemas'; import { ReactNode, useCallback, useState } from 'react'; import { isMobile } from 'react-device-detect'; -import { useFetchers, useLocation } from 'react-router-dom'; +import { Link, useFetchers, useLocation } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Empty } from './Empty'; import { FilesListItemExampleFile, FilesListItemUserFile, FilesListItems } from './FilesListItem'; @@ -124,13 +125,18 @@ export function FilesList({ /> -
  • - -
  • + {teamUuid && ( +
  • + + + New file + (or drag and drop) + +
  • + )} {filesToRender.map((file, i) => ( - Example data + Examples Research, analysis, and modeling demos @@ -112,14 +120,16 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { ); } +type LoadState = 'idle' | 'loading' | 'error'; function DialogContentNewFileFromApi({ children }: any) { - const [isLoading, setIsLoading] = useState(false); - const [json, setJson] = useState({ + const [loadState, setLoadState] = useState('idle'); + const [json, setJson] = useState>({ id: 'R7UfaahVfFd', joke: 'My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.', status: 200, }); - const [url, setUrl] = useState('https://icanhazdadjoke.com'); + const defaultUrl = 'https://icanhazdadjoke.com'; + const [url, setUrl] = useState(defaultUrl); const [activeLanguage, setActiveLanguage] = useState('Javascript'); // TODO: needs better error handling @@ -127,12 +137,15 @@ function DialogContentNewFileFromApi({ children }: any) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); + setLoadState('loading'); fetch(url, { headers: { Accept: 'application/json' } }) .then((res) => res.json()) .then((newJson) => { setJson(newJson); - setIsLoading(false); + setLoadState('idle'); + }) + .catch((err) => { + setLoadState('error'); }); }; @@ -145,8 +158,8 @@ function DialogContentNewFileFromApi({ children }: any) { way you want including custom headers, authentication, and more. -
    -
    +
    +
    {['Python', 'Javascript'].map((language) => (
    From 381826c5a9fce830b5aaf66972195d0c06fc4b8b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 10 Nov 2024 08:15:44 -0800 Subject: [PATCH 15/62] possibly fix bug --- quadratic-client/src/app/ui/components/DateFormat.tsx | 8 ++++++-- .../Validation/ValidationUI/ValidationInput.tsx | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/ui/components/DateFormat.tsx b/quadratic-client/src/app/ui/components/DateFormat.tsx index bc73db2e0b..1d9dfe2c41 100644 --- a/quadratic-client/src/app/ui/components/DateFormat.tsx +++ b/quadratic-client/src/app/ui/components/DateFormat.tsx @@ -227,9 +227,13 @@ export const DateFormat = (props: DateFormatProps) => { { + changeCustom(value); + apply(); + closeMenu(); + }} onKeyDown={(e) => { // ensures that the menu does not close when the user presses keys like arrow e.stopPropagation(); diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx index 8d44f2e1b0..982fd712f4 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationInput.tsx @@ -16,7 +16,7 @@ interface ValidationInputProps { // used to update whenever the input is changed (ie, a character is changes within the input box) onInput?: (value: string) => void; onKeyDown?: (e: KeyboardEvent) => void; - onEnter?: () => void; + onEnter?: (value: string) => void; footer?: string | JSX.Element; height?: string; @@ -106,7 +106,8 @@ export const ValidationInput = forwardRef((props: ValidationInputProps, ref: Ref } // timeout is needed to ensure the state updates before the onEnter function is called - setTimeout(onEnter, 0); + const savedValue = e.currentTarget.value; + setTimeout(() => onEnter?.(savedValue), 0); e.preventDefault(); e.stopPropagation(); } From 7ffff634d8f63b8f422b76331241378ae1d61f9f Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sun, 10 Nov 2024 23:31:15 +0530 Subject: [PATCH 16/62] perf: use plotly cdn for python charts --- .../python-wasm/quadratic_py/plotly_patch.py | 18 ++++++++++++++++-- .../python-wasm/quadratic_py/process_output.py | 4 +++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py index c3c7a45000..4673685206 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py +++ b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py @@ -29,7 +29,7 @@ def set_result(self, figure) -> None: f"Cannot produce multiple figures from a single cell. " f"First produced on line {self._result_set_from_line}, " f"then on {current_result_set_from_line}", - source_line=current_result_set_from_line + source_line=current_result_set_from_line, ) self._result = figure @@ -46,17 +46,23 @@ def result_set_from_line(self) -> int | None: async def intercept_plotly_html(code) -> _FigureHolder | None: import pyodide.code + if "plotly" not in pyodide.code.find_imports(code): return None await micropip.install("plotly") import plotly.io + from plotly.basedatatypes import BaseFigure # TODO: It would be nice if we could prevent the user from setting the default renderer plotly.io.renderers.default = "browser" figure_holder = _FigureHolder() - plotly.io._base_renderers.open_html_in_browser = _make_open_html_patch(figure_holder.set_result) + plotly.io._base_renderers.open_html_in_browser = _make_open_html_patch( + figure_holder.set_result + ) + + BaseFigure.show = _custom_show return figure_holder @@ -66,3 +72,11 @@ def open_html_in_browser(html, *args, **kwargs): figure_saver(html) return open_html_in_browser + + +# Override the default show method for plotly figures +def _custom_show(self): + html = self.to_html(include_plotlyjs="cdn", include_mathjax="cdn").replace( + ' src="https://', ' crossorigin="anonymous" src="https://' + ) + return html diff --git a/quadratic-kernels/python-wasm/quadratic_py/process_output.py b/quadratic-kernels/python-wasm/quadratic_py/process_output.py index 8832d4c4f5..67a09177cf 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/process_output.py +++ b/quadratic-kernels/python-wasm/quadratic_py/process_output.py @@ -52,7 +52,9 @@ def process_output_value(output_value): import plotly if isinstance(output_value, plotly.graph_objs._figure.Figure): - output_value = output_value.to_html() + output_value = output_value.to_html( + include_plotlyjs="cdn", include_mathjax="cdn" + ).replace(' src="https://', ' crossorigin="anonymous" src="https://') output_type = "Chart" except: pass From 69b03174a2caa8f4036ad14f7f298c211537ac2e Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Mon, 11 Nov 2024 03:24:51 +0530 Subject: [PATCH 17/62] duplicate code --- quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py | 4 ++-- quadratic-kernels/python-wasm/quadratic_py/process_output.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py index 4673685206..89e85760c9 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py +++ b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py @@ -62,7 +62,7 @@ async def intercept_plotly_html(code) -> _FigureHolder | None: figure_holder.set_result ) - BaseFigure.show = _custom_show + BaseFigure.show = to_html_with_cdn return figure_holder @@ -75,7 +75,7 @@ def open_html_in_browser(html, *args, **kwargs): # Override the default show method for plotly figures -def _custom_show(self): +def to_html_with_cdn(self): html = self.to_html(include_plotlyjs="cdn", include_mathjax="cdn").replace( ' src="https://', ' crossorigin="anonymous" src="https://' ) diff --git a/quadratic-kernels/python-wasm/quadratic_py/process_output.py b/quadratic-kernels/python-wasm/quadratic_py/process_output.py index 67a09177cf..d11d95bf50 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/process_output.py +++ b/quadratic-kernels/python-wasm/quadratic_py/process_output.py @@ -1,6 +1,7 @@ import pandas as pd from .utils import to_quadratic_type +from quadratic_py import plotly_patch def isListEmpty(inList): @@ -52,9 +53,7 @@ def process_output_value(output_value): import plotly if isinstance(output_value, plotly.graph_objs._figure.Figure): - output_value = output_value.to_html( - include_plotlyjs="cdn", include_mathjax="cdn" - ).replace(' src="https://', ' crossorigin="anonymous" src="https://') + output_value = plotly_patch.to_html_with_cdn(output_value) output_type = "Chart" except: pass From a447f9d5f673ff632caa7b60d323c615b28c7df2 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Mon, 11 Nov 2024 04:02:53 +0530 Subject: [PATCH 18/62] fix: ERROR: TypeError: this.pyodide.globals.get(...) is not a function --- .../src/app/web-workers/pythonWebWorker/worker/python.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts index 9a555e0071..10f99405e8 100644 --- a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts +++ b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts @@ -167,7 +167,7 @@ class Python { if (!this.pyodide) { console.warn('Python not loaded'); } else { - const output = await this.pyodide.globals.get('inspect_python')(pythonCode); + const output = await this.pyodide.runPythonAsync(`inspect_python(${JSON.stringify(pythonCode)})`); if (output === undefined) { return undefined; From 9f912feb5799dd9c618cc36934e65f3b391c8f73 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Mon, 11 Nov 2024 18:48:13 +0530 Subject: [PATCH 19/62] fix test --- .../python-wasm/quadratic_py/plotly_patch.py | 12 ++---------- .../python-wasm/quadratic_py/process_output.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py index 89e85760c9..adfe1e2610 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py +++ b/quadratic-kernels/python-wasm/quadratic_py/plotly_patch.py @@ -1,6 +1,6 @@ import micropip -from quadratic_py import code_trace +from quadratic_py import code_trace, process_output class FigureDisplayError(Exception): @@ -62,7 +62,7 @@ async def intercept_plotly_html(code) -> _FigureHolder | None: figure_holder.set_result ) - BaseFigure.show = to_html_with_cdn + BaseFigure.show = process_output.to_html_with_cdn return figure_holder @@ -72,11 +72,3 @@ def open_html_in_browser(html, *args, **kwargs): figure_saver(html) return open_html_in_browser - - -# Override the default show method for plotly figures -def to_html_with_cdn(self): - html = self.to_html(include_plotlyjs="cdn", include_mathjax="cdn").replace( - ' src="https://', ' crossorigin="anonymous" src="https://' - ) - return html diff --git a/quadratic-kernels/python-wasm/quadratic_py/process_output.py b/quadratic-kernels/python-wasm/quadratic_py/process_output.py index d11d95bf50..1f12acb4ee 100644 --- a/quadratic-kernels/python-wasm/quadratic_py/process_output.py +++ b/quadratic-kernels/python-wasm/quadratic_py/process_output.py @@ -1,7 +1,6 @@ import pandas as pd from .utils import to_quadratic_type -from quadratic_py import plotly_patch def isListEmpty(inList): @@ -53,7 +52,7 @@ def process_output_value(output_value): import plotly if isinstance(output_value, plotly.graph_objs._figure.Figure): - output_value = plotly_patch.to_html_with_cdn(output_value) + output_value = to_html_with_cdn(output_value) output_type = "Chart" except: pass @@ -103,3 +102,11 @@ def process_output_value(output_value): "output_type": output_type, "output_size": output_size, } + + +# Override the default show method for plotly figures +def to_html_with_cdn(self): + html = self.to_html(include_plotlyjs="cdn", include_mathjax="cdn").replace( + ' src="https://', ' crossorigin="anonymous" src="https://' + ) + return html From 3b5d0bd123355533d6053e1cb7276a03c2762bb6 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 11 Nov 2024 17:21:43 -0700 Subject: [PATCH 20/62] updates --- .../ui/menus/CommandPalette/commands/File.tsx | 1 + .../TopBar/TopBarMenus/FileMenubarMenu.tsx | 15 +++--- .../dashboard/components/DashboardSidebar.tsx | 31 +++++++---- .../src/dashboard/components/FileDragDrop.tsx | 51 +++++++++--------- .../src/dashboard/components/FilesList.tsx | 16 +----- .../dashboard/components/NewFileButton.tsx | 53 ++++++++----------- 6 files changed, 81 insertions(+), 86 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx index 749719dffa..a0579c96d5 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx @@ -21,6 +21,7 @@ const commands: CommandGroup = { keywords: ['New file', 'Create file'], isAvailable: createNewFileAction.isAvailable, Component: (props) => { + // TODO: create a private file const setEditorInteractionState = useSetRecoilState(editorInteractionStateAtom); const action = () => createNewFileAction.run({ setEditorInteractionState }); return } action={action} />; diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index b94c8ed71d..040d43a1f9 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -1,16 +1,15 @@ import { createNewFileAction, deleteFile, duplicateFileAction } from '@/app/actions'; import { Action } from '@/app/actions/actions'; -import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { useFileContext } from '@/app/ui/components/FileProvider'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; import { MenubarItemAction } from '@/app/ui/menus/TopBar/TopBarMenus/MenubarItemAction'; import { useRootRouteLoaderData } from '@/routes/_root'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { DeleteIcon, DraftIcon, FileCopyIcon } from '@/shared/components/Icons'; +import { ROUTES } from '@/shared/constants/routes'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from '@/shared/shadcn/ui/menubar'; -import { useSubmit } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { Link, useSubmit } from 'react-router-dom'; // TODO: (enhancement) move these into `fileActionsSpec` by making the `.run()` // function of each accessible from outside of react @@ -18,10 +17,11 @@ import { useSetRecoilState } from 'recoil'; export const FileMenubarMenu = () => { const { name } = useFileContext(); const submit = useSubmit(); - const setEditorInteractionState = useSetRecoilState(editorInteractionStateAtom); + const { isAuthenticated } = useRootRouteLoaderData(); const { file: { uuid: fileUuid }, + team: { uuid: teamUuid }, } = useFileRouteLoaderData(); const { addGlobalSnackbar } = useGlobalSnackbar(); const isAvailableArgs = useIsAvailableArgs(); @@ -33,8 +33,11 @@ export const FileMenubarMenu = () => { File {createNewFileAction.isAvailable(isAvailableArgs) && ( - createNewFileAction.run({ setEditorInteractionState })}> - {createNewFileAction.label} + + + + {createNewFileAction.label} + )} {duplicateFileAction.isAvailable(isAvailableArgs) && ( diff --git a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx index cd6e86e879..77bb2a98b7 100644 --- a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx +++ b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx @@ -1,5 +1,4 @@ import { colors } from '@/app/theme/colors'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { TeamSwitcher } from '@/dashboard/components/TeamSwitcher'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import { useRootRouteLoaderData } from '@/routes/_root'; @@ -27,8 +26,7 @@ import { Button } from '@/shared/shadcn/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/shared/shadcn/ui/tooltip'; import { cn } from '@/shared/shadcn/utils'; import { ReactNode, useState } from 'react'; -import { NavLink, useLocation, useNavigation, useSearchParams, useSubmit } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { Link, NavLink, useLocation, useNavigation, useSearchParams, useSubmit } from 'react-router-dom'; /** * Dashboard Navbar @@ -67,7 +65,11 @@ export function DashboardSidebar({ isLoading }: { isLoading: boolean }) { Files - {canEditTeam && New file} + {canEditTeam && ( + + New file + + )}
    {canEditTeam && ( @@ -99,7 +101,9 @@ export function DashboardSidebar({ isLoading }: { isLoading: boolean }) { My files - New private file + + New private file +
    @@ -176,18 +180,27 @@ export function DashboardSidebar({ isLoading }: { isLoading: boolean }) { ); } -function SidebarNavLinkCreateButton({ children, isPrivate }: { children: ReactNode; isPrivate: boolean }) { - const setNewFileDialogState = useSetRecoilState(newFileDialogAtom); +function SidebarNavLinkCreateButton({ + children, + isPrivate, + teamUuid, +}: { + children: ReactNode; + isPrivate: boolean; + teamUuid: string; +}) { return ( {children} diff --git a/quadratic-client/src/dashboard/components/FileDragDrop.tsx b/quadratic-client/src/dashboard/components/FileDragDrop.tsx index 3001c250d2..5c5741eaa9 100644 --- a/quadratic-client/src/dashboard/components/FileDragDrop.tsx +++ b/quadratic-client/src/dashboard/components/FileDragDrop.tsx @@ -1,5 +1,6 @@ import { useFileImport } from '@/app/ui/hooks/useFileImport'; import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; +import { CloseIcon } from '@/shared/components/Icons'; import { cn } from '@/shared/shadcn/utils'; import { DragEvent, useCallback } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; @@ -41,40 +42,38 @@ export function FileDragDrop({ className }: FileDragDropProps) { [fileDragDropModal, handleFileImport, setFileDragDropModal, setNewFileDialogState] ); + const handleClose = useCallback(() => { + setFileDragDropModal({ show: false, teamUuid: undefined, isPrivate: undefined }); + }, [setFileDragDropModal]); + if (!fileDragDropModal.show) return null; return ( - <> +
    -
    -
    - Drop file here +
    + Drop file here - - Start a new spreadsheet by importing a CSV, Parquet, Excel or Grid file(s) - -
    + + Start a new spreadsheet by importing a CSV, Parquet, Excel or Grid file(s) +
    - - + +
    ); } diff --git a/quadratic-client/src/dashboard/components/FilesList.tsx b/quadratic-client/src/dashboard/components/FilesList.tsx index 51710c89f4..c2bcdad683 100644 --- a/quadratic-client/src/dashboard/components/FilesList.tsx +++ b/quadratic-client/src/dashboard/components/FilesList.tsx @@ -3,15 +3,13 @@ import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { FileDragDrop } from '@/dashboard/components/FileDragDrop'; import { DRAWER_WIDTH } from '@/routes/_dashboard'; import { Action as FilesAction } from '@/routes/api.files.$uuid'; -import { AddIcon } from '@/shared/components/Icons'; import { ShareFileDialog } from '@/shared/components/ShareDialog'; -import { ROUTES } from '@/shared/constants/routes'; import useLocalStorage from '@/shared/hooks/useLocalStorage'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { FilePermission, PublicLinkAccess } from 'quadratic-shared/typesAndSchemas'; import { ReactNode, useCallback, useState } from 'react'; import { isMobile } from 'react-device-detect'; -import { Link, useFetchers, useLocation } from 'react-router-dom'; +import { useFetchers, useLocation } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Empty } from './Empty'; import { FilesListItemExampleFile, FilesListItemUserFile, FilesListItems } from './FilesListItem'; @@ -125,18 +123,6 @@ export function FilesList({ /> - {teamUuid && ( -
  • - - - New file - (or drag and drop) - -
  • - )} {filesToRender.map((file, i) => ( +
    + + + - - - - - - Blank - An empty spreadsheet - - + + Data from… setFileDragDropState({ show: true, isPrivate })}> - + - Imported data - .csv, .xlsx, .pqt, .grid files + Local file + .csv, .xlsx, .pqt, .grid @@ -70,8 +62,8 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { - API data - HTTP requests from code + API + Fetch data over HTTP with code @@ -82,11 +74,12 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { Examples - Research, analysis, and modeling demos + Files from the Quadratic team + Data from connections @@ -154,8 +147,8 @@ function DialogContentNewFileFromApi({ children }: any) { Fetch data from an API - Example showing how to fetch data and put it on a spreadsheet. Create a file then you can tweak the code any - way you want including custom headers, authentication, and more. + This example shows how to fetch data and output it on the sheet. Once you create the file you can edit the + code however you wish, including custom headers, authentication, and more.
    @@ -273,7 +266,7 @@ values = df.iloc[0].tolist() - + ); From 7264bbd7989f13735b8c0b6ed1fc2fe0127b1d68 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 12 Nov 2024 05:03:48 -0800 Subject: [PATCH 21/62] fix bug with a '%' in rust code --- quadratic-core/src/date_time.rs | 6 +++++- quadratic-rust-client/src/lib.rs | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/date_time.rs b/quadratic-core/src/date_time.rs index c929db9939..b2396dd98b 100644 --- a/quadratic-core/src/date_time.rs +++ b/quadratic-core/src/date_time.rs @@ -97,7 +97,11 @@ fn find_items_date_start(items: &[Item<'_>]) -> Option { /// Converts a NaiveDateTime to a date and time string using a strftime format string. pub fn date_time_to_date_time_string(date_time: NaiveDateTime, format: Option) -> String { let format = format.map_or(DEFAULT_DATE_TIME_FORMAT.to_string(), |f| f); - date_time.format(&format).to_string() + let strftime_items = StrftimeItems::new(&format); + let Ok(items) = strftime_items.parse() else { + return date_time.format(DEFAULT_DATE_TIME_FORMAT).to_string(); + }; + date_time.format_with_items(items.iter()).to_string() } /// Converts a NaiveDateTime to a date-only string using a strftime format string. diff --git a/quadratic-rust-client/src/lib.rs b/quadratic-rust-client/src/lib.rs index e4e5920177..562bf8d512 100644 --- a/quadratic-rust-client/src/lib.rs +++ b/quadratic-rust-client/src/lib.rs @@ -25,3 +25,8 @@ impl SheetOffsetsWasm { SheetOffsets::default() } } + +#[wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); +} From c6d67233345676d07faafa31ed7fef91c7efec68 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Tue, 12 Nov 2024 10:15:00 -0700 Subject: [PATCH 22/62] fix new file in ocmmand palette --- quadratic-client/src/app/actions.ts | 4 ++-- .../ui/menus/CommandPalette/commands/File.tsx | 16 +++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/quadratic-client/src/app/actions.ts b/quadratic-client/src/app/actions.ts index 13ed77464b..ca0232a6de 100644 --- a/quadratic-client/src/app/actions.ts +++ b/quadratic-client/src/app/actions.ts @@ -66,8 +66,8 @@ export const isAvailableBecauseFileLocationIsAccessibleAndWriteable = ({ export const createNewFileAction = { label: 'New', isAvailable: isAvailableBecauseFileLocationIsAccessibleAndWriteable, - run({ setEditorInteractionState }: { setEditorInteractionState: SetterOrUpdater }) { - setEditorInteractionState((prevState) => ({ ...prevState, showNewFileMenu: true })); + run({ teamUuid }: { teamUuid: string }) { + window.location.href = ROUTES.CREATE_FILE_PRIVATE(teamUuid); }, }; diff --git a/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx b/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx index 5e55100afc..f02ebcc767 100644 --- a/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx +++ b/quadratic-client/src/app/ui/menus/CommandPalette/commands/File.tsx @@ -1,17 +1,14 @@ import { createNewFileAction, deleteFile, duplicateFileAction, isAvailableBecauseCanEditFile } from '@/app/actions'; import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; -import { - editorInteractionStateAtom, - editorInteractionStateUserAtom, - editorInteractionStateUuidAtom, -} from '@/app/atoms/editorInteractionStateAtom'; +import { editorInteractionStateUserAtom, editorInteractionStateUuidAtom } from '@/app/atoms/editorInteractionStateAtom'; import { useFileContext } from '@/app/ui/components/FileProvider'; import { CommandGroup, CommandPaletteListItem } from '@/app/ui/menus/CommandPalette/CommandPaletteListItem'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { DeleteIcon, DraftIcon, FileCopyIcon } from '@/shared/components/Icons'; +import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { useParams, useSubmit } from 'react-router-dom'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; // TODO: make the types better here so it knows whether this exists const renameFileActionSpec = defaultActionSpec[Action.FileRename]; @@ -25,9 +22,10 @@ const commands: CommandGroup = { keywords: ['New file', 'Create file'], isAvailable: createNewFileAction.isAvailable, Component: (props) => { - // TODO: create a private file - const setEditorInteractionState = useSetRecoilState(editorInteractionStateAtom); - const action = () => createNewFileAction.run({ setEditorInteractionState }); + const { + team: { uuid: teamUuid }, + } = useFileRouteLoaderData(); + const action = () => createNewFileAction.run({ teamUuid }); return } action={action} />; }, }, From a08bb159f8298a2a8545c678050cde59dea58d5a Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Wed, 13 Nov 2024 02:58:42 +0530 Subject: [PATCH 23/62] prevent browser defaults from inline editor --- .../HTMLGrid/inlineEditor/inlineEditorKeyboard.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index 9ff250e73c..2127c56b45 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -337,6 +337,12 @@ class InlineEditorKeyboard { inlineEditorMonaco.insertTextAtCursor(formattedTime); } + // prevent browser default behavior for these shortcuts + else if (matchShortcut(Action.ShowGoToMenu, e) || matchShortcut(Action.FindInCurrentSheet, e)) { + e.stopPropagation(); + e.preventDefault(); + } + // Fallback for all other keys (used to end cursorIsMoving and return // control to the formula box) else { @@ -355,13 +361,7 @@ class InlineEditorKeyboard { // Resets the keyboard position after cursorIsMoving has ended. resetKeyboardPosition(skipFocus?: boolean) { const location = inlineEditorHandler.location; - if (!location) { - return; - } - - if (!inlineEditorHandler.cursorIsMoving) { - return; - } + if (!location) return; inlineEditorHandler.cursorIsMoving = false; pixiApp.cellHighlights.clearHighlightedCell(); From 4f3c1c979e0944356b632e929b6bfddc2867ab01 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Wed, 13 Nov 2024 03:11:51 +0530 Subject: [PATCH 24/62] add goto and find shortcuts to inline editor --- .../inlineEditor/inlineEditorKeyboard.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index 2127c56b45..1296b4c2f9 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -2,6 +2,7 @@ //! handles when the cursorIsMoving outside of the inline formula edit box. import { Action } from '@/app/actions/actions'; +import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { getSingleSelection } from '@/app/grid/sheet/selection'; @@ -307,6 +308,24 @@ class InlineEditorKeyboard { } } + // show go to menu + else if (matchShortcut(Action.ShowGoToMenu, e)) { + e.stopPropagation(); + e.preventDefault(); + inlineEditorHandler.close(0, 0, false).then(() => { + defaultActionSpec[Action.ShowGoToMenu].run(); + }); + } + + // show find in current sheet + else if (matchShortcut(Action.FindInCurrentSheet, e)) { + e.stopPropagation(); + e.preventDefault(); + inlineEditorHandler.close(0, 0, false).then(() => { + defaultActionSpec[Action.FindInCurrentSheet].run(); + }); + } + // trigger cell type menu else if (matchShortcut(Action.ShowCellTypeMenu, e) && inlineEditorMonaco.get().length === 0) { e.preventDefault(); @@ -337,12 +356,6 @@ class InlineEditorKeyboard { inlineEditorMonaco.insertTextAtCursor(formattedTime); } - // prevent browser default behavior for these shortcuts - else if (matchShortcut(Action.ShowGoToMenu, e) || matchShortcut(Action.FindInCurrentSheet, e)) { - e.stopPropagation(); - e.preventDefault(); - } - // Fallback for all other keys (used to end cursorIsMoving and return // control to the formula box) else { From 0045c8a3da9f36f4a1a67a499382d2d043773685 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Tue, 12 Nov 2024 15:54:15 -0700 Subject: [PATCH 25/62] tweaks --- .../src/app/actions/viewActionsSpec.ts | 1 - .../app/atoms/editorInteractionStateAtom.ts | 5 - .../src/app/helpers/codeCellLanguage.ts | 2 +- quadratic-client/src/app/ui/QuadraticUI.tsx | 17 - .../src/app/ui/components/LanguageIcon.tsx | 4 +- quadratic-client/src/app/ui/icons/index.tsx | 74 ++--- .../TopBar/TopBarMenus/FileMenubarMenu.tsx | 11 +- .../dashboard/components/DashboardSidebar.tsx | 2 +- .../components/FilesListEmptyState.tsx | 17 +- .../dashboard/components/NewFileButton.tsx | 308 ++++-------------- .../routes/teams.$teamUuid.files.private.tsx | 7 +- 11 files changed, 125 insertions(+), 323 deletions(-) diff --git a/quadratic-client/src/app/actions/viewActionsSpec.ts b/quadratic-client/src/app/actions/viewActionsSpec.ts index e3ffc2ba63..9ba8d5fa86 100644 --- a/quadratic-client/src/app/actions/viewActionsSpec.ts +++ b/quadratic-client/src/app/actions/viewActionsSpec.ts @@ -123,7 +123,6 @@ export const viewActionsSpec: ViewActionSpec = { showConnectionsMenu: false, showGoToMenu: false, showFeedbackMenu: false, - showNewFileMenu: false, showRenameFileMenu: false, showShareFileMenu: false, showSearch: false, diff --git a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts index d789fae4a3..c210751699 100644 --- a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts +++ b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts @@ -11,7 +11,6 @@ export interface EditorInteractionState { showConnectionsMenu: boolean; showGoToMenu: boolean; showFeedbackMenu: boolean; - showNewFileMenu: boolean; showRenameFileMenu: boolean; showShareFileMenu: boolean; showSearch: boolean | SearchOptions; @@ -33,7 +32,6 @@ export const defaultEditorInteractionState: EditorInteractionState = { showConnectionsMenu: false, showGoToMenu: false, showFeedbackMenu: false, - showNewFileMenu: false, showRenameFileMenu: false, showShareFileMenu: false, showSearch: false, @@ -62,7 +60,6 @@ export const editorInteractionStateAtom = atom({ oldValue.showConnectionsMenu || oldValue.showGoToMenu || oldValue.showFeedbackMenu || - oldValue.showNewFileMenu || oldValue.showRenameFileMenu || oldValue.showShareFileMenu || oldValue.showSearch || @@ -73,7 +70,6 @@ export const editorInteractionStateAtom = atom({ newValue.showConnectionsMenu || newValue.showGoToMenu || newValue.showFeedbackMenu || - newValue.showNewFileMenu || newValue.showRenameFileMenu || newValue.showShareFileMenu || newValue.showSearch || @@ -103,7 +99,6 @@ export const editorInteractionStateShowCommandPaletteAtom = createSelector('show export const editorInteractionStateShowConnectionsMenuAtom = createSelector('showConnectionsMenu'); export const editorInteractionStateShowGoToMenuAtom = createSelector('showGoToMenu'); export const editorInteractionStateShowFeedbackMenuAtom = createSelector('showFeedbackMenu'); -export const editorInteractionStateShowNewFileMenuAtom = createSelector('showNewFileMenu'); export const editorInteractionStateShowRenameFileMenuAtom = createSelector('showRenameFileMenu'); export const editorInteractionStateShowShareFileMenuAtom = createSelector('showShareFileMenu'); export const editorInteractionStateShowSearchAtom = createSelector('showSearch'); diff --git a/quadratic-client/src/app/helpers/codeCellLanguage.ts b/quadratic-client/src/app/helpers/codeCellLanguage.ts index 6de5556309..b403c2fdce 100644 --- a/quadratic-client/src/app/helpers/codeCellLanguage.ts +++ b/quadratic-client/src/app/helpers/codeCellLanguage.ts @@ -2,7 +2,7 @@ import { CodeCellLanguage } from '@/app/quadratic-core-types'; export type CodeCellType = 'Python' | 'Javascript' | 'Formula' | 'Connection'; -const codeCellsById = { +export const codeCellsById = { Formula: { id: 'Formula', label: 'Formula', type: undefined }, Javascript: { id: 'Javascript', label: 'JavaScript', type: undefined }, Python: { id: 'Python', label: 'Python', type: undefined }, diff --git a/quadratic-client/src/app/ui/QuadraticUI.tsx b/quadratic-client/src/app/ui/QuadraticUI.tsx index 4624fdc5d9..78e7b2f1e1 100644 --- a/quadratic-client/src/app/ui/QuadraticUI.tsx +++ b/quadratic-client/src/app/ui/QuadraticUI.tsx @@ -1,7 +1,6 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { editorInteractionStatePermissionsAtom, - editorInteractionStateShowNewFileMenuAtom, editorInteractionStateShowRenameFileMenuAtom, editorInteractionStateShowShareFileMenuAtom, } from '@/app/atoms/editorInteractionStateAtom'; @@ -12,7 +11,6 @@ import { FileDragDropWrapper } from '@/app/ui/components/FileDragDropWrapper'; import { useFileContext } from '@/app/ui/components/FileProvider'; import { PermissionOverlay } from '@/app/ui/components/PermissionOverlay'; import PresentationModeHint from '@/app/ui/components/PresentationModeHint'; -import { useConnectionsFetcher } from '@/app/ui/hooks/useConnectionsFetcher'; import { AIAnalyst } from '@/app/ui/menus/AIAnalyst/AIAnalyst'; import { BottomBar } from '@/app/ui/menus/BottomBar/BottomBar'; import CellTypeMenu from '@/app/ui/menus/CellTypeMenu'; @@ -26,27 +24,20 @@ import { TopBar } from '@/app/ui/menus/TopBar/TopBar'; import { ValidationPanel } from '@/app/ui/menus/Validations/ValidationPanel'; import { QuadraticSidebar } from '@/app/ui/QuadraticSidebar'; import { UpdateAlertVersion } from '@/app/ui/UpdateAlertVersion'; -import { NewFileDialog } from '@/dashboard/components/NewFileDialog'; import { useRootRouteLoaderData } from '@/routes/_root'; import { DialogRenameItem } from '@/shared/components/DialogRenameItem'; import { ShareFileDialog } from '@/shared/components/ShareDialog'; import { UserMessage } from '@/shared/components/UserMessage'; -import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { useMemo } from 'react'; import { useNavigation, useParams } from 'react-router'; import { useRecoilState, useRecoilValue } from 'recoil'; export default function QuadraticUI() { const { isAuthenticated } = useRootRouteLoaderData(); - const { - team: { uuid: teamUuid }, - } = useFileRouteLoaderData(); - const connectionsFetcher = useConnectionsFetcher(); const navigation = useNavigation(); const { uuid } = useParams() as { uuid: string }; const { name, renameFile } = useFileContext(); const [showShareFileMenu, setShowShareFileMenu] = useRecoilState(editorInteractionStateShowShareFileMenuAtom); - const [showNewFileMenu, setShowNewFileMenu] = useRecoilState(editorInteractionStateShowNewFileMenuAtom); const [showRenameFileMenu, setShowRenameFileMenu] = useRecoilState(editorInteractionStateShowRenameFileMenuAtom); const presentationMode = useRecoilValue(presentationModeAtom); const permissions = useRecoilValue(editorInteractionStatePermissionsAtom); @@ -93,14 +84,6 @@ export default function QuadraticUI() { {/* Global overlay menus */} {showShareFileMenu && setShowShareFileMenu(false)} name={name} uuid={uuid} />} - {showNewFileMenu && ( - setShowNewFileMenu(false)} - isPrivate={true} - connections={connectionsFetcher.data ? connectionsFetcher.data.connections : []} - teamUuid={teamUuid} - /> - )} {presentationMode && } diff --git a/quadratic-client/src/app/ui/components/LanguageIcon.tsx b/quadratic-client/src/app/ui/components/LanguageIcon.tsx index ed44bfe387..98660b6de6 100644 --- a/quadratic-client/src/app/ui/components/LanguageIcon.tsx +++ b/quadratic-client/src/app/ui/components/LanguageIcon.tsx @@ -25,9 +25,9 @@ export function LanguageIcon({ language, ...props }: LanguageIconProps) { ) : language && 'mysql'.startsWith(language) ? ( ) : language && 'mssql'.startsWith(language) ? ( - + ) : language && 'snowflake'.startsWith(language) ? ( - + ) : ( ); diff --git a/quadratic-client/src/app/ui/icons/index.tsx b/quadratic-client/src/app/ui/icons/index.tsx index bf316fbb28..63b6149e90 100644 --- a/quadratic-client/src/app/ui/icons/index.tsx +++ b/quadratic-client/src/app/ui/icons/index.tsx @@ -70,44 +70,42 @@ export const MysqlIcon = (props: SvgIconProps) => ( export const MssqlIcon = (props: SvgIconProps) => ( - - - - - - - - - - - + + + + + + + + + ); diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index 63c4e41ddf..1393019307 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -7,10 +7,9 @@ import { MenubarItemAction } from '@/app/ui/menus/TopBar/TopBarMenus/MenubarItem import { useRootRouteLoaderData } from '@/routes/_root'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { DeleteIcon, DraftIcon, FileCopyIcon } from '@/shared/components/Icons'; -import { ROUTES } from '@/shared/constants/routes'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from '@/shared/shadcn/ui/menubar'; -import { Link, useSubmit } from 'react-router-dom'; +import { useSubmit } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; // TODO: (enhancement) move these into `fileActionsSpec` by making the `.run()` @@ -36,11 +35,9 @@ export const FileMenubarMenu = () => { File {createNewFileAction.isAvailable(isAvailableArgs) && ( - - - - {createNewFileAction.label} - + createNewFileAction.run({ teamUuid })}> + + {createNewFileAction.label} )} {duplicateFileAction.isAvailable(isAvailableArgs) && ( diff --git a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx index 78edd624fa..ef77a3e653 100644 --- a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx +++ b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx @@ -103,7 +103,7 @@ export function DashboardSidebar({ isLoading }: { isLoading: boolean }) { My files - New private file + New personal file
    diff --git a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx index f24c900e28..a2aed802ea 100644 --- a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx +++ b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx @@ -1,11 +1,16 @@ -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { Empty } from '@/dashboard/components/Empty'; +import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; +import { ROUTES } from '@/shared/constants/routes'; import { FileIcon } from '@radix-ui/react-icons'; import mixpanel from 'mixpanel-browser'; -import { useSetRecoilState } from 'recoil'; +import { Link } from 'react-router-dom'; export const FilesListEmptyState = ({ isPrivate = false }: { isPrivate?: boolean }) => { - const setNewFileDialogState = useSetRecoilState(newFileDialogAtom); + const { + activeTeam: { + team: { uuid: teamUuid }, + }, + } = useDashboardRouteLoaderData(); return (
    @@ -15,15 +20,15 @@ export const FilesListEmptyState = ({ isPrivate = false }: { isPrivate?: boolean description={ <> You don’t have any files yet.{' '} - {' '} + {' '} or drag and drop a CSV, Excel, Parquet, or Quadratic file here. } diff --git a/quadratic-client/src/dashboard/components/NewFileButton.tsx b/quadratic-client/src/dashboard/components/NewFileButton.tsx index d34678dcaa..ff1ef8fb51 100644 --- a/quadratic-client/src/dashboard/components/NewFileButton.tsx +++ b/quadratic-client/src/dashboard/components/NewFileButton.tsx @@ -1,19 +1,13 @@ +import { codeCellsById } from '@/app/helpers/codeCellLanguage'; +import { supportedFileTypes } from '@/app/helpers/files'; import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; -import { fileDragDropModalAtom } from '@/dashboard/atoms/fileDragDropModalAtom'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; -import { ApiIcon, ArrowDropDownIcon, DatabaseIcon, DraftIcon, ExamplesIcon, UndoIcon } from '@/shared/components/Icons'; +import { useFileImport } from '@/app/ui/hooks/useFileImport'; +import { SNIPPET_PY_API } from '@/app/ui/menus/CodeEditor/snippetsPY'; +import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; +import { ApiIcon, ArrowDropDownIcon, DatabaseIcon, DraftIcon, ExamplesIcon } from '@/shared/components/Icons'; import { ROUTES } from '@/shared/constants/routes'; import { Button } from '@/shared/shadcn/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/shared/shadcn/ui/dialog'; +import { Dialog } from '@/shared/shadcn/ui/dialog'; import { DropdownMenu, DropdownMenuContent, @@ -22,24 +16,40 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/shared/shadcn/ui/dropdown-menu'; -import { Input } from '@/shared/shadcn/ui/input'; -import { Label } from '@/shared/shadcn/ui/label'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/shared/shadcn/ui/tooltip'; -import { cn } from '@/shared/shadcn/utils'; -import { useState } from 'react'; -import { Link, useParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useRef } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const stateToInsertAndRun = { language: 'Python', codeString: SNIPPET_PY_API } as const; export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { - const { teamUuid } = useParams() as { teamUuid: string }; - const setNewFileDialogState = useSetRecoilState(newFileDialogAtom); - const setFileDragDropState = useSetRecoilState(fileDragDropModalAtom); + const { + activeTeam: { + connections, + team: { uuid: teamUuid }, + }, + } = useDashboardRouteLoaderData(); + const navigate = useNavigate(); + const handleFileImport = useFileImport(); + const fileInputRef = useRef(null); return (
    + { + const files = e.target.files; + if (files) { + handleFileImport({ files, isPrivate, teamUuid }); + } + }} + /> @@ -49,28 +59,34 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { Data from… - setFileDragDropState({ show: true, isPrivate })}> - - + fileInputRef.current?.click()}> + Local file .csv, .xlsx, .pqt, .grid - - - + + + API Fetch data over HTTP with code - - + + - + Examples @@ -80,223 +96,27 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { Data from connections - - - - Quadratic (production) - Postgres connection - - - - - - Customer data - MySQL connection - - - - See all connections... + {connections.map(({ uuid, name, type }) => { + const { label } = codeCellsById[type]; + return ( + + + + {name} + {label} + + + ); + })} + + navigate(ROUTES.TEAM_CONNECTIONS(teamUuid))}> + See all connections... - -
    ); } -type LoadState = 'idle' | 'loading' | 'error'; -function DialogContentNewFileFromApi({ children }: any) { - const [loadState, setLoadState] = useState('idle'); - const [json, setJson] = useState>({ - id: 'R7UfaahVfFd', - joke: 'My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.', - status: 200, - }); - const defaultUrl = 'https://icanhazdadjoke.com'; - const [url, setUrl] = useState(defaultUrl); - const [activeLanguage, setActiveLanguage] = useState('Javascript'); - - // TODO: needs better error handling - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - setLoadState('loading'); - fetch(url, { headers: { Accept: 'application/json' } }) - .then((res) => res.json()) - .then((newJson) => { - setJson(newJson); - setLoadState('idle'); - }) - .catch((err) => { - setLoadState('error'); - }); - }; - - return ( - - - Fetch data from an API - - This example shows how to fetch data and output it on the sheet. Once you create the file you can edit the - code however you wish, including custom headers, authentication, and more. - - -
    -
    -
    - {['Python', 'Javascript'].map((language) => ( - - ))} -
    -
    - setUrl(e.target.value)} /> - -
    -
    - {loadState === 'error' && ( -
    - Error retrieiving data. Start a file anyway to continue debugging. - - { - setLoadState('idle'); - setUrl(defaultUrl); - }} - > - - - Reset - -
    - )} - -
    -
    -            {activeLanguage === 'Javascript'
    -              ? `const res = await fetch(
    -  "${url}",
    -  { headers: { Accept: 'application/json' }
    -});
    -const json = await res.json();
    -return [
    -  Object.keys(json),
    -  Object.values(json),
    -];`
    -              : `import requests
    -import pandas as pd
    -res = requests.get(
    -  'https://icanhazdadjoke.com',
    -  headers={"Accept": "application/json"}
    -)
    -df = pd.DataFrame([response.json()])
    -keys = df.columns.tolist()
    -values = df.iloc[0].tolist()
    -[keys, values]
    -`}
    -          
    -
    -
    -
    - - - - - - - - - - - - - - - - - {Object.keys(json).map((key) => ( - - ))} - - - - - - - {Object.values(json).map((value) => ( - - ))} - - - - - - - - - - - - - - -
    ABCDEFG
    1{key}
    2{typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
    3
    -
    - - - - - - -
    - ); -} - -function TD({ - children, - as = 'td', - isFirstCol = false, - className, -}: { - children?: React.ReactNode; - as?: 'td' | 'th'; - isFirstCol?: boolean; - className?: string; -}) { - const Component = as; - - return ( - - {children} - - ); -} +// newNewFileFromStateConnection diff --git a/quadratic-client/src/routes/teams.$teamUuid.files.private.tsx b/quadratic-client/src/routes/teams.$teamUuid.files.private.tsx index ee1ce55938..54e893c5a3 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.files.private.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.files.private.tsx @@ -35,7 +35,12 @@ export const Component = () => { return ( <> } /> - } teamUuid={teamUuid} isPrivate={true} /> + } + teamUuid={teamUuid} + isPrivate={true} + /> ); }; From e134f93adead74c3df2dbcdc6352c4a8809057f6 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Wed, 13 Nov 2024 21:02:29 +0530 Subject: [PATCH 26/62] command palette, switch sheet --- .../inlineEditor/inlineEditorKeyboard.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index 1296b4c2f9..5dc029f172 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts @@ -326,6 +326,29 @@ class InlineEditorKeyboard { }); } + // show command palette + else if (matchShortcut(Action.ShowCommandPalette, e)) { + e.stopPropagation(); + e.preventDefault(); + inlineEditorHandler.close(0, 0, false).then(() => { + defaultActionSpec[Action.ShowCommandPalette].run(); + }); + } + + // switch sheet next + else if (matchShortcut(Action.SwitchSheetNext, e)) { + e.stopPropagation(); + e.preventDefault(); + defaultActionSpec[Action.SwitchSheetNext].run(); + } + + // switch sheet previous + else if (matchShortcut(Action.SwitchSheetPrevious, e)) { + e.stopPropagation(); + e.preventDefault(); + defaultActionSpec[Action.SwitchSheetPrevious].run(); + } + // trigger cell type menu else if (matchShortcut(Action.ShowCellTypeMenu, e) && inlineEditorMonaco.get().length === 0) { e.preventDefault(); From 941af165db9457bd1f812a6a0266239240e1e277 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 13 Nov 2024 14:11:50 -0700 Subject: [PATCH 27/62] updates --- .../dashboard/components/NewFileButton.tsx | 42 ++++++--- ...onnectionSchemaBrowserTableQueryAction.tsx | 45 +++++---- .../connections/ConnectionSchemaBrowser.tsx | 94 +++++++++---------- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/quadratic-client/src/dashboard/components/NewFileButton.tsx b/quadratic-client/src/dashboard/components/NewFileButton.tsx index ff1ef8fb51..f7bc6d6d39 100644 --- a/quadratic-client/src/dashboard/components/NewFileButton.tsx +++ b/quadratic-client/src/dashboard/components/NewFileButton.tsx @@ -6,6 +6,7 @@ import { SNIPPET_PY_API } from '@/app/ui/menus/CodeEditor/snippetsPY'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import { ApiIcon, ArrowDropDownIcon, DatabaseIcon, DraftIcon, ExamplesIcon } from '@/shared/components/Icons'; import { ROUTES } from '@/shared/constants/routes'; +import { newNewFileFromStateConnection } from '@/shared/hooks/useNewFileFromState'; import { Button } from '@/shared/shadcn/ui/button'; import { Dialog } from '@/shared/shadcn/ui/dialog'; import { @@ -19,6 +20,7 @@ import { import { useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; +const CONNECTIONS_DISPLAY_LIMIT = 3; const stateToInsertAndRun = { language: 'Python', codeString: SNIPPET_PY_API } as const; export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { @@ -96,27 +98,41 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { Data from connections - {connections.map(({ uuid, name, type }) => { + {connections.slice(0, CONNECTIONS_DISPLAY_LIMIT).map(({ uuid, name, type }) => { const { label } = codeCellsById[type]; + const to = newNewFileFromStateConnection({ + isPrivate, + teamUuid, + query: '', + connectionType: type, + connectionUuid: uuid, + }); return ( - - - - {name} - {label} - + + + + + {name} + {label} + + ); })} - - navigate(ROUTES.TEAM_CONNECTIONS(teamUuid))}> - See all connections... - + {connections.length > CONNECTIONS_DISPLAY_LIMIT && ( + navigate(ROUTES.TEAM_CONNECTIONS(teamUuid))}> + + + All connections + + {connections.length - CONNECTIONS_DISPLAY_LIMIT} more + + + + )}
    ); } - -// newNewFileFromStateConnection diff --git a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx index 910bba8261..b44f8093b7 100644 --- a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx +++ b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx @@ -1,8 +1,6 @@ -import { NewFileIcon } from '@/dashboard/components/CustomRadixIcons'; +import { useSaveAndRunCell } from '@/app/ui/menus/CodeEditor/hooks/useSaveAndRunCell'; import { newNewFileFromStateConnection } from '@/shared/hooks/useNewFileFromState'; import { Button } from '@/shared/shadcn/ui/button'; -import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; -import { ClipboardCopyIcon } from '@radix-ui/react-icons'; import mixpanel from 'mixpanel-browser'; import * as monaco from 'monaco-editor'; import { ConnectionType } from 'quadratic-shared/typesAndSchemasConnections'; @@ -26,19 +24,17 @@ export const useConnectionSchemaBrowserTableQueryActionNewFile = ({ TableQueryAction: ({ query }: { query: string }) => { const to = newNewFileFromStateConnection({ query, isPrivate, teamUuid, connectionType, connectionUuid }); return ( - - - + ); }, }; @@ -50,12 +46,14 @@ export const useConnectionSchemaBrowserTableQueryActionInsertQuery = ({ editorInst: monaco.editor.IStandaloneCodeEditor | null; }) => { return { - TableQueryAction: ({ query }: { query: string }) => ( - + TableQueryAction: ({ query }: { query: string }) => { + const { saveAndRunCell } = useSaveAndRunCell(); + return ( - - ), + ); + }, }; }; diff --git a/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx b/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx index b9c892bfd5..ad8166a349 100644 --- a/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx +++ b/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx @@ -1,15 +1,16 @@ import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; +import { ChevronRightIcon, RefreshIcon } from '@/shared/components/Icons'; import { Type } from '@/shared/components/Type'; import { ROUTES } from '@/shared/constants/routes'; import { CONTACT_URL } from '@/shared/constants/urls'; import { useConnectionSchemaBrowser } from '@/shared/hooks/useConnectionSchemaBrowser'; import { Button } from '@/shared/shadcn/ui/button'; +import { Label } from '@/shared/shadcn/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/shared/shadcn/ui/radio-group'; import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; import { cn } from '@/shared/shadcn/utils'; -import { KeyboardArrowRight } from '@mui/icons-material'; -import { ReloadIcon } from '@radix-ui/react-icons'; import { ConnectionType } from 'quadratic-shared/typesAndSchemasConnections'; -import { ReactNode, useState } from 'react'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; export const ConnectionSchemaBrowser = ({ @@ -26,6 +27,7 @@ export const ConnectionSchemaBrowser = ({ uuid?: string; }) => { const { data, isLoading, reloadSchema } = useConnectionSchemaBrowser({ type, uuid }); + const [selectedTableIndex, setSelectedTableIndex] = useState(0); if (type === undefined || uuid === undefined) return null; @@ -34,20 +36,22 @@ export const ConnectionSchemaBrowser = ({
    -
    +
    {data && data.type ? ( -
    +
    ) : null}

    {data?.name ? data.name : ''}

    -
    - {/* {data?.tables ? data.tables.length + ' tables' : ''} */} +
    + -
    @@ -55,17 +59,20 @@ export const ConnectionSchemaBrowser = ({ {isLoading && data === undefined && (
    Loading…
    )} + {data && ( -
      + { + const newIndex = Number(newIndexStr); + setSelectedTableIndex(newIndex); + }} + className="block" + > {data.tables.map((table, i) => ( - } - /> + ))} -
    + )} {data === null && (
    @@ -113,60 +120,53 @@ type Column = { }; function TableListItem({ + index, data: { name, columns, schema }, selfContained, - tableQuery, }: { + index: number; data: Table; selfContained?: boolean; - tableQuery: ReactNode; }) { const [isExpanded, setIsExpanded] = useState(false); + const id = `sql-table-${index}`; return ( -
  • -
    +
    +
    + + + {isExpanded && ( -
      +
        {columns.length ? ( columns.map(({ name, type, is_nullable }, k) => ( -
      • +
      • {name}
        -
        +
        {type} {is_nullable && '?'}
        @@ -180,7 +180,7 @@ function TableListItem({ )}
      )} - +
    ); } From aa763821b8c1c69628763976c7516606c488a6f6 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 13 Nov 2024 14:20:22 -0700 Subject: [PATCH 28/62] Update FileDragDrop.tsx --- quadratic-client/src/dashboard/components/FileDragDrop.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/quadratic-client/src/dashboard/components/FileDragDrop.tsx b/quadratic-client/src/dashboard/components/FileDragDrop.tsx index 5c5741eaa9..a8f61c1281 100644 --- a/quadratic-client/src/dashboard/components/FileDragDrop.tsx +++ b/quadratic-client/src/dashboard/components/FileDragDrop.tsx @@ -1,6 +1,5 @@ import { useFileImport } from '@/app/ui/hooks/useFileImport'; import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; -import { CloseIcon } from '@/shared/components/Icons'; import { cn } from '@/shared/shadcn/utils'; import { DragEvent, useCallback } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; @@ -42,10 +41,6 @@ export function FileDragDrop({ className }: FileDragDropProps) { [fileDragDropModal, handleFileImport, setFileDragDropModal, setNewFileDialogState] ); - const handleClose = useCallback(() => { - setFileDragDropModal({ show: false, teamUuid: undefined, isPrivate: undefined }); - }, [setFileDragDropModal]); - if (!fileDragDropModal.show) return null; return ( @@ -55,7 +50,6 @@ export function FileDragDrop({ className }: FileDragDropProps) { 'fixed left-0 top-0 z-20 flex h-full w-full flex-col items-center justify-center bg-white opacity-90', className )} - onClick={handleClose} onDragEnter={handleDrag} onDragLeave={handleDrag} onDragOver={handleDrag} @@ -73,7 +67,6 @@ export function FileDragDrop({ className }: FileDragDropProps) {
  • -
    ); } From cba19c53aeda0e2acbc8f6ea9a44e4319e109085 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 14 Nov 2024 12:01:03 -0700 Subject: [PATCH 29/62] Update NewFileButton.tsx --- .../src/dashboard/components/NewFileButton.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/dashboard/components/NewFileButton.tsx b/quadratic-client/src/dashboard/components/NewFileButton.tsx index f7bc6d6d39..4670d8f3e4 100644 --- a/quadratic-client/src/dashboard/components/NewFileButton.tsx +++ b/quadratic-client/src/dashboard/components/NewFileButton.tsx @@ -4,7 +4,7 @@ import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; import { useFileImport } from '@/app/ui/hooks/useFileImport'; import { SNIPPET_PY_API } from '@/app/ui/menus/CodeEditor/snippetsPY'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; -import { ApiIcon, ArrowDropDownIcon, DatabaseIcon, DraftIcon, ExamplesIcon } from '@/shared/components/Icons'; +import { AddIcon, ApiIcon, ArrowDropDownIcon, DatabaseIcon, DraftIcon, ExamplesIcon } from '@/shared/components/Icons'; import { ROUTES } from '@/shared/constants/routes'; import { newNewFileFromStateConnection } from '@/shared/hooks/useNewFileFromState'; import { Button } from '@/shared/shadcn/ui/button'; @@ -109,7 +109,7 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { }); return ( - + {name} @@ -123,13 +123,22 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { navigate(ROUTES.TEAM_CONNECTIONS(teamUuid))}> - All connections + View all connections {connections.length - CONNECTIONS_DISPLAY_LIMIT} more )} + {connections.length === 0 && ( + navigate(ROUTES.TEAM_CONNECTIONS(teamUuid))}> + + + Add a connection + Postgres, SQL, Snowflake, & more + + + )} From fc5f56595478d0e65fd2072a47db9e7cd415f69f Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sun, 17 Nov 2024 09:13:15 +0530 Subject: [PATCH 30/62] fix: deleted code cell artifacts still showing up --- .../controller/execution/receive_multiplayer.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/controller/execution/receive_multiplayer.rs b/quadratic-core/src/controller/execution/receive_multiplayer.rs index 6534dd3889..9db0b5f43a 100644 --- a/quadratic-core/src/controller/execution/receive_multiplayer.rs +++ b/quadratic-core/src/controller/execution/receive_multiplayer.rs @@ -224,7 +224,7 @@ impl GridController { /// Received transactions from the server pub fn received_transactions(&mut self, transactions: Vec) { // used to track client changes when combining transactions - let results = PendingTransaction { + let mut results = PendingTransaction { transaction_type: TransactionType::Multiplayer, ..Default::default() }; @@ -245,6 +245,18 @@ impl GridController { }; self.client_apply_transaction(&mut transaction, t.sequence_num); + results.generate_thumbnail |= transaction.generate_thumbnail; + results.validations.extend(transaction.validations); + results.dirty_hashes.extend(transaction.dirty_hashes); + results.sheet_borders.extend(transaction.sheet_borders); + results.code_cells.extend(transaction.code_cells); + results.html_cells.extend(transaction.html_cells); + results.image_cells.extend(transaction.image_cells); + results.fill_cells.extend(transaction.fill_cells); + results.sheet_info.extend(transaction.sheet_info); + results + .offsets_modified + .extend(transaction.offsets_modified); } else { dbgjs!( "Unable to decompress and deserialize operations in received_transactions()" From e1c03bdff47e198f3418549075ccdd5f2c869235 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 18 Nov 2024 13:51:14 -0700 Subject: [PATCH 31/62] remove unused code --- .../src/app/ui/hooks/useFileImport.tsx | 4 - .../src/dashboard/atoms/newFileDialogAtom.ts | 16 -- .../dashboard/components/DashboardSidebar.tsx | 2 +- .../src/dashboard/components/FileDragDrop.tsx | 7 +- .../src/dashboard/components/FilesList.tsx | 8 +- .../dashboard/components/NewFileDialog.tsx | 247 ------------------ ...onnectionSchemaBrowserTableQueryAction.tsx | 4 +- quadratic-client/src/routes/_dashboard.tsx | 26 +- 8 files changed, 8 insertions(+), 306 deletions(-) delete mode 100644 quadratic-client/src/dashboard/atoms/newFileDialogAtom.ts delete mode 100644 quadratic-client/src/dashboard/components/NewFileDialog.tsx diff --git a/quadratic-client/src/app/ui/hooks/useFileImport.tsx b/quadratic-client/src/app/ui/hooks/useFileImport.tsx index 05f44fd727..b78535312d 100644 --- a/quadratic-client/src/app/ui/hooks/useFileImport.tsx +++ b/quadratic-client/src/app/ui/hooks/useFileImport.tsx @@ -3,7 +3,6 @@ import { getFileType, stripExtension, supportedFileTypes, uploadFile } from '@/a import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { FileImportProgress, filesImportProgressAtom } from '@/dashboard/atoms/filesImportProgressAtom'; import { filesImportProgressListAtom } from '@/dashboard/atoms/filesImportProgressListAtom'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { apiClient } from '@/shared/api/apiClient'; import { ApiError } from '@/shared/api/fetchFromApi'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; @@ -15,7 +14,6 @@ import { useSetRecoilState } from 'recoil'; export function useFileImport() { const setFilesImportProgressState = useSetRecoilState(filesImportProgressAtom); const setFilesImportProgressListState = useSetRecoilState(filesImportProgressListAtom); - const setNewFileDialogState = useSetRecoilState(newFileDialogAtom); const { addGlobalSnackbar } = useGlobalSnackbar(); @@ -192,7 +190,6 @@ export function useFileImport() { updateCurrentFileState({ step: 'done', progress: 100, uuid, abortController: undefined }); if (openImportedFile) { setFilesImportProgressListState({ show: false }); - setNewFileDialogState((prev) => ({ ...prev, show: false })); window.location.href = ROUTES.FILE(uuid); } }) @@ -235,7 +232,6 @@ export function useFileImport() { } setFilesImportProgressState((prev) => ({ ...prev, importing: false })); - setNewFileDialogState((prev) => ({ ...prev, show: false })); }; return handleImport; diff --git a/quadratic-client/src/dashboard/atoms/newFileDialogAtom.ts b/quadratic-client/src/dashboard/atoms/newFileDialogAtom.ts deleted file mode 100644 index 94df76eaf4..0000000000 --- a/quadratic-client/src/dashboard/atoms/newFileDialogAtom.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { atom } from 'recoil'; - -interface NewFileDialogState { - show: boolean; - isPrivate: boolean; -} - -const defaultNewFileDialogState: NewFileDialogState = { - show: false, - isPrivate: true, -}; - -export const newFileDialogAtom = atom({ - key: 'newFileDialog', - default: defaultNewFileDialogState, -}); diff --git a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx index ef77a3e653..a99f849d79 100644 --- a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx +++ b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx @@ -191,7 +191,7 @@ function SidebarNavLinkCreateButton({ asChild variant="ghost" size="icon-sm" - className="absolute right-2 top-1 ml-auto opacity-30 hover:opacity-100" + className="absolute right-2 top-1 ml-auto !bg-transparent opacity-30 hover:opacity-100" > diff --git a/quadratic-client/src/dashboard/components/FileDragDrop.tsx b/quadratic-client/src/dashboard/components/FileDragDrop.tsx index a8f61c1281..2b9bab4818 100644 --- a/quadratic-client/src/dashboard/components/FileDragDrop.tsx +++ b/quadratic-client/src/dashboard/components/FileDragDrop.tsx @@ -1,8 +1,7 @@ import { useFileImport } from '@/app/ui/hooks/useFileImport'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { cn } from '@/shared/shadcn/utils'; import { DragEvent, useCallback } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { fileDragDropModalAtom } from '../atoms/fileDragDropModalAtom'; interface FileDragDropProps { @@ -11,7 +10,6 @@ interface FileDragDropProps { export function FileDragDrop({ className }: FileDragDropProps) { const [fileDragDropModal, setFileDragDropModal] = useRecoilState(fileDragDropModalAtom); - const setNewFileDialogState = useSetRecoilState(newFileDialogAtom); const handleFileImport = useFileImport(); const handleDrag = useCallback( @@ -32,13 +30,12 @@ export function FileDragDrop({ className }: FileDragDropProps) { e.stopPropagation(); setFileDragDropModal({ show: false, teamUuid: undefined, isPrivate: undefined }); - setNewFileDialogState((prev) => ({ ...prev, show: false })); const files = e.dataTransfer.files; const { isPrivate, teamUuid } = fileDragDropModal; handleFileImport({ files, isPrivate, teamUuid }); }, - [fileDragDropModal, handleFileImport, setFileDragDropModal, setNewFileDialogState] + [fileDragDropModal, handleFileImport, setFileDragDropModal] ); if (!fileDragDropModal.show) return null; diff --git a/quadratic-client/src/dashboard/components/FilesList.tsx b/quadratic-client/src/dashboard/components/FilesList.tsx index a0a5b885cf..29af05797e 100644 --- a/quadratic-client/src/dashboard/components/FilesList.tsx +++ b/quadratic-client/src/dashboard/components/FilesList.tsx @@ -1,5 +1,4 @@ import { fileDragDropModalAtom } from '@/dashboard/atoms/fileDragDropModalAtom'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { FileDragDrop } from '@/dashboard/components/FileDragDrop'; import { DRAWER_WIDTH } from '@/routes/_dashboard'; import { Action as FilesAction } from '@/routes/api.files.$uuid'; @@ -10,7 +9,7 @@ import { FilePermission, PublicLinkAccess } from 'quadratic-shared/typesAndSchem import { ReactNode, useCallback, useState } from 'react'; import { isMobile } from 'react-device-detect'; import { useFetchers, useLocation } from 'react-router-dom'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { Empty } from './Empty'; import { FilesListItemExampleFile, FilesListItemUserFile, FilesListItems } from './FilesListItem'; import { FilesListViewControls } from './FilesListViewControls'; @@ -129,7 +128,6 @@ export function FilesList({ const filesBeingDeleted = fetchers.filter((fetcher) => (fetcher.json as FilesAction['request'])?.action === 'delete'); const activeShareMenuFileName = files.find((file) => file.uuid === activeShareMenuFileId)?.name || ''; - const { show: newFileDialogShow } = useRecoilValue(newFileDialogAtom); const setFileDragDropState = useSetRecoilState(fileDragDropModalAtom); const handleDragEnter = useCallback( (e: React.DragEvent) => { @@ -177,9 +175,7 @@ export function FilesList({ /> )} - {!newFileDialogShow && ( - - )} +
    ); } diff --git a/quadratic-client/src/dashboard/components/NewFileDialog.tsx b/quadratic-client/src/dashboard/components/NewFileDialog.tsx deleted file mode 100644 index d561cc6034..0000000000 --- a/quadratic-client/src/dashboard/components/NewFileDialog.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; -import { useFileImport } from '@/app/ui/hooks/useFileImport'; -import { fileDragDropModalAtom } from '@/dashboard/atoms/fileDragDropModalAtom'; -import { FileDragDrop } from '@/dashboard/components/FileDragDrop'; -import { useConnectionSchemaBrowserTableQueryActionNewFile } from '@/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction'; -import { ConnectionSchemaBrowser } from '@/shared/components/connections/ConnectionSchemaBrowser'; -import { PrivateFileToggle } from '@/shared/components/connections/PrivateFileToggle'; -import { AddIcon, ApiIcon, DatabaseIcon, ExamplesIcon, FilePrivateIcon, ImportIcon } from '@/shared/components/Icons'; -import { ROUTES } from '@/shared/constants/routes'; -import { useNewFileFromStatePythonApi } from '@/shared/hooks/useNewFileFromState'; -import { Button } from '@/shared/shadcn/ui/button'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/shared/shadcn/ui/dialog'; -import { cn } from '@/shared/shadcn/utils'; -import { ArrowLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons'; -import { ConnectionList, ConnectionType } from 'quadratic-shared/typesAndSchemasConnections'; -import { useCallback, useMemo, useState } from 'react'; -import { Link, useLocation, useNavigation } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; - -const SHOW_EXAMPLES = import.meta.env.VITE_STORAGE_TYPE !== 'file-system'; - -type Props = { - connections: ConnectionList; - onClose: () => void; - teamUuid: string; - isPrivate: boolean; -}; - -export function NewFileDialog({ connections, teamUuid, onClose, isPrivate: initialIsPrivate }: Props) { - const location = useLocation(); - const navigation = useNavigation(); - const [isPrivate, setIsPrivate] = useState(!!initialIsPrivate); - const [activeConnectionUuid, setActiveConnectionUuid] = useState(''); - const handleFileImport = useFileImport(); - const newFileApiHref = useNewFileFromStatePythonApi({ isPrivate, teamUuid }); - - const gridItemClassName = - 'flex flex-col items-center justify-center gap-1 rounded-lg border border-border p-4 pt-5 w-full group'; - const gridItemInteractiveClassName = 'hover:bg-accent hover:text-foreground cursor-pointer'; - - const activeConnection = useMemo( - () => connections.find((connection) => connection.uuid === activeConnectionUuid), - [activeConnectionUuid, connections] - ); - - // Do an in-memory navigation if we're not in the app - const reloadDocument = useMemo(() => location.pathname.startsWith('/file/'), [location.pathname]); - - const setFileDragDropState = useSetRecoilState(fileDragDropModalAtom); - const handleDragEnter = useCallback( - (e: React.DragEvent) => { - if (!e.dataTransfer.types.includes('Files')) return; - setFileDragDropState({ show: true, teamUuid, isPrivate }); - }, - [isPrivate, setFileDragDropState, teamUuid] - ); - - return ( - - {/* overflow: visible here fixes a bug with the tooltip being cut off */} - - {navigation.state !== 'idle' && ( -
    - )} - - - {activeConnection ? ( - <> - - New file from connection - - ) : ( - 'New file' - )} - {isPrivate && } - - - setIsPrivate((prev) => !prev)} - > - Create in:{' '} - - - - {activeConnection ? ( - - ) : ( -
      -
    • - - - - - Blank - -
    • -
    • - -
    • -
    • - - - - - Fetch data from an API - -
    • - {SHOW_EXAMPLES && ( -
    • - - - - - Learn from an example file - -
    • - )} -
    • -
      - - - - Query data from a connection -
      - {connections.length > 0 ? ( -
        - {connections.slice(0, 5).map((connection) => ( -
      • - -
      • - ))} -
      - ) : ( -
      - No connections,{' '} - - create one - - . -
      - )} -
    • -
    - )} - - - -
    - ); -} - -function ItemIcon({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) { - return ( -
    - {children} -
    - ); -} - -function SchemaBrowser({ - connectionType, - connectionUuid, - isPrivate, - teamUuid, -}: { - connectionType: ConnectionType; - connectionUuid: string; - isPrivate: boolean; - teamUuid: string; -}) { - const { TableQueryAction } = useConnectionSchemaBrowserTableQueryActionNewFile({ - connectionType, - connectionUuid, - isPrivate, - teamUuid, - }); - - return ( - - ); -} diff --git a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx index b44f8093b7..28736c4f28 100644 --- a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx +++ b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx @@ -31,8 +31,8 @@ export const useConnectionSchemaBrowserTableQueryActionNewFile = ({ mixpanel.track('[Connections].schemaViewer.newFileFromTable'); }} > - ); diff --git a/quadratic-client/src/routes/_dashboard.tsx b/quadratic-client/src/routes/_dashboard.tsx index e139089340..4cb51edd63 100644 --- a/quadratic-client/src/routes/_dashboard.tsx +++ b/quadratic-client/src/routes/_dashboard.tsx @@ -1,10 +1,8 @@ import { useCheckForAuthorizationTokenOnWindowFocus } from '@/auth/auth'; -import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { DashboardSidebar } from '@/dashboard/components/DashboardSidebar'; import { EducationDialog } from '@/dashboard/components/EducationDialog'; import { Empty } from '@/dashboard/components/Empty'; import { ImportProgressList } from '@/dashboard/components/ImportProgressList'; -import { NewFileDialog } from '@/dashboard/components/NewFileDialog'; import { apiClient } from '@/shared/api/apiClient'; import { MenuIcon } from '@/shared/components/Icons'; import { ROUTES, ROUTE_LOADER_IDS, SEARCH_PARAMS } from '@/shared/constants/routes'; @@ -31,7 +29,7 @@ import { useRouteLoaderData, useSearchParams, } from 'react-router-dom'; -import { RecoilRoot, useRecoilState } from 'recoil'; +import { RecoilRoot } from 'recoil'; export const DRAWER_WIDTH = 264; export const ACTIVE_TEAM_UUID_KEY = 'activeTeamUuid'; @@ -225,34 +223,12 @@ export const Component = () => {
    {searchParams.get(SEARCH_PARAMS.DIALOG.KEY) === SEARCH_PARAMS.DIALOG.VALUES.EDUCATION && }
    - ); }; -function NewFileDialogWrapper() { - const [newFileDialogState, setNewFileDialogState] = useRecoilState(newFileDialogAtom); - const { - activeTeam: { - connections, - team: { uuid: teamUuid }, - }, - } = useDashboardRouteLoaderData(); - - if (!newFileDialogState.show) return null; - - return ( - setNewFileDialogState((prev) => ({ ...prev, show: false }))} - isPrivate={newFileDialogState.isPrivate} - /> - ); -} - export const ErrorBoundary = () => { const error = useRouteError(); From b2a13b45dd00f56255510f694dff3d729bc0bc0c Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 19 Nov 2024 03:50:47 +0530 Subject: [PATCH 32/62] feat: request proxying for JS & fix auth header override for both JS and Python requests --- .../javascriptClientMessages.ts | 26 ++++++++-- .../javascriptWebWorker.ts | 13 ++++- .../javascript/getJavascriptFetchOverride.ts | 29 +++++++++++ .../javascript/getJavascriptXHROverride.ts | 50 +++++++++++++++++++ .../worker/javascript/javascript.ts | 6 ++- .../worker/javascript/javascriptCompile.ts | 11 +++- .../worker/javascriptClient.ts | 30 ++++++++++- .../pythonWebWorker/worker/python.ts | 27 ++++++++-- .../pythonWebWorker/worker/pythonClient.ts | 2 +- quadratic-connection/src/proxy.rs | 24 ++++++--- 10 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts create mode 100644 quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptXHROverride.ts diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptClientMessages.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptClientMessages.ts index ab4aa0a248..691b214bca 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptClientMessages.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptClientMessages.ts @@ -22,15 +22,31 @@ export interface JavascriptClientState { version?: string; } +export interface JavascriptClientInit { + type: 'javascriptClientInit'; + version: string; +} + export interface ClientJavascriptCoreChannel { type: 'clientJavascriptCoreChannel'; + env: ImportMetaEnv; } -export interface JavascriptClientInit { - type: 'javascriptClientInit'; - version: string; +export interface ClientJavascriptGetJwt { + type: 'clientJavascriptGetJwt'; + id: number; + jwt: string; +} + +export interface JavascriptClientGetJwt { + type: 'javascriptClientGetJwt'; + id: number; } -export type JavascriptClientMessage = JavascriptClientLoadError | JavascriptClientState | JavascriptClientInit; +export type JavascriptClientMessage = + | JavascriptClientLoadError + | JavascriptClientState + | JavascriptClientInit + | JavascriptClientGetJwt; -export type ClientJavascriptMessage = ClientJavascriptCoreChannel; +export type ClientJavascriptMessage = ClientJavascriptCoreChannel | ClientJavascriptGetJwt; diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptWebWorker.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptWebWorker.ts index be63341a90..d2072110c6 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptWebWorker.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/javascriptWebWorker.ts @@ -1,8 +1,9 @@ import { events } from '@/app/events/events'; +import { authClient } from '@/auth/auth'; import mixpanel from 'mixpanel-browser'; import { LanguageState } from '../languageTypes'; import { quadraticCore } from '../quadraticCore/quadraticCore'; -import { ClientJavascriptMessage, JavascriptClientMessage } from './javascriptClientMessages'; +import { ClientJavascriptMessage, JavascriptClientGetJwt, JavascriptClientMessage } from './javascriptClientMessages'; class JavascriptWebWorker { state: LanguageState = 'loading'; @@ -30,6 +31,14 @@ class JavascriptWebWorker { events.emit('javascriptState', message.data.state, message.data.current, message.data.awaitingExecution); break; + case 'javascriptClientGetJwt': + authClient.getTokenOrRedirect().then((jwt) => { + const data = message.data as JavascriptClientGetJwt; + this.send({ type: 'clientJavascriptGetJwt', id: data.id, jwt }); + }); + + break; + default: throw new Error(`Unhandled message type ${message.type}`); } @@ -40,7 +49,7 @@ class JavascriptWebWorker { this.worker.onmessage = this.handleMessage; const JavascriptCoreChannel = new MessageChannel(); - this.send({ type: 'clientJavascriptCoreChannel' }, JavascriptCoreChannel.port1); + this.send({ type: 'clientJavascriptCoreChannel', env: import.meta.env }, JavascriptCoreChannel.port1); quadraticCore.sendJavascriptInit(JavascriptCoreChannel.port2); } diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts new file mode 100644 index 0000000000..5d061aa1c8 --- /dev/null +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts @@ -0,0 +1,29 @@ +export function getJavascriptFetchOverride(proxyUrl: string, jwt: string) { + return ` +self['fetch'] = new Proxy(fetch, { + apply: function (target, thisArg, args) { + const [resource, config] = args; + + const newConfig = config || {}; + const headers = newConfig.headers || {}; + const newHeaders = {}; + + // Prefix all original request headers with X-Proxy- + for (const key in headers) { + newHeaders[\`X-Proxy-\${key}\`] = headers[key]; + } + + // Set the original request URL on X-Proxy-Url header + const url = new URL(resource, location.origin); + newHeaders['X-Proxy-Url'] = url.toString(); + + // Set the authorization header for the proxy server + newHeaders['Authorization'] = 'Bearer ${jwt}'; + + newConfig.headers = newHeaders; + + return target.call(thisArg, '${proxyUrl}', newConfig); + }, +});\n +`; +} diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptXHROverride.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptXHROverride.ts new file mode 100644 index 0000000000..65fc7f0c68 --- /dev/null +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptXHROverride.ts @@ -0,0 +1,50 @@ +export function getJavascriptXHROverride(proxyUrl: string, jwt: string) { + return ` +self['XMLHttpRequest'] = new Proxy(XMLHttpRequest, { + construct: function (target, args) { + const xhr = new target(); + + xhr.open = new Proxy(xhr.open, { + apply: function (target, thisArg, args) { + Object.defineProperty(xhr, '__url', { value: args[1].toString(), writable: true }); + args[1] = '${proxyUrl}'; + return target.apply(thisArg, args); + }, + }); + + xhr.setRequestHeader = new Proxy(xhr.setRequestHeader, { + apply: function (target, thisArg, args) { + // apply quadratic-authorization header as the only authorization header + // this is required for authentication with the proxy server + if (args[0] === 'Quadratic-Authorization') { + args[0] = 'Authorization'; + } else { + // apply all headers on the original request prefixed with X-Proxy- + args[0] = \`X-Proxy-\${args[0]}\`; + } + return target.apply(thisArg, args); + }, + }); + + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.OPENED) { + // this applies the quadratic-authorization header as the only authorization header + // this is required for authentication with the proxy server + xhr.setRequestHeader('Quadratic-Authorization', 'Bearer ${jwt}'); + + // this applies the original request URL as the x-proxy-url header + // this will get prefixed with X-Proxy due to above setRequestHeader override + xhr.setRequestHeader('Url', xhr.__url); + } + // After completion of XHR request + if (xhr.readyState === 4) { + if (xhr.status === 401) { + } + } + }; + + return xhr; + }, +});\n +`; +} diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascript.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascript.ts index 92df152202..fc5262bd35 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascript.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascript.ts @@ -34,7 +34,7 @@ export class Javascript { this.api = new JavascriptAPI(this); } - init = async () => { + private init = async () => { await esbuild.initialize({ wasmURL: '/esbuild.wasm', // this would create another worker to run the actual code. I don't @@ -97,7 +97,9 @@ export class Javascript { this.row = message.y; try { - const code = prepareJavascriptCode(transformedCode, message.x, message.y, this.withLineNumbers); + const proxyUrl = `${javascriptClient.env.VITE_QUADRATIC_CONNECTION_URL}/proxy`; + const jwt = await javascriptClient.getJwt(); + const code = prepareJavascriptCode(transformedCode, message.x, message.y, this.withLineNumbers, proxyUrl, jwt); const runner = new Worker(URL.createObjectURL(new Blob([code], { type: 'application/javascript' })), { type: 'module', name: 'javascriptWorker', diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptCompile.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptCompile.ts index 2593ab988b..71efe50ea6 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptCompile.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptCompile.ts @@ -5,10 +5,11 @@ //! numbers to all return statements via a caught thrown error (the only way to //! get line numbers in JS). +import { getJavascriptFetchOverride } from '@/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride'; +import { getJavascriptXHROverride } from '@/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptXHROverride'; import * as esbuild from 'esbuild-wasm'; import { LINE_NUMBER_VAR } from './javascript'; import { javascriptLibrary } from './runner/generateJavascriptForRunner'; - export interface JavascriptTransformedCode { imports: string; code: string; @@ -66,10 +67,16 @@ export function prepareJavascriptCode( transform: JavascriptTransformedCode, x: number, y: number, - withLineNumbers: boolean + withLineNumbers: boolean, + proxyUrl: string, + jwt: string ): string { const code = withLineNumbers ? javascriptAddLineNumberVars(transform) : transform.code; + const javascriptXHROverride = getJavascriptXHROverride(proxyUrl, jwt); + const javascriptFetchOverride = getJavascriptFetchOverride(proxyUrl, jwt); const compiledCode = + javascriptXHROverride + + javascriptFetchOverride + transform.imports + (withLineNumbers ? `let ${LINE_NUMBER_VAR} = 0;` : '') + javascriptLibrary.replace('{x:0,y:0}', `{x:${x},y:${y}}`) + // replace the pos() with the correct x,y coordinates diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascriptClient.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascriptClient.ts index 1aeeee398a..0819f640a8 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascriptClient.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascriptClient.ts @@ -1,12 +1,20 @@ import { debugWebWorkers, debugWebWorkersMessages } from '@/app/debugFlags'; import { LanguageState } from '@/app/web-workers/languageTypes'; import { CodeRun } from '../../CodeRun'; -import type { ClientJavascriptMessage, JavascriptClientMessage } from '../javascriptClientMessages'; +import type { + ClientJavascriptGetJwt, + ClientJavascriptMessage, + JavascriptClientMessage, +} from '../javascriptClientMessages'; import { javascriptCore } from './javascriptCore'; declare var self: WorkerGlobalScope & typeof globalThis; class JavascriptClient { + private id = 0; + private waitingForResponse: Record = {}; + env: Record = {}; + start() { self.onmessage = this.handleMessage; if (debugWebWorkers) console.log('[javascriptClient] initialized.'); @@ -25,11 +33,21 @@ class JavascriptClient { switch (e.data.type) { case 'clientJavascriptCoreChannel': + this.env = e.data.env; javascriptCore.init(e.ports[0]); break; default: - console.warn('[coreClient] Unhandled message type', e.data); + if (e.data.id !== undefined) { + if (this.waitingForResponse[e.data.id]) { + this.waitingForResponse[e.data.id](e.data); + delete this.waitingForResponse[e.data.id]; + } else { + console.warn('No resolve for message in javascriptClient', e.data.type); + } + } else { + console.warn('[javascriptClient] Unhandled message type', e.data); + } } }; @@ -50,6 +68,14 @@ class JavascriptClient { sendInit(version: string) { this.send({ type: 'javascriptClientInit', version }); } + + getJwt(): Promise { + return new Promise((resolve) => { + const id = this.id++; + this.waitingForResponse[id] = (message: ClientJavascriptGetJwt) => resolve(message.jwt); + this.send({ type: 'javascriptClientGetJwt', id }); + }); + } } export const javascriptClient = new JavascriptClient(); diff --git a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts index 9a555e0071..e2a0edfe43 100644 --- a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts +++ b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/python.ts @@ -51,7 +51,7 @@ class Python { } }; - init = async () => { + private init = async () => { const jwt = await pythonClient.getJwt(); // patch XMLHttpRequest to send requests to the proxy @@ -77,12 +77,31 @@ class Python { }, }); + xhr.setRequestHeader = new Proxy(xhr.setRequestHeader, { + apply: function (target, thisArg, args: [string, string]) { + // apply quadratic-authorization header as the only authorization header + // this is required for authentication with the proxy server + if (args[0] === 'Quadratic-Authorization') { + args[0] = 'Authorization'; + } else { + // apply all headers on the original request prefixed with X-Proxy + args[0] = `X-Proxy-${args[0]}`; + } + return target.apply(thisArg, args); + }, + }); + xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.OPENED) { - xhr.setRequestHeader('Proxy', (xhr as any).__url); - xhr.setRequestHeader('Authorization', `Bearer ${jwt}`); + // this applies the quadratic-authorization header as the only authorization header + // this is required for authentication with the proxy server + xhr.setRequestHeader('Quadratic-Authorization', `Bearer ${jwt}`); + + // this applies the original request URL as the x-proxy-url header + // this will get prefixed with X-Proxy due to above setRequestHeader override + xhr.setRequestHeader('Url', (xhr as any).__url); } - // After complition of XHR request + // After completion of XHR request if (xhr.readyState === 4) { if (xhr.status === 401) { } diff --git a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/pythonClient.ts b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/pythonClient.ts index 1ccaac0704..f57a3b2083 100644 --- a/quadratic-client/src/app/web-workers/pythonWebWorker/worker/pythonClient.ts +++ b/quadratic-client/src/app/web-workers/pythonWebWorker/worker/pythonClient.ts @@ -47,7 +47,7 @@ class PythonClient { this.waitingForResponse[e.data.id](e.data); delete this.waitingForResponse[e.data.id]; } else { - console.warn('No resolve for message in pythonClient', e.data.id); + console.warn('No resolve for message in pythonClient', e.data.type); } } else { console.warn('[pythonClient] Unhandled message type', e.data); diff --git a/quadratic-connection/src/proxy.rs b/quadratic-connection/src/proxy.rs index 443f8e2665..2575dcaf77 100644 --- a/quadratic-connection/src/proxy.rs +++ b/quadratic-connection/src/proxy.rs @@ -13,7 +13,9 @@ use crate::error::{proxy_error, ConnectionError, Result}; use crate::state::State; const REQUEST_TIMEOUT_SEC: u64 = 15; -const PROXY_HEADER: &str = "proxy"; +const AUTHORIZATION_HEADER: &str = "authorization"; +const PROXY_URL_HEADER: &str = "x-proxy-url"; +const PROXY_HEADER_PREFIX: &str = "x-proxy-"; pub(crate) async fn axum_to_reqwest( url: &str, @@ -24,12 +26,22 @@ pub(crate) async fn axum_to_reqwest( let method = Method::from_bytes(method_bytes).map_err(proxy_error)?; let mut headers = reqwest::header::HeaderMap::with_capacity(req.headers().len()); - let headers_to_ignore = ["host", PROXY_HEADER, "authorization"]; + let headers_to_ignore = ["host", AUTHORIZATION_HEADER, PROXY_URL_HEADER]; for (name, value) in req .headers() .into_iter() .filter(|(name, _)| !headers_to_ignore.contains(&name.as_str())) + .map(|(name, value)| { + if name.to_string().starts_with(PROXY_HEADER_PREFIX) { + ( + name.to_string().replace(PROXY_HEADER_PREFIX, ""), + value.to_str().unwrap().to_string(), + ) + } else { + (name.to_string(), value.to_str().unwrap().to_string()) + } + }) { let name = reqwest::header::HeaderName::from_bytes(name.as_ref()).map_err(proxy_error)?; let value = @@ -74,7 +86,7 @@ pub(crate) async fn proxy( let headers = req.headers().clone(); let url = headers - .get(PROXY_HEADER) + .get(PROXY_URL_HEADER) .ok_or_else(|| ConnectionError::Proxy("No proxy header found".to_string()))? .to_str() .map_err(proxy_error)?; @@ -103,7 +115,7 @@ mod tests { let mut request = Request::new(Body::empty()); request .headers_mut() - .insert(PROXY_HEADER, HeaderValue::from_static(URL)); + .insert(PROXY_URL_HEADER, HeaderValue::from_static(URL)); let data = proxy(state, request).await.unwrap(); let response = data.into_response(); @@ -122,7 +134,7 @@ mod tests { .insert(ACCEPT, HeaderValue::from_static(accept)); request .headers_mut() - .insert(PROXY_HEADER, HeaderValue::from_static(URL)); + .insert(PROXY_URL_HEADER, HeaderValue::from_static(URL)); let result = axum_to_reqwest(URL, request, state.client.clone()) .await @@ -133,7 +145,7 @@ mod tests { assert_eq!(result.method(), Method::POST); assert_eq!(result.url().to_string(), URL); - // PROXY_HEADER doesn't get copied over + // PROXY_URL_HEADER doesn't get copied over assert_eq!(result.headers().len(), 1); assert_eq!( result.headers().get(reqwest::header::ACCEPT).unwrap(), From 1200b355ee82dd82d46255f1943a76f76753a50c Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 19 Nov 2024 04:01:30 +0530 Subject: [PATCH 33/62] avoid url object --- .../worker/javascript/getJavascriptFetchOverride.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts index 5d061aa1c8..7d84d0d7e2 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/getJavascriptFetchOverride.ts @@ -2,7 +2,7 @@ export function getJavascriptFetchOverride(proxyUrl: string, jwt: string) { return ` self['fetch'] = new Proxy(fetch, { apply: function (target, thisArg, args) { - const [resource, config] = args; + const [url, config] = args; const newConfig = config || {}; const headers = newConfig.headers || {}; @@ -14,7 +14,6 @@ self['fetch'] = new Proxy(fetch, { } // Set the original request URL on X-Proxy-Url header - const url = new URL(resource, location.origin); newHeaders['X-Proxy-Url'] = url.toString(); // Set the authorization header for the proxy server From 95e9761f2358984e3bfc5b00edcbf1ab892ce517 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 18 Nov 2024 15:39:18 -0700 Subject: [PATCH 34/62] initial commit --- .../app/ui/menus/CodeEditor/CodeEditor.tsx | 108 +++++++++--------- .../ui/menus/CodeEditor/CodeEditorHeader.tsx | 31 ++++- .../CodeEditor/panels/CodeEditorPanel.tsx | 33 +----- .../src/shared/components/Icons.tsx | 8 ++ 4 files changed, 92 insertions(+), 88 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx index 19459516d9..1305635cb8 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx @@ -35,72 +35,72 @@ export const CodeEditor = () => { {showCodeEditor && ( -
    e.stopPropagation()} - onCut={(e) => e.stopPropagation()} - onPaste={(e) => e.stopPropagation()} - > +
    +
    { - // todo: handle multiplayer code editor here - multiplayer.sendMouseMove(); + width: `${ + codeEditorPanelData.editorWidth + + (codeEditorPanelData.panelPosition === 'left' ? codeEditorPanelData.panelWidth : 0) + }px`, }} + onCopy={(e) => e.stopPropagation()} + onCut={(e) => e.stopPropagation()} + onPaste={(e) => e.stopPropagation()} > - +
    { + // todo: handle multiplayer code editor here + multiplayer.sendMouseMove(); + }} + > + - + - + - + +
    - -
    +
    + +
    -
    - +
    - -
    )} diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx index a71433fc5c..5f266ff326 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx @@ -17,17 +17,24 @@ import { SnippetsPopover } from '@/app/ui/menus/CodeEditor/SnippetsPopover'; import { useCancelRun } from '@/app/ui/menus/CodeEditor/hooks/useCancelRun'; import { useCloseCodeEditor } from '@/app/ui/menus/CodeEditor/hooks/useCloseCodeEditor'; import { useSaveAndRunCell } from '@/app/ui/menus/CodeEditor/hooks/useSaveAndRunCell'; +import { PanelPosition, useCodeEditorPanelData } from '@/app/ui/menus/CodeEditor/panels/useCodeEditorPanelData'; import type { CodeRun } from '@/app/web-workers/CodeRun'; import { LanguageState } from '@/app/web-workers/languageTypes'; import { MultiplayerUser } from '@/app/web-workers/multiplayerWebWorker/multiplayerTypes'; -import { CloseIcon, SaveAndRunIcon, SaveAndRunStopIcon } from '@/shared/components/Icons'; +import { + CloseIcon, + DockToBottomIcon, + DockToRightIcon, + SaveAndRunIcon, + SaveAndRunStopIcon, +} from '@/shared/components/Icons'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { Button } from '@/shared/shadcn/ui/button'; import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; import { cn } from '@/shared/shadcn/utils'; import { CircularProgress } from '@mui/material'; import * as monaco from 'monaco-editor'; -import { useEffect, useMemo, useState } from 'react'; +import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; interface CodeEditorHeaderProps { @@ -51,7 +58,7 @@ export const CodeEditorHeader = ({ editorInst }: CodeEditorHeaderProps) => { () => hasPermissionToEditFile(permissions) && (isConnection ? teamPermissions?.includes('TEAM_EDIT') : true), [permissions, teamPermissions, isConnection] ); - + const { panelPosition, setPanelPosition } = useCodeEditorPanelData(); const connectionsFetcher = useConnectionsFetcher(); // Get the connection name (it's possible the user won't have access to it @@ -159,8 +166,16 @@ export const CodeEditorHeader = ({ editorInst }: CodeEditorHeaderProps) => { }; }, [codeCellState.pos.x, codeCellState.pos.y, codeCellState.sheetId]); + const changePanelPosition = useCallback( + (e: MouseEvent) => { + setPanelPosition((prev: PanelPosition) => (prev === 'left' ? 'bottom' : 'left')); + e.currentTarget.blur(); + }, + [setPanelPosition] + ); + return ( -
    +
    { )} +
    + + + + + - -
    - {panelPosition === 'left' && ( { return difference; }; +export const DockToBottomIcon: IconComponent = (props) => { + return dock_to_bottom; +}; + +export const DockToRightIcon: IconComponent = (props) => { + return dock_to_right; +}; + export const DownloadIcon: IconComponent = (props) => { return download; }; From 40c19dcc384e641cd36c1af2e8f1028e5cea2736 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 18 Nov 2024 16:08:16 -0700 Subject: [PATCH 35/62] initial commit --- .../CodeEditor/AIAssistant/CodeSnippet.tsx | 41 +++++++++---- .../app/ui/menus/CodeEditor/CodeEditor.tsx | 3 +- .../CodeEditor/CodeEditorDiffButtons.tsx | 26 ++++----- .../ui/menus/CodeEditor/CodeEditorHeader.tsx | 57 ++++++++----------- 4 files changed, 71 insertions(+), 56 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx index 32ece97a43..a36ab88d22 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx @@ -16,7 +16,7 @@ import mixpanel from 'mixpanel-browser'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRecoilCallback, useRecoilValue } from 'recoil'; -const MAX_LINES = 8; +const MAX_LINES = 6; interface CodeSnippetProps { code: string; @@ -69,12 +69,10 @@ export function CodeSnippet({ code, language = 'plaintext' }: CodeSnippetProps) - {!isLoading && ( -
    - - -
    - )} +
    + + +
    async () => { @@ -169,13 +175,22 @@ function CodeSnippetRunButton({ language, text }: { language: CodeSnippetProps[' size="sm" className="px-2 text-muted-foreground hover:text-foreground" onClick={handleSaveAndRun} + disabled={isLoading} > Apply ); } -function CodeSnippetCopyButton({ language, text }: { language: CodeSnippetProps['language']; text: string }) { +function CodeSnippetCopyButton({ + language, + text, + isLoading, +}: { + language: CodeSnippetProps['language']; + text: string; + isLoading: boolean; +}) { const [tooltipMsg, setTooltipMsg] = useState('Copy'); const handleCopy = useCallback( @@ -192,7 +207,13 @@ function CodeSnippetCopyButton({ language, text }: { language: CodeSnippetProps[ ); return ( - ); diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx index 19459516d9..b54f18846e 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx @@ -1,5 +1,6 @@ import { codeEditorShowCodeEditorAtom } from '@/app/atoms/codeEditorAtom'; import { CodeEditorBody } from '@/app/ui/menus/CodeEditor/CodeEditorBody'; +import { CodeEditorDiffButtons } from '@/app/ui/menus/CodeEditor/CodeEditorDiffButtons'; import { CodeEditorEffects } from '@/app/ui/menus/CodeEditor/CodeEditorEffects'; import { CodeEditorEmptyState } from '@/app/ui/menus/CodeEditor/CodeEditorEmptyState'; import { CodeEditorEscapeEffect } from '@/app/ui/menus/CodeEditor/CodeEditorEscapeEffect'; @@ -74,7 +75,7 @@ export const CodeEditor = () => { - + diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx index e3037e95d2..dc2ce21aa9 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx @@ -3,15 +3,17 @@ import { codeEditorCodeStringAtom, codeEditorDiffEditorContentAtom, codeEditorEditorContentAtom, + codeEditorShowDiffEditorAtom, } from '@/app/atoms/codeEditorAtom'; import { sheets } from '@/app/grid/controller/Sheets'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { ThumbDownIcon, ThumbUpIcon } from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; -import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; -import { useRecoilCallback } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; export const CodeEditorDiffButtons = () => { + const showDiffEditor = useRecoilValue(codeEditorShowDiffEditorAtom); + const handleDiffReject = useRecoilCallback( ({ set, snapshot }) => async () => { @@ -64,19 +66,17 @@ export const CodeEditorDiffButtons = () => { [] ); + if (!showDiffEditor) return null; + return ( -
    - - - +
    + - - - +
    ); }; diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx index a71433fc5c..36b65eb332 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx @@ -11,7 +11,6 @@ import { codeCellIsAConnection, getCodeCell, getConnectionUuid, getLanguage } fr import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; import { useConnectionsFetcher } from '@/app/ui/hooks/useConnectionsFetcher'; -import { CodeEditorDiffButtons } from '@/app/ui/menus/CodeEditor/CodeEditorDiffButtons'; import { CodeEditorRefButton } from '@/app/ui/menus/CodeEditor/CodeEditorRefButton'; import { SnippetsPopover } from '@/app/ui/menus/CodeEditor/SnippetsPopover'; import { useCancelRun } from '@/app/ui/menus/CodeEditor/hooks/useCancelRun'; @@ -195,39 +194,33 @@ export const CodeEditorHeader = ({ editorInst }: CodeEditorHeaderProps) => { )} - {hasPermission && ( + {hasPermission && !showDiffEditor && ( <> - {showDiffEditor ? ( - + {['Python', 'Javascript', 'Formula'].includes(language as string) && } + + {['Python', 'Javascript'].includes(language as string) && } + + {!isRunningComputation ? ( + + + ) : ( - <> - {['Python', 'Javascript', 'Formula'].includes(language as string) && } - - {['Python', 'Javascript'].includes(language as string) && } - - {!isRunningComputation ? ( - - - - ) : ( - - - - )} - + + + )} )} From 58db43c466cc557ad3a3ab6c52fbc7748baedd2c Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 18 Nov 2024 16:15:52 -0700 Subject: [PATCH 36/62] updates --- .../src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx | 2 +- .../src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx index a36ab88d22..8f5a81afab 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx @@ -177,7 +177,7 @@ function CodeSnippetRunButton({ onClick={handleSaveAndRun} disabled={isLoading} > - Apply + Apply & run ); } diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx index dc2ce21aa9..d047a15b6e 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx @@ -70,11 +70,11 @@ export const CodeEditorDiffButtons = () => { return (
    - -
    From 921d17f422f310a63c750848698db82315b01f84 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Mon, 18 Nov 2024 16:44:48 -0700 Subject: [PATCH 37/62] fix bug --- ...onnectionSchemaBrowserTableQueryAction.tsx | 25 +++++++++++++------ .../connections/ConnectionSchemaBrowser.tsx | 6 ++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx index 28736c4f28..f6e42d9ae4 100644 --- a/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx +++ b/quadratic-client/src/dashboard/hooks/useConnectionSchemaBrowserTableQueryAction.tsx @@ -1,3 +1,4 @@ +import ConditionalWrapper from '@/app/ui/components/ConditionalWrapper'; import { useSaveAndRunCell } from '@/app/ui/menus/CodeEditor/hooks/useSaveAndRunCell'; import { newNewFileFromStateConnection } from '@/shared/hooks/useNewFileFromState'; import { Button } from '@/shared/shadcn/ui/button'; @@ -22,19 +23,27 @@ export const useConnectionSchemaBrowserTableQueryActionNewFile = ({ }) => { return { TableQueryAction: ({ query }: { query: string }) => { + const isValidQuery = query.length > 0; const to = newNewFileFromStateConnection({ query, isPrivate, teamUuid, connectionType, connectionUuid }); return ( - { - mixpanel.track('[Connections].schemaViewer.newFileFromTable'); - }} + ( + { + mixpanel.track('[Connections].schemaViewer.newFileFromTable'); + }} + > + {children} + + )} > - - + ); }, }; diff --git a/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx b/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx index ad8166a349..b029121c4a 100644 --- a/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx +++ b/quadratic-client/src/shared/components/connections/ConnectionSchemaBrowser.tsx @@ -47,7 +47,11 @@ export const ConnectionSchemaBrowser = ({
    diff --git a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx index a2aed802ea..b4d8b38906 100644 --- a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx +++ b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx @@ -22,6 +22,7 @@ export const FilesListEmptyState = ({ isPrivate = false }: { isPrivate?: boolean You don’t have any files yet.{' '} { mixpanel.track('[FilesEmptyState].clickCreateBlankFile'); diff --git a/quadratic-client/src/dashboard/components/NewFileButton.tsx b/quadratic-client/src/dashboard/components/NewFileButton.tsx index 4670d8f3e4..a568937a91 100644 --- a/quadratic-client/src/dashboard/components/NewFileButton.tsx +++ b/quadratic-client/src/dashboard/components/NewFileButton.tsx @@ -36,7 +36,7 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { return (
    - + Date: Tue, 19 Nov 2024 16:13:12 -0700 Subject: [PATCH 45/62] Update AIAnalystExamplePrompts.tsx --- .../src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx index 5e1bf75031..500477d43c 100644 --- a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx +++ b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx @@ -28,7 +28,8 @@ export function AIAnalystExamplePrompts() { const { submitPrompt } = useSubmitAIAnalystPrompt(); return ( -
    +
    +

    What can I help with?

    {examples.map(({ title, description, icon, prompt }) => ( ) : null} diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index ee329b7e1f..2c5ee652a2 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -256,7 +256,7 @@ export class Cursor extends Container { if (!inlineShowing) return; const { visible, editMode, formula } = pixiAppSettings.inlineEditorState; - if (!visible || editMode) return; + if (!visible || !editMode) return; let { x, y, width, height } = sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y); width = Math.max(inlineEditorHandler.width + CURSOR_THICKNESS * (formula ? 1 : 2), width); diff --git a/quadratic-client/src/app/ui/icons/index.tsx b/quadratic-client/src/app/ui/icons/index.tsx index 704235855e..63b6149e90 100644 --- a/quadratic-client/src/app/ui/icons/index.tsx +++ b/quadratic-client/src/app/ui/icons/index.tsx @@ -180,31 +180,3 @@ export const BoxIcon = (props: SvgIconProps) => ( ); - -export const SidebarRightIcon = (props: SvgIconProps) => ( - - - - - - -); diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 905cbad643..3ad366a91c 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -216,6 +216,10 @@ export const DecimalIncreaseIcon: IconComponent = (props) => { return decimal_increase; }; +export const DockToLeftIcon: IconComponent = (props) => { + return dock_to_left; +}; + export const DeleteIcon: IconComponent = (props) => { return delete; }; From 5933819a5a8bcf7d634a86e8c67e13d742d5c278 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 21 Nov 2024 01:13:07 +0530 Subject: [PATCH 48/62] fix height --- .../src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index daa64107bd..df9cebf0c4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -27,7 +27,7 @@ export const InlineEditor = () => { height += CURSOR_THICKNESS * 1.5; const inlineShowing = inlineEditorHandler.getShowing(); if (inlineShowing) { - height = Math.max(height, sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y).height); + height = Math.min(height, sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y).height); } return ( From 5809b4afc0079e43dc90d4afe197cba6d3b2c3b5 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 21 Nov 2024 01:36:01 +0530 Subject: [PATCH 49/62] revert height --- .../src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index df9cebf0c4..daa64107bd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx @@ -27,7 +27,7 @@ export const InlineEditor = () => { height += CURSOR_THICKNESS * 1.5; const inlineShowing = inlineEditorHandler.getShowing(); if (inlineShowing) { - height = Math.min(height, sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y).height); + height = Math.max(height, sheets.sheet.getCellOffsets(inlineShowing.x, inlineShowing.y).height); } return ( From 987a7e7514bab97905d8a92a9dbe2d248786efcd Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 20 Nov 2024 13:48:16 -0700 Subject: [PATCH 50/62] fixes --- .../AIAnalyst/AIAnalystExamplePrompts.tsx | 40 ++++++++++++++++++- .../ui/menus/AIAnalyst/AIAnalystHeader.tsx | 24 +++++------ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx index 500477d43c..84499d7c6e 100644 --- a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx +++ b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx @@ -2,6 +2,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { useSubmitAIAnalystPrompt } from '@/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt'; import { CodeIcon, InsertChartIcon, TableIcon } from '@/shared/components/Icons'; import mixpanel from 'mixpanel-browser'; +import { useEffect, useState } from 'react'; const examples = [ { @@ -29,7 +30,7 @@ export function AIAnalystExamplePrompts() { return (
    -

    What can I help with?

    + {examples.map(({ title, description, icon, prompt }) => ( - - + +